libtz_sys/
localtime.rs

1// Thin Rust FFI layer around libtz
2//
3// Copyright © 2023 David Caldwell <david@porkrind.org>
4// License: MIT (see LICENSE.md file)
5
6#![doc = include_str!("README.md")]
7
8use std::cell::RefCell;
9use std::ffi::{CString,CStr};
10use std::os::unix::ffi::OsStringExt;
11use std::os::raw::{c_void,c_char,c_int,c_long};
12
13thread_local!(static ENV_STORAGE: RefCell<CString> = RefCell::new(CString::default()));
14
15#[doc(hidden)]
16#[no_mangle]
17pub extern "C" fn rust_getenv(name: *const c_char) -> *const c_char {
18    rust_getenv_internal(name).unwrap_or(std::ptr::null())
19}
20
21fn rust_getenv_internal(name: *const c_char) -> Option<*const c_char> {
22    let name: &str = unsafe { CStr::from_ptr(name).to_str().ok()? };
23    let value_cstr = CString::new(std::env::var_os(name)?.into_vec()).ok()?;
24    ENV_STORAGE.with(|storage_cell| {
25        let mut storage = storage_cell.borrow_mut();
26        *storage = value_cstr;
27        Some(storage.as_ptr())
28    })
29}
30
31/// System time. On unix, the number of seconds since 00:00:00 UTC on 1 January 1970.
32///
33/// Note: This is libtz's `time_t`, which currently hardcoded to i64 regardless
34/// of the system's `time_t`.
35pub type TimeT = i64; // See `-Dtime_tz` in build.rs
36
37/// A broken down time representation, logically equivalent to `struct tm` in unix.
38///
39/// Note: This is libtz's `struct tm` and doesn't necessarily match the system's
40/// in terms of member ordering.
41///
42/// Reference: <https://pubs.opengroup.org/onlinepubs/7908799/xsh/time.h.html>
43#[repr(C)]
44#[derive(Clone, Copy, Debug)]
45pub struct Tm {
46    /** Seconds          [0, 60] */                 pub tm_sec   : c_int,
47    /** Minutes          [0, 59] */                 pub tm_min   : c_int,
48    /** Hour             [0, 23] */                 pub tm_hour  : c_int,
49    /** Day of the month [1, 31] */                 pub tm_mday  : c_int,
50    /** Month            [0, 11]  (January = 0) */  pub tm_mon   : c_int,
51    /** Year minus 1900 */                          pub tm_year  : c_int,
52    /** Day of the week  [0, 6]   (Sunday = 0) */   pub tm_wday  : c_int,
53    /** Day of the year  [0, 365] (Jan/01 = 0) */   pub tm_yday  : c_int,
54    /** Daylight savings flag */                    pub tm_isdst : c_int,
55
56    /** Seconds East of UTC */                      pub tm_gmtoff : c_long,
57    /** Timezone abbreviation */                    pub tm_zone: *const c_char,
58}
59
60/// Opaque pointer for timezone data. Defined in the C code as:
61/// ```C
62/// typedef struct state *timezone_t;
63/// ```
64///
65/// This is returned by [`tzalloc`]. It should be explicitly freed with [`tzfree`].
66pub type TimezoneT = *const c_void;
67
68extern "C" {
69    /// Convert system time to a UTC [`Tm`].
70    ///
71    /// Rust interface for the C function:
72    /// ```C
73    /// struct tm *gmtime_r(time_t const *restrict, struct tm *restrict);
74    /// ```
75    ///
76    /// The `gmtime_r` function converts to Coordinated Universal Time, returning a pointer to [`Tm`]
77    /// structures, described below. The storage for the returned [`Tm`] should be passed in as the 2nd
78    /// argument. If it encounters an error, it will return a `NULL` and set `errno` (see
79    /// [`std::io::Error::last_os_error`]).
80    #[link_name = "tz_gmtime_r"]
81    pub fn gmtime_r(timep: *const TimeT, tmp: *mut Tm) -> *mut Tm;
82
83    /// Convert system time to a local time [`Tm`] using globally configured time zone.
84    ///
85    /// Rust interface for the C function:
86    /// ```C
87    /// struct tm *localtime_r(time_t const *restrict, struct tm *restrict);
88    /// ```
89    ///
90    /// The `localtime_r` function corrects for the time zone and any time zone adjustments (such as Daylight
91    /// Saving Time in the United States).  The storage for the returned [`Tm`] should be passed in as the 2nd
92    /// argument. If it encounters an error, it will return a `NULL` and set `errno` (see
93    /// [`std::io::Error::last_os_error`]). The pointer returned in [`Tm::tm_zone`] will only be valid until
94    /// the next [`tzet()`][tzset] call.
95    #[link_name = "tz_localtime_r"]
96    pub fn localtime_r(timep: *const TimeT, tmp: *mut Tm) -> *mut Tm;
97
98    /// Convert UTC [`Tm`] to system time.
99    ///
100    /// Rust interface for the C function:
101    /// ```C
102    /// time_t timegm(struct tm *);
103    /// ```
104    ///
105    /// This function is like [`mktime`] except that it treats the `tmp` as UTC (ignoring the `tm_idst` and
106    /// `tm_zone` members).
107    #[link_name = "tz_timegm"]
108    pub fn timegm(tmp: *const Tm) -> TimeT;
109
110    /// Convert local time [`Tm`] to system time using globally configured time zone.
111    ///
112    /// Rust interface for the C function:
113    /// ```C
114    /// time_t mktime(struct tm *);
115    /// ```
116    ///
117    /// The `mktime` function converts the broken-down time, expressed as local time, in the structure pointed
118    /// to by `tm` into a calendar time value with the same encoding as that of the values returned by the
119    /// `time` function.  The original values of the `tm_wday` and `tm_yday` components of the structure are
120    /// ignored, and the original values of the other components are not restricted to their normal ranges.
121    /// (A positive or zero value for `tm_isdst` causes `mktime` to presume initially that daylight saving
122    /// time respectively, is or is not in effect for the specified time.
123    ///
124    /// A negative value for `tm_isdst` causes the `mktime` function to attempt to divine whether daylight
125    /// saving time is in effect for the specified time; in this case it does not use a consistent rule and
126    /// may give a different answer when later presented with the same argument.)  On successful completion,
127    /// the values of the `tm_wday` and `tm_yday` components of the structure are set appropriately, and the
128    /// other components are set to represent the specified calendar time, but with their values forced to
129    /// their normal ranges; the final value of `tm_mday` is not set until `tm_mon` and `tm_year` are
130    /// determined.  The `mktime` function returns the specified calendar time; If the calendar time cannot be
131    /// represented, it returns -1.
132    #[link_name = "tz_mktime"]
133    pub fn mktime(tmp: *const Tm) -> TimeT;
134
135    /// Re-read `TZ` environment variable and configure global time zone.
136    ///
137    /// Rust interface for the C function:
138    /// ```C
139    /// void tzset(void);
140    /// ```
141    ///
142    /// The `tzset` function acts like `tzalloc(getenv("TZ"))`, except it saves any resulting timezone object
143    /// into internal storage that is accessed by `localtime_r`, and `mktime`.  The anonymous shared timezone
144    /// object is freed by the next call to `tzset`.  If the implied call to `tzalloc` fails, `tzset` falls
145    /// back on Universal Time (UT).
146    #[link_name = "tz_tzset"]
147    pub fn tzset();
148
149    // -DNETBSD_INSPIRED
150    /// Allocate a configured time zone.
151    ///
152    /// Rust interface for the C function:
153    /// ```C
154    /// timezone_t tzalloc(char const *);
155    /// ```
156    ///
157    /// The `tzalloc` function allocates and returns a timezone object described
158    /// by `TZ`.  If `TZ` is not a valid timezone description,  or  if  the  object
159    /// cannot be allocated, `tzalloc` returns a null pointer and sets `errno`.
160    ///
161    #[doc = include_str!("tzalloc.md")]
162    #[link_name = "tz_tzalloc"]
163    pub fn tzalloc(zone: *const c_char) -> TimezoneT;
164
165    /// Free allocated time zone.
166    ///
167    /// Rust interface for the C function:
168    /// ```C
169    /// void tzfree(timezone_t);
170    /// ```
171    ///
172    /// The `tzfree` function frees a timezone object `tz`, which should have been successfully allocated by
173    /// `tzalloc`.  This invalidates any `tm_zone` pointers that `tz` was used to set.
174    #[link_name = "tz_tzfree"]
175    pub fn tzfree(tz: TimezoneT);
176
177    /// Convert system time to local time using passed-in time zone.
178    ///
179    /// Rust interface for the C function:
180    /// ```C
181    /// struct tm *localtime_rz(timezone_t restrict, time_t const *restrict, struct tm *restrict);
182    /// ```
183    ///
184    /// This acts like [`localtime_r`] except it uses the passed in TimezoneT instead of the shared global
185    /// configuration. The pointer returned in [`Tm::tm_zone`] will be valid until the [`TimezoneT`] is freed
186    /// with [`tzfree()`][tzfree].
187    #[link_name = "tz_localtime_rz"]
188    pub fn localtime_rz(tz: TimezoneT, timep: *const TimeT, tmp: *mut Tm) -> *mut Tm;
189
190    /// Convert local time `Tm` to system time using passed-in time zone.
191    ///
192    /// Rust interface for the C function:
193    /// ```C
194    /// time_t mktime_z(timezone_t restrict, struct tm *restrict);
195    /// ```
196    ///
197    /// This acts like [`mktime`] except it uses the passed in TimezoneT instead of the shared global
198    /// configuration.
199    #[link_name = "tz_mktime_z"]
200    pub fn mktime_z(tz: TimezoneT, tmp: *const Tm) -> TimeT;
201
202    /// Convert from leap-second to POSIX `time_t`s.
203    ///
204    /// Rust interface for the C function:
205    /// ```C
206    /// time_t posix2time_z(timezone_t, time_t);
207    /// ```
208    ///
209    #[doc = include_str!("time2posix.md")]
210    #[link_name = "tz_posix2time_z"]
211    pub fn posix2time_z(tz: TimezoneT, t: TimeT) -> TimeT;
212
213    /// Convert from POSIX to leap-second `time_t`s.
214    ///
215    /// Rust interface for the C function:
216    /// ```C
217    /// time_t time2posix_z(timezone_t, time_t);
218    /// ```
219    ///
220    #[doc = include_str!("time2posix.md")]
221    #[link_name = "tz_time2posix_z"]
222    pub fn time2posix_z(tz: TimezoneT, t: TimeT) -> TimeT;
223}
224
225#[cfg(test)]
226mod tests {
227    use std::ffi::{CString,CStr};
228    use std::mem::MaybeUninit;
229    use super::*;
230    #[test]
231    fn basic() {
232        let time: TimeT = 127810800;
233        std::env::set_var("TZ", "America/Los_Angeles");
234        unsafe { tzset() };
235        let mut tm = MaybeUninit::<Tm>::uninit();
236        let tmp = tm.as_mut_ptr();
237        let ret = unsafe { localtime_r(&time, tmp) };
238        assert_ne!(ret, std::ptr::null_mut());
239        assert_eq!(ret, tmp);
240        let tm = unsafe { tm.assume_init() };
241        let zone: &str = unsafe { CStr::from_ptr(tm.tm_zone).to_str().expect("correct utf8") };
242        assert_eq!((tm.tm_sec, tm.tm_min, tm.tm_hour, tm.tm_mday, tm.tm_mon, tm.tm_year, tm.tm_wday, tm.tm_yday, tm.tm_isdst, tm.tm_gmtoff, zone),
243                   (0,         0,         0,          19,         0,         74,         6,          18,         1,           -25200,       "PDT"));
244        assert_eq!(unsafe { mktime(&tm) }, time); // Round trip
245        let time: TimeT = time + tm.tm_gmtoff;
246        assert_eq!(unsafe { timegm(&tm) }, time);
247
248        let mut tm = MaybeUninit::<Tm>::uninit();
249        let tmp = tm.as_mut_ptr();
250        let ret = unsafe { gmtime_r(&time, tmp) };
251        assert_ne!(ret, std::ptr::null_mut());
252        assert_eq!(ret, tmp);
253        let tm = unsafe { tm.assume_init() };
254        let zone: &str = unsafe { CStr::from_ptr(tm.tm_zone).to_str().expect("correct utf8") };
255        assert_eq!((tm.tm_sec, tm.tm_min, tm.tm_hour, tm.tm_mday, tm.tm_mon, tm.tm_year, tm.tm_wday, tm.tm_yday, tm.tm_isdst, tm.tm_gmtoff, zone),
256                   (0,         0,         0,          19,         0,         74,         6,          18,         0,           0,           "UTC"));
257    }
258
259    #[test]
260    fn localtime_rz_test() {
261        let tzname = CString::new("America/New_York").unwrap();
262        let tz = unsafe { tzalloc(tzname.as_ptr()) };
263        assert_ne!(tz, std::ptr::null_mut());
264        let time: TimeT = 127810800;
265        let mut tm = MaybeUninit::<Tm>::uninit();
266        let ret = unsafe { localtime_rz(tz, &time, tm.as_mut_ptr()) };
267        assert_ne!(ret, std::ptr::null_mut());
268        let tm = unsafe { tm.assume_init() };
269        let zone: &str = unsafe { CStr::from_ptr(tm.tm_zone).to_str().expect("correct utf8") };
270        assert_eq!((tm.tm_sec, tm.tm_min, tm.tm_hour, tm.tm_mday, tm.tm_mon, tm.tm_year, tm.tm_wday, tm.tm_yday, tm.tm_isdst, tm.tm_gmtoff, zone),
271                   (0,         0,         3,          19,         0,         74,         6,          18,         1,           -14400,       "EDT"));
272        assert_eq!(unsafe { mktime_z(tz, &tm) }, time); // Round trip
273        unsafe { tzfree(tz) };
274    }
275
276    #[test]
277    fn posix_conversions() {
278        let posixtime: TimeT = 536457599;
279        let tzname = CString::new("UTC").unwrap();
280        let tz = unsafe { tzalloc(tzname.as_ptr()) };
281        assert_ne!(tz, std::ptr::null_mut());
282        let time = unsafe { posix2time_z(tz, posixtime) };
283        let mut tm = MaybeUninit::<Tm>::uninit();
284        let ret = unsafe { localtime_rz(tz, &time, tm.as_mut_ptr()) };
285        assert_ne!(ret, std::ptr::null_mut());
286        let tm = unsafe { tm.assume_init() };
287        let zone: &str = unsafe { CStr::from_ptr(tm.tm_zone).to_str().expect("correct utf8") };
288        assert_eq!((tm.tm_sec, tm.tm_min, tm.tm_hour, tm.tm_mday, tm.tm_mon, tm.tm_year, tm.tm_wday, tm.tm_yday, tm.tm_isdst, tm.tm_gmtoff, zone),
289                   (59,        59,        23,         31,         11,        86,         3,          364,        0,           0,            "UTC"));
290        assert_eq!(unsafe { time2posix_z(tz, time) }, posixtime); // Round Trip
291        unsafe { tzfree(tz) };
292    }
293
294    #[test]
295    fn test_readme_deps() {
296        version_sync::assert_markdown_deps_updated!("README.md");
297    }
298}