libtz-sys 0.1.1

An FFI interface to IANA's timezone library, libtz [WIP, don't use yet]
Documentation
#![doc = include_str!("README.md")]

use std::cell::RefCell;
use std::ffi::{CString,CStr};
use std::os::unix::ffi::OsStringExt;
use std::os::raw::{c_void,c_char,c_int,c_long};

thread_local!(static ENV_STORAGE: RefCell<CString> = RefCell::new(CString::default()));

#[doc(hidden)]
#[no_mangle]
pub extern "C" fn rust_getenv(name: *const c_char) -> *const c_char {
    rust_getenv_internal(name).unwrap_or(std::ptr::null())
}

fn rust_getenv_internal(name: *const c_char) -> Option<*const c_char> {
    let name: &str = unsafe { CStr::from_ptr(name).to_str().ok()? };
    let value_cstr = CString::new(std::env::var_os(name)?.into_vec()).ok()?;
    ENV_STORAGE.with(|storage_cell| {
        let mut storage = storage_cell.borrow_mut();
        *storage = value_cstr;
        Some(storage.as_ptr())
    })
}

/// System time. On unix, the number of seconds since 00:00:00 UTC on 1 January 1970.
///
/// FIXME: This is libtz's `time_t`, which currently mirrors the system's `time_t`. This makes hardcoding it
/// as i64 incorrect! Should we try to mirror the system `time_t`? Or should we force libtz to compile with a
/// 64 bit `time_t` (which appears to be possible)?
pub type TimeT = i64; // FIXME: This isn't in Rust yet.
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct Tm {
    /** Seconds          [0, 60] */                 pub tm_sec   : c_int,
    /** Minutes          [0, 59] */                 pub tm_min   : c_int,
    /** Hour             [0, 23] */                 pub tm_hour  : c_int,
    /** Day of the month [1, 31] */                 pub tm_mday  : c_int,
    /** Month            [0, 11]  (January = 0) */  pub tm_mon   : c_int,
    /** Year minus 1900 */                          pub tm_year  : c_int,
    /** Day of the week  [0, 6]   (Sunday = 0) */   pub tm_wday  : c_int,
    /** Day of the year  [0, 365] (Jan/01 = 0) */   pub tm_yday  : c_int,
    /** Daylight savings flag */                    pub tm_isdst : c_int,

    /** Seconds East of UTC */                      pub tm_gmtoff : c_long,
    /** Timezone abbreviation */                    pub tm_zone: *const c_char,
}

/// Opaque pointer for timezone data. Defined in the C code as:
/// ```C
/// typedef struct state *timezone_t;
/// ```
///
/// This is returned by [`tzalloc`]. It should be explicitly freed with [`tzfree`].
pub type TimezoneT = *const c_void;

