From 7e9a4722e4711ae0497712b3e54acf7b3706f2a9 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Mon, 17 Apr 2023 20:20:47 +0200 Subject: [PATCH 1/4] Windows: rewrite using `GetTimeZoneInformationForYear` And add test against previous implementation --- src/naive/datetime/mod.rs | 17 ++ src/offset/local/mod.rs | 259 +++++++++++++++++++++++ src/offset/local/win_bindings.rs | 20 ++ src/offset/local/win_bindings.txt | 1 + src/offset/local/windows.rs | 335 ++++++++++++++++++++---------- 5 files changed, 526 insertions(+), 106 deletions(-) diff --git a/src/naive/datetime/mod.rs b/src/naive/datetime/mod.rs index c5c8953c16..d767483571 100644 --- a/src/naive/datetime/mod.rs +++ b/src/naive/datetime/mod.rs @@ -790,6 +790,23 @@ impl NaiveDateTime { NaiveDateTime { date, time } } + /// Subtracts given `FixedOffset` from the current datetime. + /// The resulting value may be outside the valid range of [`NaiveDateTime`]. + /// + /// This can be useful for intermediate values, but the resulting out-of-range `NaiveDate` + /// should not be exposed to library users. + #[must_use] + #[allow(unused)] // currently only used in `Local` but not on all platforms + pub(crate) fn overflowing_sub_offset(self, rhs: FixedOffset) -> NaiveDateTime { + let (time, days) = self.time.overflowing_sub_offset(rhs); + let date = match days { + -1 => self.date.pred_opt().unwrap_or(NaiveDate::BEFORE_MIN), + 1 => self.date.succ_opt().unwrap_or(NaiveDate::AFTER_MAX), + _ => self.date, + }; + NaiveDateTime { date, time } + } + /// Subtracts given `TimeDelta` from the current date and time. /// /// As a part of Chrono's [leap second handling](./struct.NaiveTime.html#leap-second-handling), diff --git a/src/offset/local/mod.rs b/src/offset/local/mod.rs index 724f3597ad..51bd852ef7 100644 --- a/src/offset/local/mod.rs +++ b/src/offset/local/mod.rs @@ -3,6 +3,9 @@ //! The local (system) time zone. +#[cfg(windows)] +use std::cmp::Ordering; + #[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] use rkyv::{Archive, Deserialize, Serialize}; @@ -183,11 +186,96 @@ impl TimeZone for Local { } } +#[cfg(windows)] +#[derive(Copy, Clone, Eq, PartialEq)] +struct Transition { + transition_utc: NaiveDateTime, + offset_before: FixedOffset, + offset_after: FixedOffset, +} + +#[cfg(windows)] +impl Transition { + fn new( + transition_local: NaiveDateTime, + offset_before: FixedOffset, + offset_after: FixedOffset, + ) -> Transition { + // It is no problem if the transition time in UTC falls a couple of hours inside the buffer + // space around the `NaiveDateTime` range (although it is very theoretical to have a + // transition at midnight around `NaiveDate::(MIN|MAX)`. + let transition_utc = transition_local.overflowing_sub_offset(offset_before); + Transition { transition_utc, offset_before, offset_after } + } +} + +#[cfg(windows)] +impl PartialOrd for Transition { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.transition_utc.cmp(&other.transition_utc)) + } +} + +#[cfg(windows)] +impl Ord for Transition { + fn cmp(&self, other: &Self) -> Ordering { + self.transition_utc.cmp(&other.transition_utc) + } +} + +// Calculate the time in UTC given a local time and transitions. +// `transitions` must be sorted. +#[cfg(windows)] +fn lookup_with_dst_transitions( + transitions: &[Transition], + dt: NaiveDateTime, +) -> LocalResult { + for t in transitions.iter() { + // A transition can result in the wall clock time going forward (creating a gap) or going + // backward (creating a fold). We are interested in the earliest and latest wall time of the + // transition, as this are the times between which `dt` does may not exist or is ambiguous. + // + // It is no problem if the transition times falls a couple of hours inside the buffer + // space around the `NaiveDateTime` range (although it is very theoretical to have a + // transition at midnight around `NaiveDate::(MIN|MAX)`. + let (offset_min, offset_max) = + match t.offset_after.local_minus_utc() > t.offset_before.local_minus_utc() { + true => (t.offset_before, t.offset_after), + false => (t.offset_after, t.offset_before), + }; + let wall_earliest = t.transition_utc.overflowing_add_offset(offset_min); + let wall_latest = t.transition_utc.overflowing_add_offset(offset_max); + + if dt < wall_earliest { + return LocalResult::Single(t.offset_before); + } else if dt <= wall_latest { + return match t.offset_after.local_minus_utc().cmp(&t.offset_before.local_minus_utc()) { + Ordering::Equal => LocalResult::Single(t.offset_before), + Ordering::Less => LocalResult::Ambiguous(t.offset_before, t.offset_after), + Ordering::Greater => { + if dt == wall_earliest { + LocalResult::Single(t.offset_before) + } else if dt == wall_latest { + LocalResult::Single(t.offset_after) + } else { + LocalResult::None + } + } + }; + } + } + LocalResult::Single(transitions.last().unwrap().offset_after) +} + #[cfg(test)] mod tests { use super::Local; + #[cfg(windows)] + use crate::offset::local::{lookup_with_dst_transitions, Transition}; use crate::offset::TimeZone; use crate::{Datelike, TimeDelta, Utc}; + #[cfg(windows)] + use crate::{FixedOffset, LocalResult, NaiveDate, NaiveDateTime}; #[test] fn verify_correct_offsets() { @@ -264,6 +352,177 @@ mod tests { } } + #[test] + #[cfg(windows)] + fn test_lookup_with_dst_transitions() { + let ymdhms = |y, m, d, h, n, s| { + NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap() + }; + + #[track_caller] + #[allow(clippy::too_many_arguments)] + fn compare_lookup( + transitions: &[Transition], + y: i32, + m: u32, + d: u32, + h: u32, + n: u32, + s: u32, + result: LocalResult, + ) { + let dt = NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, n, s).unwrap(); + assert_eq!(lookup_with_dst_transitions(transitions, dt), result); + } + + // dst transition before std transition + // dst offset > std offset + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, dst), + Transition::new(ymdhms(2023, 10, 29, 3, 0, 0), dst, std), + ]; + compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 26, 4, 0, 0, LocalResult::Single(dst)); + + compare_lookup(&transitions, 2023, 10, 29, 1, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 10, 29, 2, 0, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 10, 29, 2, 30, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 10, 29, 4, 0, 0, LocalResult::Single(std)); + + // std transition before dst transition + // dst offset > std offset + let std = FixedOffset::east_opt(-5 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt(-4 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 24, 3, 0, 0), dst, std), + Transition::new(ymdhms(2023, 10, 27, 2, 0, 0), std, dst), + ]; + compare_lookup(&transitions, 2023, 3, 24, 1, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 24, 2, 0, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 3, 24, 2, 30, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 3, 24, 3, 0, 0, LocalResult::Ambiguous(dst, std)); + compare_lookup(&transitions, 2023, 3, 24, 4, 0, 0, LocalResult::Single(std)); + + compare_lookup(&transitions, 2023, 10, 27, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 27, 2, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 27, 2, 30, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 10, 27, 3, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 10, 27, 4, 0, 0, LocalResult::Single(dst)); + + // dst transition before std transition + // dst offset < std offset + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt((2 * 60 + 30) * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 26, 2, 30, 0), std, dst), + Transition::new(ymdhms(2023, 10, 29, 2, 0, 0), dst, std), + ]; + compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 3, 26, 2, 15, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, LocalResult::Single(dst)); + + compare_lookup(&transitions, 2023, 10, 29, 1, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 10, 29, 2, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 10, 29, 2, 15, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 10, 29, 2, 30, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, LocalResult::Single(std)); + + // std transition before dst transition + // dst offset < std offset + let std = FixedOffset::east_opt(-(4 * 60 + 30) * 60).unwrap(); + let dst = FixedOffset::east_opt(-5 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 24, 2, 0, 0), dst, std), + Transition::new(ymdhms(2023, 10, 27, 2, 30, 0), std, dst), + ]; + compare_lookup(&transitions, 2023, 3, 24, 1, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 24, 2, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 24, 2, 15, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 3, 24, 2, 30, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 24, 3, 0, 0, LocalResult::Single(std)); + + compare_lookup(&transitions, 2023, 10, 27, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 27, 2, 0, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 10, 27, 2, 15, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 10, 27, 2, 30, 0, LocalResult::Ambiguous(std, dst)); + compare_lookup(&transitions, 2023, 10, 27, 3, 0, 0, LocalResult::Single(dst)); + + // offset stays the same + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, std), + Transition::new(ymdhms(2023, 10, 29, 3, 0, 0), std, std), + ]; + compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 10, 29, 3, 0, 0, LocalResult::Single(std)); + + // single transition + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap(); + let transitions = [Transition::new(ymdhms(2023, 3, 26, 2, 0, 0), std, dst)]; + compare_lookup(&transitions, 2023, 3, 26, 1, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 0, 0, LocalResult::Single(std)); + compare_lookup(&transitions, 2023, 3, 26, 2, 30, 0, LocalResult::None); + compare_lookup(&transitions, 2023, 3, 26, 3, 0, 0, LocalResult::Single(dst)); + compare_lookup(&transitions, 2023, 3, 26, 4, 0, 0, LocalResult::Single(dst)); + } + + #[test] + #[cfg(windows)] + fn test_lookup_with_dst_transitions_limits() { + // Transition beyond UTC year end doesn't panic in year of `NaiveDate::MAX` + let std = FixedOffset::east_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::east_opt(4 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(NaiveDateTime::MAX.with_month(7).unwrap(), std, dst), + Transition::new(NaiveDateTime::MAX, dst, std), + ]; + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX.with_month(3).unwrap()), + LocalResult::Single(std) + ); + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX.with_month(8).unwrap()), + LocalResult::Single(dst) + ); + // Doesn't panic with `NaiveDateTime::MAX` as argument (which would be out of range when + // converted to UTC). + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MAX), + LocalResult::Ambiguous(dst, std) + ); + + // Transition before UTC year end doesn't panic in year of `NaiveDate::MIN` + let std = FixedOffset::west_opt(3 * 60 * 60).unwrap(); + let dst = FixedOffset::west_opt(4 * 60 * 60).unwrap(); + let transitions = [ + Transition::new(NaiveDateTime::MIN, std, dst), + Transition::new(NaiveDateTime::MIN.with_month(6).unwrap(), dst, std), + ]; + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN.with_month(3).unwrap()), + LocalResult::Single(dst) + ); + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN.with_month(8).unwrap()), + LocalResult::Single(std) + ); + // Doesn't panic with `NaiveDateTime::MIN` as argument (which would be out of range when + // converted to UTC). + assert_eq!( + lookup_with_dst_transitions(&transitions, NaiveDateTime::MIN), + LocalResult::Ambiguous(std, dst) + ); + } + #[test] #[cfg(feature = "rkyv-validation")] fn test_rkyv_validation() { diff --git a/src/offset/local/win_bindings.rs b/src/offset/local/win_bindings.rs index 592726c732..7574fb3317 100644 --- a/src/offset/local/win_bindings.rs +++ b/src/offset/local/win_bindings.rs @@ -1,10 +1,30 @@ // Bindings generated by `windows-bindgen` 0.52.0 #![allow(non_snake_case, non_upper_case_globals, non_camel_case_types, dead_code, clippy::all)] +::windows_targets::link!("kernel32.dll" "system" fn GetTimeZoneInformationForYear(wyear : u16, pdtzi : *const DYNAMIC_TIME_ZONE_INFORMATION, ptzi : *mut TIME_ZONE_INFORMATION) -> BOOL); ::windows_targets::link!("kernel32.dll" "system" fn SystemTimeToFileTime(lpsystemtime : *const SYSTEMTIME, lpfiletime : *mut FILETIME) -> BOOL); ::windows_targets::link!("kernel32.dll" "system" fn SystemTimeToTzSpecificLocalTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lpuniversaltime : *const SYSTEMTIME, lplocaltime : *mut SYSTEMTIME) -> BOOL); ::windows_targets::link!("kernel32.dll" "system" fn TzSpecificLocalTimeToSystemTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lplocaltime : *const SYSTEMTIME, lpuniversaltime : *mut SYSTEMTIME) -> BOOL); pub type BOOL = i32; +pub type BOOLEAN = u8; +#[repr(C)] +pub struct DYNAMIC_TIME_ZONE_INFORMATION { + pub Bias: i32, + pub StandardName: [u16; 32], + pub StandardDate: SYSTEMTIME, + pub StandardBias: i32, + pub DaylightName: [u16; 32], + pub DaylightDate: SYSTEMTIME, + pub DaylightBias: i32, + pub TimeZoneKeyName: [u16; 128], + pub DynamicDaylightTimeDisabled: BOOLEAN, +} +impl ::core::marker::Copy for DYNAMIC_TIME_ZONE_INFORMATION {} +impl ::core::clone::Clone for DYNAMIC_TIME_ZONE_INFORMATION { + fn clone(&self) -> Self { + *self + } +} #[repr(C)] pub struct FILETIME { pub dwLowDateTime: u32, diff --git a/src/offset/local/win_bindings.txt b/src/offset/local/win_bindings.txt index ce5d3acfe2..7fb3e2fa1c 100644 --- a/src/offset/local/win_bindings.txt +++ b/src/offset/local/win_bindings.txt @@ -1,6 +1,7 @@ --out src/offset/local/win_bindings.rs --config flatten sys --filter + Windows.Win32.System.Time.GetTimeZoneInformationForYear Windows.Win32.System.Time.SystemTimeToFileTime Windows.Win32.System.Time.SystemTimeToTzSpecificLocalTime Windows.Win32.System.Time.TzSpecificLocalTimeToSystemTime diff --git a/src/offset/local/windows.rs b/src/offset/local/windows.rs index 539b6880fd..cee09eca8b 100644 --- a/src/offset/local/windows.rs +++ b/src/offset/local/windows.rs @@ -8,132 +8,255 @@ // 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::cmp::Ordering; +use std::convert::TryFrom; +use std::mem::MaybeUninit; use std::ptr; -use std::result::Result; -use super::win_bindings::{ - SystemTimeToFileTime, SystemTimeToTzSpecificLocalTime, TzSpecificLocalTimeToSystemTime, - FILETIME, SYSTEMTIME, -}; +use super::win_bindings::{GetTimeZoneInformationForYear, SYSTEMTIME, TIME_ZONE_INFORMATION}; -use super::FixedOffset; -use crate::{Datelike, LocalResult, NaiveDateTime, 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 crate::offset::local::{lookup_with_dst_transitions, Transition}; +use crate::{Datelike, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Weekday}; +// 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. +// +// This method uses `overflowing_sub_offset` because it is no problem if the transition time in UTC +// falls a couple of hours inside the buffer space around the `NaiveDateTime` range (although it is +// very theoretical to have a transition at midnight around `NaiveDate::(MIN|MAX)`. pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult { - offset(utc, false) + // 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 tz_info = match TzInfo::for_year(utc.year()) { + Some(tz_info) => tz_info, + None => return LocalResult::None, + }; + let offset = match (tz_info.std_transition, tz_info.dst_transition) { + (Some(std_transition), Some(dst_transition)) => { + let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset); + let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset); + if dst_transition_utc < std_transition_utc { + match utc >= &dst_transition_utc && utc < &std_transition_utc { + true => tz_info.dst_offset, + false => tz_info.std_offset, + } + } else { + match utc >= &std_transition_utc && utc < &dst_transition_utc { + true => tz_info.std_offset, + false => tz_info.dst_offset, + } + } + } + (Some(std_transition), None) => { + let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset); + match utc < &std_transition_utc { + true => tz_info.dst_offset, + false => tz_info.std_offset, + } + } + (None, Some(dst_transition)) => { + let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset); + match utc < &dst_transition_utc { + true => tz_info.std_offset, + false => tz_info.dst_offset, + } + } + (None, None) => tz_info.std_offset, + }; + LocalResult::Single(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. pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult { - offset(local, true) -} - -/// Converts a local `NaiveDateTime` to the `time::Timespec`. -pub(super) fn offset(d: &NaiveDateTime, local: bool) -> LocalResult { - let naive_sys_time = system_time_from_naive_date_time(d); - - let local_sys_time = match local { - false => from_utc_time(naive_sys_time), - true => from_local_time(naive_sys_time), + let tz_info = match TzInfo::for_year(local.year()) { + Some(tz_info) => tz_info, + None => return LocalResult::None, }; - - if let Ok(offset) = local_sys_time { - return LocalResult::Single(offset); + // Create a sorted slice of transitions and use `lookup_with_dst_transitions`. + match (tz_info.std_transition, tz_info.dst_transition) { + (Some(std_transition), Some(dst_transition)) => { + let std_transition = + Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset); + let dst_transition = + Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset); + let transitions = match std_transition.cmp(&dst_transition) { + Ordering::Less => [std_transition, dst_transition], + Ordering::Greater => [dst_transition, std_transition], + Ordering::Equal => { + // This doesn't make sense. Let's just return the standard offset. + return LocalResult::Single(tz_info.std_offset); + } + }; + lookup_with_dst_transitions(&transitions, *local) + } + (Some(std_transition), None) => { + let transitions = + [Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset)]; + lookup_with_dst_transitions(&transitions, *local) + } + (None, Some(dst_transition)) => { + let transitions = + [Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset)]; + lookup_with_dst_transitions(&transitions, *local) + } + (None, None) => return LocalResult::Single(tz_info.std_offset), } - LocalResult::None -} - -fn from_utc_time(utc_time: SYSTEMTIME) -> Result { - 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(FixedOffset::east_opt(offset).unwrap()) } -fn from_local_time(local_time: SYSTEMTIME) -> Result { - 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(FixedOffset::east_opt(offset).unwrap()) +// 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 seem to be either zero or two DST transitions (but we support having just one). +// - 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. +// +// These details don't matter much, we just work with the offsets and transition dates Windows +// returns through `GetTimeZoneInformationForYear` for a particular year. +struct TzInfo { + // Offset from UTC during standard time. + std_offset: FixedOffset, + // Offset from UTC during daylight saving time. + dst_offset: FixedOffset, + // Transition from standard time to daylight saving time, given in local standard time. + std_transition: Option, + // Transition from daylight saving time to standard time, given in local daylight saving time. + dst_transition: Option, } -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, +impl TzInfo { + fn for_year(year: i32) -> Option { + // 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 = year.clamp(1601, 30827) as u16; + let tz_info = unsafe { + let mut tz_info = MaybeUninit::::uninit(); + if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 { + return None; + } + tz_info.assume_init() + }; + Some(TzInfo { + std_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.StandardBias) * 60)?, + dst_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.DaylightBias) * 60)?, + std_transition: system_time_from_naive_date_time(tz_info.StandardDate, year), + dst_transition: system_time_from_naive_date_time(tz_info.DaylightDate, year), + }) } } -pub(crate) fn local_to_utc_time(local: &SYSTEMTIME) -> Result { - let mut sys_time = MaybeUninit::::uninit(); - unsafe { - windows_sys_call!( - TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()), - 0 - ) +fn system_time_from_naive_date_time(st: SYSTEMTIME, year: i32) -> Option { + 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, + )?; + // In Chrono's Weekday, Monday is 0 whereas in SYSTEMTIME Monday is 1 and Sunday is 0. + // Therefore we move back one day after converting the u16 value to a Weekday. + let day_of_week = Weekday::try_from(u8::try_from(st.wDayOfWeek).ok()?).ok()?.pred(); + if st.wYear != 0 { + return NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32) + .map(|d| d.and_time(time)); + } + let date = if let Some(date) = + NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, day_of_week, st.wDay as u8) + { + date + } else if st.wDay == 5 { + NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, day_of_week, 4)? + } else { + return None; }; - // 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 { - let mut local = MaybeUninit::::uninit(); - unsafe { - windows_sys_call!( - SystemTimeToTzSpecificLocalTime(ptr::null(), utc_time, local.as_mut_ptr()), - 0 - ) +#[cfg(test)] +mod tests { + use crate::offset::local::win_bindings::{ + SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime, FILETIME, SYSTEMTIME, }; - // SAFETY: SystemTimeToTzSpecificLocalTime must have succeeded at this point, so we can - // assume the value is initialized. - Ok(unsafe { local.assume_init() }) -} + use crate::{DateTime, Duration, FixedOffset, Local, NaiveDate, NaiveDateTime}; + use crate::{Datelike, TimeZone, Timelike}; + use std::mem::MaybeUninit; + use std::ptr; -/// Returns a i64 value representing the unix seconds conversion of the current `WinSystemTime`. -pub(crate) fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> Result { - let mut init = MaybeUninit::::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) + #[test] + fn verify_against_tz_specific_local_time_to_system_time() { + // The implementation in Windows itself is the source of truth on how to work with the OS + // timezone information. This test compares for every hour over a period of 125 years our + // implementation to `TzSpecificLocalTimeToSystemTime`. + // + // This uses parts of a previous Windows `Local` implementation in chrono. + fn from_local_time(dt: &NaiveDateTime) -> DateTime { + 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_naive_utc_and_offset(*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, + } + } + fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME { + let mut sys_time = MaybeUninit::::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; + fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 { + let mut init = MaybeUninit::::uninit(); + unsafe { + SystemTimeToFileTime(st, init.as_mut_ptr()); + } + // SystemTimeToFileTime must have succeeded at this point, so we can assume the value is + // initalized. + let filetime = unsafe { init.assume_init() }; + let bit_shift = + ((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64); + (bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC + } + + 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); + } + } } From 7fdd17f865f139a74efe94d29a920f654dd377c6 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Mon, 17 Apr 2023 14:42:44 +0200 Subject: [PATCH 2/4] Remove obsolete test --- src/datetime/tests.rs | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 88fcd80dec..1fddd62ec1 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -1499,41 +1499,6 @@ fn test_core_duration_max() { utc_dt += Duration::MAX; } -#[test] -#[cfg(all(target_os = "windows", feature = "clock"))] -fn test_from_naive_date_time_windows() { - let min_year = NaiveDate::from_ymd_opt(1601, 1, 3).unwrap().and_hms_opt(0, 0, 0).unwrap(); - - let max_year = NaiveDate::from_ymd_opt(30827, 12, 29).unwrap().and_hms_opt(23, 59, 59).unwrap(); - - let too_low_year = - NaiveDate::from_ymd_opt(1600, 12, 29).unwrap().and_hms_opt(23, 59, 59).unwrap(); - - let too_high_year = NaiveDate::from_ymd_opt(30829, 1, 3).unwrap().and_hms_opt(0, 0, 0).unwrap(); - - let _ = Local.from_utc_datetime(&min_year); - let _ = Local.from_utc_datetime(&max_year); - - let _ = Local.from_local_datetime(&min_year); - let _ = Local.from_local_datetime(&max_year); - - let local_too_low = Local.from_local_datetime(&too_low_year); - let local_too_high = Local.from_local_datetime(&too_high_year); - - assert_eq!(local_too_low, LocalResult::None); - assert_eq!(local_too_high, LocalResult::None); - - let err = std::panic::catch_unwind(|| { - Local.from_utc_datetime(&too_low_year); - }); - assert!(err.is_err()); - - let err = std::panic::catch_unwind(|| { - Local.from_utc_datetime(&too_high_year); - }); - assert!(err.is_err()); -} - #[test] #[cfg(feature = "clock")] fn test_datetime_local_from_preserves_offset() { From 38edc494646f379537c553214d8b1b0f1e614e82 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Mon, 17 Apr 2023 14:43:10 +0200 Subject: [PATCH 3/4] Extend test to more distant dates --- src/offset/local/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/offset/local/mod.rs b/src/offset/local/mod.rs index 51bd852ef7..37e78898c9 100644 --- a/src/offset/local/mod.rs +++ b/src/offset/local/mod.rs @@ -292,8 +292,7 @@ mod tests { #[test] fn verify_correct_offsets_distant_past() { - // let distant_past = Local::now() - TimeDelta::days(365 * 100); - let distant_past = Local::now() - TimeDelta::days(250 * 31); + let distant_past = Local::now() - TimeDelta::days(365 * 500); let from_local = Local.from_local_datetime(&distant_past.naive_local()).unwrap(); let from_utc = Local.from_utc_datetime(&distant_past.naive_utc()); @@ -306,7 +305,7 @@ mod tests { #[test] fn verify_correct_offsets_distant_future() { - let distant_future = Local::now() + TimeDelta::days(250 * 31); + let distant_future = Local::now() + TimeDelta::days(365 * 35000); let from_local = Local.from_local_datetime(&distant_future.naive_local()).unwrap(); let from_utc = Local.from_utc_datetime(&distant_future.naive_utc()); From 3d7daa5f5667e2e9d9252b2e94433cce9f263ada Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Wed, 19 Apr 2023 07:26:38 +0200 Subject: [PATCH 4/4] Add test for issue 651 --- src/datetime/tests.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 1fddd62ec1..be83955ee7 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -1264,6 +1264,18 @@ fn test_datetime_from_timestamp_millis() { ); } +#[test] +#[cfg(feature = "clock")] +fn test_datetime_before_windows_api_limits() { + // dt corresponds to `FILETIME = 147221225472` from issue 651. + // (https://github.com/chronotope/chrono/issues/651) + // This used to fail on Windows for timezones with an offset of -5:00 or greater. + // The API limits years to 1601..=30827. + let dt = NaiveDate::from_ymd_opt(1601, 1, 1).unwrap().and_hms_milli_opt(4, 5, 22, 122).unwrap(); + let local_dt = Local.from_utc_datetime(&dt); + dbg!(local_dt); +} + #[test] #[cfg(feature = "clock")] fn test_years_elapsed() {