]> git.proxmox.com Git - pmg-log-tracker.git/blame - src/time.rs
rfc3339: move timezone offset compatibility code to old time parser
[pmg-log-tracker.git] / src / time.rs
CommitLineData
8f1719ee
WB
1//! Time support library.
2//!
3//! Contains wrappers for `strftime`, `strptime`, `libc::localtime_r`.
4
5use std::ffi::{CStr, CString};
6use std::fmt;
7use std::mem::MaybeUninit;
8
9use anyhow::{bail, format_err, Error};
10
11/// Calender month index to *non-leap-year* day-of-the-year.
12pub const CAL_MTOD: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
13
14/// Shortcut for generating an `&'static CStr`.
15#[macro_export]
16macro_rules! c_str {
17 ($data:expr) => {{
18 let bytes = concat!($data, "\0");
19 unsafe { ::std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) }
20 }};
21}
22
23// Wrapper around `libc::tm` providing `Debug` and some helper methods.
24pub struct Tm(libc::tm);
25
26impl std::ops::Deref for Tm {
27 type Target = libc::tm;
28
29 fn deref(&self) -> &libc::tm {
30 &self.0
31 }
32}
33
34impl std::ops::DerefMut for Tm {
35 fn deref_mut(&mut self) -> &mut libc::tm {
36 &mut self.0
37 }
38}
39
40// (or add libc feature 'extra-traits' but that's the only struct we need it for...)
41impl fmt::Debug for Tm {
42 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43 f.debug_struct("Tm")
44 .field("tm_sec", &self.tm_sec)
45 .field("tm_min", &self.tm_min)
46 .field("tm_hour", &self.tm_hour)
47 .field("tm_mday", &self.tm_mday)
48 .field("tm_mon", &self.tm_mon)
49 .field("tm_year", &self.tm_year)
50 .field("tm_wday", &self.tm_wday)
51 .field("tm_yday", &self.tm_yday)
52 .field("tm_isdst", &self.tm_isdst)
53 .field("tm_gmtoff", &self.tm_gmtoff)
54 .field("tm_zone", &self.tm_zone)
55 .finish()
56 }
57}
58
59/// These are now exposed via `libc`, but they're part of glibc.
60mod imports {
61 extern "C" {
62 pub fn strftime(
63 s: *mut libc::c_char,
64 max: libc::size_t,
65 format: *const libc::c_char,
66 tm: *const libc::tm,
67 ) -> libc::size_t;
68
69 pub fn strptime(
70 s: *const libc::c_char,
71 format: *const libc::c_char,
72 tm: *mut libc::tm,
73 ) -> *const libc::c_char;
74 }
75}
76
77impl Tm {
78 /// A zero-initialized time struct.
79 #[inline(always)]
80 pub fn zero() -> Self {
81 unsafe { std::mem::zeroed() }
82 }
83
84 /// Get the current time in the local timezone.
85 pub fn now_local() -> Result<Self, Error> {
86 Self::at_local(unsafe { libc::time(std::ptr::null_mut()) })
87 }
88
89 /// Get a local time from a unix time stamp.
90 pub fn at_local(time: libc::time_t) -> Result<Self, Error> {
91 let mut out = MaybeUninit::<libc::tm>::uninit();
92 Ok(Self(unsafe {
93 if libc::localtime_r(&time, out.as_mut_ptr()).is_null() {
94 bail!("failed to convert timestamp to local time");
95 }
96 out.assume_init()
97 }))
98 }
99
100 /// Assume this is an UTC time and convert it to a unix time stamp.
101 ///
102 /// Equivalent to `timegm(3)`, the gmt equivalent of `mktime(3)`.
103 pub fn as_utc_to_epoch(&self) -> libc::time_t {
104 let mut year = self.0.tm_year as i64 + 1900;
105 let mon = self.0.tm_mon;
106
107 let mut res: libc::time_t = (year - 1970) * 365 + CAL_MTOD[mon as usize];
108
109 if mon <= 1 {
110 year -= 1;
111 }
112
113 res += (year - 1968) / 4;
114 res -= (year - 1900) / 100;
115 res += (year - 1600) / 400;
116
117 res += (self.0.tm_mday - 1) as i64;
118 res = res * 24 + self.0.tm_hour as i64;
119 res = res * 60 + self.0.tm_min as i64;
120 res = res * 60 + self.0.tm_sec as i64;
121
122 res
123 }
124}
125
126/// Wrapper around `strftime(3)` to format time strings.
127pub fn strftime(format: &CStr, tm: &Tm) -> Result<String, Error> {
128 let mut buf = MaybeUninit::<[u8; 64]>::uninit();
129
130 let size = unsafe {
131 imports::strftime(
132 buf.as_mut_ptr() as *mut libc::c_char,
133 64,
134 format.as_ptr(),
135 &tm.0,
136 )
137 };
138 if size == 0 {
139 bail!("failed to format time");
140 }
141 let size = size as usize;
142
143 let buf = unsafe { buf.assume_init() };
144
145 std::str::from_utf8(&buf[..size])
146 .map_err(|_| format_err!("formatted time was not valid utf-8"))
147 .map(str::to_string)
148}
149
150/// Wrapper around `strptime(3)` to parse time strings.
151pub fn strptime(time: &str, format: &CStr) -> Result<Tm, Error> {
8a5b28ff
ML
152 // zero memory because strptime does not necessarily initialize tm_isdst, tm_gmtoff and tm_zone
153 let mut out = MaybeUninit::<libc::tm>::zeroed();
8f1719ee
WB
154
155 let time = CString::new(time).map_err(|_| format_err!("time string contains nul bytes"))?;
156
157 let end = unsafe {
158 imports::strptime(
159 time.as_ptr() as *const libc::c_char,
160 format.as_ptr(),
161 out.as_mut_ptr(),
162 )
163 };
164
165 if end.is_null() {
166 bail!("failed to parse time string {:?}", time);
167 }
168
169 Ok(Tm(unsafe { out.assume_init() }))
170}
8a5b28ff
ML
171
172pub fn date_to_rfc3339(date: &str) -> Result<String, Error> {
173 // parse the YYYY-MM-DD HH:MM:SS format for the timezone info
174 let ltime = strptime(date, c_str!("%F %T"))?;
175
176 // strptime assume it is in UTC, but we want to interpret it as local time and get the timezone
177 // offset (tm_gmtoff) which we can then append to be able to parse it as rfc3339
178 let ltime = Tm::at_local(ltime.as_utc_to_epoch())?;
179
180 let mut s = date.replacen(' ', "T", 1);
181 if ltime.tm_gmtoff != 0 {
182 let sign = if ltime.tm_gmtoff < 0 { '-' } else { '+' };
183 let minutes = (ltime.tm_gmtoff / 60) % 60;
184 let hours = ltime.tm_gmtoff / (60 * 60);
185 s.push_str(&format!("{}{:02}:{:02}", sign, hours, minutes));
186 } else {
187 s.push('Z');
188 }
189 Ok(s)
190}