//! Get the system's UTC offset on Unix. use core::mem::MaybeUninit; use crate::{OffsetDateTime, UtcOffset}; /// Convert the given Unix timestamp to a `libc::tm`. Returns `None` on any error. /// /// # Safety /// /// This method must only be called when the process is single-threaded. /// /// This method will remain `unsafe` until `std::env::set_var` is deprecated or has its behavior /// altered. This method is, on its own, safe. It is the presence of a safe, unsound way to set /// environment variables that makes it unsafe. unsafe fn timestamp_to_tm(timestamp: i64) -> Option { extern "C" { #[cfg_attr(target_os = "netbsd", link_name = "__tzset50")] fn tzset(); } // The exact type of `timestamp` beforehand can vary, so this conversion is necessary. #[allow(clippy::useless_conversion)] let timestamp = timestamp.try_into().ok()?; let mut tm = MaybeUninit::uninit(); // Update timezone information from system. `localtime_r` does not do this for us. // // Safety: tzset is thread-safe. unsafe { tzset() }; // Safety: We are calling a system API, which mutates the `tm` variable. If a null // pointer is returned, an error occurred. let tm_ptr = unsafe { libc::localtime_r(×tamp, tm.as_mut_ptr()) }; if tm_ptr.is_null() { None } else { // Safety: The value was initialized, as we no longer have a null pointer. Some(unsafe { tm.assume_init() }) } } /// Convert a `libc::tm` to a `UtcOffset`. Returns `None` on any error. // This is available to any target known to have the `tm_gmtoff` extension. #[cfg(any( target_os = "redox", target_os = "linux", target_os = "l4re", target_os = "android", target_os = "emscripten", target_os = "macos", target_os = "ios", target_os = "watchos", target_os = "freebsd", target_os = "dragonfly", target_os = "openbsd", target_os = "netbsd", target_os = "haiku", ))] fn tm_to_offset(tm: libc::tm) -> Option { let seconds: i32 = tm.tm_gmtoff.try_into().ok()?; UtcOffset::from_hms( (seconds / 3_600) as _, ((seconds / 60) % 60) as _, (seconds % 60) as _, ) .ok() } /// Convert a `libc::tm` to a `UtcOffset`. Returns `None` on any error. #[cfg(all( not(unsound_local_offset), not(any( target_os = "redox", target_os = "linux", target_os = "l4re", target_os = "android", target_os = "emscripten", target_os = "macos", target_os = "ios", target_os = "watchos", target_os = "freebsd", target_os = "dragonfly", target_os = "openbsd", target_os = "netbsd", target_os = "haiku", )) ))] #[allow(unused_variables, clippy::missing_const_for_fn)] fn tm_to_offset(tm: libc::tm) -> Option { None } /// Convert a `libc::tm` to a `UtcOffset`. Returns `None` on any error. // This method can return an incorrect value, as it only approximates the `tm_gmtoff` field. As such // it is gated behind `--cfg unsound_local_offset`. The reason it can return an incorrect value is // that daylight saving time does not start on the same date every year, nor are the rules for // daylight saving time the same for every year. This implementation assumes 1970 is equivalent to // every other year, which is not always the case. #[cfg(all( unsound_local_offset, not(any( target_os = "redox", target_os = "linux", target_os = "l4re", target_os = "android", target_os = "emscripten", target_os = "macos", target_os = "ios", target_os = "watchos", target_os = "freebsd", target_os = "dragonfly", target_os = "openbsd", target_os = "netbsd", target_os = "haiku", )) ))] fn tm_to_offset(tm: libc::tm) -> Option { use crate::Date; let mut tm = tm; if tm.tm_sec == 60 { // Leap seconds are not currently supported. tm.tm_sec = 59; } let local_timestamp = Date::from_ordinal_date(1900 + tm.tm_year, u16::try_from(tm.tm_yday).ok()? + 1) .ok()? .with_hms( tm.tm_hour.try_into().ok()?, tm.tm_min.try_into().ok()?, tm.tm_sec.try_into().ok()?, ) .ok()? .assume_utc() .unix_timestamp(); let diff_secs: i32 = (local_timestamp - datetime.unix_timestamp()) .try_into() .ok()?; UtcOffset::from_hms( (diff_secs / 3_600) as _, ((diff_secs / 60) % 60) as _, (diff_secs % 60) as _, ) .ok() } /// Obtain the system's UTC offset. pub(super) fn local_offset_at(datetime: OffsetDateTime) -> Option { // Ensure that the process is single-threaded unless the user has explicitly opted out of this // check. This is to prevent issues with the environment being mutated by a different thread in // the process while execution of this function is taking place, which can cause a segmentation // fault by dereferencing a dangling pointer. // If the `num_threads` crate is incapable of determining the number of running threads, then // we conservatively return `None` to avoid a soundness bug. if !cfg!(unsound_local_offset) && num_threads::is_single_threaded() != Some(true) { return None; } // Safety: We have just confirmed that the process is single-threaded or the user has explicitly // opted out of soundness. let tm = unsafe { timestamp_to_tm(datetime.unix_timestamp()) }?; tm_to_offset(tm) }