Skip to content

Commit

Permalink
Move local offset function to proper sys directory
Browse files Browse the repository at this point in the history
  • Loading branch information
jhpratt committed Nov 11, 2021
1 parent fff7f80 commit a2b67af
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 233 deletions.
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ mod rand;
#[cfg_attr(__time_03_docs, doc(cfg(feature = "serde")))]
#[allow(missing_copy_implementations, missing_debug_implementations)]
pub mod serde;
mod sys;
#[cfg(test)]
mod tests;
mod time;
Expand Down
7 changes: 7 additions & 0 deletions src/sys/local_offset_at/imp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! A fallback for any OS not covered.

use crate::{OffsetDateTime, UtcOffset};

pub(super) fn local_offset_at(_datetime: OffsetDateTime) -> Option<UtcOffset> {
None
}
13 changes: 13 additions & 0 deletions src/sys/local_offset_at/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! A method to obtain the local offset from UTC.

#[cfg_attr(target_family = "windows", path = "windows.rs")]
#[cfg_attr(target_family = "unix", path = "unix.rs")]
mod imp;

use crate::{OffsetDateTime, UtcOffset};

/// Attempt to obtain the system's UTC offset. If the offset cannot be determined, `None` is
/// returned.
pub(crate) fn local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
imp::local_offset_at(datetime)
}
112 changes: 112 additions & 0 deletions src/sys/local_offset_at/unix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! Get the system's UTC offset on Unix.

#[cfg(unsound_local_offset)]
use core::convert::TryInto;
#[cfg(unsound_local_offset)]
use core::mem::MaybeUninit;

use crate::{OffsetDateTime, UtcOffset};

/// Obtain the system's UTC offset.
// See #293 for details.
#[cfg(not(unsound_local_offset))]
#[allow(clippy::missing_const_for_fn)]
pub(super) fn local_offset_at(_datetime: OffsetDateTime) -> Option<UtcOffset> {
None
}

/// Convert the given Unix timestamp to a `libc::tm`. Returns `None` on any error.
#[cfg(unsound_local_offset)]
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.
#[allow(unsafe_code)]
unsafe {
tzset();
}

// Safety: We are calling a system API, which mutates the `tm` variable. If a null
// pointer is returned, an error occurred.
#[allow(unsafe_code)]
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.
#[allow(unsafe_code)]
{
Some(unsafe { tm.assume_init() })
}
}
}

/// Convert a `libc::tm` to a `UtcOffset`. Returns `None` on any error.
// `tm_gmtoff` extension
#[cfg(unsound_local_offset)]
#[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
fn tm_to_offset(tm: libc::tm) -> Option<UtcOffset> {
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(unsound_local_offset)]
#[cfg(any(target_os = "solaris", target_os = "illumos"))]
fn tm_to_offset(tm: libc::tm) -> Option<UtcOffset> {
use core::convert::TryFrom;

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.
#[cfg(unsound_local_offset)]
pub(super) fn local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
tm_to_offset(timestamp_to_tm(datetime.unix_timestamp())?)
}
117 changes: 117 additions & 0 deletions src/sys/local_offset_at/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! Get the system's UTC offset on Windows.

use core::convert::TryInto;
use core::mem::MaybeUninit;

// ffi: WINAPI FILETIME struct
#[repr(C)]
#[allow(non_snake_case)]
struct FileTime {
dwLowDateTime: u32,
dwHighDateTime: u32,
}

// ffi: WINAPI SYSTEMTIME struct
#[repr(C)]
#[allow(non_snake_case)]
struct SystemTime {
wYear: u16,
wMonth: u16,
wDayOfWeek: u16,
wDay: u16,
wHour: u16,
wMinute: u16,
wSecond: u16,
wMilliseconds: u16,
}

#[link(name = "kernel32")]
extern "system" {
// https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-systemtimetofiletime
fn SystemTimeToFileTime(lpSystemTime: *const SystemTime, lpFileTime: *mut FileTime) -> i32;

// https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-systemtimetotzspecificlocaltime
fn SystemTimeToTzSpecificLocalTime(
lpTimeZoneInformation: *const std::ffi::c_void, // We only pass `nullptr` here
lpUniversalTime: *const SystemTime,
lpLocalTime: *mut SystemTime,
) -> i32;
}

/// Convert a `SYSTEMTIME` to a `FILETIME`. Returns `None` if any error occurred.
fn systemtime_to_filetime(systime: &SystemTime) -> Option<FileTime> {
let mut ft = MaybeUninit::uninit();

// Safety: `SystemTimeToFileTime` is thread-safe. We are only assuming initialization if
// the call succeeded.
#[allow(unsafe_code)]
{
if 0 == unsafe { SystemTimeToFileTime(systime, ft.as_mut_ptr()) } {
// failed
None
} else {
Some(unsafe { ft.assume_init() })
}
}
}

/// Convert a `FILETIME` to an `i64`, representing a number of seconds.
fn filetime_to_secs(filetime: &FileTime) -> i64 {
/// FILETIME represents 100-nanosecond intervals
const FT_TO_SECS: i64 = 10_000_000;
((filetime.dwHighDateTime as i64) << 32 | filetime.dwLowDateTime as i64) / FT_TO_SECS
}

/// Convert an [`OffsetDateTime`] to a `SYSTEMTIME`.
fn offset_to_systemtime(datetime: OffsetDateTime) -> SystemTime {
let (_, month, day_of_month) = datetime.to_offset(UtcOffset::UTC).date().to_calendar_date();
SystemTime {
wYear: datetime.year() as _,
wMonth: month as _,
wDay: day_of_month as _,
wDayOfWeek: 0, // ignored
wHour: datetime.hour() as _,
wMinute: datetime.minute() as _,
wSecond: datetime.second() as _,
wMilliseconds: datetime.millisecond(),
}
}

/// Obtain the system's UTC offset.
pub(super) fn local_offset_at(datetime: OffsetDateTime) -> Option<UtcOffset> {
// This function falls back to UTC if any system call fails.
let systime_utc = offset_to_systemtime(datetime.to_offset(UtcOffset::UTC));

// Safety: `local_time` is only read if it is properly initialized, and
// `SystemTimeToTzSpecificLocalTime` is thread-safe.
#[allow(unsafe_code)]
let systime_local = unsafe {
let mut local_time = MaybeUninit::uninit();

if 0 == SystemTimeToTzSpecificLocalTime(
core::ptr::null(), // use system's current timezone
&systime_utc,
local_time.as_mut_ptr(),
) {
// call failed
return None;
} else {
local_time.assume_init()
}
};

// Convert SYSTEMTIMEs to FILETIMEs so we can perform arithmetic on them.
let ft_system = systemtime_to_filetime(&systime_utc)?;
let ft_local = systemtime_to_filetime(&systime_local)?;

let diff_secs: i32 = (filetime_to_secs(&ft_local) - filetime_to_secs(&ft_system))
.try_into()
.ok()?;

UtcOffset::from_hms(
(diff_secs / 3_600) as _,
((diff_secs / 60) % 60) as _,
(diff_secs % 60) as _,
)
.ok()
}
7 changes: 7 additions & 0 deletions src/sys/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Functions with a common interface that rely on system calls.

#[cfg(feature = "local-offset")]
mod local_offset_at;

#[cfg(feature = "local-offset")]
pub(crate) use local_offset_at::local_offset_at;
Loading

0 comments on commit a2b67af

Please sign in to comment.