mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
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:
1 parent
dcc5619795
commit
54dfc8d584
4 files changed
+118
-15
No files matched your search
@@ -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() })
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user