Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor windows module in Local #992

Merged
merged 5 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/datetime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -950,3 +950,38 @@ fn test_datetime_sub_assign_local() {
assert_eq!(datetime_sub, datetime - Duration::days(i))
}
}

#[test]
#[cfg(target_os = "windows")]
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());
}
4 changes: 2 additions & 2 deletions src/format/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)
let mut n = 0i64;
for (i, c) in bytes.iter().take(max).cloned().enumerate() {
// cloned() = copied()
if !(b'0'..=b'9').contains(&c) {
if !c.is_ascii_digit() {
if i < min {
return Err(INVALID);
} else {
Expand Down Expand Up @@ -79,7 +79,7 @@ pub(super) fn nanosecond(s: &str) -> ParseResult<(&str, i64)> {
let v = v.checked_mul(SCALE[consumed]).ok_or(OUT_OF_RANGE)?;

// if there are more than 9 digits, skip next digits.
let s = s.trim_left_matches(|c: char| ('0'..='9').contains(&c));
let s = s.trim_left_matches(|c: char| c.is_ascii_digit());

Ok((s, v))
}
Expand Down
3 changes: 3 additions & 0 deletions src/offset/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ mod inner;
#[path = "windows.rs"]
mod inner;

#[cfg(windows)]
mod windows_sys;

#[cfg(unix)]
mod tz_info;

Expand Down
278 changes: 56 additions & 222 deletions src/offset/local/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,271 +8,105 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::io;
use std::mem;
use std::io::Error;
use std::result::Result;
use std::time::{SystemTime, UNIX_EPOCH};

use winapi::shared::minwindef::*;
use winapi::um::minwinbase::SYSTEMTIME;
use winapi::um::timezoneapi::*;
use super::windows_sys::WinSystemTime;

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

pub(super) fn now() -> DateTime<Local> {
tm_to_datetime(Timespec::now().local())
InnerDuration::now_locally().datetime()
}

/// Converts a local `NaiveDateTime` to the `time::Timespec`.
pub(super) fn naive_to_local(d: &NaiveDateTime, local: bool) -> LocalResult<DateTime<Local>> {
let tm = Tm {
tm_sec: d.second() as i32,
tm_min: d.minute() as i32,
tm_hour: d.hour() as i32,
tm_mday: d.day() as i32,
tm_mon: d.month0() as i32, // yes, C is that strange...
tm_year: d.year() - 1900, // this doesn't underflow, we know that d is `NaiveDateTime`.
tm_wday: 0, // to_local ignores this
tm_yday: 0, // and this
tm_isdst: -1,
// This seems pretty fake?
tm_utcoff: if local { 1 } else { 0 },
// do not set this, OS APIs are heavily inconsistent in terms of leap second handling
tm_nsec: 0,
};
let naive_sys_time = WinSystemTime::from_naive_datetime(d);

let spec = Timespec {
sec: match local {
false => utc_tm_to_time(&tm),
true => local_tm_to_time(&tm),
},
nsec: tm.tm_nsec,
let local_sys_time = match local {
false => LocalSysTime::from_utc_time(naive_sys_time),
true => LocalSysTime::from_local_time(naive_sys_time),
};

// Adjust for leap seconds
let mut tm = spec.local();
assert_eq!(tm.tm_nsec, 0);
tm.tm_nsec = d.nanosecond() as i32;

// #TODO - there should be ambiguous cases, investigate?
LocalResult::Single(tm_to_datetime(tm))
}
if let Ok(mut local) = local_sys_time {
assert_eq!(local.nsecs, 0);
local.set_nsecs(d.nanosecond() as i32);

/// Converts a `time::Tm` struct into the timezone-aware `DateTime`.
fn tm_to_datetime(mut tm: Tm) -> DateTime<Local> {
if tm.tm_sec >= 60 {
tm.tm_nsec += (tm.tm_sec - 59) * 1_000_000_000;
tm.tm_sec = 59;
return LocalResult::Single(local.datetime());
}

let date = NaiveDate::from_ymd_opt(tm.tm_year + 1900, tm.tm_mon as u32 + 1, tm.tm_mday as u32)
.unwrap();
let time = NaiveTime::from_hms_nano(
tm.tm_hour as u32,
tm.tm_min as u32,
tm.tm_sec as u32,
tm.tm_nsec as u32,
);

let offset = FixedOffset::east_opt(tm.tm_utcoff).unwrap();
DateTime::from_utc(date.and_time(time) - offset, offset)
LocalResult::None
}

/// A record specifying a time value in seconds and nanoseconds, where
/// nanoseconds represent the offset from the given second.
///
/// For example a timespec of 1.2 seconds after the beginning of the epoch would
/// be represented as {sec: 1, nsec: 200000000}.
struct Timespec {
struct InnerDuration {
sec: i64,
nsec: i32,
}

impl Timespec {
impl InnerDuration {
/// Constructs a new LocalSysTime representing the current time in UTC
/// Constructs a timespec representing the current time in UTC.
fn now() -> Timespec {
fn now_locally() -> LocalSysTime {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like there should be Windows API that would let us get both the current WinSystemTime and the current timezone offset in one system call, instead of doing this stuff?

let st =
SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before Unix epoch");
Timespec { sec: st.as_secs() as i64, nsec: st.subsec_nanos() as i32 }
let now_duration = Self { sec: st.as_secs() as i64, nsec: st.subsec_nanos() as i32 };
LocalSysTime::from_duration(now_duration)
.expect("Now should not fail to produce a local sys time")
}

/// Converts this timespec into the system's local time.
fn local(self) -> Tm {
let mut tm = Tm {
tm_sec: 0,
tm_min: 0,
tm_hour: 0,
tm_mday: 0,
tm_mon: 0,
tm_year: 0,
tm_wday: 0,
tm_yday: 0,
tm_isdst: 0,
tm_utcoff: 0,
tm_nsec: 0,
};
time_to_local_tm(self.sec, &mut tm);
tm.tm_nsec = self.nsec;
tm
fn from_seconds(sec: i64) -> Self {
Self { sec, nsec: 0 }
}
}

/// Holds a calendar date and time broken down into its components (year, month,
/// day, and so on), also called a broken-down time value.
// FIXME: use c_int instead of i32?
#[repr(C)]
struct Tm {
/// Seconds after the minute - [0, 60]
tm_sec: i32,

/// Minutes after the hour - [0, 59]
tm_min: i32,

/// Hours after midnight - [0, 23]
tm_hour: i32,

/// Day of the month - [1, 31]
tm_mday: i32,

/// Months since January - [0, 11]
tm_mon: i32,

/// Years since 1900
tm_year: i32,

/// Days since Sunday - [0, 6]. 0 = Sunday, 1 = Monday, ..., 6 = Saturday.
tm_wday: i32,

/// Days since January 1 - [0, 365]
tm_yday: i32,

/// Daylight Saving Time flag.
///
/// This value is positive if Daylight Saving Time is in effect, zero if
/// Daylight Saving Time is not in effect, and negative if this information
/// is not available.
tm_isdst: i32,

/// Identifies the time zone that was used to compute this broken-down time
/// value, including any adjustment for Daylight Saving Time. This is the
/// number of seconds east of UTC. For example, for U.S. Pacific Daylight
/// Time, the value is `-7*60*60 = -25200`.
tm_utcoff: i32,

/// Nanoseconds after the second - [0, 10<sup>9</sup> - 1]
tm_nsec: i32,
struct LocalSysTime {
inner: WinSystemTime,
offset: i32,
nsecs: i32,
}

const HECTONANOSECS_IN_SEC: i64 = 10_000_000;
const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC;

fn time_to_file_time(sec: i64) -> FILETIME {
let t = ((sec * HECTONANOSECS_IN_SEC) + HECTONANOSEC_TO_UNIX_EPOCH) as u64;
FILETIME { dwLowDateTime: t as DWORD, dwHighDateTime: (t >> 32) as DWORD }
}
impl LocalSysTime {
pub(crate) fn from_duration(dur: InnerDuration) -> Result<Self, Error> {
let utc_sys_time = WinSystemTime::from_unix_seconds(dur.sec)?;

fn file_time_as_u64(ft: &FILETIME) -> u64 {
((ft.dwHighDateTime as u64) << 32) | (ft.dwLowDateTime as u64)
}
let local_sys_time = utc_sys_time.as_time_zone_specific()?;
let local_secs = local_sys_time.as_unix_seconds()?;

fn file_time_to_unix_seconds(ft: &FILETIME) -> i64 {
let t = file_time_as_u64(ft) as i64;
((t - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC) as i64
}
let offset = (local_secs - dur.sec) as i32;

fn system_time_to_file_time(sys: &SYSTEMTIME) -> FILETIME {
unsafe {
let mut ft = mem::zeroed();
SystemTimeToFileTime(sys, &mut ft);
ft
Ok(Self { inner: local_sys_time, offset, nsecs: dur.nsec })
}
}

fn tm_to_system_time(tm: &Tm) -> SYSTEMTIME {
let mut sys: SYSTEMTIME = unsafe { mem::zeroed() };
sys.wSecond = tm.tm_sec as WORD;
sys.wMinute = tm.tm_min as WORD;
sys.wHour = tm.tm_hour as WORD;
sys.wDay = tm.tm_mday as WORD;
sys.wDayOfWeek = tm.tm_wday as WORD;
sys.wMonth = (tm.tm_mon + 1) as WORD;
sys.wYear = (tm.tm_year + 1900) as WORD;
sys
}

fn system_time_to_tm(sys: &SYSTEMTIME, tm: &mut Tm) {
tm.tm_sec = sys.wSecond as i32;
tm.tm_min = sys.wMinute as i32;
tm.tm_hour = sys.wHour as i32;
tm.tm_mday = sys.wDay as i32;
tm.tm_wday = sys.wDayOfWeek as i32;
tm.tm_mon = (sys.wMonth - 1) as i32;
tm.tm_year = (sys.wYear - 1900) as i32;
tm.tm_yday = yday(tm.tm_year, tm.tm_mon + 1, tm.tm_mday);

fn yday(year: i32, month: i32, day: i32) -> i32 {
let leap = if month > 2 {
if year % 4 == 0 {
1
} else {
2
}
} else {
0
};
let july = if month > 7 { 1 } else { 0 };

(month - 1) * 30 + month / 2 + (day - 1) - leap + july
fn from_utc_time(utc_time: WinSystemTime) -> Result<Self, Error> {
let duration = InnerDuration::from_seconds(utc_time.as_unix_seconds()?);
Self::from_duration(duration)
}
}

macro_rules! call {
($name:ident($($arg:expr),*)) => {
if $name($($arg),*) == 0 {
panic!(concat!(stringify!($name), " failed with: {}"),
io::Error::last_os_error());
}
fn from_local_time(local_time: WinSystemTime) -> Result<Self, Error> {
let utc_time = local_time.as_utc_time()?;
let duration = InnerDuration::from_seconds(utc_time.as_unix_seconds()?);
Self::from_duration(duration)
}
}

fn time_to_local_tm(sec: i64, tm: &mut Tm) {
let ft = time_to_file_time(sec);
unsafe {
let mut utc = mem::zeroed();
let mut local = mem::zeroed();
call!(FileTimeToSystemTime(&ft, &mut utc));
call!(SystemTimeToTzSpecificLocalTime(0 as *const _, &mut utc, &mut local));
system_time_to_tm(&local, tm);

let local = system_time_to_file_time(&local);
let local_sec = file_time_to_unix_seconds(&local);

let mut tz = mem::zeroed();
GetTimeZoneInformation(&mut tz);

// SystemTimeToTzSpecificLocalTime already applied the biases so
// check if it non standard
tm.tm_utcoff = (local_sec - sec) as i32;
tm.tm_isdst = if tm.tm_utcoff == -60 * (tz.Bias + tz.StandardBias) { 0 } else { 1 };
fn set_nsecs(&mut self, nsecs: i32) {
self.nsecs = nsecs;
}
}

fn utc_tm_to_time(tm: &Tm) -> i64 {
unsafe {
let mut ft = mem::zeroed();
let sys_time = tm_to_system_time(tm);
call!(SystemTimeToFileTime(&sys_time, &mut ft));
file_time_to_unix_seconds(&ft)
}
}
fn datetime(self) -> DateTime<Local> {
let st = self.inner.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_nano(
st.wHour as u32,
st.wMinute as u32,
st.wSecond as u32,
self.nsecs as u32,
);

fn local_tm_to_time(tm: &Tm) -> i64 {
unsafe {
let mut ft = mem::zeroed();
let mut utc = mem::zeroed();
let mut sys_time = tm_to_system_time(tm);
call!(TzSpecificLocalTimeToSystemTime(0 as *mut _, &mut sys_time, &mut utc));
call!(SystemTimeToFileTime(&utc, &mut ft));
file_time_to_unix_seconds(&ft)
let offset = FixedOffset::east_opt(self.offset).unwrap();
DateTime::from_utc(date.and_time(time) - offset, offset)
}
}