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