1 // Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
4 //
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
10 
11 use std::cmp::Ordering;
12 use std::convert::TryFrom;
13 use std::mem::MaybeUninit;
14 use std::ptr;
15 
16 use super::win_bindings::{GetTimeZoneInformationForYear, SYSTEMTIME, TIME_ZONE_INFORMATION};
17 
18 use crate::offset::local::{lookup_with_dst_transitions, Transition};
19 use crate::{Datelike, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Weekday};
20 
21 // We don't use `SystemTimeToTzSpecificLocalTime` because it doesn't support the same range of dates
22 // as Chrono. Also it really isn't that difficult to work out the correct offset from the provided
23 // DST rules.
24 //
25 // This method uses `overflowing_sub_offset` because it is no problem if the transition time in UTC
26 // falls a couple of hours inside the buffer space around the `NaiveDateTime` range (although it is
27 // very theoretical to have a transition at midnight around `NaiveDate::(MIN|MAX)`.
offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset>28 pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> LocalResult<FixedOffset> {
29     // Using a `TzInfo` based on the year of an UTC datetime is technically wrong, we should be
30     // using the rules for the year of the corresponding local time. But this matches what
31     // `SystemTimeToTzSpecificLocalTime` is documented to do.
32     let tz_info = match TzInfo::for_year(utc.year()) {
33         Some(tz_info) => tz_info,
34         None => return LocalResult::None,
35     };
36     let offset = match (tz_info.std_transition, tz_info.dst_transition) {
37         (Some(std_transition), Some(dst_transition)) => {
38             let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
39             let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
40             if dst_transition_utc < std_transition_utc {
41                 match utc >= &dst_transition_utc && utc < &std_transition_utc {
42                     true => tz_info.dst_offset,
43                     false => tz_info.std_offset,
44                 }
45             } else {
46                 match utc >= &std_transition_utc && utc < &dst_transition_utc {
47                     true => tz_info.std_offset,
48                     false => tz_info.dst_offset,
49                 }
50             }
51         }
52         (Some(std_transition), None) => {
53             let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
54             match utc < &std_transition_utc {
55                 true => tz_info.dst_offset,
56                 false => tz_info.std_offset,
57             }
58         }
59         (None, Some(dst_transition)) => {
60             let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
61             match utc < &dst_transition_utc {
62                 true => tz_info.std_offset,
63                 false => tz_info.dst_offset,
64             }
65         }
66         (None, None) => tz_info.std_offset,
67     };
68     LocalResult::Single(offset)
69 }
70 
71 // We don't use `TzSpecificLocalTimeToSystemTime` because it doesn't let us choose how to handle
72 // ambiguous cases (during a DST transition). Instead we get the timezone information for the
73 // current year and compute it ourselves, like we do on Unix.
offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset>74 pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> LocalResult<FixedOffset> {
75     let tz_info = match TzInfo::for_year(local.year()) {
76         Some(tz_info) => tz_info,
77         None => return LocalResult::None,
78     };
79     // Create a sorted slice of transitions and use `lookup_with_dst_transitions`.
80     match (tz_info.std_transition, tz_info.dst_transition) {
81         (Some(std_transition), Some(dst_transition)) => {
82             let std_transition =
83                 Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset);
84             let dst_transition =
85                 Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset);
86             let transitions = match std_transition.cmp(&dst_transition) {
87                 Ordering::Less => [std_transition, dst_transition],
88                 Ordering::Greater => [dst_transition, std_transition],
89                 Ordering::Equal => {
90                     // This doesn't make sense. Let's just return the standard offset.
91                     return LocalResult::Single(tz_info.std_offset);
92                 }
93             };
94             lookup_with_dst_transitions(&transitions, *local)
95         }
96         (Some(std_transition), None) => {
97             let transitions =
98                 [Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset)];
99             lookup_with_dst_transitions(&transitions, *local)
100         }
101         (None, Some(dst_transition)) => {
102             let transitions =
103                 [Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset)];
104             lookup_with_dst_transitions(&transitions, *local)
105         }
106         (None, None) => return LocalResult::Single(tz_info.std_offset),
107     }
108 }
109 
110 // The basis for Windows timezone and DST support has been in place since Windows 2000. It does not
111 // allow for complex rules like the IANA timezone database:
112 // - A timezone has the same base offset the whole year.
113 // - There seem to be either zero or two DST transitions (but we support having just one).
114 // - As of Vista(?) only years from 2004 until a few years into the future are supported.
115 // - All other years get the base settings, which seem to be that of the current year.
116 //
117 // These details don't matter much, we just work with the offsets and transition dates Windows
118 // returns through `GetTimeZoneInformationForYear` for a particular year.
119 struct TzInfo {
120     // Offset from UTC during standard time.
121     std_offset: FixedOffset,
122     // Offset from UTC during daylight saving time.
123     dst_offset: FixedOffset,
124     // Transition from standard time to daylight saving time, given in local standard time.
125     std_transition: Option<NaiveDateTime>,
126     // Transition from daylight saving time to standard time, given in local daylight saving time.
127     dst_transition: Option<NaiveDateTime>,
128 }
129 
130 impl TzInfo {
for_year(year: i32) -> Option<TzInfo>131     fn for_year(year: i32) -> Option<TzInfo> {
132         // The API limits years to 1601..=30827.
133         // Working with timezones and daylight saving time this far into the past or future makes
134         // little sense. But whatever is extrapolated for 1601 or 30827 is what can be extrapolated
135         // for years beyond.
136         let ref_year = year.clamp(1601, 30827) as u16;
137         let tz_info = unsafe {
138             let mut tz_info = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit();
139             if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 {
140                 return None;
141             }
142             tz_info.assume_init()
143         };
144         Some(TzInfo {
145             std_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.StandardBias) * 60)?,
146             dst_offset: FixedOffset::west_opt((tz_info.Bias + tz_info.DaylightBias) * 60)?,
147             std_transition: system_time_from_naive_date_time(tz_info.StandardDate, year),
148             dst_transition: system_time_from_naive_date_time(tz_info.DaylightDate, year),
149         })
150     }
151 }
152 
system_time_from_naive_date_time(st: SYSTEMTIME, year: i32) -> Option<NaiveDateTime>153 fn system_time_from_naive_date_time(st: SYSTEMTIME, year: i32) -> Option<NaiveDateTime> {
154     if st.wYear == 0 && st.wMonth == 0 {
155         return None; // No DST transitions for this year in this timezone.
156     }
157     let time = NaiveTime::from_hms_milli_opt(
158         st.wHour as u32,
159         st.wMinute as u32,
160         st.wSecond as u32,
161         st.wMilliseconds as u32,
162     )?;
163     // In Chrono's Weekday, Monday is 0 whereas in SYSTEMTIME Monday is 1 and Sunday is 0.
164     // Therefore we move back one day after converting the u16 value to a Weekday.
165     let day_of_week = Weekday::try_from(u8::try_from(st.wDayOfWeek).ok()?).ok()?.pred();
166     if st.wYear != 0 {
167         return NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32)
168             .map(|d| d.and_time(time));
169     }
170     let date = if let Some(date) =
171         NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, day_of_week, st.wDay as u8)
172     {
173         date
174     } else if st.wDay == 5 {
175         NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, day_of_week, 4)?
176     } else {
177         return None;
178     };
179     Some(date.and_time(time))
180 }
181 
182 #[cfg(test)]
183 mod tests {
184     use crate::offset::local::win_bindings::{
185         SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime, FILETIME, SYSTEMTIME,
186     };
187     use crate::{DateTime, Duration, FixedOffset, Local, NaiveDate, NaiveDateTime};
188     use crate::{Datelike, TimeZone, Timelike};
189     use std::mem::MaybeUninit;
190     use std::ptr;
191 
192     #[test]
verify_against_tz_specific_local_time_to_system_time()193     fn verify_against_tz_specific_local_time_to_system_time() {
194         // The implementation in Windows itself is the source of truth on how to work with the OS
195         // timezone information. This test compares for every hour over a period of 125 years our
196         // implementation to `TzSpecificLocalTimeToSystemTime`.
197         //
198         // This uses parts of a previous Windows `Local` implementation in chrono.
199         fn from_local_time(dt: &NaiveDateTime) -> DateTime<Local> {
200             let st = system_time_from_naive_date_time(dt);
201             let utc_time = local_to_utc_time(&st);
202             let utc_secs = system_time_as_unix_seconds(&utc_time);
203             let local_secs = system_time_as_unix_seconds(&st);
204             let offset = (local_secs - utc_secs) as i32;
205             let offset = FixedOffset::east_opt(offset).unwrap();
206             DateTime::from_naive_utc_and_offset(*dt - offset, offset)
207         }
208         fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME {
209             SYSTEMTIME {
210                 // Valid values: 1601-30827
211                 wYear: dt.year() as u16,
212                 // Valid values:1-12
213                 wMonth: dt.month() as u16,
214                 // Valid values: 0-6, starting Sunday.
215                 // NOTE: enum returns 1-7, starting Monday, so we are
216                 // off here, but this is not currently used in local.
217                 wDayOfWeek: dt.weekday() as u16,
218                 // Valid values: 1-31
219                 wDay: dt.day() as u16,
220                 // Valid values: 0-23
221                 wHour: dt.hour() as u16,
222                 // Valid values: 0-59
223                 wMinute: dt.minute() as u16,
224                 // Valid values: 0-59
225                 wSecond: dt.second() as u16,
226                 // Valid values: 0-999
227                 wMilliseconds: 0,
228             }
229         }
230         fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME {
231             let mut sys_time = MaybeUninit::<SYSTEMTIME>::uninit();
232             unsafe { TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()) };
233             // SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can
234             // assume the value is initialized.
235             unsafe { sys_time.assume_init() }
236         }
237         const HECTONANOSECS_IN_SEC: i64 = 10_000_000;
238         const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC;
239         fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 {
240             let mut init = MaybeUninit::<FILETIME>::uninit();
241             unsafe {
242                 SystemTimeToFileTime(st, init.as_mut_ptr());
243             }
244             // SystemTimeToFileTime must have succeeded at this point, so we can assume the value is
245             // initalized.
246             let filetime = unsafe { init.assume_init() };
247             let bit_shift =
248                 ((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64);
249             (bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC
250         }
251 
252         let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 30, 0).unwrap();
253 
254         while date.year() < 2078 {
255             // Windows doesn't handle non-existing dates, it just treats it as valid.
256             if let Some(our_result) = Local.from_local_datetime(&date).earliest() {
257                 assert_eq!(from_local_time(&date), our_result);
258             }
259             date += Duration::hours(1);
260         }
261     }
262 }
263