Skip to content

Commit

Permalink
Windows: handle DST transitions better by calculating it from timezon…
Browse files Browse the repository at this point in the history
…e info
  • Loading branch information
pitdicker committed Apr 18, 2023
1 parent 18f6678 commit c290c0f
Showing 1 changed file with 191 additions and 127 deletions.
318 changes: 191 additions & 127 deletions src/offset/local/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,158 +8,222 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use core::mem::MaybeUninit;
use std::io::Error;
use std::mem::MaybeUninit;
use std::ptr;
use std::result::Result;

use winapi::shared::minwindef::FILETIME;
use num_traits::FromPrimitive;
use winapi::um::minwinbase::SYSTEMTIME;
use winapi::um::sysinfoapi::GetLocalTime;
use winapi::um::timezoneapi::{
SystemTimeToFileTime, SystemTimeToTzSpecificLocalTime, TzSpecificLocalTimeToSystemTime,
};
use winapi::um::sysinfoapi::GetSystemTime;
use winapi::um::timezoneapi::{GetTimeZoneInformationForYear, TIME_ZONE_INFORMATION};

use super::{FixedOffset, Local};
use crate::{DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Timelike};

/// This macro calls a Windows API FFI and checks whether the function errored with the provided error_id. If an error returns,
/// the macro will return an `Error::last_os_error()`.
///
/// # Safety
///
/// The provided error ID must align with the provided Windows API, providing the wrong ID could lead to UB.
macro_rules! windows_sys_call {
($name:ident($($arg:expr),*), $error_id:expr) => {
if $name($($arg),*) == $error_id {
return Err(Error::last_os_error());
}
}
}

const HECTONANOSECS_IN_SEC: i64 = 10_000_000;
const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC;
use super::{FixedOffset, Local, TzInfo};
use crate::{DateTime, Datelike, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Weekday};

// We don't use `GetLocalTime` because its results can be ambiguous, while conversion from UTC
// never fails.
pub(super) fn now() -> DateTime<Local> {
LocalSysTime::local().datetime()
let mut now = MaybeUninit::<SYSTEMTIME>::uninit();
unsafe { GetSystemTime(now.as_mut_ptr()) }
// SAFETY: GetSystemTime cannot fail according to spec, so we can assume the value
// is initialized.
let now_naive = systemtime_to_naive_dt(unsafe { now.assume_init() }, 0).unwrap();

naive_to_local(&now_naive, false).single().expect("Current local time must exist")
}

