Support final Python 3.15 Windows negative timestamps

Use FILETIME for pre-epoch conversions and preserve precise Windows timestamp errors.

Assisted-by: OpenAI Codex:model identifier unavailable
This commit is contained in:
youknowdot committed 2026-10-10 09:46:31 +09:00
1 parent dcc5619795
commit 54dfc8d584
4 files changed
+118 -15

No files matched your search

+67 -6
View File
@@ -145,23 +145,84 @@ pub fn current_time_t() -> TimeT {
}
#[cfg(windows)]
#[cfg_attr(target_env = "musl", allow(deprecated))]
pub fn gmtime_from_timestamp(when: TimeT) -> std::io::Result<libc::tm> {
#[derive(Debug)]
pub enum TimestampError {
FiletimeOutOfRange,
Os(std::io::Error),
}
#[cfg(windows)]
fn windows_filetime(when: TimeT, local: bool) -> Result<libc::tm, TimestampError> {
use windows_sys::Win32::{
Foundation::{FILETIME, SYSTEMTIME},
System::Time::{FileTimeToSystemTime, SystemTimeToTzSpecificLocalTime},
};
const SECS_BETWEEN_EPOCHS: i64 = 11_644_473_600;
if when < -SECS_BETWEEN_EPOCHS {
return Err(TimestampError::FiletimeOutOfRange);
}
// Only negative timestamps reach this fallback, so the FILETIME cannot overflow.
debug_assert!(when < 0);
let ticks = ((when + SECS_BETWEEN_EPOCHS) as u64) * 10_000_000;
let filetime = FILETIME {
dwLowDateTime: ticks as u32,
dwHighDateTime: (ticks >> 32) as u32,
};
let mut utc = SYSTEMTIME::default();
if unsafe { FileTimeToSystemTime(&filetime, &mut utc) } == 0 {
return Err(TimestampError::Os(std::io::Error::last_os_error()));
}
let time = if local {
let mut result = SYSTEMTIME::default();
// Unlike FileTimeToLocalFileTime, this applies DST rules for the timestamp.
if unsafe { SystemTimeToTzSpecificLocalTime(core::ptr::null(), &utc, &mut result) } == 0 {
return Err(TimestampError::Os(std::io::Error::last_os_error()));
}
result
} else {
utc
};
const DAYS_BEFORE_MONTH: [i32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let year = i32::from(time.wYear);
let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
let yday = DAYS_BEFORE_MONTH[usize::from(time.wMonth - 1)] + i32::from(time.wDay) - 1
+ i32::from(time.wMonth > 2 && leap);
Ok(libc::tm {
tm_year: year - 1900,
tm_mon: i32::from(time.wMonth) - 1,
tm_mday: i32::from(time.wDay),
tm_hour: i32::from(time.wHour),
tm_min: i32::from(time.wMinute),
tm_sec: i32::from(time.wSecond),
tm_wday: i32::from(time.wDayOfWeek),
tm_yday: yday,
tm_isdst: if local { -1 } else { 0 },
})
}
#[cfg(windows)]
pub fn gmtime_from_timestamp(when: TimeT) -> Result<libc::tm, TimestampError> {
if when < 0 {
return windows_filetime(when, false);
}
let mut out = core::mem::MaybeUninit::<libc::tm>::uninit();
let err = unsafe { _gmtime64_s(out.as_mut_ptr(), &when) };
if err != 0 {
return Err(crate::os::io_error_from_errno(err));
return Err(TimestampError::Os(crate::os::io_error_from_errno(err)));
}
Ok(unsafe { out.assume_init() })
}
#[cfg(windows)]
#[cfg_attr(target_env = "musl", allow(deprecated))]
pub fn localtime_from_timestamp(when: TimeT) -> std::io::Result<libc::tm> {
pub fn localtime_from_timestamp(when: TimeT) -> Result<libc::tm, TimestampError> {
if when < 0 {
return windows_filetime(when, true);
}
let mut out = core::mem::MaybeUninit::<libc::tm>::uninit();
let err = unsafe { _localtime64_s(out.as_mut_ptr(), &when) };
if err != 0 {
return Err(crate::os::io_error_from_errno(err));
return Err(TimestampError::Os(crate::os::io_error_from_errno(err)));
}
Ok(unsafe { out.assume_init() })
}
+12
View File
@@ -1576,6 +1576,18 @@ impl ToPyException for rustpython_host_env::socket::AncillaryPackError {
}
}
#[cfg(windows)]
impl ToPyException for rustpython_host_env::time::TimestampError {
fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef {
match self {
Self::FiletimeOutOfRange => {
vm.new_overflow_error("timestamp out of range for Windows FILETIME")
}
Self::Os(error) => error.to_pyexception(vm),
}
}
}
#[cfg(any(unix, windows))]
impl ToPyException for rustpython_host_env::time::CheckedTmError {
fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef {
+15 -6
View File
@@ -3951,12 +3951,21 @@ def test_timestamp_errno():
raise AssertionError(
"Windows localtime accepted a timestamp beyond the CRT range"
)
try:
datetime_module.datetime.fromtimestamp(-86400, datetime_module.timezone.utc)
except OSError as error:
assert error.errno == errno.EINVAL
else:
raise AssertionError("Windows gmtime accepted an out-of-range timestamp")
assert datetime_module.datetime.fromtimestamp(
-86400, datetime_module.timezone.utc
) == datetime_module.datetime(1969, 12, 31, tzinfo=datetime_module.timezone.utc)
assert datetime_module.datetime.fromtimestamp(
-11_644_473_600, datetime_module.timezone.utc
) == datetime_module.datetime(1601, 1, 1, tzinfo=datetime_module.timezone.utc)
for zone in (None, datetime_module.timezone.utc):
try:
datetime_module.datetime.fromtimestamp(-11_644_473_601, zone)
except OverflowError as error:
assert str(error) == "timestamp out of range for Windows FILETIME"
else:
raise AssertionError(
"pre-FILETIME datetime did not raise OverflowError"
)
test_astimezone_subclass_state()
+24 -3
View File
@@ -73,9 +73,8 @@ if sys.platform == "win32":
import errno
for converter in (time.gmtime, time.localtime, time.ctime):
# Python 3.14 preserves UCRT conversion failures as OSError. Use a
# negative timestamp below gmtime's near-epoch allowance in UCRT.
for timestamp in (-86400, 2**40, 2**63 - 1, -(2**63), -float(2**63)):
# Non-negative timestamps still use UCRT and preserve errno.
for timestamp in (2**40, 2**63 - 1):
try:
converter(timestamp)
except OSError as error:
@@ -83,6 +82,28 @@ if sys.platform == "win32":
else:
raise AssertionError("UCRT timestamp failure did not raise OSError")
# Negative timestamps use FILETIME, whose epoch is 1601-01-01.
for timestamp in (-11_644_473_601, -(2**63), -float(2**63)):
try:
converter(timestamp)
except OverflowError as error:
assert str(error) == "timestamp out of range for Windows FILETIME"
else:
raise AssertionError(
"pre-FILETIME timestamp did not raise OverflowError"
)
assert time.gmtime(-11_644_473_600) == (1601, 1, 1, 0, 0, 0, 0, 1, 0)
assert time.gmtime(-86400) == (1969, 12, 31, 0, 0, 0, 2, 365, 0)
assert time.gmtime(-1) == (1969, 12, 31, 23, 59, 59, 2, 365, 0)
# Leap-year accounting uses the historical date, including century rules.
assert time.gmtime(-2_203_891_200).tm_yday == 60 # 1900-03-01
assert time.gmtime(-2_330_035_200).tm_yday == 61 # 1896-03-01
for timestamp in (-86400, -43201, -1):
local = time.localtime(timestamp)
assert local.tm_isdst == -1
assert time.ctime(timestamp) == time.asctime(local)
x = time.gmtime(1000)
assert x.tm_year == 1970