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