1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #include "utils/time_utils.hpp" 4 5 #include "error_messages.hpp" 6 #include "http_response.hpp" 7 #include "logging.hpp" 8 9 #include <version> 10 11 #if __cpp_lib_chrono < 201907L 12 #include "utils/extern/date.h" 13 #endif 14 #include <array> 15 #include <charconv> 16 #include <chrono> 17 #include <cstddef> 18 #include <cstdint> 19 #include <ctime> 20 #include <format> 21 #include <optional> 22 #include <ratio> 23 #include <sstream> 24 #include <string> 25 #include <string_view> 26 #include <system_error> 27 #include <utility> 28 29 namespace redfish::time_utils 30 { 31 32 /** 33 * @brief Convert string that represents value in Duration Format to its numeric 34 * equivalent. 35 */ fromDurationString(std::string_view v)36 std::optional<std::chrono::milliseconds> fromDurationString(std::string_view v) 37 { 38 std::chrono::milliseconds out = std::chrono::milliseconds::zero(); 39 enum class ProcessingStage 40 { 41 // P1DT1H1M1.100S 42 P, 43 Days, 44 Hours, 45 Minutes, 46 Seconds, 47 Milliseconds, 48 Done, 49 }; 50 ProcessingStage stage = ProcessingStage::P; 51 52 while (!v.empty()) 53 { 54 if (stage == ProcessingStage::P) 55 { 56 if (v.front() != 'P') 57 { 58 return std::nullopt; 59 } 60 v.remove_prefix(1); 61 stage = ProcessingStage::Days; 62 continue; 63 } 64 if (stage == ProcessingStage::Days) 65 { 66 if (v.front() == 'T') 67 { 68 v.remove_prefix(1); 69 stage = ProcessingStage::Hours; 70 continue; 71 } 72 } 73 uint64_t ticks = 0; 74 auto [ptr, ec] = std::from_chars(v.begin(), v.end(), ticks); 75 if (ec != std::errc()) 76 { 77 BMCWEB_LOG_ERROR("Failed to convert string \"{}\" to decimal", v); 78 return std::nullopt; 79 } 80 size_t charactersRead = static_cast<size_t>(ptr - v.data()); 81 if (ptr >= v.end()) 82 { 83 BMCWEB_LOG_ERROR("Missing postfix"); 84 return std::nullopt; 85 } 86 if (*ptr == 'D') 87 { 88 if (stage > ProcessingStage::Days) 89 { 90 return std::nullopt; 91 } 92 out += std::chrono::days(ticks); 93 } 94 else if (*ptr == 'H') 95 { 96 if (stage > ProcessingStage::Hours) 97 { 98 return std::nullopt; 99 } 100 out += std::chrono::hours(ticks); 101 } 102 else if (*ptr == 'M') 103 { 104 if (stage > ProcessingStage::Minutes) 105 { 106 return std::nullopt; 107 } 108 out += std::chrono::minutes(ticks); 109 } 110 else if (*ptr == '.') 111 { 112 if (stage > ProcessingStage::Seconds) 113 { 114 return std::nullopt; 115 } 116 out += std::chrono::seconds(ticks); 117 stage = ProcessingStage::Milliseconds; 118 } 119 else if (*ptr == 'S') 120 { 121 // We could be seeing seconds for the first time, (as is the case in 122 // 1S) or for the second time (in the case of 1.1S). 123 if (stage <= ProcessingStage::Seconds) 124 { 125 out += std::chrono::seconds(ticks); 126 stage = ProcessingStage::Milliseconds; 127 } 128 else if (stage > ProcessingStage::Milliseconds) 129 { 130 BMCWEB_LOG_ERROR("Got unexpected information at end of parse"); 131 return std::nullopt; 132 } 133 else 134 { 135 // Seconds could be any form of (1S, 1.1S, 1.11S, 1.111S); 136 // Handle them all milliseconds are after the decimal point, 137 // so they need right padded. 138 if (charactersRead == 1) 139 { 140 ticks *= 100; 141 } 142 else if (charactersRead == 2) 143 { 144 ticks *= 10; 145 } 146 out += std::chrono::milliseconds(ticks); 147 stage = ProcessingStage::Milliseconds; 148 } 149 } 150 else 151 { 152 BMCWEB_LOG_ERROR("Unknown postfix {}", *ptr); 153 return std::nullopt; 154 } 155 156 v.remove_prefix(charactersRead + 1U); 157 } 158 return out; 159 } 160 161 /** 162 * @brief Convert time value into duration format that is based on ISO 8601. 163 * Example output: "P12DT1M5.5S" 164 * Ref: Redfish Specification, Section 9.4.4. Duration values 165 */ toDurationString(std::chrono::milliseconds ms)166 std::string toDurationString(std::chrono::milliseconds ms) 167 { 168 if (ms < std::chrono::milliseconds::zero()) 169 { 170 return ""; 171 } 172 173 std::chrono::days days = std::chrono::floor<std::chrono::days>(ms); 174 ms -= days; 175 176 std::chrono::hours hours = std::chrono::floor<std::chrono::hours>(ms); 177 ms -= hours; 178 179 std::chrono::minutes minutes = std::chrono::floor<std::chrono::minutes>(ms); 180 ms -= minutes; 181 182 std::chrono::seconds seconds = std::chrono::floor<std::chrono::seconds>(ms); 183 ms -= seconds; 184 std::string daysStr; 185 if (days.count() > 0) 186 { 187 daysStr = std::format("{}D", days.count()); 188 } 189 std::string hoursStr; 190 if (hours.count() > 0) 191 { 192 hoursStr = std::format("{}H", hours.count()); 193 } 194 std::string minStr; 195 if (minutes.count() > 0) 196 { 197 minStr = std::format("{}M", minutes.count()); 198 } 199 std::string secStr; 200 if (seconds.count() != 0 || ms.count() != 0) 201 { 202 secStr = std::format("{}.{:03}S", seconds.count(), ms.count()); 203 } 204 205 return std::format("P{}T{}{}{}", daysStr, hoursStr, minStr, secStr); 206 } 207 toDurationStringFromUint(uint64_t timeMs)208 std::optional<std::string> toDurationStringFromUint(uint64_t timeMs) 209 { 210 constexpr uint64_t maxTimeMs = 211 static_cast<uint64_t>(std::chrono::milliseconds::max().count()); 212 213 if (maxTimeMs < timeMs) 214 { 215 return std::nullopt; 216 } 217 218 std::string duration = toDurationString(std::chrono::milliseconds(timeMs)); 219 if (duration.empty()) 220 { 221 return std::nullopt; 222 } 223 224 return std::make_optional(duration); 225 } 226 227 namespace details 228 { 229 // This code is left for support of gcc < 13 which didn't have support for 230 // timezones. It should be removed at some point in the future. 231 #if __cpp_lib_chrono < 201907L 232 233 // Returns year/month/day triple in civil calendar 234 // Preconditions: z is number of days since 1970-01-01 and is in the range: 235 // [numeric_limits<Int>::min(), 236 // numeric_limits<Int>::max()-719468]. 237 // Algorithm sourced from 238 // https://howardhinnant.github.io/date_algorithms.html#civil_from_days 239 // All constants are explained in the above 240 template <class IntType> civilFromDays(IntType z)241 constexpr std::tuple<IntType, unsigned, unsigned> civilFromDays( 242 IntType z) noexcept 243 { 244 z += 719468; 245 IntType era = (z >= 0 ? z : z - 146096) / 146097; 246 unsigned doe = static_cast<unsigned>(z - era * 146097); // [0, 146096] 247 unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 248 365; // [0, 399] 249 IntType y = static_cast<IntType>(yoe) + era * 400; 250 unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] 251 unsigned mp = (5 * doy + 2) / 153; // [0, 11] 252 unsigned d = doy - (153 * mp + 2) / 5 + 1; // [1, 31] 253 unsigned m = mp < 10 ? mp + 3 : mp - 9; // [1, 12] 254 255 return std::tuple<IntType, unsigned, unsigned>(y + (m <= 2), m, d); 256 } 257 258 template <typename IntType, typename Period> toISO8061ExtendedStr(std::chrono::duration<IntType,Period> t)259 std::string toISO8061ExtendedStr(std::chrono::duration<IntType, Period> t) 260 { 261 using seconds = std::chrono::duration<int>; 262 using minutes = std::chrono::duration<int, std::ratio<60>>; 263 using hours = std::chrono::duration<int, std::ratio<3600>>; 264 using days = std::chrono::duration< 265 IntType, std::ratio_multiply<hours::period, std::ratio<24>>>; 266 267 // d is days since 1970-01-01 268 days d = std::chrono::duration_cast<days>(t); 269 270 // t is now time duration since midnight of day d 271 t -= d; 272 273 // break d down into year/month/day 274 int year = 0; 275 int month = 0; 276 int day = 0; 277 std::tie(year, month, day) = details::civilFromDays(d.count()); 278 // Check against limits. Can't go above year 9999, and can't go below epoch 279 // (1970) 280 if (year >= 10000) 281 { 282 year = 9999; 283 month = 12; 284 day = 31; 285 t = days(1) - std::chrono::duration<IntType, Period>(1); 286 } 287 else if (year < 1970) 288 { 289 year = 1970; 290 month = 1; 291 day = 1; 292 t = std::chrono::duration<IntType, Period>::zero(); 293 } 294 295 hours hr = std::chrono::duration_cast<hours>(t); 296 t -= hr; 297 298 minutes mt = std::chrono::duration_cast<minutes>(t); 299 t -= mt; 300 301 seconds se = std::chrono::duration_cast<seconds>(t); 302 303 t -= se; 304 305 std::string subseconds; 306 if constexpr (std::is_same_v<typename decltype(t)::period, std::milli>) 307 { 308 using MilliDuration = std::chrono::duration<int, std::milli>; 309 MilliDuration subsec = std::chrono::duration_cast<MilliDuration>(t); 310 subseconds = std::format(".{:03}", subsec.count()); 311 } 312 else if constexpr (std::is_same_v<typename decltype(t)::period, std::micro>) 313 { 314 using MicroDuration = std::chrono::duration<int, std::micro>; 315 MicroDuration subsec = std::chrono::duration_cast<MicroDuration>(t); 316 subseconds = std::format(".{:06}", subsec.count()); 317 } 318 319 return std::format("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}+00:00", year, 320 month, day, hr.count(), mt.count(), se.count(), 321 subseconds); 322 } 323 324 #else 325 326 template <typename IntType, typename Period> 327 328 std::string toISO8061ExtendedStr(std::chrono::duration<IntType, Period> dur) 329 { 330 using namespace std::literals::chrono_literals; 331 332 using SubType = std::chrono::duration<IntType, Period>; 333 334 // d is days since 1970-01-01 335 std::chrono::days days = std::chrono::floor<std::chrono::days>(dur); 336 std::chrono::sys_days sysDays(days); 337 std::chrono::year_month_day ymd(sysDays); 338 339 // Enforce 3 constraints 340 // the result cant under or overflow the calculation 341 // the resulting string needs to be representable as 4 digits 342 // The resulting string can't be before epoch 343 if (dur.count() <= 0) 344 { 345 BMCWEB_LOG_WARNING("Underflow from value {}", dur.count()); 346 ymd = 1970y / std::chrono::January / 1d; 347 dur = std::chrono::duration<IntType, Period>::zero(); 348 } 349 else if (dur > SubType::max() - std::chrono::days(1)) 350 { 351 BMCWEB_LOG_WARNING("Overflow from value {}", dur.count()); 352 ymd = 9999y / std::chrono::December / 31d; 353 dur = std::chrono::days(1) - SubType(1); 354 } 355 else if (ymd.year() >= 10000y) 356 { 357 BMCWEB_LOG_WARNING("Year {} not representable", ymd.year()); 358 ymd = 9999y / std::chrono::December / 31d; 359 dur = std::chrono::days(1) - SubType(1); 360 } 361 else if (ymd.year() < 1970y) 362 { 363 BMCWEB_LOG_WARNING("Year {} not representable", ymd.year()); 364 ymd = 1970y / std::chrono::January / 1d; 365 dur = SubType::zero(); 366 } 367 else 368 { 369 // t is now time duration since midnight of day d 370 dur -= days; 371 } 372 std::chrono::hh_mm_ss<SubType> hms(dur); 373 374 return std::format("{}T{}+00:00", ymd, hms); 375 } 376 377 #endif 378 } // namespace details 379 380 // Returns the formatted date time string. 381 // Note that the maximum supported date is 9999-12-31T23:59:59+00:00, if 382 // the given |secondsSinceEpoch| is too large, we return the maximum supported 383 // date. getDateTimeUint(uint64_t secondsSinceEpoch)384 std::string getDateTimeUint(uint64_t secondsSinceEpoch) 385 { 386 using DurationType = std::chrono::duration<uint64_t>; 387 DurationType sinceEpoch(secondsSinceEpoch); 388 return details::toISO8061ExtendedStr(sinceEpoch); 389 } 390 391 // Returns the formatted date time string with millisecond precision 392 // Note that the maximum supported date is 9999-12-31T23:59:59+00:00, if 393 // the given |secondsSinceEpoch| is too large, we return the maximum supported 394 // date. getDateTimeUintMs(uint64_t milliSecondsSinceEpoch)395 std::string getDateTimeUintMs(uint64_t milliSecondsSinceEpoch) 396 { 397 using DurationType = std::chrono::duration<uint64_t, std::milli>; 398 DurationType sinceEpoch(milliSecondsSinceEpoch); 399 return details::toISO8061ExtendedStr(sinceEpoch); 400 } 401 402 // Returns the formatted date time string with microsecond precision getDateTimeUintUs(uint64_t microSecondsSinceEpoch)403 std::string getDateTimeUintUs(uint64_t microSecondsSinceEpoch) 404 { 405 using DurationType = std::chrono::duration<uint64_t, std::micro>; 406 DurationType sinceEpoch(microSecondsSinceEpoch); 407 return details::toISO8061ExtendedStr(sinceEpoch); 408 } 409 getDateTimeStdtime(std::time_t secondsSinceEpoch)410 std::string getDateTimeStdtime(std::time_t secondsSinceEpoch) 411 { 412 using DurationType = std::chrono::duration<std::time_t>; 413 DurationType sinceEpoch(secondsSinceEpoch); 414 return details::toISO8061ExtendedStr(sinceEpoch); 415 } 416 417 /** 418 * Returns the current Date, Time & the local Time Offset 419 * information in a pair 420 * 421 * @param[in] None 422 * 423 * @return std::pair<std::string, std::string>, which consist 424 * of current DateTime & the TimeOffset strings respectively. 425 */ getDateTimeOffsetNow()426 std::pair<std::string, std::string> getDateTimeOffsetNow() 427 { 428 std::time_t time = std::time(nullptr); 429 std::string dateTime = getDateTimeStdtime(time); 430 431 /* extract the local Time Offset value from the 432 * received dateTime string. 433 */ 434 std::string timeOffset("Z00:00"); 435 std::size_t lastPos = dateTime.size(); 436 std::size_t len = timeOffset.size(); 437 if (lastPos > len) 438 { 439 timeOffset = dateTime.substr(lastPos - len); 440 } 441 442 return std::make_pair(dateTime, timeOffset); 443 } 444 445 using usSinceEpoch = std::chrono::duration<int64_t, std::micro>; 446 447 /** 448 * @brief Returns the datetime in ISO 8601 format 449 * 450 * @param[in] std::string_view the date of item manufacture in ISO 8601 format, 451 * either as YYYYMMDD or YYYYMMDDThhmmssZ 452 * Ref: https://github.com/openbmc/phosphor-dbus-interfaces/blob/master/yaml/ 453 * xyz/openbmc_project/Inventory/Decorator/Asset.interface.yaml#L16 454 * 455 * @return std::string which consist the datetime 456 */ getDateTimeIso8601(std::string_view datetime)457 std::optional<std::string> getDateTimeIso8601(std::string_view datetime) 458 { 459 std::optional<usSinceEpoch> us = dateStringToEpoch(datetime); 460 if (!us) 461 { 462 return std::nullopt; 463 } 464 auto secondsDuration = 465 std::chrono::duration_cast<std::chrono::seconds>(*us); 466 467 return std::make_optional( 468 getDateTimeUint(static_cast<uint64_t>(secondsDuration.count()))); 469 } 470 471 /** 472 * @brief ProductionDate report 473 */ productionDateReport(crow::Response & res,const std::string & buildDate)474 void productionDateReport(crow::Response& res, const std::string& buildDate) 475 { 476 std::optional<std::string> valueStr = getDateTimeIso8601(buildDate); 477 if (!valueStr) 478 { 479 messages::internalError(); 480 return; 481 } 482 res.jsonValue["ProductionDate"] = *valueStr; 483 } 484 dateStringToEpoch(std::string_view datetime)485 std::optional<usSinceEpoch> dateStringToEpoch(std::string_view datetime) 486 { 487 for (const char* format : std::to_array( 488 {"%FT%T%Ez", "%FT%TZ", "%FT%T", "%Y%m%d", "%Y%m%dT%H%M%SZ"})) 489 { 490 // Parse using signed so we can detect negative dates 491 std::chrono::sys_time<usSinceEpoch> date; 492 std::istringstream iss(std::string{datetime}); 493 #if __cpp_lib_chrono >= 201907L 494 namespace chrono_from_stream = std::chrono; 495 #else 496 namespace chrono_from_stream = date; 497 #endif 498 if (chrono_from_stream::from_stream(iss, format, date)) 499 { 500 if (date.time_since_epoch().count() < 0) 501 { 502 return std::nullopt; 503 } 504 if (iss.rdbuf()->in_avail() != 0) 505 { 506 // More information left at end of string. 507 continue; 508 } 509 return date.time_since_epoch(); 510 } 511 } 512 return std::nullopt; 513 } 514 } // namespace redfish::time_utils 515