1 #pragma once
2 
3 #include "logging.hpp"
4 
5 #include <charconv>
6 #include <chrono>
7 #include <cmath>
8 #include <optional>
9 #include <string>
10 #include <system_error>
11 
12 namespace redfish
13 {
14 
15 namespace time_utils
16 {
17 
18 namespace details
19 {
20 
21 using Days = std::chrono::duration<long long, std::ratio<24 * 60 * 60>>;
22 
23 inline void leftZeroPadding(std::string& str, const std::size_t padding)
24 {
25     if (str.size() < padding)
26     {
27         str.insert(0, padding - str.size(), '0');
28     }
29 }
30 
31 template <typename FromTime>
32 bool fromDurationItem(std::string_view& fmt, const char postfix,
33                       std::chrono::milliseconds& out)
34 {
35     const size_t pos = fmt.find(postfix);
36     if (pos == std::string::npos)
37     {
38         return true;
39     }
40     if ((pos + 1U) > fmt.size())
41     {
42         return false;
43     }
44 
45     const char* end;
46     std::chrono::milliseconds::rep ticks = 0;
47     if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>)
48     {
49         end = fmt.data() + std::min<size_t>(pos, 3U);
50     }
51     else
52     {
53         end = fmt.data() + pos;
54     }
55 
56     auto [ptr, ec] = std::from_chars(fmt.data(), end, ticks);
57     if (ptr != end || ec != std::errc())
58     {
59         BMCWEB_LOG_ERROR << "Failed to convert string to decimal with err: "
60                          << static_cast<int>(ec) << "("
61                          << std::make_error_code(ec).message() << "), ptr{"
62                          << static_cast<const void*>(ptr) << "} != end{"
63                          << static_cast<const void*>(end) << "})";
64         return false;
65     }
66 
67     if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>)
68     {
69         ticks *= static_cast<std::chrono::milliseconds::rep>(
70             std::pow(10, 3 - std::min<size_t>(pos, 3U)));
71     }
72     if (ticks < 0)
73     {
74         return false;
75     }
76 
77     out += FromTime(ticks);
78     const auto maxConversionRange =
79         std::chrono::duration_cast<FromTime>(std::chrono::milliseconds::max())
80             .count();
81     if (out < FromTime(ticks) || maxConversionRange < ticks)
82     {
83         return false;
84     }
85 
86     fmt.remove_prefix(pos + 1U);
87     return true;
88 }
89 } // namespace details
90 
91 /**
92  * @brief Convert string that represents value in Duration Format to its numeric
93  *        equivalent.
94  */
95 inline std::optional<std::chrono::milliseconds>
96     fromDurationString(const std::string& str)
97 {
98     std::chrono::milliseconds out = std::chrono::milliseconds::zero();
99     std::string_view v = str;
100 
101     if (v.empty())
102     {
103         return out;
104     }
105     if (v.front() != 'P')
106     {
107         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
108         return std::nullopt;
109     }
110 
111     v.remove_prefix(1);
112     if (!details::fromDurationItem<details::Days>(v, 'D', out))
113     {
114         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
115         return std::nullopt;
116     }
117 
118     if (v.empty())
119     {
120         return out;
121     }
122     if (v.front() != 'T')
123     {
124         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
125         return std::nullopt;
126     }
127 
128     v.remove_prefix(1);
129     if (!details::fromDurationItem<std::chrono::hours>(v, 'H', out) ||
130         !details::fromDurationItem<std::chrono::minutes>(v, 'M', out))
131     {
132         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
133         return std::nullopt;
134     }
135 
136     if (v.find('.') != std::string::npos && v.find('S') != std::string::npos)
137     {
138         if (!details::fromDurationItem<std::chrono::seconds>(v, '.', out) ||
139             !details::fromDurationItem<std::chrono::milliseconds>(v, 'S', out))
140         {
141             BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
142             return std::nullopt;
143         }
144     }
145     else if (!details::fromDurationItem<std::chrono::seconds>(v, 'S', out))
146     {
147         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
148         return std::nullopt;
149     }
150 
151     if (!v.empty())
152     {
153         BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
154         return std::nullopt;
155     }
156     return out;
157 }
158 
159 /**
160  * @brief Convert time value into duration format that is based on ISO 8601.
161  *        Example output: "P12DT1M5.5S"
162  *        Ref: Redfish Specification, Section 9.4.4. Duration values
163  */
164 inline std::string toDurationString(std::chrono::milliseconds ms)
165 {
166     if (ms < std::chrono::milliseconds::zero())
167     {
168         return "";
169     }
170 
171     std::string fmt;
172     fmt.reserve(sizeof("PxxxxxxxxxxxxDTxxHxxMxx.xxxxxxS"));
173 
174     details::Days days = std::chrono::floor<details::Days>(ms);
175     ms -= days;
176 
177     std::chrono::hours hours = std::chrono::floor<std::chrono::hours>(ms);
178     ms -= hours;
179 
180     std::chrono::minutes minutes = std::chrono::floor<std::chrono::minutes>(ms);
181     ms -= minutes;
182 
183     std::chrono::seconds seconds = std::chrono::floor<std::chrono::seconds>(ms);
184     ms -= seconds;
185 
186     fmt = "P";
187     if (days.count() > 0)
188     {
189         fmt += std::to_string(days.count()) + "D";
190     }
191     fmt += "T";
192     if (hours.count() > 0)
193     {
194         fmt += std::to_string(hours.count()) + "H";
195     }
196     if (minutes.count() > 0)
197     {
198         fmt += std::to_string(minutes.count()) + "M";
199     }
200     if (seconds.count() != 0 || ms.count() != 0)
201     {
202         fmt += std::to_string(seconds.count()) + ".";
203         std::string msStr = std::to_string(ms.count());
204         details::leftZeroPadding(msStr, 3);
205         fmt += msStr + "S";
206     }
207 
208     return fmt;
209 }
210 
211 } // namespace time_utils
212 } // namespace redfish
213