Skip to content

Commit dfd1eed

Browse files
committed
Preserve Windows CRT time conversion errors
Return CRT errno failures from the Windows calendar helpers and translate them to Python OSError. Adapt the datetime caller while retaining its Unix behavior. Use an exclusive signed float upper bound so float(2**63) still raises OverflowError before reaching the CRT. Keep Windows negative-time FILETIME fallback and time64 ABI changes outside this Python 3.14 extraction. Existing datetime local-error coverage is retained and a UTC error control is added for the observed rejected timestamp -86400. Hosted native diagnostics prove CPython 3.14.7 raises OSError(errno 22) where the baseline raises OverflowError. Windows-GNU host_env check and warnings-denied Clippy pass. Selected canonical datetime and complete time/datetime snippet Linux controls pass, with six existing baseline canonical skips. Fixed native Windows verification remains pending CI. AI assistance covered source comparison, implementation, regression design, and automated verification. No manual validation is claimed. Assisted-by: Codex:model-version-unavailable
1 parent 1fce7d4 commit dfd1eed

4 files changed

Lines changed: 29 additions & 9 deletions

File tree

‎crates/host_env/src/time.rs‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,24 @@ pub fn current_time_t() -> TimeT {
146146

147147
#[cfg(windows)]
148148
#[cfg_attr(target_env = "musl", allow(deprecated))]
149-
pub fn gmtime_from_timestamp(when: TimeT) -> Option<libc::tm> {
149+
pub fn gmtime_from_timestamp(when: TimeT) -> std::io::Result<libc::tm> {
150150
let mut out = core::mem::MaybeUninit::<libc::tm>::uninit();
151151
let err = unsafe { _gmtime64_s(out.as_mut_ptr(), &when) };
152-
(err == 0).then(|| unsafe { out.assume_init() })
152+
if err != 0 {
153+
return Err(crate::os::io_error_from_errno(err));
154+
}
155+
Ok(unsafe { out.assume_init() })
153156
}
154157

155158
#[cfg(windows)]
156159
#[cfg_attr(target_env = "musl", allow(deprecated))]
157-
pub fn localtime_from_timestamp(when: TimeT) -> Option<libc::tm> {
160+
pub fn localtime_from_timestamp(when: TimeT) -> std::io::Result<libc::tm> {
158161
let mut out = core::mem::MaybeUninit::<libc::tm>::uninit();
159162
let err = unsafe { _localtime64_s(out.as_mut_ptr(), &when) };
160-
(err == 0).then(|| unsafe { out.assume_init() })
163+
if err != 0 {
164+
return Err(crate::os::io_error_from_errno(err));
165+
}
166+
Ok(unsafe { out.assume_init() })
161167
}
162168

163169
#[cfg(windows)]

‎crates/stdlib/src/_datetime.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub(crate) use _datetime::{PyTzInfo, datetime_type, module_def, timedelta_from_s
99

1010
#[pymodule]
1111
mod _datetime {
12+
#[cfg(windows)]
13+
use crate::vm::convert::ToPyException;
1214
use crate::vm::{
1315
AsObject, Py, PyObject, PyObjectCell, PyObjectRef, PyPayload, PyRef, PyResult,
1416
VirtualMachine,
@@ -1825,6 +1827,9 @@ mod _datetime {
18251827
} else {
18261828
rustpython_host_env::time::gmtime_from_timestamp(t_c)
18271829
};
1830+
#[cfg(windows)]
1831+
let tm = tm.map_err(|error| error.to_pyexception(vm))?;
1832+
#[cfg(unix)]
18281833
let Some(tm) = tm else {
18291834
return Err(vm.new_last_errno_error());
18301835
};

‎crates/vm/src/stdlib/time.rs‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,9 @@ mod decl {
914914
}
915915
let secs = float.floor();
916916
#[cfg_attr(target_env = "musl", allow(deprecated))]
917-
if secs < libc::time_t::MIN as f64 || secs > libc::time_t::MAX as f64 {
917+
let minimum = libc::time_t::MIN as f64;
918+
// The signed maximum rounds up when represented by f64.
919+
if !(minimum..-minimum).contains(&secs) {
918920
return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
919921
}
920922
#[cfg_attr(target_env = "musl", allow(deprecated))]
@@ -1248,6 +1250,7 @@ mod platform {
12481250
use crate::{
12491251
PyRef, PyResult, VirtualMachine,
12501252
builtins::{PyNamespace, PyUtf8StrRef},
1253+
convert::ToPyException,
12511254
};
12521255
use core::time::Duration;
12531256
use rustpython_host_env::time as host_time;
@@ -1281,17 +1284,17 @@ mod platform {
12811284
when: host_time::TimeT,
12821285
vm: &VirtualMachine,
12831286
) -> PyResult<StructTimeData> {
1284-
let tm = host_time::gmtime_from_timestamp(when)
1285-
.ok_or_else(|| vm.new_overflow_error("timestamp out of range for platform time_t"))?;
1287+
let tm =
1288+
host_time::gmtime_from_timestamp(when).map_err(|error| error.to_pyexception(vm))?;
12861289
Ok(struct_time_from_tm(vm, tm, "UTC", 0))
12871290
}
12881291

12891292
pub(super) fn localtime_from_timestamp(
12901293
when: host_time::TimeT,
12911294
vm: &VirtualMachine,
12921295
) -> PyResult<StructTimeData> {
1293-
let tm = host_time::localtime_from_timestamp(when)
1294-
.ok_or_else(|| vm.new_overflow_error("timestamp out of range for platform time_t"))?;
1296+
let tm =
1297+
host_time::localtime_from_timestamp(when).map_err(|error| error.to_pyexception(vm))?;
12951298

12961299
// Get timezone info from Windows API
12971300
let info = get_tz_info();

‎extra_tests/snippets/stdlib_datetime.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3949,6 +3949,12 @@ def test_timestamp_errno():
39493949
assert error.errno == errno.EINVAL
39503950
else:
39513951
raise AssertionError("Windows localtime accepted a negative timestamp")
3952+
try:
3953+
datetime_module.datetime.fromtimestamp(-86400, datetime_module.timezone.utc)
3954+
except OSError as error:
3955+
assert error.errno == errno.EINVAL
3956+
else:
3957+
raise AssertionError("Windows gmtime accepted an out-of-range timestamp")
39523958

39533959

39543960
test_astimezone_subclass_state()

0 commit comments

Comments
 (0)