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