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