From a94fc72537124b75c8168f121e14c04384f78e35 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Wed, 14 Oct 2026 20:09:28 +0900 Subject: [PATCH] Use 64-bit timestamps for Windows CRT and FILETIME conversions Use the explicit 64-bit CRT ABI regardless of libc time_t width, and preserve 64-bit FILETIME seconds throughout stat producers and consumers. Add bounded Windows regressions for 2040 calendar and stat conversions while retaining existing historical timestamps and error behavior. AI assistance: OpenAI Codex implemented and independently source-reviewed this split, prepared bounded Python regressions and performed cross-target compile checks. Windows runtime and human manual testing are not claimed. Assisted-by: OpenAI Codex:model version unavailable --- crates/host_env/src/fileutils.rs | 14 +++++++------- crates/host_env/src/nt.rs | 12 ++++++------ crates/host_env/src/time.rs | 12 +++++++----- crates/stdlib/src/_datetime.rs | 3 ++- crates/vm/src/stdlib/time.rs | 8 ++++---- extra_tests/snippets/stdlib_datetime.py | 11 +++++++++++ extra_tests/snippets/stdlib_os.py | 22 ++++++++++++++++++++++ extra_tests/snippets/stdlib_time.py | 6 ++++++ 8 files changed, 65 insertions(+), 23 deletions(-) diff --git a/crates/host_env/src/fileutils.rs b/crates/host_env/src/fileutils.rs index 1674c90743b..974484c6ec9 100644 --- a/crates/host_env/src/fileutils.rs +++ b/crates/host_env/src/fileutils.rs @@ -55,13 +55,13 @@ pub mod windows { pub st_gid: i32, pub st_rdev: libc::c_ulong, pub st_size: u64, - pub st_atime: libc::time_t, + pub st_atime: crate::time::TimeT, pub st_atime_nsec: i32, - pub st_mtime: libc::time_t, + pub st_mtime: crate::time::TimeT, pub st_mtime_nsec: i32, - pub st_ctime: libc::time_t, + pub st_ctime: crate::time::TimeT, pub st_ctime_nsec: i32, - pub st_birthtime: libc::time_t, + pub st_birthtime: crate::time::TimeT, pub st_birthtime_nsec: i32, pub st_file_attributes: libc::c_ulong, pub st_reparse_tag: u32, @@ -155,13 +155,13 @@ pub mod windows { )) } - fn large_integer_to_time_t_nsec(input: i64) -> (libc::time_t, libc::c_int) { + fn large_integer_to_time_t_nsec(input: i64) -> (crate::time::TimeT, libc::c_int) { let nsec_out = (input % 10_000_000) * 100; // FILETIME is in units of 100 nsec. - let time_out = ((input / 10_000_000) - SECS_BETWEEN_EPOCHS) as libc::time_t; + let time_out = (input / 10_000_000) - SECS_BETWEEN_EPOCHS; (time_out, nsec_out as _) } - fn file_time_to_time_t_nsec(in_ptr: FILETIME) -> (libc::time_t, libc::c_int) { + fn file_time_to_time_t_nsec(in_ptr: FILETIME) -> (crate::time::TimeT, libc::c_int) { let in_val: i64 = unsafe { core::mem::transmute_copy(&in_ptr) }; let nsec_out = (in_val % 10_000_000) * 100; // FILETIME is in units of 100 nsec. let time_out = (in_val / 10_000_000) - SECS_BETWEEN_EPOCHS; diff --git a/crates/host_env/src/nt.rs b/crates/host_env/src/nt.rs index 9f1c1c23de4..9157de3ba56 100644 --- a/crates/host_env/src/nt.rs +++ b/crates/host_env/src/nt.rs @@ -129,16 +129,16 @@ struct FileAttributeTagInfo { reparse_tag: u32, } -const fn win32_large_integer_to_time(li: i64) -> (libc::time_t, i32) { +const fn win32_large_integer_to_time(li: i64) -> (crate::time::TimeT, i32) { let nsec = ((li % 10_000_000) * 100) as i32; - let sec = (li / 10_000_000 - crate::fileutils::windows::SECS_BETWEEN_EPOCHS) as libc::time_t; + let sec = li / 10_000_000 - crate::fileutils::windows::SECS_BETWEEN_EPOCHS; (sec, nsec) } -const fn win32_filetime_to_time(ft_low: u32, ft_high: u32) -> (libc::time_t, i32) { +const fn win32_filetime_to_time(ft_low: u32, ft_high: u32) -> (crate::time::TimeT, i32) { let ticks = ((ft_high as i64) << 32) | (ft_low as i64); let nsec = ((ticks % 10_000_000) * 100) as i32; - let sec = (ticks / 10_000_000 - crate::fileutils::windows::SECS_BETWEEN_EPOCHS) as libc::time_t; + let sec = ticks / 10_000_000 - crate::fileutils::windows::SECS_BETWEEN_EPOCHS; (sec, nsec) } @@ -591,10 +591,10 @@ impl ScandirEntry { if reparse_tag == IO_REPARSE_TAG_SYMLINK { mode = (mode & !libc::S_IFMT) | S_IFLNK; } - let to_time = |ticks: u64| -> (libc::time_t, i32) { + let to_time = |ticks: u64| -> (crate::time::TimeT, i32) { let ticks = ticks as i64; ( - (ticks / 10_000_000 - SECS_BETWEEN_EPOCHS) as libc::time_t, + ticks / 10_000_000 - SECS_BETWEEN_EPOCHS, ((ticks % 10_000_000) * 100) as i32, ) }; diff --git a/crates/host_env/src/time.rs b/crates/host_env/src/time.rs index 94448a8acbb..e79be43a501 100644 --- a/crates/host_env/src/time.rs +++ b/crates/host_env/src/time.rs @@ -91,7 +91,7 @@ pub fn duration_since_system_now() -> Result { pub type TimeT = libc::time_t; #[cfg(windows)] -pub type TimeT = libc::time_t; +pub type TimeT = libc::time64_t; #[cfg(unix)] #[derive(Clone, Copy, Debug)] @@ -132,16 +132,18 @@ pub fn mktime(tm: &mut libc::tm) -> TimeT { #[cfg(windows)] unsafe extern "C" { - fn _gmtime64_s(tm: *mut libc::tm, time: *const libc::time_t) -> libc::c_int; - fn _localtime64_s(tm: *mut libc::tm, time: *const libc::time_t) -> libc::c_int; + fn _gmtime64_s(tm: *mut libc::tm, time: *const TimeT) -> libc::c_int; + fn _localtime64_s(tm: *mut libc::tm, time: *const TimeT) -> libc::c_int; #[link_name = "_mktime64"] - fn c_mktime(tm: *mut libc::tm) -> libc::time_t; + fn c_mktime(tm: *mut libc::tm) -> TimeT; + #[link_name = "_time64"] + fn c_time(time: *mut TimeT) -> TimeT; } #[cfg(windows)] #[cfg_attr(target_env = "musl", allow(deprecated))] pub fn current_time_t() -> TimeT { - unsafe { libc::time(core::ptr::null_mut()) } + unsafe { c_time(core::ptr::null_mut()) } } #[cfg(windows)] diff --git a/crates/stdlib/src/_datetime.rs b/crates/stdlib/src/_datetime.rs index 8fb4cc29352..dc89b9bbdb0 100644 --- a/crates/stdlib/src/_datetime.rs +++ b/crates/stdlib/src/_datetime.rs @@ -1821,7 +1821,8 @@ mod _datetime { fn platform_time(t: i64, local: bool, vm: &VirtualMachine) -> PyResult { #[cfg(any(unix, windows))] { - let t_c = libc::time_t::try_from(t).map_err(|_| time_t_overflow(vm))?; + let t_c = + rustpython_host_env::time::TimeT::try_from(t).map_err(|_| time_t_overflow(vm))?; let tm = if local { rustpython_host_env::time::localtime_from_timestamp(t_c) } else { diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 867891effc0..ec6db117cea 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -927,7 +927,7 @@ mod decl { #[cfg(any(unix, windows))] #[cfg_attr(target_env = "musl", allow(deprecated))] - fn pyobj_to_time_t(value: Either, vm: &VirtualMachine) -> PyResult { + fn pyobj_to_time_t(value: Either, vm: &VirtualMachine) -> PyResult { match value { Either::A(float) => { if !float.is_finite() { @@ -935,19 +935,19 @@ mod decl { } let secs = float.floor(); #[cfg_attr(target_env = "musl", allow(deprecated))] - let minimum = libc::time_t::MIN as f64; + let minimum = host_time::TimeT::MIN as f64; // The signed maximum rounds up when represented by f64. if !(minimum..-minimum).contains(&secs) { return Err(vm.new_overflow_error("timestamp out of range for platform time_t")); } #[cfg_attr(target_env = "musl", allow(deprecated))] - Ok(secs as libc::time_t) + Ok(secs as host_time::TimeT) } Either::B(int) => { // try_into is needed on 32-bit platforms where time_t != i64 #[allow(clippy::useless_conversion)] #[cfg_attr(target_env = "musl", allow(deprecated))] - let ts: libc::time_t = int.try_into().map_err(|_| { + let ts: host_time::TimeT = int.try_into().map_err(|_| { vm.new_overflow_error("timestamp out of range for platform time_t") })?; Ok(ts) diff --git a/extra_tests/snippets/stdlib_datetime.py b/extra_tests/snippets/stdlib_datetime.py index 817f9237114..62fd04f4d6b 100644 --- a/extra_tests/snippets/stdlib_datetime.py +++ b/extra_tests/snippets/stdlib_datetime.py @@ -3941,6 +3941,16 @@ class Name(str): assert reference() is None +def test_windows_time64(): + if sys.platform == "win32": + for timestamp in (2_208_988_800, 2_208_988_800.0): + assert datetime_module.datetime.fromtimestamp( + timestamp, datetime_module.timezone.utc + ) == datetime_module.datetime( + 2040, 1, 1, tzinfo=datetime_module.timezone.utc + ) + + def test_timestamp_errno(): if sys.platform == "win32": try: @@ -3970,6 +3980,7 @@ def test_timestamp_errno(): test_astimezone_subclass_state() test_timezone_cycles() +test_windows_time64() test_timestamp_errno() diff --git a/extra_tests/snippets/stdlib_os.py b/extra_tests/snippets/stdlib_os.py index 50ce16be191..3c6132045cb 100644 --- a/extra_tests/snippets/stdlib_os.py +++ b/extra_tests/snippets/stdlib_os.py @@ -9,6 +9,28 @@ assert os.name == "posix" or os.name == "nt" +def windows_stat_preserves_time64(): + import tempfile + + with tempfile.TemporaryDirectory() as directory: + filename = os.path.join(directory, "time64") + with open(filename, "wb"): + pass + timestamp = 2_208_988_800 # 2040-01-01, beyond signed 32-bit seconds. + os.utime(filename, (timestamp, timestamp)) + with open(filename, "rb") as file: + results = [os.stat(filename), os.fstat(file.fileno())] + with os.scandir(directory) as entries: + results.append(next(entries).stat()) + for result in results: + assert result.st_mtime == timestamp + assert result.st_mtime_ns == timestamp * 1_000_000_000 + + +if sys.platform == "win32": + windows_stat_preserves_time64() + + def stat_rejects_negative_file_descriptors(): import errno import os diff --git a/extra_tests/snippets/stdlib_time.py b/extra_tests/snippets/stdlib_time.py index 614d9610576..1aa2627ce42 100644 --- a/extra_tests/snippets/stdlib_time.py +++ b/extra_tests/snippets/stdlib_time.py @@ -107,6 +107,12 @@ assert local[:6] == time.gmtime(timestamp + local.tm_gmtoff)[:6] assert time.ctime(timestamp) == time.asctime(local) +if sys.platform == "win32": + # The explicit 64-bit CRT ABI must accept timestamps beyond 2038. + for timestamp in (2_208_988_800, 2_208_988_800.0): + assert time.gmtime(timestamp) == (2040, 1, 1, 0, 0, 0, 6, 1, 0) + + x = time.gmtime(1000) assert x.tm_year == 1970