diff --git a/crates/host_env/src/time.rs b/crates/host_env/src/time.rs index 14d77fe7a51..5cac2ff4887 100644 --- a/crates/host_env/src/time.rs +++ b/crates/host_env/src/time.rs @@ -146,18 +146,24 @@ pub fn current_time_t() -> TimeT { #[cfg(windows)] #[cfg_attr(target_env = "musl", allow(deprecated))] -pub fn gmtime_from_timestamp(when: TimeT) -> Option { +pub fn gmtime_from_timestamp(when: TimeT) -> std::io::Result { let mut out = core::mem::MaybeUninit::::uninit(); let err = unsafe { _gmtime64_s(out.as_mut_ptr(), &when) }; - (err == 0).then(|| unsafe { out.assume_init() }) + if err != 0 { + return Err(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) -> Option { +pub fn localtime_from_timestamp(when: TimeT) -> std::io::Result { let mut out = core::mem::MaybeUninit::::uninit(); let err = unsafe { _localtime64_s(out.as_mut_ptr(), &when) }; - (err == 0).then(|| unsafe { out.assume_init() }) + if err != 0 { + return Err(crate::os::io_error_from_errno(err)); + } + Ok(unsafe { out.assume_init() }) } #[cfg(windows)] diff --git a/crates/stdlib/src/_datetime.rs b/crates/stdlib/src/_datetime.rs index e424d903b1d..8fb4cc29352 100644 --- a/crates/stdlib/src/_datetime.rs +++ b/crates/stdlib/src/_datetime.rs @@ -9,6 +9,8 @@ pub(crate) use _datetime::{PyTzInfo, datetime_type, module_def, timedelta_from_s #[pymodule] mod _datetime { + #[cfg(windows)] + use crate::vm::convert::ToPyException; use crate::vm::{ AsObject, Py, PyObject, PyObjectCell, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, @@ -1825,6 +1827,9 @@ mod _datetime { } else { rustpython_host_env::time::gmtime_from_timestamp(t_c) }; + #[cfg(windows)] + let tm = tm.map_err(|error| error.to_pyexception(vm))?; + #[cfg(unix)] let Some(tm) = tm else { return Err(vm.new_last_errno_error()); }; diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 70b6479a5d3..7760b7ed8d2 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -914,7 +914,9 @@ mod decl { } let secs = float.floor(); #[cfg_attr(target_env = "musl", allow(deprecated))] - if secs < libc::time_t::MIN as f64 || secs > libc::time_t::MAX as f64 { + let minimum = libc::time_t::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))] @@ -1248,6 +1250,7 @@ mod platform { use crate::{ PyRef, PyResult, VirtualMachine, builtins::{PyNamespace, PyUtf8StrRef}, + convert::ToPyException, }; use core::time::Duration; use rustpython_host_env::time as host_time; @@ -1281,8 +1284,8 @@ mod platform { when: host_time::TimeT, vm: &VirtualMachine, ) -> PyResult { - let tm = host_time::gmtime_from_timestamp(when) - .ok_or_else(|| vm.new_overflow_error("timestamp out of range for platform time_t"))?; + let tm = + host_time::gmtime_from_timestamp(when).map_err(|error| error.to_pyexception(vm))?; Ok(struct_time_from_tm(vm, tm, "UTC", 0)) } @@ -1290,8 +1293,8 @@ mod platform { when: host_time::TimeT, vm: &VirtualMachine, ) -> PyResult { - let tm = host_time::localtime_from_timestamp(when) - .ok_or_else(|| vm.new_overflow_error("timestamp out of range for platform time_t"))?; + let tm = + host_time::localtime_from_timestamp(when).map_err(|error| error.to_pyexception(vm))?; // Get timezone info from Windows API let info = get_tz_info(); diff --git a/extra_tests/snippets/stdlib_datetime.py b/extra_tests/snippets/stdlib_datetime.py index d6609528cca..41efd03a837 100644 --- a/extra_tests/snippets/stdlib_datetime.py +++ b/extra_tests/snippets/stdlib_datetime.py @@ -3949,6 +3949,12 @@ def test_timestamp_errno(): assert error.errno == errno.EINVAL else: raise AssertionError("Windows localtime accepted a negative timestamp") + 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") test_astimezone_subclass_state() diff --git a/extra_tests/snippets/stdlib_time.py b/extra_tests/snippets/stdlib_time.py index b74d5bbc638..dd43d3f0b5a 100644 --- a/extra_tests/snippets/stdlib_time.py +++ b/extra_tests/snippets/stdlib_time.py @@ -1,6 +1,88 @@ import sys import time +if sys.platform == "win32": + # Record the native runtime before evaluating the range regression below. + print("Windows time runtime:", sys.version, sys.implementation, sys.flags) + print("Windows time word size:", sys.maxsize) + if hasattr(sys, "getwindowsversion"): + print("Windows version:", sys.getwindowsversion()) + try: + import sysconfig + + build_keys = ( + "Py_DEBUG", + "Py_GIL_DISABLED", + "Py_ENABLE_SHARED", + "SIZEOF_TIME_T", + "CC", + "CFLAGS", + ) + print( + "Windows time build:", + dict(zip(build_keys, sysconfig.get_config_vars(*build_keys))), + ) + except Exception as error: + print("Windows time build unavailable:", type(error).__name__, str(error)) + for converter in (time.gmtime, time.localtime, time.ctime): + for timestamp in ( + 0, + -1, + -43200, + -43201, + -86400, + 2**40, + 2**63 - 1, + -(2**63), + -float(2**63), + float(2**63), + ): + try: + result = converter(timestamp) + except Exception as error: + print( + "Windows time result:", + converter.__name__, + repr(timestamp), + type(error).__name__, + str(error), + getattr(error, "errno", None), + getattr(error, "winerror", None), + ) + else: + print( + "Windows time result:", + converter.__name__, + repr(timestamp), + "success", + repr(result), + ) + +if sys.platform != "wasi": + for converter in (time.gmtime, time.localtime, time.ctime): + # 2**63 is outside signed time_t even though its maximum rounds up to + # that value when represented as a float. + try: + converter(float(2**63)) + except OverflowError: + pass + else: + raise AssertionError("out-of-range timestamp did not raise OverflowError") + +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)): + try: + converter(timestamp) + except OSError as error: + assert error.errno == errno.EINVAL, (converter, timestamp, error) + else: + raise AssertionError("UCRT timestamp failure did not raise OSError") + x = time.gmtime(1000) assert x.tm_year == 1970