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