Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use thread safe libtz instead of libc to get local timezone offset.
Fixes #293 without needing to disable the feature.
- Loading branch information
Showing
3 changed files
with
7 additions
and
140 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,135 +1,13 @@ | ||
//! Get the system's UTC offset on Unix. | ||
|
||
use core::mem::MaybeUninit; | ||
use libtz::Timezone; | ||
|
||
use crate::util::local_offset::{self, Soundness}; | ||
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<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(×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(_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> { | ||
// 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 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 | ||
} | ||
let tz = Timezone::default().ok()?; | ||
let tm = tz.localtime(datetime.unix_timestamp()).ok()?; | ||
let seconds = tm.tm_gmtoff.try_into().ok()?; | ||
UtcOffset::from_whole_seconds(seconds).ok() | ||
} |