time/sys/local_offset_at/
unix.rs

1//! Get the system's UTC offset on Unix.
2
3use core::mem::MaybeUninit;
4
5use crate::{OffsetDateTime, UtcOffset};
6
7/// Convert the given Unix timestamp to a `libc::tm`. Returns `None` on any error.
8#[inline]
9fn timestamp_to_tm(timestamp: i64) -> Option<libc::tm> {
10    #[allow(
11        clippy::useless_conversion,
12        reason = "the exact type of `timestamp` beforehand can vary, so this conversion is \
13                  necessary"
14    )]
15    let timestamp = timestamp.try_into().ok()?;
16
17    let mut tm = MaybeUninit::uninit();
18
19    // Safety: We are calling a system API, which mutates the `tm` variable. If a null
20    // pointer is returned, an error occurred.
21    let tm_ptr = unsafe { libc::localtime_r(&timestamp, tm.as_mut_ptr()) };
22
23    if tm_ptr.is_null() {
24        None
25    } else {
26        // Safety: The value was initialized, as we no longer have a null pointer.
27        Some(unsafe { tm.assume_init() })
28    }
29}
30
31/// Convert a `libc::tm` to a `UtcOffset`. Returns `None` on any error.
32// This is available to any target known to have the `tm_gmtoff` extension.
33#[cfg(any(
34    target_os = "redox",
35    target_os = "linux",
36    target_os = "l4re",
37    target_os = "android",
38    target_os = "emscripten",
39    target_os = "macos",
40    target_os = "ios",
41    target_os = "watchos",
42    target_os = "freebsd",
43    target_os = "dragonfly",
44    target_os = "openbsd",
45    target_os = "netbsd",
46    target_os = "haiku",
47))]
48#[inline]
49fn tm_to_offset(_unix_timestamp: i64, tm: libc::tm) -> Option<UtcOffset> {
50    let seconds = tm.tm_gmtoff.try_into().ok()?;
51    UtcOffset::from_whole_seconds(seconds).ok()
52}
53
54/// Convert a `libc::tm` to a `UtcOffset`. Returns `None` on any error.
55///
56/// This method can return an incorrect value, as it only approximates the `tm_gmtoff` field. The
57/// reason for this is that daylight saving time does not start on the same date every year, nor are
58/// the rules for daylight saving time the same for every year. This implementation assumes 1970 is
59/// equivalent to every other year, which is not always the case.
60#[cfg(not(any(
61    target_os = "redox",
62    target_os = "linux",
63    target_os = "l4re",
64    target_os = "android",
65    target_os = "emscripten",
66    target_os = "macos",
67    target_os = "ios",
68    target_os = "watchos",
69    target_os = "freebsd",
70    target_os = "dragonfly",
71    target_os = "openbsd",
72    target_os = "netbsd",
73    target_os = "haiku",
74)))]
75#[inline]
76fn tm_to_offset(unix_timestamp: i64, tm: libc::tm) -> Option<UtcOffset> {
77    use crate::Date;
78
79    let mut tm = tm;
80    if tm.tm_sec == 60 {
81        // Leap seconds are not currently supported.
82        tm.tm_sec = 59;
83    }
84
85    let local_timestamp =
86        Date::from_ordinal_date(1900 + tm.tm_year, u16::try_from(tm.tm_yday).ok()? + 1)
87            .ok()?
88            .with_hms(
89                tm.tm_hour.try_into().ok()?,
90                tm.tm_min.try_into().ok()?,
91                tm.tm_sec.try_into().ok()?,
92            )
93            .ok()?
94            .assume_utc()
95            .unix_timestamp();
96
97    let diff_secs = (local_timestamp - unix_timestamp).try_into().ok()?;
98
99    UtcOffset::from_whole_seconds(diff_secs).ok()
100}
101
102/// Obtain the system's UTC offset.
103#[inline]
104pub(super) fn local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
105    let unix_timestamp = datetime.unix_timestamp();
106    let tm = timestamp_to_tm(unix_timestamp)?;
107    tm_to_offset(unix_timestamp, tm)
108}