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