1 #pragma once 2 3 #include "logging.hpp" 4 5 #include <boost/date_time.hpp> 6 7 #include <algorithm> 8 #include <charconv> 9 #include <chrono> 10 #include <cmath> 11 #include <compare> 12 #include <cstddef> 13 #include <cstdint> 14 #include <optional> 15 #include <ratio> 16 #include <string> 17 #include <string_view> 18 #include <system_error> 19 20 // IWYU pragma: no_include <stddef.h> 21 // IWYU pragma: no_include <stdint.h> 22 23 namespace redfish 24 { 25 26 namespace time_utils 27 { 28 29 namespace details 30 { 31 32 constexpr intmax_t dayDuration = static_cast<intmax_t>(24 * 60 * 60); 33 using Days = std::chrono::duration<long long, std::ratio<dayDuration>>; 34 35 // Creates a string from an integer in the most efficient way possible without 36 // using std::locale. Adds an exact zero pad based on the pad input parameter. 37 // Does not handle negative numbers. 38 inline std::string padZeros(int64_t value, size_t pad) 39 { 40 std::string result(pad, '0'); 41 for (int64_t val = value; pad > 0; pad--) 42 { 43 result[pad - 1] = static_cast<char>('0' + val % 10); 44 val /= 10; 45 } 46 return result; 47 } 48 49 template <typename FromTime> 50 bool fromDurationItem(std::string_view& fmt, const char postfix, 51 std::chrono::milliseconds& out) 52 { 53 const size_t pos = fmt.find(postfix); 54 if (pos == std::string::npos) 55 { 56 return true; 57 } 58 if ((pos + 1U) > fmt.size()) 59 { 60 return false; 61 } 62 63 const char* end = nullptr; 64 std::chrono::milliseconds::rep ticks = 0; 65 if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>) 66 { 67 end = &fmt[std::min<size_t>(pos, 3U)]; 68 } 69 else 70 { 71 end = &fmt[pos]; 72 } 73 74 auto [ptr, ec] = std::from_chars(fmt.data(), end, ticks); 75 if (ptr != end || ec != std::errc()) 76 { 77 BMCWEB_LOG_ERROR << "Failed to convert string to decimal with err: " 78 << static_cast<int>(ec) << "(" 79 << std::make_error_code(ec).message() << "), ptr{" 80 << static_cast<const void*>(ptr) << "} != end{" 81 << static_cast<const void*>(end) << "})"; 82 return false; 83 } 84 85 if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>) 86 { 87 ticks *= static_cast<std::chrono::milliseconds::rep>( 88 std::pow(10, 3 - std::min<size_t>(pos, 3U))); 89 } 90 if (ticks < 0) 91 { 92 return false; 93 } 94 95 out += FromTime(ticks); 96 const auto maxConversionRange = 97 std::chrono::duration_cast<FromTime>(std::chrono::milliseconds::max()) 98 .count(); 99 if (out < FromTime(ticks) || maxConversionRange < ticks) 100 { 101 return false; 102 } 103 104 fmt.remove_prefix(pos + 1U); 105 return true; 106 } 107 } // namespace details 108 109 /** 110 * @brief Convert string that represents value in Duration Format to its numeric 111 * equivalent. 112 */ 113 inline std::optional<std::chrono::milliseconds> 114 fromDurationString(const std::string& str) 115 { 116 std::chrono::milliseconds out = std::chrono::milliseconds::zero(); 117 std::string_view v = str; 118 119 if (v.empty()) 120 { 121 return out; 122 } 123 if (v.front() != 'P') 124 { 125 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 126 return std::nullopt; 127 } 128 129 v.remove_prefix(1); 130 if (!details::fromDurationItem<details::Days>(v, 'D', out)) 131 { 132 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 133 return std::nullopt; 134 } 135 136 if (v.empty()) 137 { 138 return out; 139 } 140 if (v.front() != 'T') 141 { 142 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 143 return std::nullopt; 144 } 145 146 v.remove_prefix(1); 147 if (!details::fromDurationItem<std::chrono::hours>(v, 'H', out) || 148 !details::fromDurationItem<std::chrono::minutes>(v, 'M', out)) 149 { 150 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 151 return std::nullopt; 152 } 153 154 if (v.find('.') != std::string::npos && v.find('S') != std::string::npos) 155 { 156 if (!details::fromDurationItem<std::chrono::seconds>(v, '.', out) || 157 !details::fromDurationItem<std::chrono::milliseconds>(v, 'S', out)) 158 { 159 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 160 return std::nullopt; 161 } 162 } 163 else if (!details::fromDurationItem<std::chrono::seconds>(v, 'S', out)) 164 { 165 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 166 return std::nullopt; 167 } 168 169 if (!v.empty()) 170 { 171 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 172 return std::nullopt; 173 } 174 return out; 175 } 176 177 /** 178 * @brief Convert time value into duration format that is based on ISO 8601. 179 * Example output: "P12DT1M5.5S" 180 * Ref: Redfish Specification, Section 9.4.4. Duration values 181 */ 182 inline std::string toDurationString(std::chrono::milliseconds ms) 183 { 184 if (ms < std::chrono::milliseconds::zero()) 185 { 186 return ""; 187 } 188 189 std::string fmt; 190 fmt.reserve(sizeof("PxxxxxxxxxxxxDTxxHxxMxx.xxxxxxS")); 191 192 details::Days days = std::chrono::floor<details::Days>(ms); 193 ms -= days; 194 195 std::chrono::hours hours = std::chrono::floor<std::chrono::hours>(ms); 196 ms -= hours; 197 198 std::chrono::minutes minutes = std::chrono::floor<std::chrono::minutes>(ms); 199 ms -= minutes; 200 201 std::chrono::seconds seconds = std::chrono::floor<std::chrono::seconds>(ms); 202 ms -= seconds; 203 204 fmt = "P"; 205 if (days.count() > 0) 206 { 207 fmt += std::to_string(days.count()) + "D"; 208 } 209 fmt += "T"; 210 if (hours.count() > 0) 211 { 212 fmt += std::to_string(hours.count()) + "H"; 213 } 214 if (minutes.count() > 0) 215 { 216 fmt += std::to_string(minutes.count()) + "M"; 217 } 218 if (seconds.count() != 0 || ms.count() != 0) 219 { 220 fmt += std::to_string(seconds.count()) + "."; 221 fmt += details::padZeros(ms.count(), 3); 222 fmt += "S"; 223 } 224 225 return fmt; 226 } 227 228 inline std::optional<std::string> 229 toDurationStringFromUint(const uint64_t timeMs) 230 { 231 static const uint64_t maxTimeMs = 232 static_cast<uint64_t>(std::chrono::milliseconds::max().count()); 233 234 if (maxTimeMs < timeMs) 235 { 236 return std::nullopt; 237 } 238 239 std::string duration = toDurationString(std::chrono::milliseconds(timeMs)); 240 if (duration.empty()) 241 { 242 return std::nullopt; 243 } 244 245 return std::make_optional(duration); 246 } 247 248 namespace details 249 { 250 // Returns year/month/day triple in civil calendar 251 // Preconditions: z is number of days since 1970-01-01 and is in the range: 252 // [numeric_limits<Int>::min(), 253 // numeric_limits<Int>::max()-719468]. 254 // Algorithm sourced from 255 // https://howardhinnant.github.io/date_algorithms.html#civil_from_days 256 // All constants are explained in the above 257 template <class IntType> 258 constexpr std::tuple<IntType, unsigned, unsigned> 259 civilFromDays(IntType z) noexcept 260 { 261 z += 719468; 262 IntType era = (z >= 0 ? z : z - 146096) / 146097; 263 unsigned doe = static_cast<unsigned>(z - era * 146097); // [0, 146096] 264 unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 265 365; // [0, 399] 266 IntType y = static_cast<IntType>(yoe) + era * 400; 267 unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] 268 unsigned mp = (5 * doy + 2) / 153; // [0, 11] 269 unsigned d = doy - (153 * mp + 2) / 5 + 1; // [1, 31] 270 unsigned m = mp < 10 ? mp + 3 : mp - 9; // [1, 12] 271 272 return std::tuple<IntType, unsigned, unsigned>(y + (m <= 2), m, d); 273 } 274 275 template <typename IntType, typename Period> 276 std::string toISO8061ExtendedStr(std::chrono::duration<IntType, Period> t) 277 { 278 using seconds = std::chrono::duration<int>; 279 using minutes = std::chrono::duration<int, std::ratio<60>>; 280 using hours = std::chrono::duration<int, std::ratio<3600>>; 281 using days = std::chrono::duration< 282 IntType, std::ratio_multiply<hours::period, std::ratio<24>>>; 283 284 // d is days since 1970-01-01 285 days d = std::chrono::duration_cast<days>(t); 286 287 // t is now time duration since midnight of day d 288 t -= d; 289 290 // break d down into year/month/day 291 int year = 0; 292 int month = 0; 293 int day = 0; 294 std::tie(year, month, day) = details::civilFromDays(d.count()); 295 // Check against limits. Can't go above year 9999, and can't go below epoch 296 // (1970) 297 if (year >= 10000) 298 { 299 year = 9999; 300 month = 12; 301 day = 31; 302 t = days(1) - std::chrono::duration<IntType, Period>(1); 303 } 304 else if (year < 1970) 305 { 306 year = 1970; 307 month = 1; 308 day = 1; 309 t = std::chrono::duration<IntType, Period>::zero(); 310 } 311 312 std::string out; 313 out += details::padZeros(year, 4); 314 out += '-'; 315 out += details::padZeros(month, 2); 316 out += '-'; 317 out += details::padZeros(day, 2); 318 out += 'T'; 319 hours hr = duration_cast<hours>(t); 320 out += details::padZeros(hr.count(), 2); 321 t -= hr; 322 out += ':'; 323 324 minutes mt = duration_cast<minutes>(t); 325 out += details::padZeros(mt.count(), 2); 326 t -= mt; 327 out += ':'; 328 329 seconds se = duration_cast<seconds>(t); 330 out += details::padZeros(se.count(), 2); 331 t -= se; 332 333 if constexpr (std::is_same_v<typename decltype(t)::period, std::milli>) 334 { 335 out += '.'; 336 using MilliDuration = std::chrono::duration<int, std::milli>; 337 MilliDuration subsec = duration_cast<MilliDuration>(t); 338 out += details::padZeros(subsec.count(), 3); 339 } 340 else if constexpr (std::is_same_v<typename decltype(t)::period, std::micro>) 341 { 342 out += '.'; 343 344 using MicroDuration = std::chrono::duration<int, std::micro>; 345 MicroDuration subsec = duration_cast<MicroDuration>(t); 346 out += details::padZeros(subsec.count(), 6); 347 } 348 349 out += "+00:00"; 350 return out; 351 } 352 } // namespace details 353 354 // Returns the formatted date time string. 355 // Note that the maximum supported date is 9999-12-31T23:59:59+00:00, if 356 // the given |secondsSinceEpoch| is too large, we return the maximum supported 357 // date. 358 inline std::string getDateTimeUint(uint64_t secondsSinceEpoch) 359 { 360 using DurationType = std::chrono::duration<uint64_t>; 361 DurationType sinceEpoch(secondsSinceEpoch); 362 return details::toISO8061ExtendedStr(sinceEpoch); 363 } 364 365 // Returns the formatted date time string with millisecond precision 366 // Note that the maximum supported date is 9999-12-31T23:59:59+00:00, if 367 // the given |secondsSinceEpoch| is too large, we return the maximum supported 368 // date. 369 inline std::string getDateTimeUintMs(uint64_t milliSecondsSinceEpoch) 370 { 371 using DurationType = std::chrono::duration<uint64_t, std::milli>; 372 DurationType sinceEpoch(milliSecondsSinceEpoch); 373 return details::toISO8061ExtendedStr(sinceEpoch); 374 } 375 376 // Returns the formatted date time string with microsecond precision 377 inline std::string getDateTimeUintUs(uint64_t microSecondsSinceEpoch) 378 { 379 using DurationType = std::chrono::duration<uint64_t, std::micro>; 380 DurationType sinceEpoch(microSecondsSinceEpoch); 381 return details::toISO8061ExtendedStr(sinceEpoch); 382 } 383 384 inline std::string getDateTimeStdtime(std::time_t secondsSinceEpoch) 385 { 386 using DurationType = std::chrono::duration<std::time_t>; 387 DurationType sinceEpoch(secondsSinceEpoch); 388 return details::toISO8061ExtendedStr(sinceEpoch); 389 } 390 391 /** 392 * Returns the current Date, Time & the local Time Offset 393 * infromation in a pair 394 * 395 * @param[in] None 396 * 397 * @return std::pair<std::string, std::string>, which consist 398 * of current DateTime & the TimeOffset strings respectively. 399 */ 400 inline std::pair<std::string, std::string> getDateTimeOffsetNow() 401 { 402 std::time_t time = std::time(nullptr); 403 std::string dateTime = getDateTimeStdtime(time); 404 405 /* extract the local Time Offset value from the 406 * recevied dateTime string. 407 */ 408 std::string timeOffset("Z00:00"); 409 std::size_t lastPos = dateTime.size(); 410 std::size_t len = timeOffset.size(); 411 if (lastPos > len) 412 { 413 timeOffset = dateTime.substr(lastPos - len); 414 } 415 416 return std::make_pair(dateTime, timeOffset); 417 } 418 419 using usSinceEpoch = std::chrono::duration<uint64_t, std::micro>; 420 inline std::optional<usSinceEpoch> dateStringToEpoch(std::string_view datetime) 421 { 422 std::string date(datetime); 423 std::stringstream stream(date); 424 // Convert from ISO 8601 to boost local_time 425 // (BMC only has time in UTC) 426 boost::posix_time::ptime posixTime; 427 boost::posix_time::ptime epoch(boost::gregorian::date(1970, 1, 1)); 428 // Facet gets deleted with the stringsteam 429 auto ifc = std::make_unique<boost::local_time::local_time_input_facet>( 430 "%Y-%m-%d %H:%M:%S%F %ZP"); 431 stream.imbue(std::locale(stream.getloc(), ifc.release())); 432 433 boost::local_time::local_date_time ldt(boost::local_time::not_a_date_time); 434 435 if (!(stream >> ldt)) 436 { 437 return std::nullopt; 438 } 439 posixTime = ldt.utc_time(); 440 boost::posix_time::time_duration dur = posixTime - epoch; 441 uint64_t durMicroSecs = static_cast<uint64_t>(dur.total_microseconds()); 442 return std::chrono::duration<uint64_t, std::micro>{durMicroSecs}; 443 } 444 445 } // namespace time_utils 446 } // namespace redfish 447