]> git.proxmox.com Git - proxmox.git/blob - proxmox-time/src/posix.rs
5913816e0a9a57a0813874d321461814ace14dfe
[proxmox.git] / proxmox-time / src / posix.rs
1 #![allow(clippy::manual_range_contains)]
2
3 use std::ffi::{CStr, CString};
4
5 use anyhow::{bail, format_err, Error};
6
7 /// Safe bindings to libc timelocal
8 ///
9 /// We set tm_isdst to -1.
10 /// This also normalizes the parameter
11 pub fn timelocal(t: &mut libc::tm) -> Result<i64, Error> {
12 t.tm_isdst = -1;
13
14 let epoch = unsafe { libc::mktime(t) };
15 if epoch == -1 {
16 bail!("libc::mktime failed for {t:?}");
17 }
18 Ok(epoch)
19 }
20
21 /// Safe bindings to libc timegm
22 ///
23 /// We set tm_isdst to 0.
24 /// This also normalizes the parameter
25 pub fn timegm(t: &mut libc::tm) -> Result<i64, Error> {
26 t.tm_isdst = 0;
27
28 let epoch = unsafe { libc::timegm(t) };
29 if epoch == -1 {
30 bail!("libc::timegm failed for {t:?}");
31 }
32 Ok(epoch)
33 }
34
35 fn new_libc_tm() -> libc::tm {
36 libc::tm {
37 tm_sec: 0,
38 tm_min: 0,
39 tm_hour: 0,
40 tm_mday: 0,
41 tm_mon: 0,
42 tm_year: 0,
43 tm_wday: 0,
44 tm_yday: 0,
45 tm_isdst: 0,
46 tm_gmtoff: 0,
47 tm_zone: std::ptr::null(),
48 }
49 }
50
51 /// Safe bindings to libc localtime
52 pub fn localtime(epoch: i64) -> Result<libc::tm, Error> {
53 let mut result = new_libc_tm();
54
55 unsafe {
56 if libc::localtime_r(&epoch, &mut result).is_null() {
57 bail!("libc::localtime failed for '{epoch}'");
58 }
59 }
60
61 Ok(result)
62 }
63
64 /// Safe bindings to libc gmtime
65 pub fn gmtime(epoch: i64) -> Result<libc::tm, Error> {
66 let mut result = new_libc_tm();
67
68 unsafe {
69 if libc::gmtime_r(&epoch, &mut result).is_null() {
70 bail!("libc::gmtime failed for '{epoch}'");
71 }
72 }
73
74 Ok(result)
75 }
76
77 /// Returns Unix Epoch (now)
78 ///
79 /// Note: This panics if the SystemTime::now() returns values not
80 /// repesentable as i64 (should never happen).
81 pub fn epoch_i64() -> i64 {
82 use std::time::{SystemTime, UNIX_EPOCH};
83
84 let now = SystemTime::now();
85
86 if now > UNIX_EPOCH {
87 i64::try_from(now.duration_since(UNIX_EPOCH).unwrap().as_secs())
88 .expect("epoch_i64: now is too large")
89 } else {
90 -i64::try_from(UNIX_EPOCH.duration_since(now).unwrap().as_secs())
91 .expect("epoch_i64: now is too small")
92 }
93 }
94
95 /// Returns Unix Epoch (now) as f64 with subseconds resolution
96 ///
97 /// Note: This can be inacurrate for values greater the 2^53. But this
98 /// should never happen.
99 pub fn epoch_f64() -> f64 {
100 use std::time::{SystemTime, UNIX_EPOCH};
101
102 let now = SystemTime::now();
103
104 if now > UNIX_EPOCH {
105 now.duration_since(UNIX_EPOCH).unwrap().as_secs_f64()
106 } else {
107 -UNIX_EPOCH.duration_since(now).unwrap().as_secs_f64()
108 }
109 }
110
111 /// Safe bindings to libc strftime
112 pub fn strftime(format: &str, t: &libc::tm) -> Result<String, Error> {
113 let format = CString::new(format).map_err(|err| format_err!("{err}"))?;
114 let mut buf = vec![0u8; 8192];
115
116 let res = unsafe {
117 libc::strftime(
118 buf.as_mut_ptr() as *mut libc::c_char,
119 buf.len() as libc::size_t,
120 format.as_ptr(),
121 t as *const libc::tm,
122 )
123 };
124 if res == !0 {
125 // -1,, it's unsigned
126 bail!("strftime failed");
127 }
128
129 // `res` is a `libc::size_t`, which on a different target architecture might not be directly
130 // assignable to a `usize`. Thus, we actually want a cast here.
131 #[allow(clippy::unnecessary_cast)]
132 let len = res as usize;
133
134 if len == 0 {
135 bail!("strftime: result len is 0 (string too large)");
136 };
137
138 let c_str = CStr::from_bytes_with_nul(&buf[..len + 1]).map_err(|err| format_err!("{err}"))?;
139 let str_slice: &str = c_str.to_str().unwrap();
140 Ok(str_slice.to_owned())
141 }
142
143 /// Format epoch as local time
144 pub fn strftime_local(format: &str, epoch: i64) -> Result<String, Error> {
145 let localtime = localtime(epoch)?;
146 strftime(format, &localtime)
147 }
148
149 /// Format epoch as utc time
150 pub fn strftime_utc(format: &str, epoch: i64) -> Result<String, Error> {
151 let gmtime = gmtime(epoch)?;
152 strftime(format, &gmtime)
153 }
154
155 /// Convert Unix epoch into RFC3339 UTC string
156 pub fn epoch_to_rfc3339_utc(epoch: i64) -> Result<String, Error> {
157 let gmtime = gmtime(epoch)?;
158
159 let year = gmtime.tm_year + 1900;
160 if year < 0 || year > 9999 {
161 bail!("epoch_to_rfc3339_utc: wrong year '{year}'");
162 }
163
164 strftime("%010FT%TZ", &gmtime)
165 }
166
167 /// Convert Unix epoch into RFC3339 local time with TZ
168 pub fn epoch_to_rfc3339(epoch: i64) -> Result<String, Error> {
169 use std::fmt::Write as _;
170
171 let localtime = localtime(epoch)?;
172
173 let year = localtime.tm_year + 1900;
174 if year < 0 || year > 9999 {
175 bail!("epoch_to_rfc3339: wrong year '{year}'");
176 }
177
178 // Note: We cannot use strftime %z because of missing collon
179
180 let mut offset = localtime.tm_gmtoff;
181
182 let prefix = if offset < 0 {
183 offset = -offset;
184 '-'
185 } else {
186 '+'
187 };
188
189 let mins = offset / 60;
190 let hours = mins / 60;
191 let mins = mins % 60;
192
193 let mut s = strftime("%10FT%T", &localtime)?;
194 s.push(prefix);
195 let _ = write!(s, "{hours:02}:{mins:02}");
196
197 Ok(s)
198 }
199
200 /// Parse RFC3339 into Unix epoch
201 pub fn parse_rfc3339(input_str: &str) -> Result<i64, Error> {
202 parse_rfc3339_do(input_str)
203 .map_err(|err| format_err!("failed to parse rfc3339 timestamp ({input_str:?}) - {err}",))
204 }
205
206 fn parse_rfc3339_do(input_str: &str) -> Result<i64, Error> {
207 let input = input_str.as_bytes();
208
209 let expect = |pos: usize, c: u8| {
210 if input[pos] != c {
211 bail!("unexpected char at pos {pos}");
212 }
213 Ok(())
214 };
215
216 let digit = |pos: usize| -> Result<i32, Error> {
217 let digit = input[pos] as i32;
218 if digit < 48 || digit > 57 {
219 bail!("unexpected char at pos {pos}");
220 }
221 Ok(digit - 48)
222 };
223
224 fn check_max(i: i32, max: i32) -> Result<i32, Error> {
225 if i > max {
226 bail!("value too large ({i} > {max})");
227 }
228 Ok(i)
229 }
230
231 if input.len() < 20 || input.len() > 25 {
232 bail!("timestamp of unexpected length");
233 }
234
235 let tz = input[19];
236
237 match tz {
238 b'Z' => {
239 if input.len() != 20 {
240 bail!("unexpected length in UTC timestamp");
241 }
242 }
243 b'+' | b'-' => {
244 if input.len() != 25 {
245 bail!("unexpected length in timestamp");
246 }
247 }
248 _ => bail!("unexpected timezone indicator"),
249 }
250
251 let mut tm = crate::TmEditor::new(true);
252
253 tm.set_year(digit(0)? * 1000 + digit(1)? * 100 + digit(2)? * 10 + digit(3)?)?;
254 expect(4, b'-')?;
255 tm.set_mon(check_max(digit(5)? * 10 + digit(6)?, 12)?)?;
256 expect(7, b'-')?;
257 tm.set_mday(check_max(digit(8)? * 10 + digit(9)?, 31)?)?;
258
259 expect(10, b'T')?;
260
261 tm.set_hour(check_max(digit(11)? * 10 + digit(12)?, 23)?)?;
262 expect(13, b':')?;
263 tm.set_min(check_max(digit(14)? * 10 + digit(15)?, 59)?)?;
264 expect(16, b':')?;
265 tm.set_sec(check_max(digit(17)? * 10 + digit(18)?, 60)?)?;
266
267 let epoch = tm.into_epoch()?;
268 if tz == b'Z' {
269 return Ok(epoch);
270 }
271
272 let hours = check_max(digit(20)? * 10 + digit(21)?, 23)?;
273 expect(22, b':')?;
274 let mins = check_max(digit(23)? * 10 + digit(24)?, 59)?;
275
276 let offset = (hours * 3600 + mins * 60) as i64;
277
278 let epoch = match tz {
279 b'+' => epoch - offset,
280 b'-' => epoch + offset,
281 _ => unreachable!(), // already checked above
282 };
283
284 Ok(epoch)
285 }
286
287 #[test]
288 fn test_leap_seconds() {
289 let convert_reconvert = |epoch| {
290 let rfc3339 =
291 epoch_to_rfc3339_utc(epoch).expect("leap second epoch to rfc3339 should work");
292
293 let parsed =
294 parse_rfc3339(&rfc3339).expect("parsing converted leap second epoch should work");
295
296 assert_eq!(epoch, parsed);
297 };
298
299 // 2005-12-31T23:59:59Z was followed by a leap second
300 let epoch = 1136073599;
301 convert_reconvert(epoch);
302 convert_reconvert(epoch + 1);
303 convert_reconvert(epoch + 2);
304
305 let parsed = parse_rfc3339("2005-12-31T23:59:60Z").expect("parsing leap second should work");
306 assert_eq!(parsed, epoch + 1);
307 }
308
309 #[test]
310 fn test_rfc3339_range() {
311 // also tests single-digit years/first decade values
312 let lower = -62167219200;
313 let lower_str = "0000-01-01T00:00:00Z";
314
315 let upper = 253402300799;
316 let upper_str = "9999-12-31T23:59:59Z";
317
318 let converted =
319 epoch_to_rfc3339_utc(lower).expect("converting lower bound of RFC3339 range should work");
320 assert_eq!(converted, lower_str);
321
322 let converted =
323 epoch_to_rfc3339_utc(upper).expect("converting upper bound of RFC3339 range should work");
324 assert_eq!(converted, upper_str);
325
326 let parsed =
327 parse_rfc3339(lower_str).expect("parsing lower bound of RFC3339 range should work");
328 assert_eq!(parsed, lower);
329
330 let parsed =
331 parse_rfc3339(upper_str).expect("parsing upper bound of RFC3339 range should work");
332 assert_eq!(parsed, upper);
333
334 epoch_to_rfc3339_utc(lower - 1)
335 .expect_err("converting below lower bound of RFC3339 range should fail");
336
337 epoch_to_rfc3339_utc(upper + 1)
338 .expect_err("converting above upper bound of RFC3339 range should fail");
339
340 let first_century = -59011459201;
341 let first_century_str = "0099-12-31T23:59:59Z";
342
343 let converted = epoch_to_rfc3339_utc(first_century)
344 .expect("converting epoch representing first century year should work");
345 assert_eq!(converted, first_century_str);
346
347 let parsed =
348 parse_rfc3339(first_century_str).expect("parsing first century string should work");
349 assert_eq!(parsed, first_century);
350
351 let first_millenium = -59011459200;
352 let first_millenium_str = "0100-01-01T00:00:00Z";
353
354 let converted = epoch_to_rfc3339_utc(first_millenium)
355 .expect("converting epoch representing first millenium year should work");
356 assert_eq!(converted, first_millenium_str);
357
358 let parsed =
359 parse_rfc3339(first_millenium_str).expect("parsing first millenium string should work");
360 assert_eq!(parsed, first_millenium);
361 }
362
363 #[test]
364 fn test_gmtime_range() {
365 // year must fit into i32
366 let lower = -67768040609740800;
367 let upper = 67768036191676799;
368
369 let mut lower_tm = gmtime(lower).expect("gmtime should work as long as years fit into i32");
370 let res = timegm(&mut lower_tm).expect("converting back to epoch should work");
371 assert_eq!(lower, res);
372
373 gmtime(lower - 1).expect_err("gmtime should fail for years not fitting into i32");
374
375 let mut upper_tm = gmtime(upper).expect("gmtime should work as long as years fit into i32");
376 let res = timegm(&mut upper_tm).expect("converting back to epoch should work");
377 assert_eq!(upper, res);
378
379 gmtime(upper + 1).expect_err("gmtime should fail for years not fitting into i32");
380 }
381
382 #[test]
383 fn test_timezones() {
384 let input = "2020-12-30T00:00:00+06:30";
385 let epoch = 1609263000;
386 let expected_utc = "2020-12-29T17:30:00Z";
387
388 let parsed = parse_rfc3339(input).expect("parsing failed");
389 assert_eq!(parsed, epoch);
390
391 let res = epoch_to_rfc3339_utc(parsed).expect("converting to RFC failed");
392 assert_eq!(expected_utc, res);
393 }