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