]> git.proxmox.com Git - proxmox.git/blob - proxmox-time/src/posix.rs
6157f8bcc4c71375838d88bb7e34d2d08981afab
[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 // rust libc bindings do not include strftime
112 #[link(name = "c")]
113 extern "C" {
114 #[link_name = "strftime"]
115 fn libc_strftime(
116 s: *mut libc::c_char,
117 max: libc::size_t,
118 format: *const libc::c_char,
119 time: *const libc::tm,
120 ) -> libc::size_t;
121 }
122
123 /// Safe bindings to libc strftime
124 pub fn strftime(format: &str, t: &libc::tm) -> Result<String, Error> {
125 let format = CString::new(format).map_err(|err| format_err!("{}", err))?;
126 let mut buf = vec![0u8; 8192];
127
128 let res = unsafe {
129 libc_strftime(
130 buf.as_mut_ptr() as *mut libc::c_char,
131 buf.len() as libc::size_t,
132 format.as_ptr(),
133 t as *const libc::tm,
134 )
135 };
136 if res == !0 {
137 // -1,, it's unsigned
138 bail!("strftime failed");
139 }
140
141 // `res` is a `libc::size_t`, which on a different target architecture might not be directly
142 // assignable to a `usize`. Thus, we actually want a cast here.
143 #[allow(clippy::unnecessary_cast)]
144 let len = res as usize;
145
146 if len == 0 {
147 bail!("strftime: result len is 0 (string too large)");
148 };
149
150 let c_str = CStr::from_bytes_with_nul(&buf[..len + 1]).map_err(|err| format_err!("{}", err))?;
151 let str_slice: &str = c_str.to_str().unwrap();
152 Ok(str_slice.to_owned())
153 }
154
155 /// Format epoch as local time
156 pub fn strftime_local(format: &str, epoch: i64) -> Result<String, Error> {
157 let localtime = localtime(epoch)?;
158 strftime(format, &localtime)
159 }
160
161 /// Format epoch as utc time
162 pub fn strftime_utc(format: &str, epoch: i64) -> Result<String, Error> {
163 let gmtime = gmtime(epoch)?;
164 strftime(format, &gmtime)
165 }
166
167 /// Convert Unix epoch into RFC3339 UTC string
168 pub fn epoch_to_rfc3339_utc(epoch: i64) -> Result<String, Error> {
169 let gmtime = gmtime(epoch)?;
170
171 let year = gmtime.tm_year + 1900;
172 if year < 0 || year > 9999 {
173 bail!("epoch_to_rfc3339_utc: wrong year '{}'", year);
174 }
175
176 strftime("%010FT%TZ", &gmtime)
177 }
178
179 /// Convert Unix epoch into RFC3339 local time with TZ
180 pub fn epoch_to_rfc3339(epoch: i64) -> Result<String, Error> {
181 use std::fmt::Write as _;
182
183 let localtime = localtime(epoch)?;
184
185 let year = localtime.tm_year + 1900;
186 if year < 0 || year > 9999 {
187 bail!("epoch_to_rfc3339: wrong year '{}'", year);
188 }
189
190 // Note: We cannot use strftime %z because of missing collon
191
192 let mut offset = localtime.tm_gmtoff;
193
194 let prefix = if offset < 0 {
195 offset = -offset;
196 '-'
197 } else {
198 '+'
199 };
200
201 let mins = offset / 60;
202 let hours = mins / 60;
203 let mins = mins % 60;
204
205 let mut s = strftime("%10FT%T", &localtime)?;
206 s.push(prefix);
207 let _ = write!(s, "{:02}:{:02}", hours, mins);
208
209 Ok(s)
210 }
211
212 /// Parse RFC3339 into Unix epoch
213 pub fn parse_rfc3339(input_str: &str) -> Result<i64, Error> {
214 parse_rfc3339_do(input_str).map_err(|err| {
215 format_err!(
216 "failed to parse rfc3339 timestamp ({:?}) - {}",
217 input_str,
218 err
219 )
220 })
221 }
222
223 fn parse_rfc3339_do(input_str: &str) -> Result<i64, Error> {
224 let input = input_str.as_bytes();
225
226 let expect = |pos: usize, c: u8| {
227 if input[pos] != c {
228 bail!("unexpected char at pos {}", pos);
229 }
230 Ok(())
231 };
232
233 let digit = |pos: usize| -> Result<i32, Error> {
234 let digit = input[pos] as i32;
235 if digit < 48 || digit > 57 {
236 bail!("unexpected char at pos {}", pos);
237 }
238 Ok(digit - 48)
239 };
240
241 fn check_max(i: i32, max: i32) -> Result<i32, Error> {
242 if i > max {
243 bail!("value too large ({} > {})", i, max);
244 }
245 Ok(i)
246 }
247
248 if input.len() < 20 || input.len() > 25 {
249 bail!("timestamp of unexpected length");
250 }
251
252 let tz = input[19];
253
254 match tz {
255 b'Z' => {
256 if input.len() != 20 {
257 bail!("unexpected length in UTC timestamp");
258 }
259 }
260 b'+' | b'-' => {
261 if input.len() != 25 {
262 bail!("unexpected length in timestamp");
263 }
264 }
265 _ => bail!("unexpected timezone indicator"),
266 }
267
268 let mut tm = crate::TmEditor::new(true);
269
270 tm.set_year(digit(0)? * 1000 + digit(1)? * 100 + digit(2)? * 10 + digit(3)?)?;
271 expect(4, b'-')?;
272 tm.set_mon(check_max(digit(5)? * 10 + digit(6)?, 12)?)?;
273 expect(7, b'-')?;
274 tm.set_mday(check_max(digit(8)? * 10 + digit(9)?, 31)?)?;
275
276 expect(10, b'T')?;
277
278 tm.set_hour(check_max(digit(11)? * 10 + digit(12)?, 23)?)?;
279 expect(13, b':')?;
280 tm.set_min(check_max(digit(14)? * 10 + digit(15)?, 59)?)?;
281 expect(16, b':')?;
282 tm.set_sec(check_max(digit(17)? * 10 + digit(18)?, 60)?)?;
283
284 let epoch = tm.into_epoch()?;
285 if tz == b'Z' {
286 return Ok(epoch);
287 }
288
289 let hours = check_max(digit(20)? * 10 + digit(21)?, 23)?;
290 expect(22, b':')?;
291 let mins = check_max(digit(23)? * 10 + digit(24)?, 59)?;
292
293 let offset = (hours * 3600 + mins * 60) as i64;
294
295 let epoch = match tz {
296 b'+' => epoch - offset,
297 b'-' => epoch + offset,
298 _ => unreachable!(), // already checked above
299 };
300
301 Ok(epoch)
302 }
303
304 #[test]
305 fn test_leap_seconds() {
306 let convert_reconvert = |epoch| {
307 let rfc3339 =
308 epoch_to_rfc3339_utc(epoch).expect("leap second epoch to rfc3339 should work");
309
310 let parsed =
311 parse_rfc3339(&rfc3339).expect("parsing converted leap second epoch should work");
312
313 assert_eq!(epoch, parsed);
314 };
315
316 // 2005-12-31T23:59:59Z was followed by a leap second
317 let epoch = 1136073599;
318 convert_reconvert(epoch);
319 convert_reconvert(epoch + 1);
320 convert_reconvert(epoch + 2);
321
322 let parsed = parse_rfc3339("2005-12-31T23:59:60Z").expect("parsing leap second should work");
323 assert_eq!(parsed, epoch + 1);
324 }
325
326 #[test]
327 fn test_rfc3339_range() {
328 // also tests single-digit years/first decade values
329 let lower = -62167219200;
330 let lower_str = "0000-01-01T00:00:00Z";
331
332 let upper = 253402300799;
333 let upper_str = "9999-12-31T23:59:59Z";
334
335 let converted =
336 epoch_to_rfc3339_utc(lower).expect("converting lower bound of RFC3339 range should work");
337 assert_eq!(converted, lower_str);
338
339 let converted =
340 epoch_to_rfc3339_utc(upper).expect("converting upper bound of RFC3339 range should work");
341 assert_eq!(converted, upper_str);
342
343 let parsed =
344 parse_rfc3339(lower_str).expect("parsing lower bound of RFC3339 range should work");
345 assert_eq!(parsed, lower);
346
347 let parsed =
348 parse_rfc3339(upper_str).expect("parsing upper bound of RFC3339 range should work");
349 assert_eq!(parsed, upper);
350
351 epoch_to_rfc3339_utc(lower - 1)
352 .expect_err("converting below lower bound of RFC3339 range should fail");
353
354 epoch_to_rfc3339_utc(upper + 1)
355 .expect_err("converting above upper bound of RFC3339 range should fail");
356
357 let first_century = -59011459201;
358 let first_century_str = "0099-12-31T23:59:59Z";
359
360 let converted = epoch_to_rfc3339_utc(first_century)
361 .expect("converting epoch representing first century year should work");
362 assert_eq!(converted, first_century_str);
363
364 let parsed =
365 parse_rfc3339(first_century_str).expect("parsing first century string should work");
366 assert_eq!(parsed, first_century);
367
368 let first_millenium = -59011459200;
369 let first_millenium_str = "0100-01-01T00:00:00Z";
370
371 let converted = epoch_to_rfc3339_utc(first_millenium)
372 .expect("converting epoch representing first millenium year should work");
373 assert_eq!(converted, first_millenium_str);
374
375 let parsed =
376 parse_rfc3339(first_millenium_str).expect("parsing first millenium string should work");
377 assert_eq!(parsed, first_millenium);
378 }
379
380 #[test]
381 fn test_gmtime_range() {
382 // year must fit into i32
383 let lower = -67768040609740800;
384 let upper = 67768036191676799;
385
386 let mut lower_tm = gmtime(lower).expect("gmtime should work as long as years fit into i32");
387 let res = timegm(&mut lower_tm).expect("converting back to epoch should work");
388 assert_eq!(lower, res);
389
390 gmtime(lower - 1).expect_err("gmtime should fail for years not fitting into i32");
391
392 let mut upper_tm = gmtime(upper).expect("gmtime should work as long as years fit into i32");
393 let res = timegm(&mut upper_tm).expect("converting back to epoch should work");
394 assert_eq!(upper, res);
395
396 gmtime(upper + 1).expect_err("gmtime should fail for years not fitting into i32");
397 }
398
399 #[test]
400 fn test_timezones() {
401 let input = "2020-12-30T00:00:00+06:30";
402 let epoch = 1609263000;
403 let expected_utc = "2020-12-29T17:30:00Z";
404
405 let parsed = parse_rfc3339(input).expect("parsing failed");
406 assert_eq!(parsed, epoch);
407
408 let res = epoch_to_rfc3339_utc(parsed).expect("converting to RFC failed");
409 assert_eq!(expected_utc, res);
410 }