xref: /openbmc/bmcweb/redfish-core/src/utils/time_utils.cpp (revision a93e9c77c09e4429b5c447dbc1480278bb2a28e3)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #include "utils/time_utils.hpp"
4 
5 #include "error_messages.hpp"
6 #include "http_response.hpp"
7 #include "logging.hpp"
8 
9 #include <version>
10 
11 #if __cpp_lib_chrono < 201907L
12 #include "utils/extern/date.h"
13 #endif
14 #include <array>
15 #include <charconv>
16 #include <chrono>
17 #include <cstddef>
18 #include <cstdint>
19 #include <ctime>
20 #include <format>
21 #include <optional>
22 #include <ratio>
23 #include <sstream>
24 #include <string>
25 #include <string_view>
26 #include <system_error>
27 #include <utility>
28 
29 namespace redfish::time_utils
30 {
31 
32 /**
33  * @brief Convert string that represents value in Duration Format to its numeric
34  *        equivalent.
35  */
fromDurationString(std::string_view v)36 std::optional<std::chrono::milliseconds> fromDurationString(std::string_view v)
37 {
38     std::chrono::milliseconds out = std::chrono::milliseconds::zero();
39     enum class ProcessingStage
40     {
41         // P1DT1H1M1.100S
42         P,
43         Days,
44         Hours,
45         Minutes,
46         Seconds,
47         Milliseconds,
48         Done,
49     };
50     ProcessingStage stage = ProcessingStage::P;
51 
52     while (!v.empty())
53     {
54         if (stage == ProcessingStage::P)
55         {
56             if (v.front() != 'P')
57             {
58                 return std::nullopt;
59             }
60             v.remove_prefix(1);
61             stage = ProcessingStage::Days;
62             continue;
63         }
64         if (stage == ProcessingStage::Days)
65         {
66             if (v.front() == 'T')
67             {
68                 v.remove_prefix(1);
69                 stage = ProcessingStage::Hours;
70                 continue;
71             }
72         }
73         uint64_t ticks = 0;
74         auto [ptr, ec] = std::from_chars(v.begin(), v.end(), ticks);
75         if (ec != std::errc())
76         {
77             BMCWEB_LOG_ERROR("Failed to convert string \"{}\" to decimal", v);
78             return std::nullopt;
79         }
80         size_t charactersRead = static_cast<size_t>(ptr - v.data());
81         if (ptr >= v.end())
82         {
83             BMCWEB_LOG_ERROR("Missing postfix");
84             return std::nullopt;
85         }
86         if (*ptr == 'D')
87         {
88             if (stage > ProcessingStage::Days)
89             {
90                 return std::nullopt;
91             }
92             out += std::chrono::days(ticks);
93         }
94         else if (*ptr == 'H')
95         {
96             if (stage > ProcessingStage::Hours)
97             {
98                 return std::nullopt;
99             }
100             out += std::chrono::hours(ticks);
101         }
102         else if (*ptr == 'M')
103         {
104             if (stage > ProcessingStage::Minutes)
105             {
106                 return std::nullopt;
107             }
108             out += std::chrono::minutes(ticks);
109         }
110         else if (*ptr == '.')
111         {
112             if (stage > ProcessingStage::Seconds)
113             {
114                 return std::nullopt;
115             }
116             out += std::chrono::seconds(ticks);
117             stage = ProcessingStage::Milliseconds;
118         }
119         else if (*ptr == 'S')
120         {
121             // We could be seeing seconds for the first time, (as is the case in
122             // 1S) or for the second time (in the case of 1.1S).
123             if (stage <= ProcessingStage::Seconds)
124             {
125                 out += std::chrono::seconds(ticks);
126                 stage = ProcessingStage::Milliseconds;
127             }
128             else if (stage > ProcessingStage::Milliseconds)
129             {
130                 BMCWEB_LOG_ERROR("Got unexpected information at end of parse");
131                 return std::nullopt;
132             }
133             else
134             {
135                 // Seconds could be any form of (1S, 1.1S, 1.11S, 1.111S);
136                 // Handle them all milliseconds are after the decimal point,
137                 // so they need right padded.
138                 if (charactersRead == 1)
139                 {
140                     ticks *= 100;
141                 }
142                 else if (charactersRead == 2)
143                 {
144                     ticks *= 10;
145                 }
146                 out += std::chrono::milliseconds(ticks);
147                 stage = ProcessingStage::Milliseconds;
148             }
149         }
150         else
151         {
152             BMCWEB_LOG_ERROR("Unknown postfix {}", *ptr);
153             return std::nullopt;
154         }
155 
156         v.remove_prefix(charactersRead + 1U);
157     }
158     return out;
159 }
160 
161 /**
162  * @brief Convert time value into duration format that is based on ISO 8601.
163  *        Example output: "P12DT1M5.5S"
164  *        Ref: Redfish Specification, Section 9.4.4. Duration values
165  */
toDurationString(std::chrono::milliseconds ms)166 std::string toDurationString(std::chrono::milliseconds ms)
167 {
168     if (ms < std::chrono::milliseconds::zero())
169     {
170         return "";
171     }
172 
173     std::chrono::days days = std::chrono::floor<std::chrono::days>(ms);
174     ms -= days;
175 
176     std::chrono::hours hours = std::chrono::floor<std::chrono::hours>(ms);
177     ms -= hours;
178 
179     std::chrono::minutes minutes = std::chrono::floor<std::chrono::minutes>(ms);
180     ms -= minutes;
181 
182     std::chrono::seconds seconds = std::chrono::floor<std::chrono::seconds>(ms);
183     ms -= seconds;
184     std::string daysStr;
185     if (days.count() > 0)
186     {
187         daysStr = std::format("{}D", days.count());
188     }
189     std::string hoursStr;
190     if (hours.count() > 0)
191     {
192         hoursStr = std::format("{}H", hours.count());
193     }
194     std::string minStr;
195     if (minutes.count() > 0)
196     {
197         minStr = std::format("{}M", minutes.count());
198     }
199     std::string secStr;
200     if (seconds.count() != 0 || ms.count() != 0)
201     {
202         secStr = std::format("{}.{:03}S", seconds.count(), ms.count());
203     }
204 
205     return std::format("P{}T{}{}{}", daysStr, hoursStr, minStr, secStr);
206 }
207 
toDurationStringFromUint(uint64_t timeMs)208 std::optional<std::string> toDurationStringFromUint(uint64_t timeMs)
209 {
210     constexpr uint64_t maxTimeMs =
211         static_cast<uint64_t>(std::chrono::milliseconds::max().count());
212 
213     if (maxTimeMs < timeMs)
214     {
215         return std::nullopt;
216     }
217 
218     std::string duration = toDurationString(std::chrono::milliseconds(timeMs));
219     if (duration.empty())
220     {
221         return std::nullopt;
222     }
223 
224     return std::make_optional(duration);
225 }
226 
227 namespace details
228 {
229 // This code is left for support of gcc < 13 which didn't have support for
230 // timezones. It should be removed at some point in the future.
231 #if __cpp_lib_chrono < 201907L
232 
233 // Returns year/month/day triple in civil calendar
234 // Preconditions:  z is number of days since 1970-01-01 and is in the range:
235 //                   [numeric_limits<Int>::min(),
236 //                   numeric_limits<Int>::max()-719468].
237 // Algorithm sourced from
238 // https://howardhinnant.github.io/date_algorithms.html#civil_from_days
239 // All constants are explained in the above
240 template <class IntType>
241 constexpr std::tuple<IntType, unsigned, unsigned>
civilFromDays(IntType z)242     civilFromDays(IntType z) noexcept
243 {
244     z += 719468;
245     IntType era = (z >= 0 ? z : z - 146096) / 146097;
246     unsigned doe = static_cast<unsigned>(z - era * 146097); // [0, 146096]
247     unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) /
248                    365; // [0, 399]
249     IntType y = static_cast<IntType>(yoe) + era * 400;
250     unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
251     unsigned mp = (5 * doy + 2) / 153; // [0, 11]
252     unsigned d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
253     unsigned m = mp < 10 ? mp + 3 : mp - 9; // [1, 12]
254 
255     return std::tuple<IntType, unsigned, unsigned>(y + (m <= 2), m, d);
256 }
257 
258 template <typename IntType, typename Period>
toISO8061ExtendedStr(std::chrono::duration<IntType,Period> t)259 std::string toISO8061ExtendedStr(std::chrono::duration<IntType, Period> t)
260 {
261     using seconds = std::chrono::duration<int>;
262     using minutes = std::chrono::duration<int, std::ratio<60>>;
263     using hours = std::chrono::duration<int, std::ratio<3600>>;
264     using days = std::chrono::duration<
265         IntType, std::ratio_multiply<hours::period, std::ratio<24>>>;
266 
267     // d is days since 1970-01-01
268     days d = std::chrono::duration_cast<days>(t);
269 
270     // t is now time duration since midnight of day d
271     t -= d;
272 
273     // break d down into year/month/day
274     int year = 0;
275     int month = 0;
276     int day = 0;
277     std::tie(year, month, day) = details::civilFromDays(d.count());
278     // Check against limits.  Can't go above year 9999, and can't go below epoch
279     // (1970)
280     if (year >= 10000)
281     {
282         year = 9999;
283         month = 12;
284         day = 31;
285         t = days(1) - std::chrono::duration<IntType, Period>(1);
286     }
287     else if (year < 1970)
288     {
289         year = 1970;
290         month = 1;
291         day = 1;
292         t = std::chrono::duration<IntType, Period>::zero();
293     }
294 
295     hours hr = std::chrono::duration_cast<hours>(t);
296     t -= hr;
297 
298     minutes mt = std::chrono::duration_cast<minutes>(t);
299     t -= mt;
300 
301     seconds se = std::chrono::duration_cast<seconds>(t);
302 
303     t -= se;
304 
305     std::string subseconds;
306     if constexpr (std::is_same_v<typename decltype(t)::period, std::milli>)
307     {
308         using MilliDuration = std::chrono::duration<int, std::milli>;
309         MilliDuration subsec = std::chrono::duration_cast<MilliDuration>(t);
310         subseconds = std::format(".{:03}", subsec.count());
311     }
312     else if constexpr (std::is_same_v<typename decltype(t)::period, std::micro>)
313     {
314         using MicroDuration = std::chrono::duration<int, std::micro>;
315         MicroDuration subsec = std::chrono::duration_cast<MicroDuration>(t);
316         subseconds = std::format(".{:06}", subsec.count());
317     }
318 
319     return std::format("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}+00:00", year,
320                        month, day, hr.count(), mt.count(), se.count(),
321                        subseconds);
322 }
323 
324 #else
325 
326 template <typename IntType, typename Period>
327 
328 std::string toISO8061ExtendedStr(std::chrono::duration<IntType, Period> dur)
329 {
330     using namespace std::literals::chrono_literals;
331 
332     using SubType = std::chrono::duration<IntType, Period>;
333 
334     // d is days since 1970-01-01
335     std::chrono::days days = std::chrono::floor<std::chrono::days>(dur);
336     std::chrono::sys_days sysDays(days);
337     std::chrono::year_month_day ymd(sysDays);
338 
339     // Enforce 3 constraints
340     // the result cant under or overflow the calculation
341     // the resulting string needs to be representable as 4 digits
342     // The resulting string can't be before epoch
343     if (dur.count() <= 0)
344     {
345         BMCWEB_LOG_WARNING("Underflow from value {}", dur.count());
346         ymd = 1970y / std::chrono::January / 1d;
347         dur = std::chrono::duration<IntType, Period>::zero();
348     }
349     else if (dur > SubType::max() - std::chrono::days(1))
350     {
351         BMCWEB_LOG_WARNING("Overflow from value {}", dur.count());
352         ymd = 9999y / std::chrono::December / 31d;
353         dur = std::chrono::days(1) - SubType(1);
354     }
355     else if (ymd.year() >= 10000y)
356     {
357         BMCWEB_LOG_WARNING("Year {} not representable", ymd.year());
358         ymd = 9999y / std::chrono::December / 31d;
359         dur = std::chrono::days(1) - SubType(1);
360     }
361     else if (ymd.year() < 1970y)
362     {
363         BMCWEB_LOG_WARNING("Year {} not representable", ymd.year());
364         ymd = 1970y / std::chrono::January / 1d;
365         dur = SubType::zero();
366     }
367     else
368     {
369         // t is now time duration since midnight of day d
370         dur -= days;
371     }
372     std::chrono::hh_mm_ss<SubType> hms(dur);
373 
374     return std::format("{}T{}+00:00", ymd, hms);
375 }
376 
377 #endif
378 } // namespace details
379 
380 // Returns the formatted date time string.
381 // Note that the maximum supported date is 9999-12-31T23:59:59+00:00, if
382 // the given |secondsSinceEpoch| is too large, we return the maximum supported
383 // date.
getDateTimeUint(uint64_t secondsSinceEpoch)384 std::string getDateTimeUint(uint64_t secondsSinceEpoch)
385 {
386     using DurationType = std::chrono::duration<uint64_t>;
387     DurationType sinceEpoch(secondsSinceEpoch);
388     return details::toISO8061ExtendedStr(sinceEpoch);
389 }
390 
391 // Returns the formatted date time string with millisecond precision
392 // Note that the maximum supported date is 9999-12-31T23:59:59+00:00, if
393 // the given |secondsSinceEpoch| is too large, we return the maximum supported
394 // date.
getDateTimeUintMs(uint64_t milliSecondsSinceEpoch)395 std::string getDateTimeUintMs(uint64_t milliSecondsSinceEpoch)
396 {
397     using DurationType = std::chrono::duration<uint64_t, std::milli>;
398     DurationType sinceEpoch(milliSecondsSinceEpoch);
399     return details::toISO8061ExtendedStr(sinceEpoch);
400 }
401 
402 // Returns the formatted date time string with microsecond precision
getDateTimeUintUs(uint64_t microSecondsSinceEpoch)403 std::string getDateTimeUintUs(uint64_t microSecondsSinceEpoch)
404 {
405     using DurationType = std::chrono::duration<uint64_t, std::micro>;
406     DurationType sinceEpoch(microSecondsSinceEpoch);
407     return details::toISO8061ExtendedStr(sinceEpoch);
408 }
409 
getDateTimeStdtime(std::time_t secondsSinceEpoch)410 std::string getDateTimeStdtime(std::time_t secondsSinceEpoch)
411 {
412     using DurationType = std::chrono::duration<std::time_t>;
413     DurationType sinceEpoch(secondsSinceEpoch);
414     return details::toISO8061ExtendedStr(sinceEpoch);
415 }
416 
417 /**
418  * Returns the current Date, Time & the local Time Offset
419  * information in a pair
420  *
421  * @param[in] None
422  *
423  * @return std::pair<std::string, std::string>, which consist
424  * of current DateTime & the TimeOffset strings respectively.
425  */
getDateTimeOffsetNow()426 std::pair<std::string, std::string> getDateTimeOffsetNow()
427 {
428     std::time_t time = std::time(nullptr);
429     std::string dateTime = getDateTimeStdtime(time);
430 
431     /* extract the local Time Offset value from the
432      * received dateTime string.
433      */
434     std::string timeOffset("Z00:00");
435     std::size_t lastPos = dateTime.size();
436     std::size_t len = timeOffset.size();
437     if (lastPos > len)
438     {
439         timeOffset = dateTime.substr(lastPos - len);
440     }
441 
442     return std::make_pair(dateTime, timeOffset);
443 }
444 
445 using usSinceEpoch = std::chrono::duration<int64_t, std::micro>;
446 
447 /**
448  * @brief Returns the datetime in ISO 8601 format
449  *
450  * @param[in] std::string_view the date of item manufacture in ISO 8601 format,
451  *            either as YYYYMMDD or YYYYMMDDThhmmssZ
452  * Ref: https://github.com/openbmc/phosphor-dbus-interfaces/blob/master/yaml/
453  *      xyz/openbmc_project/Inventory/Decorator/Asset.interface.yaml#L16
454  *
455  * @return std::string which consist the datetime
456  */
getDateTimeIso8601(std::string_view datetime)457 std::optional<std::string> getDateTimeIso8601(std::string_view datetime)
458 {
459     std::optional<usSinceEpoch> us = dateStringToEpoch(datetime);
460     if (!us)
461     {
462         return std::nullopt;
463     }
464     auto secondsDuration =
465         std::chrono::duration_cast<std::chrono::seconds>(*us);
466 
467     return std::make_optional(
468         getDateTimeUint(static_cast<uint64_t>(secondsDuration.count())));
469 }
470 
471 /**
472  * @brief ProductionDate report
473  */
productionDateReport(crow::Response & res,const std::string & buildDate)474 void productionDateReport(crow::Response& res, const std::string& buildDate)
475 {
476     std::optional<std::string> valueStr = getDateTimeIso8601(buildDate);
477     if (!valueStr)
478     {
479         messages::internalError();
480         return;
481     }
482     res.jsonValue["ProductionDate"] = *valueStr;
483 }
484 
dateStringToEpoch(std::string_view datetime)485 std::optional<usSinceEpoch> dateStringToEpoch(std::string_view datetime)
486 {
487     for (const char* format : std::to_array(
488              {"%FT%T%Ez", "%FT%TZ", "%FT%T", "%Y%m%d", "%Y%m%dT%H%M%SZ"}))
489     {
490         // Parse using signed so we can detect negative dates
491         std::chrono::sys_time<usSinceEpoch> date;
492         std::istringstream iss(std::string{datetime});
493 #if __cpp_lib_chrono >= 201907L
494         namespace chrono_from_stream = std::chrono;
495 #else
496         namespace chrono_from_stream = date;
497 #endif
498         if (chrono_from_stream::from_stream(iss, format, date))
499         {
500             if (date.time_since_epoch().count() < 0)
501             {
502                 return std::nullopt;
503             }
504             if (iss.rdbuf()->in_avail() != 0)
505             {
506                 // More information left at end of string.
507                 continue;
508             }
509             return date.time_since_epoch();
510         }
511     }
512     return std::nullopt;
513 }
514 } // namespace redfish::time_utils
515