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