]>
Commit | Line | Data |
---|---|---|
8f1719ee WB |
1 | //! Time support library. |
2 | //! | |
3 | //! Contains wrappers for `strftime`, `strptime`, `libc::localtime_r`. | |
4 | ||
5 | use std::ffi::{CStr, CString}; | |
6 | use std::fmt; | |
7 | use std::mem::MaybeUninit; | |
8 | ||
9 | use anyhow::{bail, format_err, Error}; | |
10 | ||
11 | /// Calender month index to *non-leap-year* day-of-the-year. | |
12 | pub 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] | |
16 | macro_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. | |
24 | pub struct Tm(libc::tm); | |
25 | ||
26 | impl std::ops::Deref for Tm { | |
27 | type Target = libc::tm; | |
28 | ||
29 | fn deref(&self) -> &libc::tm { | |
30 | &self.0 | |
31 | } | |
32 | } | |
33 | ||
34 | impl 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...) | |
41 | impl 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. | |
60 | mod imports { | |
61 | extern "C" { | |
8f1719ee WB |
62 | pub fn strptime( |
63 | s: *const libc::c_char, | |
64 | format: *const libc::c_char, | |
65 | tm: *mut libc::tm, | |
66 | ) -> *const libc::c_char; | |
67 | } | |
68 | } | |
69 | ||
70 | impl Tm { | |
71 | /// A zero-initialized time struct. | |
72 | #[inline(always)] | |
73 | pub fn zero() -> Self { | |
74 | unsafe { std::mem::zeroed() } | |
75 | } | |
76 | ||
77 | /// Get the current time in the local timezone. | |
78 | pub fn now_local() -> Result<Self, Error> { | |
79 | Self::at_local(unsafe { libc::time(std::ptr::null_mut()) }) | |
80 | } | |
81 | ||
82 | /// Get a local time from a unix time stamp. | |
83 | pub fn at_local(time: libc::time_t) -> Result<Self, Error> { | |
84 | let mut out = MaybeUninit::<libc::tm>::uninit(); | |
85 | Ok(Self(unsafe { | |
86 | if libc::localtime_r(&time, out.as_mut_ptr()).is_null() { | |
87 | bail!("failed to convert timestamp to local time"); | |
88 | } | |
89 | out.assume_init() | |
90 | })) | |
91 | } | |
92 | ||
93 | /// Assume this is an UTC time and convert it to a unix time stamp. | |
94 | /// | |
95 | /// Equivalent to `timegm(3)`, the gmt equivalent of `mktime(3)`. | |
96 | pub fn as_utc_to_epoch(&self) -> libc::time_t { | |
97 | let mut year = self.0.tm_year as i64 + 1900; | |
98 | let mon = self.0.tm_mon; | |
99 | ||
100 | let mut res: libc::time_t = (year - 1970) * 365 + CAL_MTOD[mon as usize]; | |
101 | ||
102 | if mon <= 1 { | |
103 | year -= 1; | |
104 | } | |
105 | ||
106 | res += (year - 1968) / 4; | |
107 | res -= (year - 1900) / 100; | |
108 | res += (year - 1600) / 400; | |
109 | ||
110 | res += (self.0.tm_mday - 1) as i64; | |
111 | res = res * 24 + self.0.tm_hour as i64; | |
112 | res = res * 60 + self.0.tm_min as i64; | |
113 | res = res * 60 + self.0.tm_sec as i64; | |
114 | ||
115 | res | |
116 | } | |
117 | } | |
118 | ||
8f1719ee WB |
119 | /// Wrapper around `strptime(3)` to parse time strings. |
120 | pub fn strptime(time: &str, format: &CStr) -> Result<Tm, Error> { | |
8a5b28ff ML |
121 | // zero memory because strptime does not necessarily initialize tm_isdst, tm_gmtoff and tm_zone |
122 | let mut out = MaybeUninit::<libc::tm>::zeroed(); | |
8f1719ee WB |
123 | |
124 | let time = CString::new(time).map_err(|_| format_err!("time string contains nul bytes"))?; | |
125 | ||
126 | let end = unsafe { | |
127 | imports::strptime( | |
128 | time.as_ptr() as *const libc::c_char, | |
129 | format.as_ptr(), | |
130 | out.as_mut_ptr(), | |
131 | ) | |
132 | }; | |
133 | ||
134 | if end.is_null() { | |
135 | bail!("failed to parse time string {:?}", time); | |
136 | } | |
137 | ||
138 | Ok(Tm(unsafe { out.assume_init() })) | |
139 | } | |
8a5b28ff ML |
140 | |
141 | pub fn date_to_rfc3339(date: &str) -> Result<String, Error> { | |
142 | // parse the YYYY-MM-DD HH:MM:SS format for the timezone info | |
143 | let ltime = strptime(date, c_str!("%F %T"))?; | |
144 | ||
145 | // strptime assume it is in UTC, but we want to interpret it as local time and get the timezone | |
146 | // offset (tm_gmtoff) which we can then append to be able to parse it as rfc3339 | |
147 | let ltime = Tm::at_local(ltime.as_utc_to_epoch())?; | |
148 | ||
149 | let mut s = date.replacen(' ', "T", 1); | |
150 | if ltime.tm_gmtoff != 0 { | |
151 | let sign = if ltime.tm_gmtoff < 0 { '-' } else { '+' }; | |
152 | let minutes = (ltime.tm_gmtoff / 60) % 60; | |
153 | let hours = ltime.tm_gmtoff / (60 * 60); | |
154 | s.push_str(&format!("{}{:02}:{:02}", sign, hours, minutes)); | |
155 | } else { | |
156 | s.push('Z'); | |
157 | } | |
158 | Ok(s) | |
159 | } |