From f44fff0ccec7b500ea411fd0f1e6f5bce6df98b3 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 12 Feb 2026 01:16:30 +0900 Subject: [PATCH] Use CRT functions for time module on Windows - Replace chrono with CRT functions (wcsftime, _gmtime64_s, _localtime64_s, _mktime64) on Windows for correct behavior - Fix timezone name corruption: use take_while instead of filter for null-terminated wide strings in TIME_ZONE_INFORMATION - Fix wcsftime symbol name (_wcsftime -> wcsftime) - Fix CString import cfg to unix-only - Remove 13 expectedFailure markers from test_time for Windows --- Lib/test/datetimetester.py | 1 - Lib/test/test_strftime.py | 31 +- Lib/test/test_time.py | 40 +-- crates/vm/src/stdlib/time.rs | 626 ++++++++++++++++++++++------------- 4 files changed, 414 insertions(+), 284 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index af11a611887..fcf308a8e78 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1519,7 +1519,6 @@ def test_strftime(self): # bpo-41260: The parameter was named "fmt" in the pure python impl. t.strftime(format="%f") - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON; chrono fallback on Windows") def test_strftime_trailing_percent(self): # bpo-35066: Make sure trailing '%' doesn't cause datetime's strftime to # complain. Different libcs have different handling of trailing diff --git a/Lib/test/test_strftime.py b/Lib/test/test_strftime.py index be43c49e40a..375f6aaedd8 100644 --- a/Lib/test/test_strftime.py +++ b/Lib/test/test_strftime.py @@ -39,7 +39,21 @@ def _update_variables(self, now): if now[3] < 12: self.ampm='(AM|am)' else: self.ampm='(PM|pm)' - self.jan1 = time.localtime(time.mktime((now[0], 1, 1, 0, 0, 0, 0, 1, 0))) + jan1 = time.struct_time( + ( + now.tm_year, # Year + 1, # Month (January) + 1, # Day (1st) + 0, # Hour (0) + 0, # Minute (0) + 0, # Second (0) + -1, # tm_wday (will be determined) + 1, # tm_yday (day 1 of the year) + -1, # tm_isdst (let the system determine) + ) + ) + # use mktime to get the correct tm_wday and tm_isdst values + self.jan1 = time.localtime(time.mktime(jan1)) try: if now[8]: self.tz = time.tzname[1] @@ -54,14 +68,10 @@ def _update_variables(self, now): self.now = now def setUp(self): - try: - import java - java.util.Locale.setDefault(java.util.Locale.US) - except ImportError: - from locale import setlocale, LC_TIME - saved_locale = setlocale(LC_TIME) - setlocale(LC_TIME, 'C') - self.addCleanup(setlocale, LC_TIME, saved_locale) + from locale import setlocale, LC_TIME + saved_locale = setlocale(LC_TIME) + setlocale(LC_TIME, 'C') + self.addCleanup(setlocale, LC_TIME, saved_locale) def test_strftime(self): now = time.time() @@ -187,8 +197,7 @@ class Y1900Tests(unittest.TestCase): def test_y_before_1900(self): # Issue #13674, #19634 t = (1899, 1, 1, 0, 0, 0, 0, 0, 0) - if (sys.platform == "win32" - or sys.platform.startswith(("aix", "sunos", "solaris"))): + if sys.platform.startswith(("aix", "sunos", "solaris")): with self.assertRaises(ValueError): time.strftime("%y", t) else: diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 1464aaffa86..b575ec657fc 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -208,8 +208,6 @@ def test_strftime_invalid_format(self): except ValueError as exc: self.assertEqual(str(exc), 'Invalid format string') - # TODO: RUSTPYTHON; chrono fallback on Windows does not preserve surrogates - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_strftime_special(self): tt = time.gmtime(self.t) s1 = time.strftime('%c', tt) @@ -294,8 +292,6 @@ def _bounds_checking(self, func): self.assertRaises(ValueError, func, (1900, 1, 1, 0, 0, 0, 0, 367, -1)) - # TODO: RUSTPYTHON; chrono on Windows rejects month=0/day=0 and raises wrong error type - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_strftime_bounding_check(self): self._bounds_checking(lambda tup: time.strftime('', tup)) @@ -312,8 +308,6 @@ def test_strftime_format_check(self): except ValueError: pass - # TODO: RUSTPYTHON; chrono on Windows does not handle month=0/day=0 in struct_time - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_default_values_for_zero(self): # Make sure that using all zeros uses the proper default # values. No test for daylight savings since strftime() does @@ -324,8 +318,6 @@ def test_default_values_for_zero(self): result = time.strftime("%Y %m %d %H %M %S %w %j", (2000,)+(0,)*8) self.assertEqual(expected, result) - # TODO: RUSTPYTHON; chrono %Z on Windows not compatible with strptime - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") @skip_if_buggy_ucrt_strfptime def test_strptime(self): # Should be able to go round-trip from strftime to strptime without @@ -382,13 +374,9 @@ def test_asctime(self): self.assertRaises(TypeError, time.asctime, ()) self.assertRaises(TypeError, time.asctime, (0,) * 10) - # TODO: RUSTPYTHON; chrono on Windows rejects month=0/day=0 - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_asctime_bounding_check(self): self._bounds_checking(time.asctime) - # TODO: RUSTPYTHON; chrono on Windows formats negative years differently - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") def test_ctime(self): t = time.mktime((1973, 9, 16, 1, 3, 52, 0, 0, -1)) self.assertEqual(time.ctime(t), 'Sun Sep 16 01:03:52 1973') @@ -761,37 +749,13 @@ def test_negative(self): class TestAsctime4dyear(_TestAsctimeYear, _Test4dYear, unittest.TestCase): - # TODO: RUSTPYTHON; chrono on Windows cannot handle month=0/day=0 in struct_time - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") - def test_year(self, *args, **kwargs): - return super().test_year(*args, **kwargs) - - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") - def test_large_year(self): - return super().test_large_year() - - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") - def test_negative(self): - return super().test_negative() + pass class TestStrftime4dyear(_TestStrftimeYear, _Test4dYear, unittest.TestCase): - # TODO: RUSTPYTHON; chrono on Windows cannot handle month=0/day=0 in struct_time - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") - def test_year(self, *args, **kwargs): - return super().test_year(*args, **kwargs) - - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") - def test_large_year(self): - return super().test_large_year() - - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") - def test_negative(self): - return super().test_negative() + pass class TestPytime(unittest.TestCase): - # TODO: RUSTPYTHON; chrono %Z on Windows gives offset instead of timezone name - @unittest.expectedFailureIf(sys.platform == "win32", "TODO: RUSTPYTHON") @skip_if_buggy_ucrt_strfptime @unittest.skipUnless(time._STRUCT_TM_ITEMS == 11, "needs tm_zone support") def test_localtime_timezone(self): diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index eb6265a37cc..eafd8e47b0b 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -31,11 +31,11 @@ mod decl { function::{Either, FuncArgs, OptionalArg}, types::{PyStructSequence, struct_sequence_new}, }; - #[cfg(unix)] + #[cfg(any(unix, windows))] use crate::{common::wtf8::Wtf8Buf, convert::ToPyObject}; #[cfg(unix)] use alloc::ffi::CString; - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] use chrono::{ DateTime, Datelike, TimeZone, Timelike, naive::{NaiveDate, NaiveDateTime, NaiveTime}, @@ -45,6 +45,16 @@ mod decl { #[cfg(not(target_arch = "wasm32"))] use windows_sys::Win32::System::Time::{GetTimeZoneInformation, TIME_ZONE_INFORMATION}; + #[cfg(windows)] + unsafe extern "C" { + fn wcsftime( + s: *mut libc::wchar_t, + max: libc::size_t, + format: *const libc::wchar_t, + tm: *const libc::tm, + ) -> libc::size_t; + } + #[allow(dead_code)] pub(super) const SEC_TO_MS: i64 = 1000; #[allow(dead_code)] @@ -189,7 +199,7 @@ mod decl { #[cfg(target_env = "msvc")] #[cfg(not(target_arch = "wasm32"))] - fn get_tz_info() -> TIME_ZONE_INFORMATION { + pub(super) fn get_tz_info() -> TIME_ZONE_INFORMATION { let mut info: TIME_ZONE_INFORMATION = unsafe { core::mem::zeroed() }; unsafe { GetTimeZoneInformation(&mut info) }; info @@ -271,16 +281,16 @@ mod decl { use crate::builtins::tuple::IntoPyTuple; let info = get_tz_info(); let standard = widestring::decode_utf16_lossy(info.StandardName) - .filter(|&c| c != '\0') + .take_while(|&c| c != '\0') .collect::(); let daylight = widestring::decode_utf16_lossy(info.DaylightName) - .filter(|&c| c != '\0') + .take_while(|&c| c != '\0') .collect::(); let tz_name = (&*standard, &*daylight); tz_name.into_pytuple(vm) } - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] fn pyobj_to_date_time( value: Either, vm: &VirtualMachine, @@ -298,7 +308,7 @@ mod decl { .ok_or_else(|| vm.new_overflow_error("timestamp out of range for platform time_t")) } - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] impl OptionalArg>> { /// Construct a localtime from the optional seconds, or get the current local time. fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { @@ -311,13 +321,14 @@ mod decl { } } - #[cfg(unix)] + #[cfg(any(unix, windows))] struct CheckedTm { tm: libc::tm, + #[cfg(unix)] zone: Option, } - #[cfg(unix)] + #[cfg(any(unix, windows))] fn checked_tm_from_struct_time( t: &StructTimeData, vm: &VirtualMachine, @@ -337,55 +348,61 @@ mod decl { return Err(vm.new_overflow_error("year out of range")); } let year = year as i32; - let mut tm = libc::tm { - tm_year: year - 1900, - tm_mon: t - .tm_mon - .clone() - .try_into_value::(vm) - .map_err(|_| invalid_tuple())? - - 1, - tm_mday: t - .tm_mday - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_hour: t - .tm_hour - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_min: t - .tm_min - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_sec: t - .tm_sec - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_wday: (t - .tm_wday - .clone() - .try_into_value::(vm) - .map_err(|_| invalid_tuple())? - + 1) - % 7, - tm_yday: t - .tm_yday - .clone() - .try_into_value::(vm) - .map_err(|_| invalid_tuple())? - - 1, - tm_isdst: t - .tm_isdst - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_gmtoff: 0, - tm_zone: core::ptr::null_mut(), - }; + let tm_mon = t + .tm_mon + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + - 1; + let tm_mday = t + .tm_mday + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + let tm_hour = t + .tm_hour + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + let tm_min = t + .tm_min + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + let tm_sec = t + .tm_sec + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + let tm_wday = (t + .tm_wday + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + + 1) + % 7; + let tm_yday = t + .tm_yday + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + - 1; + let tm_isdst = t + .tm_isdst + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + + let mut tm: libc::tm = unsafe { core::mem::zeroed() }; + tm.tm_year = year - 1900; + tm.tm_mon = tm_mon; + tm.tm_mday = tm_mday; + tm.tm_hour = tm_hour; + tm.tm_min = tm_min; + tm.tm_sec = tm_sec; + tm.tm_wday = tm_wday; + tm.tm_yday = tm_yday; + tm.tm_isdst = tm_isdst; if tm.tm_mon == -1 { tm.tm_mon = 0; @@ -415,35 +432,42 @@ mod decl { return Err(vm.new_value_error("day of year out of range")); } - let zone = if t.tm_zone.is(&vm.ctx.none) { - None - } else { - let zone: PyStrRef = t - .tm_zone - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; - Some( - CString::new(zone.as_str()) - .map_err(|_| vm.new_value_error("embedded null character"))?, - ) - }; - if let Some(zone) = &zone { - tm.tm_zone = zone.as_ptr().cast_mut(); + #[cfg(unix)] + { + let zone = if t.tm_zone.is(&vm.ctx.none) { + None + } else { + let zone: PyStrRef = t + .tm_zone + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + Some( + CString::new(zone.as_str()) + .map_err(|_| vm.new_value_error("embedded null character"))?, + ) + }; + if let Some(zone) = &zone { + tm.tm_zone = zone.as_ptr().cast_mut(); + } + if !t.tm_gmtoff.is(&vm.ctx.none) { + let gmtoff: i64 = t + .tm_gmtoff + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + tm.tm_gmtoff = gmtoff as _; + } + + Ok(CheckedTm { tm, zone }) } - if !t.tm_gmtoff.is(&vm.ctx.none) { - let gmtoff: i64 = t - .tm_gmtoff - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; - tm.tm_gmtoff = gmtoff as _; + #[cfg(windows)] + { + Ok(CheckedTm { tm }) } - - Ok(CheckedTm { tm, zone }) } - #[cfg(unix)] + #[cfg(any(unix, windows))] fn asctime_from_tm(tm: &libc::tm) -> String { const WDAY_NAME: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const MON_NAME: [&str; 12] = [ @@ -461,7 +485,7 @@ mod decl { ) } - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] impl OptionalArg { fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { Ok(match self { @@ -477,7 +501,7 @@ mod decl { secs: OptionalArg>>, vm: &VirtualMachine, ) -> PyResult { - #[cfg(unix)] + #[cfg(any(unix, windows))] { let ts = match secs { OptionalArg::Present(Some(value)) => pyobj_to_time_t(value, vm)?, @@ -486,7 +510,7 @@ mod decl { gmtime_from_timestamp(ts, vm) } - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] { let instant = match secs { OptionalArg::Present(Some(secs)) => pyobj_to_date_time(secs, vm)?.naive_utc(), @@ -503,7 +527,7 @@ mod decl { secs: OptionalArg>>, vm: &VirtualMachine, ) -> PyResult { - #[cfg(unix)] + #[cfg(any(unix, windows))] { let ts = match secs { OptionalArg::Present(Some(value)) => pyobj_to_time_t(value, vm)?, @@ -512,9 +536,9 @@ mod decl { localtime_from_timestamp(ts, vm) } - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] let instant = secs.naive_or_local(vm)?; - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] { Ok(StructTimeData::new_local(vm, instant, 0)) } @@ -527,7 +551,12 @@ mod decl { unix_mktime(&t, vm) } - #[cfg(not(unix))] + #[cfg(windows)] + { + win_mktime(&t, vm) + } + + #[cfg(not(any(unix, windows)))] { let datetime = t.to_date_time(vm)?; // mktime interprets struct_time as local time @@ -540,12 +569,12 @@ mod decl { } } - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] const CFMT: &str = "%a %b %e %H:%M:%S %Y"; #[pyfunction] fn asctime(t: OptionalArg, vm: &VirtualMachine) -> PyResult { - #[cfg(unix)] + #[cfg(any(unix, windows))] { let tm = match t { OptionalArg::Present(value) => { @@ -560,7 +589,7 @@ mod decl { Ok(vm.ctx.new_str(asctime_from_tm(&tm)).into()) } - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] { let instant = t.naive_or_local(vm)?; let formatted_time = instant.format(CFMT).to_string(); @@ -570,7 +599,7 @@ mod decl { #[pyfunction] fn ctime(secs: OptionalArg>>, vm: &VirtualMachine) -> PyResult { - #[cfg(unix)] + #[cfg(any(unix, windows))] { let ts = match secs { OptionalArg::Present(Some(value)) => pyobj_to_time_t(value, vm)?, @@ -581,86 +610,128 @@ mod decl { Ok(asctime_from_tm(&tm)) } - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] { let instant = secs.naive_or_local(vm)?; Ok(instant.format(CFMT).to_string()) } } - #[pyfunction] - fn strftime(format: PyStrRef, t: OptionalArg, vm: &VirtualMachine) -> PyResult { + #[cfg(any(unix, windows))] + fn strftime_crt(format: &PyStrRef, checked_tm: CheckedTm, vm: &VirtualMachine) -> PyResult { #[cfg(unix)] + let _keep_zone_alive = &checked_tm.zone; + let mut tm = checked_tm.tm; + tm.tm_isdst = tm.tm_isdst.clamp(-1, 1); + + // MSVC strftime requires year in [1; 9999] + #[cfg(windows)] { - let checked_tm = match t { - OptionalArg::Present(value) => checked_tm_from_struct_time(&value, vm, "strftime")?, - OptionalArg::Missing => { - let now = current_time_t(); - let local = localtime_from_timestamp(now, vm)?; - checked_tm_from_struct_time(&local, vm, "strftime")? - } - }; - let _keep_zone_alive = &checked_tm.zone; - let mut tm = checked_tm.tm; - tm.tm_isdst = tm.tm_isdst.clamp(-1, 1); - - fn strftime_ascii(fmt: &str, tm: &libc::tm, vm: &VirtualMachine) -> PyResult { - let fmt_c = - CString::new(fmt).map_err(|_| vm.new_value_error("embedded null character"))?; - let mut size = 1024usize; - let max_scale = 256usize.saturating_mul(fmt.len().max(1)); - loop { - let mut out = vec![0u8; size]; - let written = unsafe { - libc::strftime( - out.as_mut_ptr().cast(), - out.len(), - fmt_c.as_ptr(), - tm as *const libc::tm, - ) - }; - if written > 0 || size >= max_scale { - return Ok(String::from_utf8_lossy(&out[..written]).into_owned()); - } - size = size.saturating_mul(2); - } + let year = tm.tm_year + 1900; + if !(1..=9999).contains(&year) { + return Err(vm.new_value_error("strftime() requires year in [1; 9999]".to_owned())); } + } - let mut out = Wtf8Buf::new(); - let mut ascii = String::new(); - - for codepoint in format.as_wtf8().code_points() { - if codepoint.to_u32() == 0 { - if !ascii.is_empty() { - let part = strftime_ascii(&ascii, &tm, vm)?; - out.extend(part.chars()); - ascii.clear(); - } - out.push(codepoint); - continue; + #[cfg(unix)] + fn strftime_ascii(fmt: &str, tm: &libc::tm, vm: &VirtualMachine) -> PyResult { + let fmt_c = + CString::new(fmt).map_err(|_| vm.new_value_error("embedded null character"))?; + let mut size = 1024usize; + let max_scale = 256usize.saturating_mul(fmt.len().max(1)); + loop { + let mut out = vec![0u8; size]; + let written = unsafe { + libc::strftime( + out.as_mut_ptr().cast(), + out.len(), + fmt_c.as_ptr(), + tm as *const libc::tm, + ) + }; + if written > 0 || size >= max_scale { + return Ok(String::from_utf8_lossy(&out[..written]).into_owned()); } - if let Some(ch) = codepoint.to_char() - && ch.is_ascii() - { - ascii.push(ch); - continue; + size = size.saturating_mul(2); + } + } + + #[cfg(windows)] + fn strftime_ascii(fmt: &str, tm: &libc::tm, vm: &VirtualMachine) -> PyResult { + if fmt.contains('\0') { + return Err(vm.new_value_error("embedded null character")); + } + // Use wcsftime for proper Unicode output (e.g. %Z timezone names) + let fmt_wide: Vec = fmt.encode_utf16().chain(core::iter::once(0)).collect(); + let mut size = 1024usize; + let max_scale = 256usize.saturating_mul(fmt.len().max(1)); + loop { + let mut out = vec![0u16; size]; + let written = unsafe { + rustpython_common::suppress_iph!(wcsftime( + out.as_mut_ptr(), + out.len(), + fmt_wide.as_ptr(), + tm as *const libc::tm, + )) + }; + if written > 0 || size >= max_scale { + return Ok(String::from_utf16_lossy(&out[..written])); } + size = size.saturating_mul(2); + } + } + + let mut out = Wtf8Buf::new(); + let mut ascii = String::new(); + for codepoint in format.as_wtf8().code_points() { + if codepoint.to_u32() == 0 { if !ascii.is_empty() { let part = strftime_ascii(&ascii, &tm, vm)?; out.extend(part.chars()); ascii.clear(); } out.push(codepoint); + continue; + } + if let Some(ch) = codepoint.to_char() + && ch.is_ascii() + { + ascii.push(ch); + continue; } + if !ascii.is_empty() { let part = strftime_ascii(&ascii, &tm, vm)?; out.extend(part.chars()); + ascii.clear(); } - Ok(out.to_pyobject(vm)) + out.push(codepoint); } + if !ascii.is_empty() { + let part = strftime_ascii(&ascii, &tm, vm)?; + out.extend(part.chars()); + } + Ok(out.to_pyobject(vm)) + } - #[cfg(not(unix))] + #[pyfunction] + fn strftime(format: PyStrRef, t: OptionalArg, vm: &VirtualMachine) -> PyResult { + #[cfg(any(unix, windows))] + { + let checked_tm = match t { + OptionalArg::Present(value) => checked_tm_from_struct_time(&value, vm, "strftime")?, + OptionalArg::Missing => { + let now = current_time_t(); + let local = localtime_from_timestamp(now, vm)?; + checked_tm_from_struct_time(&local, vm, "strftime")? + } + }; + strftime_crt(&format, checked_tm, vm) + } + + #[cfg(not(any(unix, windows)))] { use core::fmt::Write; @@ -674,13 +745,6 @@ mod decl { Err(_) => return Ok(vm.ctx.new_str(fmt_lossy.into_owned()).into()), }; - // On Windows/AIX/Solaris, %y format with year < 1900 is not supported - #[cfg(any(windows, target_os = "aix", target_os = "solaris"))] - if instant.year() < 1900 && fmt_lossy.contains("%y") { - let msg = "format %y requires year >= 1900 on Windows"; - return Err(vm.new_value_error(msg.to_owned())); - } - let mut formatted_time = String::new(); write!(&mut formatted_time, "{}", instant.format(&fmt_lossy)) .unwrap_or_else(|_| formatted_time = format.to_string()); @@ -802,7 +866,7 @@ mod decl { } impl StructTimeData { - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] fn new_inner( vm: &VirtualMachine, tm: NaiveDateTime, @@ -826,13 +890,13 @@ mod decl { } /// Create struct_time for UTC (gmtime) - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] fn new_utc(vm: &VirtualMachine, tm: NaiveDateTime) -> Self { Self::new_inner(vm, tm, 0, 0, "UTC") } /// Create struct_time for local timezone (localtime) - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] fn new_local(vm: &VirtualMachine, tm: NaiveDateTime, isdst: i32) -> Self { let local_time = chrono::Local.from_local_datetime(&tm).unwrap(); let offset_seconds = local_time.offset().local_minus_utc(); @@ -840,7 +904,7 @@ mod decl { Self::new_inner(vm, tm, isdst, offset_seconds, &tz_abbr) } - #[cfg(not(unix))] + #[cfg(not(any(unix, windows)))] fn to_date_time(&self, vm: &VirtualMachine) -> PyResult { let invalid_overflow = || vm.new_overflow_error("mktime argument out of range"); let invalid_value = || vm.new_value_error("invalid struct_time parameter"); @@ -873,6 +937,88 @@ mod decl { } } + /// Extract fields from StructTimeData into a libc::tm for mktime. + #[cfg(any(unix, windows))] + pub(super) fn tm_from_struct_time(t: &StructTimeData, vm: &VirtualMachine) -> PyResult { + let invalid_tuple = || vm.new_type_error("mktime(): illegal time tuple argument"); + let year: i32 = t + .tm_year + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + if year < i32::MIN + 1900 { + return Err(vm.new_overflow_error("year out of range")); + } + + let mut tm: libc::tm = unsafe { core::mem::zeroed() }; + tm.tm_sec = t + .tm_sec + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + tm.tm_min = t + .tm_min + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + tm.tm_hour = t + .tm_hour + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + tm.tm_mday = t + .tm_mday + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + tm.tm_mon = t + .tm_mon + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + - 1; + tm.tm_year = year - 1900; + tm.tm_wday = -1; + tm.tm_yday = t + .tm_yday + .clone() + .try_into_value::(vm) + .map_err(|_| invalid_tuple())? + - 1; + tm.tm_isdst = t + .tm_isdst + .clone() + .try_into_value(vm) + .map_err(|_| invalid_tuple())?; + Ok(tm) + } + + #[cfg(any(unix, windows))] + fn pyobj_to_time_t(value: Either, vm: &VirtualMachine) -> PyResult { + match value { + Either::A(float) => { + if !float.is_finite() { + return Err(vm.new_value_error("Invalid value for timestamp")); + } + let secs = float.floor(); + if secs < libc::time_t::MIN as f64 || secs > libc::time_t::MAX as f64 { + return Err( + vm.new_overflow_error("timestamp out of range for platform time_t") + ); + } + Ok(secs as libc::time_t) + } + Either::B(int) => { + // try_into is needed on 32-bit platforms where time_t != i64 + #[allow(clippy::useless_conversion)] + let ts: libc::time_t = int.try_into().map_err(|_| { + vm.new_overflow_error("timestamp out of range for platform time_t") + })?; + Ok(ts) + } + } + } + #[cfg(any(unix, windows))] #[allow(unused_imports)] use super::platform::*; @@ -902,7 +1048,6 @@ mod platform { PyObject, PyRef, PyResult, TryFromBorrowedObject, VirtualMachine, builtins::{PyNamespace, PyStrRef}, convert::IntoPyException, - function::Either, }; use core::time::Duration; use libc::time_t; @@ -944,25 +1089,6 @@ mod platform { } } - pub(super) fn pyobj_to_time_t( - value: Either, - vm: &VirtualMachine, - ) -> PyResult { - let secs = match value { - Either::A(float) => { - if !float.is_finite() { - return Err(vm.new_value_error("Invalid value for timestamp")); - } - float.floor() - } - Either::B(int) => int as f64, - }; - if secs < time_t::MIN as f64 || secs > time_t::MAX as f64 { - return Err(vm.new_overflow_error("timestamp out of range for platform time_t")); - } - Ok(secs as time_t) - } - fn struct_time_from_tm(vm: &VirtualMachine, tm: libc::tm) -> StructTimeData { let zone = unsafe { if tm.tm_zone.is_null() { @@ -1017,59 +1143,7 @@ mod platform { } pub(super) fn unix_mktime(t: &StructTimeData, vm: &VirtualMachine) -> PyResult { - let invalid_tuple = || vm.new_type_error("mktime(): illegal time tuple argument"); - let year: i32 = t - .tm_year - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; - if year < i32::MIN + 1900 { - return Err(vm.new_overflow_error("year out of range")); - } - - let mut tm = libc::tm { - tm_sec: t - .tm_sec - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_min: t - .tm_min - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_hour: t - .tm_hour - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_mday: t - .tm_mday - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_mon: t - .tm_mon - .clone() - .try_into_value::(vm) - .map_err(|_| invalid_tuple())? - - 1, - tm_year: year - 1900, - tm_wday: -1, - tm_yday: t - .tm_yday - .clone() - .try_into_value::(vm) - .map_err(|_| invalid_tuple())? - - 1, - tm_isdst: t - .tm_isdst - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?, - tm_gmtoff: 0, - tm_zone: core::ptr::null_mut(), - }; + let mut tm = super::decl::tm_from_struct_time(t, vm)?; let timestamp = unsafe { libc::mktime(&mut tm) }; if timestamp == -1 && tm.tm_wday == -1 { return Err(vm.new_overflow_error("mktime argument out of range")); @@ -1250,9 +1324,9 @@ mod platform { } #[cfg(windows)] -#[pymodule] +#[pymodule(sub)] mod platform { - use super::decl::{MS_TO_NS, SEC_TO_NS, time_muldiv}; + use super::decl::{MS_TO_NS, SEC_TO_NS, StructTimeData, get_tz_info, time_muldiv}; use crate::{ PyRef, PyResult, VirtualMachine, builtins::{PyNamespace, PyStrRef}, @@ -1265,6 +1339,90 @@ mod platform { System::Threading::{GetCurrentProcess, GetCurrentThread, GetProcessTimes, GetThreadTimes}, }; + 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; + #[link_name = "_mktime64"] + fn c_mktime(tm: *mut libc::tm) -> libc::time_t; + } + + fn struct_time_from_tm( + vm: &VirtualMachine, + tm: libc::tm, + zone: &str, + gmtoff: i32, + ) -> StructTimeData { + StructTimeData { + tm_year: vm.ctx.new_int(tm.tm_year + 1900).into(), + tm_mon: vm.ctx.new_int(tm.tm_mon + 1).into(), + tm_mday: vm.ctx.new_int(tm.tm_mday).into(), + tm_hour: vm.ctx.new_int(tm.tm_hour).into(), + tm_min: vm.ctx.new_int(tm.tm_min).into(), + tm_sec: vm.ctx.new_int(tm.tm_sec).into(), + tm_wday: vm.ctx.new_int((tm.tm_wday + 6) % 7).into(), + tm_yday: vm.ctx.new_int(tm.tm_yday + 1).into(), + tm_isdst: vm.ctx.new_int(tm.tm_isdst).into(), + tm_zone: vm.ctx.new_str(zone).into(), + tm_gmtoff: vm.ctx.new_int(gmtoff).into(), + } + } + + pub(super) fn current_time_t() -> libc::time_t { + unsafe { libc::time(core::ptr::null_mut()) } + } + + pub(super) fn gmtime_from_timestamp( + when: libc::time_t, + vm: &VirtualMachine, + ) -> PyResult { + let mut out = core::mem::MaybeUninit::::uninit(); + let err = unsafe { _gmtime64_s(out.as_mut_ptr(), &when) }; + if err != 0 { + return Err(vm.new_overflow_error("timestamp out of range for platform time_t")); + } + Ok(struct_time_from_tm( + vm, + unsafe { out.assume_init() }, + "UTC", + 0, + )) + } + + pub(super) fn localtime_from_timestamp( + when: libc::time_t, + vm: &VirtualMachine, + ) -> PyResult { + let mut out = core::mem::MaybeUninit::::uninit(); + let err = unsafe { _localtime64_s(out.as_mut_ptr(), &when) }; + if err != 0 { + return Err(vm.new_overflow_error("timestamp out of range for platform time_t")); + } + let tm = unsafe { out.assume_init() }; + + // Get timezone info from Windows API + let info = get_tz_info(); + let (bias, name) = if tm.tm_isdst > 0 { + (info.DaylightBias, &info.DaylightName) + } else { + (info.StandardBias, &info.StandardName) + }; + let zone = widestring::decode_utf16_lossy(name.iter().copied()) + .take_while(|&c| c != '\0') + .collect::(); + let gmtoff = -((info.Bias + bias) as i32) * 60; + + Ok(struct_time_from_tm(vm, tm, &zone, gmtoff)) + } + + pub(super) fn win_mktime(t: &StructTimeData, vm: &VirtualMachine) -> PyResult { + let mut tm = super::decl::tm_from_struct_time(t, vm)?; + let timestamp = unsafe { rustpython_common::suppress_iph!(c_mktime(&mut tm)) }; + if timestamp == -1 && tm.tm_wday == -1 { + return Err(vm.new_overflow_error("mktime argument out of range")); + } + Ok(timestamp as f64) + } + fn u64_from_filetime(time: FILETIME) -> u64 { let large: [u32; 2] = [time.dwLowDateTime, time.dwHighDateTime]; unsafe { core::mem::transmute(large) }