1 #pragma once 2 3 #include "logging.hpp" 4 5 #include <algorithm> 6 #include <charconv> 7 #include <chrono> 8 #include <cmath> 9 #include <compare> 10 #include <cstddef> 11 #include <optional> 12 #include <ratio> 13 #include <string> 14 #include <string_view> 15 #include <system_error> 16 17 namespace redfish 18 { 19 20 namespace time_utils 21 { 22 23 namespace details 24 { 25 26 constexpr intmax_t dayDuration = static_cast<intmax_t>(24 * 60 * 60); 27 using Days = std::chrono::duration<long long, std::ratio<dayDuration>>; 28 29 inline void leftZeroPadding(std::string& str, const std::size_t padding) 30 { 31 if (str.size() < padding) 32 { 33 str.insert(0, padding - str.size(), '0'); 34 } 35 } 36 37 template <typename FromTime> 38 bool fromDurationItem(std::string_view& fmt, const char postfix, 39 std::chrono::milliseconds& out) 40 { 41 const size_t pos = fmt.find(postfix); 42 if (pos == std::string::npos) 43 { 44 return true; 45 } 46 if ((pos + 1U) > fmt.size()) 47 { 48 return false; 49 } 50 51 const char* end = nullptr; 52 std::chrono::milliseconds::rep ticks = 0; 53 if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>) 54 { 55 end = fmt.data() + std::min<size_t>(pos, 3U); 56 } 57 else 58 { 59 end = fmt.data() + pos; 60 } 61 62 auto [ptr, ec] = std::from_chars(fmt.data(), end, ticks); 63 if (ptr != end || ec != std::errc()) 64 { 65 BMCWEB_LOG_ERROR << "Failed to convert string to decimal with err: " 66 << static_cast<int>(ec) << "(" 67 << std::make_error_code(ec).message() << "), ptr{" 68 << static_cast<const void*>(ptr) << "} != end{" 69 << static_cast<const void*>(end) << "})"; 70 return false; 71 } 72 73 if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>) 74 { 75 ticks *= static_cast<std::chrono::milliseconds::rep>( 76 std::pow(10, 3 - std::min<size_t>(pos, 3U))); 77 } 78 if (ticks < 0) 79 { 80 return false; 81 } 82 83 out += FromTime(ticks); 84 const auto maxConversionRange = 85 std::chrono::duration_cast<FromTime>(std::chrono::milliseconds::max()) 86 .count(); 87 if (out < FromTime(ticks) || maxConversionRange < ticks) 88 { 89 return false; 90 } 91 92 fmt.remove_prefix(pos + 1U); 93 return true; 94 } 95 } // namespace details 96 97 /** 98 * @brief Convert string that represents value in Duration Format to its numeric 99 * equivalent. 100 */ 101 inline std::optional<std::chrono::milliseconds> 102 fromDurationString(const std::string& str) 103 { 104 std::chrono::milliseconds out = std::chrono::milliseconds::zero(); 105 std::string_view v = str; 106 107 if (v.empty()) 108 { 109 return out; 110 } 111 if (v.front() != 'P') 112 { 113 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 114 return std::nullopt; 115 } 116 117 v.remove_prefix(1); 118 if (!details::fromDurationItem<details::Days>(v, 'D', out)) 119 { 120 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 121 return std::nullopt; 122 } 123 124 if (v.empty()) 125 { 126 return out; 127 } 128 if (v.front() != 'T') 129 { 130 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 131 return std::nullopt; 132 } 133 134 v.remove_prefix(1); 135 if (!details::fromDurationItem<std::chrono::hours>(v, 'H', out) || 136 !details::fromDurationItem<std::chrono::minutes>(v, 'M', out)) 137 { 138 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 139 return std::nullopt; 140 } 141 142 if (v.find('.') != std::string::npos && v.find('S') != std::string::npos) 143 { 144 if (!details::fromDurationItem<std::chrono::seconds>(v, '.', out) || 145 !details::fromDurationItem<std::chrono::milliseconds>(v, 'S', out)) 146 { 147 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 148 return std::nullopt; 149 } 150 } 151 else if (!details::fromDurationItem<std::chrono::seconds>(v, 'S', out)) 152 { 153 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 154 return std::nullopt; 155 } 156 157 if (!v.empty()) 158 { 159 BMCWEB_LOG_ERROR << "Invalid duration format: " << str; 160 return std::nullopt; 161 } 162 return out; 163 } 164 165 /** 166 * @brief Convert time value into duration format that is based on ISO 8601. 167 * Example output: "P12DT1M5.5S" 168 * Ref: Redfish Specification, Section 9.4.4. Duration values 169 */ 170 inline std::string toDurationString(std::chrono::milliseconds ms) 171 { 172 if (ms < std::chrono::milliseconds::zero()) 173 { 174 return ""; 175 } 176 177 std::string fmt; 178 fmt.reserve(sizeof("PxxxxxxxxxxxxDTxxHxxMxx.xxxxxxS")); 179 180 details::Days days = std::chrono::floor<details::Days>(ms); 181 ms -= days; 182 183 std::chrono::hours hours = std::chrono::floor<std::chrono::hours>(ms); 184 ms -= hours; 185 186 std::chrono::minutes minutes = std::chrono::floor<std::chrono::minutes>(ms); 187 ms -= minutes; 188 189 std::chrono::seconds seconds = std::chrono::floor<std::chrono::seconds>(ms); 190 ms -= seconds; 191 192 fmt = "P"; 193 if (days.count() > 0) 194 { 195 fmt += std::to_string(days.count()) + "D"; 196 } 197 fmt += "T"; 198 if (hours.count() > 0) 199 { 200 fmt += std::to_string(hours.count()) + "H"; 201 } 202 if (minutes.count() > 0) 203 { 204 fmt += std::to_string(minutes.count()) + "M"; 205 } 206 if (seconds.count() != 0 || ms.count() != 0) 207 { 208 fmt += std::to_string(seconds.count()) + "."; 209 std::string msStr = std::to_string(ms.count()); 210 details::leftZeroPadding(msStr, 3); 211 fmt += msStr + "S"; 212 } 213 214 return fmt; 215 } 216 217 inline std::optional<std::string> 218 toDurationStringFromUint(const uint64_t timeMs) 219 { 220 static const uint64_t maxTimeMs = 221 static_cast<uint64_t>(std::chrono::milliseconds::max().count()); 222 223 if (maxTimeMs < timeMs) 224 { 225 return std::nullopt; 226 } 227 228 std::string duration = toDurationString(std::chrono::milliseconds(timeMs)); 229 if (duration.empty()) 230 { 231 return std::nullopt; 232 } 233 234 return std::make_optional(duration); 235 } 236 237 } // namespace time_utils 238 } // namespace redfish 239