extern "C" {
    /// Convert system time to UTC.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// struct tm *gmtime_r(time_t const *restrict, struct tm *restrict);
    /// ```
    ///
    /// The `gmtime_r` function converts to Coordinated Universal Time, returning a pointer to [`Tm`]
    /// structures, described below. The storage for the returned [`Tm`] should be passed in as the 2nd
    /// argument. If it encounters an error, it will return a `NULL` and set `errno` (see
    /// [`std::io::Error::last_os_error`]).
    pub fn gmtime_r(timep: *const TimeT, tmp: *mut Tm) -> *mut Tm;

    /// Convert system time to local time using globally configured time zone.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// struct tm *localtime_r(time_t const *restrict, struct tm *restrict);
    /// ```
    ///
    /// The `localtime_r` function corrects for the time zone and any time zone adjustments (such as Daylight
    /// Saving Time in the United States).  The storage for the returned [`Tm`] should be passed in as the 2nd
    /// argument. If it encounters an error, it will return a `NULL` and set `errno` (see
    /// [`std::io::Error::last_os_error`]).
    pub fn localtime_r(timep: *const TimeT, tmp: *mut Tm) -> *mut Tm;

    /// Convert UTC `Tm` to system time.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// time_t timegm(struct tm *);
    /// ```
    ///
    /// This function is like [`mktime`] except that it treats the `tmp` as UTC (ignoring the `tm_idst` and
    /// `tm_zone` members).
    ///
    /// ```text
    /// bad_rust_code();
    /// ```
    pub fn timegm(tmp: *const Tm) -> TimeT;

    /// Convert local time `Tm` to system time using globally configured time zone.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// time_t mktime(struct tm *);
    /// ```
    ///
    /// The `mktime` function converts the broken-down time, expressed as local time, in the structure pointed
    /// to by `tm` into a calendar time value with the same encoding as that of the values returned by the
    /// `time` function.  The original values of the `tm_wday` and `tm_yday` components of the structure are
    /// ignored, and the original values of the other components are not restricted to their normal ranges.
    /// (A positive or zero value for `tm_isdst` causes `mktime` to presume initially that daylight saving
    /// time respectively, is or is not in effect for the specified time.
    ///
    /// A negative value for `tm_isdst` causes the `mktime` function to attempt to divine whether daylight
    /// saving time is in effect for the specified time; in this case it does not use a consistent rule and
    /// may give a different answer when later presented with the same argument.)  On successful completion,
    /// the values of the `tm_wday` and `tm_yday` components of the structure are set appropriately, and the
    /// other components are set to represent the specified calendar time, but with their values forced to
    /// their normal ranges; the final value of `tm_mday` is not set until `tm_mon` and `tm_year` are
    /// determined.  The `mktime` function returns the specified calendar time; If the calendar time cannot be
    /// represented, it returns -1.
    pub fn mktime(tmp: *const Tm) -> TimeT;

    /// Re-read `TZ` environment variable and configure global time zone.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// void tzset(void);
    /// ```
    ///
    /// The `tzset` function acts like `tzalloc(getenv("TZ"))`, except it saves any resulting timezone object
    /// into internal storage that is accessed by `localtime_r`, and `mktime`.  The anonymous shared timezone
    /// object is freed by the next call to `tzset`.  If the implied call to `tzalloc` fails, `tzset` falls
    /// back on Universal Time (UT).
    pub fn tzset();

    // -DNETBSD_INSPIRED
    /// Allocate a configured time zone.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// timezone_t tzalloc(char const *);
    /// ```
    ///
    /// The `tzalloc` function allocates and returns a timezone object described
    /// by `TZ`.  If `TZ` is not a valid timezone description,  or  if  the  object
    /// cannot be allocated, `tzalloc` returns a null pointer and sets `errno`.
    ///
    #[doc = include_str!("tzalloc.md")]
    pub fn tzalloc(zone: *const c_char) -> TimezoneT;

    /// Free allocated time zone.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// void tzfree(timezone_t);
    /// ```
    ///
    /// The `tzfree` function frees a timezone object `tz`, which should have been successfully allocated by
    /// `tzalloc`.  This invalidates any `tm_zone` pointers that `tz` was used to set.
    pub fn tzfree(tz: TimezoneT);

    /// Convert system time to local time using passed-in time zone.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// struct tm *localtime_rz(timezone_t restrict, time_t const *restrict, struct tm *restrict);
    /// ```
    ///
    /// This acts like [`localtime_r`] except it uses the passed in TimezoneT instead of the shared global
    /// configuration.
    pub fn localtime_rz(tz: TimezoneT, timep: *const TimeT, tmp: *mut Tm) -> *mut Tm;

    /// Convert local time `Tm` to system time using passed-in time zone.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// time_t mktime_z(timezone_t restrict, struct tm *restrict);
    /// ```
    ///
    /// This acts like [`mktime`] except it uses the passed in TimezoneT instead of the shared global
    /// configuration.
    pub fn mktime_z(tz: TimezoneT, tmp: *const Tm) -> TimeT;

    /// Convert from leap-second to POSIX `time_t`s.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// time_t posix2time_z(timezone_t, time_t);
    /// ```
    ///
    #[doc = include_str!("time2posix.md")]
    pub fn posix2time_z(tz: TimezoneT, t: TimeT) -> TimeT;

    /// Convert from POSIX to leap-second `time_t`s.
    ///
    /// Rust interface for the C function:
    /// ```C
    /// time_t time2posix_z(timezone_t, time_t);
    /// ```
    ///
    #[doc = include_str!("time2posix.md")]
    pub fn time2posix_z(tz: TimezoneT, t: TimeT) -> TimeT;
}