/// Converts a local `NaiveDateTime` to the `time::Timespec`.
pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult<DateTime<Local>> {
let naive_sys_time = system_time_from_naive_date_time(d);

let local_sys_time = match local {
false => LocalSysTime::from_utc_time(naive_sys_time),
true => LocalSysTime::from_local_time(naive_sys_time),
let tz_info = match TzInfo::get_current_for_year(d.year()) {
Some(i) => i,
None => return LocalResult::None,
};

if let Ok(local) = local_sys_time {
return LocalResult::Single(local.datetime());
match local {
false => tz_info.local_from_utc_time(d),
true => tz_info.local_from_local_time(d),
}
LocalResult::None
}

struct LocalSysTime {
inner: SYSTEMTIME,
offset: i32,
}

impl LocalSysTime {
fn local() -> Self {
let mut now = MaybeUninit::<SYSTEMTIME>::uninit();
unsafe { GetLocalTime(now.as_mut_ptr()) }
// SAFETY: GetLocalTime cannot fail according to spec, so we can assume the value
// is initialized.
let st = unsafe { now.assume_init() };

Self::from_local_time(st).expect("Current local time must exist")
// The basis for Windows timezone and DST support has been in place since Windows 2000. It does not
// allow for complex rules like the IANA timezone database:
// - A timezone has the same base offset the whole year.
// - There are either zero or two DST transitions.
// - As of Vista(?) only years from 2004 until a few years into the future are supported.
// - All other years get the base settings, which seem to be that of the current year.
impl TzInfo {
fn get_current_for_year(year: i32) -> Option<Self> {
// The API limits years to 1601..=30827.
// Working with timezones and daylight saving time this far into the past or future makes
// little sense. But whatever is extrapolated for 1601 or 30827 is what can be extrapolated
// for years beyond.
let ref_year = clamp(year, 1601, 30827) as u16;
let tz_info = unsafe {
let mut tz_info = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit();
if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 {
return None;
}
tz_info.assume_init()
};
Some(Self {
std_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.StandardBias) as i32 * 60)?,
dst_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.DaylightBias) as i32 * 60)?,
std_transition: systemtime_to_naive_dt(tz_info.StandardDate, year),
dst_transition: systemtime_to_naive_dt(tz_info.DaylightDate, year),
})
}

fn from_utc_time(utc_time: SYSTEMTIME) -> Result<Self, Error> {
let local_time = utc_to_local_time(&utc_time)?;
let utc_secs = system_time_as_unix_seconds(&utc_time)?;
let local_secs = system_time_as_unix_seconds(&local_time)?;
let offset = (local_secs - utc_secs) as i32;
Ok(Self { inner: local_time, offset })
// We don't use `SystemTimeToTzSpecificLocalTime` because it doesn't support the same range of
// dates as Chrono. Also it really isn't that difficult to work out the correct offset from the
// provided DST rules.
fn local_from_utc_time(&self, utc_time: &NaiveDateTime) -> LocalResult<DateTime<Local>> {
// Using a `TzInfo` based on the year of an UTC datetime is technically wrong, we should be
// using the rules for the year of the corresponding local time. But this matches what
//`SystemTimeToTzSpecificLocalTime` is documented to do.
let offset = match (self.std_transition, self.dst_transition) {
(Some(std_transition), Some(dst_transition)) => {
let std_transition_utc = std_transition - self.dst_offset;
let dst_transition_utc = dst_transition - self.std_offset;
if dst_transition_utc < std_transition_utc {
if utc_time >= &dst_transition_utc && utc_time < &std_transition_utc {
self.dst_offset
} else {
self.std_offset
}
} else {
if utc_time >= &std_transition_utc && utc_time < &dst_transition_utc {
self.std_offset
} else {
self.dst_offset
}
}
}
_ => self.std_offset,
};
LocalResult::Single(DateTime::from_utc(*utc_time, offset))
}

fn from_local_time(local_time: SYSTEMTIME) -> Result<Self, Error> {
let utc_time = local_to_utc_time(&local_time)?;
let utc_secs = system_time_as_unix_seconds(&utc_time)?;
let local_secs = system_time_as_unix_seconds(&local_time)?;
let offset = (local_secs - utc_secs) as i32;
Ok(Self { inner: local_time, offset })
}

fn datetime(self) -> DateTime<Local> {
let st = self.inner;

let date =
NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32).unwrap();
let time = NaiveTime::from_hms(st.wHour as u32, st.wMinute as u32, st.wSecond as u32);

let offset = FixedOffset::east_opt(self.offset).unwrap();
DateTime::from_utc(date.and_time(time) - offset, offset)
// We don't use `TzSpecificLocalTimeToSystemTime` because it doesn't let us choose how to handle
// ambiguous cases (during a DST transition). Instead we get the timezone information for the
// current year and compute it ourselves, like we do on Unix.
fn local_from_local_time(&self, local_time: &NaiveDateTime) -> LocalResult<DateTime<Local>> {
let local_result_offset = match (self.std_transition, self.dst_transition) {
(Some(_), Some(_)) => self.lookup_with_dst_transitions(local_time),
_ => LocalResult::Single(self.std_offset),
};
local_result_offset.map(|offset| DateTime::from_utc(*local_time - offset, offset))
}
}

fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME {
SYSTEMTIME {
// Valid values: 1601-30827
wYear: dt.year() as u16,
// Valid values:1-12
wMonth: dt.month() as u16,
// Valid values: 0-6, starting Sunday.
// NOTE: enum returns 1-7, starting Monday, so we are
// off here, but this is not currently used in local.
wDayOfWeek: dt.weekday() as u16,
// Valid values: 1-31
wDay: dt.day() as u16,
// Valid values: 0-23
wHour: dt.hour() as u16,
// Valid values: 0-59
wMinute: dt.minute() as u16,
// Valid values: 0-59
wSecond: dt.second() as u16,
// Valid values: 0-999
wMilliseconds: 0,
// FIXME: use std::cmp::clamp when MSRV >= 1.50
fn clamp(val: i32, min: i32, max: i32) -> i32 {
assert!(min <= max);
if val < min {
min
} else if val > max {
max
} else {
val
}
}

pub(crate) fn local_to_utc_time(local: &SYSTEMTIME) -> Result<SYSTEMTIME, Error> {
let mut sys_time = MaybeUninit::<SYSTEMTIME>::uninit();
unsafe {
windows_sys_call!(
TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()),
0
)
fn systemtime_to_naive_dt(st: SYSTEMTIME, year: i32) -> Option<NaiveDateTime> {
if st.wYear == 0 && st.wMonth == 0 {
return None; // No DST transitions for this year in this timezone.
}
let time = NaiveTime::from_hms_milli_opt(
st.wHour as u32,
st.wMinute as u32,
st.wSecond as u32,
st.wMilliseconds as u32,
)?;
let day_of_week = Weekday::from_u16(st.wDayOfWeek)?.pred();
let date = if st.wYear == 0 {
if let Some(date) = NaiveDate::from_weekday_of_month_opt(
year as i32,
st.wMonth as u32,
day_of_week,
st.wDay as u8,
) {
date
} else if st.wDay == 5 {
NaiveDate::from_weekday_of_month_opt(year as i32, st.wMonth as u32, day_of_week, 4)?
} else {
return None;
}
} else {
NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32)?
};
// SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can
// assume the value is initialized.
Ok(unsafe { sys_time.assume_init() })
Some(date.and_time(time))
}

pub(crate) fn utc_to_local_time(utc_time: &SYSTEMTIME) -> Result<SYSTEMTIME, Error> {
let mut local = MaybeUninit::<SYSTEMTIME>::uninit();
unsafe {
windows_sys_call!(
SystemTimeToTzSpecificLocalTime(ptr::null(), utc_time, local.as_mut_ptr()),
0
)
};
// SAFETY: SystemTimeToTzSpecificLocalTime must have succeeded at this point, so we can
// assume the value is initialized.
Ok(unsafe { local.assume_init() })
}
#[test]
fn verify_against_tz_specific_local_time_to_system_time() {
use crate::offset::TimeZone;
use crate::{DateTime, Duration, FixedOffset, Timelike};
use std::mem::MaybeUninit;
use std::ptr;
use winapi::shared::minwindef::FILETIME;
use winapi::um::minwinbase::SYSTEMTIME;
use winapi::um::timezoneapi::{SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime};

fn from_local_time(dt: &NaiveDateTime) -> DateTime<Local> {
let st = system_time_from_naive_date_time(dt);
let utc_time = local_to_utc_time(&st);
let utc_secs = system_time_as_unix_seconds(&utc_time);
let local_secs = system_time_as_unix_seconds(&st);
let offset = (local_secs - utc_secs) as i32;
let offset = FixedOffset::east_opt(offset).unwrap();
DateTime::from_utc(*dt - offset, offset)
}
fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME {
SYSTEMTIME {
// Valid values: 1601-30827
wYear: dt.year() as u16,
// Valid values:1-12
wMonth: dt.month() as u16,
// Valid values: 0-6, starting Sunday.
// NOTE: enum returns 1-7, starting Monday, so we are
// off here, but this is not currently used in local.
wDayOfWeek: dt.weekday() as u16,
// Valid values: 1-31
wDay: dt.day() as u16,
// Valid values: 0-23
wHour: dt.hour() as u16,
// Valid values: 0-59
wMinute: dt.minute() as u16,
// Valid values: 0-59
wSecond: dt.second() as u16,
// Valid values: 0-999
wMilliseconds: 0,
}
}
pub fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME {
let mut sys_time = MaybeUninit::<SYSTEMTIME>::uninit();
unsafe { TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()) };
// SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can
// assume the value is initialized.
unsafe { sys_time.assume_init() }
}
const HECTONANOSECS_IN_SEC: i64 = 10_000_000;
const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC;
pub fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 {
let mut init = MaybeUninit::<FILETIME>::uninit();
unsafe {
SystemTimeToFileTime(st, init.as_mut_ptr());
}
// SystemTimeToFileTime must have succeeded at this point, so we can assum the value is
// initalized.
let filetime = unsafe { init.assume_init() };
let bit_shift = ((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64);
let unix_secs = (bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC;
unix_secs
}

/// Returns a i64 value representing the unix seconds conversion of the current `WinSystemTime`.
pub(crate) fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> Result<i64, Error> {
let mut init = MaybeUninit::<FILETIME>::uninit();
unsafe { windows_sys_call!(SystemTimeToFileTime(st, init.as_mut_ptr()), 0) }
// SystemTimeToFileTime must have succeeded at this point, so we can assum the value is
// initalized.
let filetime = unsafe { init.assume_init() };
let bit_shift = ((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64);
let unix_secs = (bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC;
Ok(unix_secs)
let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 30, 0).unwrap();

while date.year() < 2078 {
// Windows doesn't handle non-existing dates, it just treats it as valid.
if let Some(our_result) = Local.from_local_datetime(&date).earliest() {
assert_eq!(from_local_time(&date), our_result);
}
date += Duration::hours(1);
}
}

0 comments on commit c290c0f

Please sign in to comment.