1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! Get the system's UTC offset on Unix.

use core::mem::MaybeUninit;

use crate::util::local_offset::{self, Soundness};
use crate::{OffsetDateTime, UtcOffset};

/// Whether the operating system has a thread-safe environment. This allows bypassing the check for
/// if the process is multi-threaded.
// This is the same value as `cfg!(target_os = "x")`.
// Use byte-strings to work around current limitations of const eval.
const OS_HAS_THREAD_SAFE_ENVIRONMENT: bool = match std::env::consts::OS.as_bytes() {
    // https://github.com/illumos/illumos-gate/blob/0fb96ba1f1ce26ff8b286f8f928769a6afcb00a6/usr/src/lib/libc/port/gen/getenv.c
    b"illumos"
    // https://github.com/NetBSD/src/blob/f45028636a44111bc4af44d460924958a4460844/lib/libc/stdlib/getenv.c
    // https://github.com/NetBSD/src/blob/f45028636a44111bc4af44d460924958a4460844/lib/libc/stdlib/setenv.c
    | b"netbsd"
    => true,
    _ => false,
};

/// 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<libc::tm> {
    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(&timestamp, 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(_unix_timestamp: i64, tm: libc::tm) -> Option<UtcOffset> {
    let seconds = tm.tm_gmtoff.try_into().ok()?;
    UtcOffset::from_whole_seconds(seconds).ok()
}

/// 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. The
/// reason for this 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(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(unix_timestamp: i64, tm: libc::tm) -> Option<UtcOffset> {
    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 = (local_timestamp - unix_timestamp).try_into().ok()?;

    UtcOffset::from_whole_seconds(diff_secs).ok()
}

/// Obtain the system's UTC offset.
pub(super) fn local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
    // Continue to obtaining the UTC offset if and only if the call is sound or the user has
    // explicitly opted out of soundness.
    //
    // Soundness can be guaranteed either by knowledge of the operating system or knowledge that the
    // process is single-threaded. If the process is single-threaded, then the environment cannot
    // be 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 OS_HAS_THREAD_SAFE_ENVIRONMENT
        || local_offset::get_soundness() == Soundness::Unsound
        || num_threads::is_single_threaded() == Some(true)
    {
        let unix_timestamp = datetime.unix_timestamp();
        // 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(unix_timestamp) }?;
        tm_to_offset(unix_timestamp, tm)
    } else {
        None
    }
}