xref: /openbmc/bmcweb/redfish-core/src/utils/time_utils.cpp (revision 504af5a0568171b72caf13234cc81380b261fa21)
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>
civilFromDays(IntType z)241  constexpr std::tuple<IntType, unsigned, unsigned> civilFromDays(
242      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