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