#[cfg(test)]
mod tests {
    use std::ffi::{CString,CStr};
    use std::mem::MaybeUninit;
    use super::*;
    #[test]
    fn basic() {
        let time: super::TimeT = 127810800;
        std::env::set_var("TZ", "America/Los_Angeles");
        unsafe { super::tzset() };
        let mut tm = MaybeUninit::<super::Tm>::uninit();
        let tmp = tm.as_mut_ptr();
        let ret = unsafe { super::localtime_r(&time, tmp) };
        assert_ne!(ret, std::ptr::null_mut());
        assert_eq!(ret, tmp);
        let tm = unsafe { tm.assume_init() };
        let zone: &str = unsafe { CStr::from_ptr(tm.tm_zone).to_str().expect("correct utf8") };
        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),
                   (0,         0,         0,          19,         0,         74,         6,          18,         1,           -25200,       "PDT"));
        assert_eq!(unsafe { super::mktime(&tm) }, time); // Round trip
        let time: TimeT = time + tm.tm_gmtoff;
        assert_eq!(unsafe { super::timegm(&tm) }, time);

        let mut tm = MaybeUninit::<super::Tm>::uninit();
        let tmp = tm.as_mut_ptr();
        let ret = unsafe { super::gmtime_r(&time, tmp) };
        assert_ne!(ret, std::ptr::null_mut());
        assert_eq!(ret, tmp);
        let tm = unsafe { tm.assume_init() };
        let zone: &str = unsafe { CStr::from_ptr(tm.tm_zone).to_str().expect("correct utf8") };
        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),
                   (0,         0,         0,          19,         0,         74,         6,          18,         0,           0,           "UTC"));
    }

    #[test]
    fn localtime_rz_test() {
        let tzname = CString::new("America/New_York").unwrap();
        let tz = unsafe { super::tzalloc(tzname.as_ptr()) };
        assert_ne!(tz, std::ptr::null_mut());
        let time: super::TimeT = 127810800;
        let mut tm = MaybeUninit::<super::Tm>::uninit();
        let ret = unsafe { super::localtime_rz(tz, &time, tm.as_mut_ptr()) };
        assert_ne!(ret, std::ptr::null_mut());
        let tm = unsafe { tm.assume_init() };
        let zone: &str = unsafe { CStr::from_ptr(tm.tm_zone).to_str().expect("correct utf8") };
        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),
                   (0,         0,         3,          19,         0,         74,         6,          18,         1,           -14400,       "EDT"));
        assert_eq!(unsafe { super::mktime_z(tz, &tm) }, time); // Round trip
        unsafe { super::tzfree(tz) };
    }

    #[test]
    fn posix_conversions() {
        let posixtime: TimeT = 536457599;
        let tzname = CString::new("UTC").unwrap();
        let tz = unsafe { tzalloc(tzname.as_ptr()) };
        assert_ne!(tz, std::ptr::null_mut());
        let time = unsafe { posix2time_z(tz, posixtime) };
        let mut tm = MaybeUninit::<Tm>::uninit();
        let ret = unsafe { localtime_rz(tz, &time, tm.as_mut_ptr()) };
        assert_ne!(ret, std::ptr::null_mut());
        let tm = unsafe { tm.assume_init() };
        let zone: &str = unsafe { CStr::from_ptr(tm.tm_zone).to_str().expect("correct utf8") };
        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),
                   (59,        59,        23,         31,         11,        86,         3,          364,        0,           0,            "UTC"));
        assert_eq!(unsafe { time2posix_z(tz, time) }, posixtime); // Round Trip
        unsafe { super::tzfree(tz) };
    }

    #[test]
    fn test_readme_deps() {
        version_sync::assert_markdown_deps_updated!("README.md");
    }
}