Skip to main content

rustpython_vm/stdlib/
time.rs

1//cspell:ignore cfmt
2//! The python `time` module.
3
4// See also:
5// https://docs.python.org/3/library/time.html
6
7pub use decl::time;
8
9pub(crate) use decl::module_def;
10
11#[cfg(not(target_env = "msvc"))]
12#[cfg(not(target_arch = "wasm32"))]
13unsafe extern "C" {
14    #[cfg(not(target_os = "freebsd"))]
15    #[link_name = "daylight"]
16    static c_daylight: core::ffi::c_int;
17    // pub static dstbias: std::ffi::c_int;
18    #[link_name = "timezone"]
19    static c_timezone: core::ffi::c_long;
20    #[link_name = "tzname"]
21    static c_tzname: [*const core::ffi::c_char; 2];
22    #[link_name = "tzset"]
23    fn c_tzset();
24}
25
26#[pymodule(name = "time", with(#[cfg(any(unix, windows))] platform))]
27mod decl {
28    use crate::{
29        AsObject, Py, PyObjectRef, PyResult, VirtualMachine,
30        builtins::{PyStrRef, PyTypeRef},
31        function::{Either, FuncArgs, OptionalArg},
32        types::{PyStructSequence, struct_sequence_new},
33    };
34    #[cfg(any(unix, windows))]
35    use crate::{common::wtf8::Wtf8Buf, convert::ToPyObject};
36    #[cfg(unix)]
37    use alloc::ffi::CString;
38    #[cfg(not(any(unix, windows)))]
39    use chrono::{
40        DateTime, Datelike, TimeZone, Timelike,
41        naive::{NaiveDate, NaiveDateTime, NaiveTime},
42    };
43    use core::time::Duration;
44    #[cfg(target_env = "msvc")]
45    #[cfg(not(target_arch = "wasm32"))]
46    use windows_sys::Win32::System::Time::{GetTimeZoneInformation, TIME_ZONE_INFORMATION};
47
48    #[cfg(windows)]
49    unsafe extern "C" {
50        fn wcsftime(
51            s: *mut libc::wchar_t,
52            max: libc::size_t,
53            format: *const libc::wchar_t,
54            tm: *const libc::tm,
55        ) -> libc::size_t;
56    }
57
58    #[allow(dead_code)]
59    pub(super) const SEC_TO_MS: i64 = 1000;
60    #[allow(dead_code)]
61    pub(super) const MS_TO_US: i64 = 1000;
62    #[allow(dead_code)]
63    pub(super) const SEC_TO_US: i64 = SEC_TO_MS * MS_TO_US;
64    #[allow(dead_code)]
65    pub(super) const US_TO_NS: i64 = 1000;
66    #[allow(dead_code)]
67    pub(super) const MS_TO_NS: i64 = MS_TO_US * US_TO_NS;
68    #[allow(dead_code)]
69    pub(super) const SEC_TO_NS: i64 = SEC_TO_MS * MS_TO_NS;
70    #[allow(dead_code)]
71    pub(super) const NS_TO_MS: i64 = 1000 * 1000;
72    #[allow(dead_code)]
73    pub(super) const NS_TO_US: i64 = 1000;
74
75    fn duration_since_system_now(vm: &VirtualMachine) -> PyResult<Duration> {
76        use std::time::{SystemTime, UNIX_EPOCH};
77
78        SystemTime::now()
79            .duration_since(UNIX_EPOCH)
80            .map_err(|e| vm.new_value_error(format!("Time error: {e:?}")))
81    }
82
83    #[pyattr]
84    pub const _STRUCT_TM_ITEMS: usize = 11;
85
86    // TODO: implement proper monotonic time for wasm/wasi.
87    #[cfg(not(any(unix, windows)))]
88    fn get_monotonic_time(vm: &VirtualMachine) -> PyResult<Duration> {
89        duration_since_system_now(vm)
90    }
91
92    // TODO: implement proper perf time for wasm/wasi.
93    #[cfg(not(any(unix, windows)))]
94    fn get_perf_time(vm: &VirtualMachine) -> PyResult<Duration> {
95        duration_since_system_now(vm)
96    }
97
98    #[pyfunction]
99    fn sleep(seconds: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
100        let seconds_type_name = seconds.clone().class().name().to_owned();
101        let dur = seconds.try_into_value::<Duration>(vm).map_err(|e| {
102            if e.class().is(vm.ctx.exceptions.value_error)
103                && let Some(s) = e.args().first().and_then(|arg| arg.str(vm).ok())
104                && s.as_bytes() == b"negative duration"
105            {
106                return vm.new_value_error("sleep length must be non-negative");
107            }
108            if e.class().is(vm.ctx.exceptions.type_error) {
109                return vm.new_type_error(format!(
110                    "'{seconds_type_name}' object cannot be interpreted as an integer or float"
111                ));
112            }
113            e
114        })?;
115
116        #[cfg(unix)]
117        {
118            // Loop on nanosleep, recomputing the
119            // remaining timeout after each EINTR so that signals don't
120            // shorten the requested sleep duration.
121            use std::time::Instant;
122            let deadline = Instant::now() + dur;
123            loop {
124                let remaining = deadline.saturating_duration_since(Instant::now());
125                if remaining.is_zero() {
126                    break;
127                }
128                let ts = nix::sys::time::TimeSpec::from(remaining);
129                let (res, err) = vm.allow_threads(|| {
130                    let r = unsafe { libc::nanosleep(ts.as_ref(), core::ptr::null_mut()) };
131                    (r, nix::Error::last_raw())
132                });
133                if res == 0 {
134                    break;
135                }
136                if err != libc::EINTR {
137                    return Err(
138                        vm.new_os_error(format!("nanosleep: {}", nix::Error::from_raw(err)))
139                    );
140                }
141                // EINTR: run signal handlers, then retry with remaining time
142                vm.check_signals()?;
143            }
144        }
145
146        #[cfg(not(unix))]
147        {
148            vm.allow_threads(|| std::thread::sleep(dur));
149        }
150
151        Ok(())
152    }
153
154    #[pyfunction]
155    fn time_ns(vm: &VirtualMachine) -> PyResult<u64> {
156        Ok(duration_since_system_now(vm)?.as_nanos() as u64)
157    }
158
159    #[pyfunction]
160    pub fn time(vm: &VirtualMachine) -> PyResult<f64> {
161        _time(vm)
162    }
163
164    #[cfg(not(all(
165        target_arch = "wasm32",
166        not(any(target_os = "emscripten", target_os = "wasi")),
167    )))]
168    fn _time(vm: &VirtualMachine) -> PyResult<f64> {
169        Ok(duration_since_system_now(vm)?.as_secs_f64())
170    }
171
172    #[cfg(all(
173        target_arch = "wasm32",
174        feature = "wasmbind",
175        not(any(target_os = "emscripten", target_os = "wasi"))
176    ))]
177    fn _time(_vm: &VirtualMachine) -> PyResult<f64> {
178        use wasm_bindgen::prelude::*;
179        #[wasm_bindgen]
180        extern "C" {
181            type Date;
182            #[wasm_bindgen(static_method_of = Date)]
183            fn now() -> f64;
184        }
185        // Date.now returns unix time in milliseconds, we want it in seconds
186        Ok(Date::now() / 1000.0)
187    }
188
189    #[cfg(all(
190        target_arch = "wasm32",
191        not(feature = "wasmbind"),
192        not(any(target_os = "emscripten", target_os = "wasi"))
193    ))]
194    fn _time(vm: &VirtualMachine) -> PyResult<f64> {
195        Err(vm.new_not_implemented_error("time.time"))
196    }
197
198    #[pyfunction]
199    fn monotonic(vm: &VirtualMachine) -> PyResult<f64> {
200        Ok(get_monotonic_time(vm)?.as_secs_f64())
201    }
202
203    #[pyfunction]
204    fn monotonic_ns(vm: &VirtualMachine) -> PyResult<u128> {
205        Ok(get_monotonic_time(vm)?.as_nanos())
206    }
207
208    #[pyfunction]
209    fn perf_counter(vm: &VirtualMachine) -> PyResult<f64> {
210        Ok(get_perf_time(vm)?.as_secs_f64())
211    }
212
213    #[pyfunction]
214    fn perf_counter_ns(vm: &VirtualMachine) -> PyResult<u128> {
215        Ok(get_perf_time(vm)?.as_nanos())
216    }
217
218    #[cfg(target_env = "msvc")]
219    #[cfg(not(target_arch = "wasm32"))]
220    pub(super) fn get_tz_info() -> TIME_ZONE_INFORMATION {
221        let mut info: TIME_ZONE_INFORMATION = unsafe { core::mem::zeroed() };
222        unsafe { GetTimeZoneInformation(&mut info) };
223        info
224    }
225
226    // #[pyfunction]
227    // fn tzset() {
228    //     unsafe { super::_tzset() };
229    // }
230
231    #[cfg(not(target_env = "msvc"))]
232    #[cfg(not(target_arch = "wasm32"))]
233    #[pyattr]
234    fn altzone(_vm: &VirtualMachine) -> core::ffi::c_long {
235        // TODO: RUSTPYTHON; Add support for using the C altzone
236        unsafe { super::c_timezone - 3600 }
237    }
238
239    #[cfg(target_env = "msvc")]
240    #[cfg(not(target_arch = "wasm32"))]
241    #[pyattr]
242    fn altzone(_vm: &VirtualMachine) -> i32 {
243        let info = get_tz_info();
244        // https://users.rust-lang.org/t/accessing-tzname-and-similar-constants-in-windows/125771/3
245        (info.Bias + info.StandardBias) * 60 - 3600
246    }
247
248    #[cfg(not(target_env = "msvc"))]
249    #[cfg(not(target_arch = "wasm32"))]
250    #[pyattr]
251    fn timezone(_vm: &VirtualMachine) -> core::ffi::c_long {
252        unsafe { super::c_timezone }
253    }
254
255    #[cfg(target_env = "msvc")]
256    #[cfg(not(target_arch = "wasm32"))]
257    #[pyattr]
258    fn timezone(_vm: &VirtualMachine) -> i32 {
259        let info = get_tz_info();
260        // https://users.rust-lang.org/t/accessing-tzname-and-similar-constants-in-windows/125771/3
261        (info.Bias + info.StandardBias) * 60
262    }
263
264    #[cfg(not(target_os = "freebsd"))]
265    #[cfg(not(target_env = "msvc"))]
266    #[cfg(not(target_arch = "wasm32"))]
267    #[pyattr]
268    fn daylight(_vm: &VirtualMachine) -> core::ffi::c_int {
269        unsafe { super::c_daylight }
270    }
271
272    #[cfg(target_env = "msvc")]
273    #[cfg(not(target_arch = "wasm32"))]
274    #[pyattr]
275    fn daylight(_vm: &VirtualMachine) -> i32 {
276        let info = get_tz_info();
277        // https://users.rust-lang.org/t/accessing-tzname-and-similar-constants-in-windows/125771/3
278        (info.StandardBias != info.DaylightBias) as i32
279    }
280
281    #[cfg(not(target_env = "msvc"))]
282    #[cfg(not(target_arch = "wasm32"))]
283    #[pyattr]
284    fn tzname(vm: &VirtualMachine) -> crate::builtins::PyTupleRef {
285        use crate::builtins::tuple::IntoPyTuple;
286
287        unsafe fn to_str(s: *const core::ffi::c_char) -> String {
288            unsafe { core::ffi::CStr::from_ptr(s) }
289                .to_string_lossy()
290                .into_owned()
291        }
292        unsafe { (to_str(super::c_tzname[0]), to_str(super::c_tzname[1])) }.into_pytuple(vm)
293    }
294
295    #[cfg(target_env = "msvc")]
296    #[cfg(not(target_arch = "wasm32"))]
297    #[pyattr]
298    fn tzname(vm: &VirtualMachine) -> crate::builtins::PyTupleRef {
299        use crate::builtins::tuple::IntoPyTuple;
300        let info = get_tz_info();
301        let standard = widestring::decode_utf16_lossy(info.StandardName)
302            .take_while(|&c| c != '\0')
303            .collect::<String>();
304        let daylight = widestring::decode_utf16_lossy(info.DaylightName)
305            .take_while(|&c| c != '\0')
306            .collect::<String>();
307        let tz_name = (&*standard, &*daylight);
308        tz_name.into_pytuple(vm)
309    }
310
311    #[cfg(not(any(unix, windows)))]
312    fn pyobj_to_date_time(
313        value: Either<f64, i64>,
314        vm: &VirtualMachine,
315    ) -> PyResult<DateTime<chrono::offset::Utc>> {
316        let secs = match value {
317            Either::A(float) => {
318                if !float.is_finite() {
319                    return Err(vm.new_value_error("Invalid value for timestamp"));
320                }
321                float.floor() as i64
322            }
323            Either::B(int) => int,
324        };
325        DateTime::<chrono::offset::Utc>::from_timestamp(secs, 0)
326            .ok_or_else(|| vm.new_overflow_error("timestamp out of range for platform time_t"))
327    }
328
329    #[cfg(not(any(unix, windows)))]
330    impl OptionalArg<Option<Either<f64, i64>>> {
331        /// Construct a localtime from the optional seconds, or get the current local time.
332        fn naive_or_local(self, vm: &VirtualMachine) -> PyResult<NaiveDateTime> {
333            Ok(match self {
334                Self::Present(Some(secs)) => pyobj_to_date_time(secs, vm)?
335                    .with_timezone(&chrono::Local)
336                    .naive_local(),
337                Self::Present(None) | Self::Missing => chrono::offset::Local::now().naive_local(),
338            })
339        }
340    }
341
342    #[cfg(any(unix, windows))]
343    struct CheckedTm {
344        tm: libc::tm,
345        #[cfg(unix)]
346        zone: Option<CString>,
347    }
348
349    #[cfg(any(unix, windows))]
350    fn checked_tm_from_struct_time(
351        t: &StructTimeData,
352        vm: &VirtualMachine,
353        func_name: &'static str,
354    ) -> PyResult<CheckedTm> {
355        let invalid_tuple =
356            || vm.new_type_error(format!("{func_name}(): illegal time tuple argument"));
357
358        let year: i64 = t.tm_year.clone().try_into_value(vm).map_err(|e| {
359            if e.class().is(vm.ctx.exceptions.overflow_error) {
360                vm.new_overflow_error("year out of range")
361            } else {
362                invalid_tuple()
363            }
364        })?;
365        if year < i64::from(i32::MIN) + 1900 || year > i64::from(i32::MAX) {
366            return Err(vm.new_overflow_error("year out of range"));
367        }
368        let year = year as i32;
369        let tm_mon = t
370            .tm_mon
371            .clone()
372            .try_into_value::<i32>(vm)
373            .map_err(|_| invalid_tuple())?
374            - 1;
375        let tm_mday = t
376            .tm_mday
377            .clone()
378            .try_into_value(vm)
379            .map_err(|_| invalid_tuple())?;
380        let tm_hour = t
381            .tm_hour
382            .clone()
383            .try_into_value(vm)
384            .map_err(|_| invalid_tuple())?;
385        let tm_min = t
386            .tm_min
387            .clone()
388            .try_into_value(vm)
389            .map_err(|_| invalid_tuple())?;
390        let tm_sec = t
391            .tm_sec
392            .clone()
393            .try_into_value(vm)
394            .map_err(|_| invalid_tuple())?;
395        let tm_wday = (t
396            .tm_wday
397            .clone()
398            .try_into_value::<i32>(vm)
399            .map_err(|_| invalid_tuple())?
400            + 1)
401            % 7;
402        let tm_yday = t
403            .tm_yday
404            .clone()
405            .try_into_value::<i32>(vm)
406            .map_err(|_| invalid_tuple())?
407            - 1;
408        let tm_isdst = t
409            .tm_isdst
410            .clone()
411            .try_into_value(vm)
412            .map_err(|_| invalid_tuple())?;
413
414        let mut tm: libc::tm = unsafe { core::mem::zeroed() };
415        tm.tm_year = year - 1900;
416        tm.tm_mon = tm_mon;
417        tm.tm_mday = tm_mday;
418        tm.tm_hour = tm_hour;
419        tm.tm_min = tm_min;
420        tm.tm_sec = tm_sec;
421        tm.tm_wday = tm_wday;
422        tm.tm_yday = tm_yday;
423        tm.tm_isdst = tm_isdst;
424
425        if tm.tm_mon == -1 {
426            tm.tm_mon = 0;
427        } else if tm.tm_mon < 0 || tm.tm_mon > 11 {
428            return Err(vm.new_value_error("month out of range"));
429        }
430        if tm.tm_mday == 0 {
431            tm.tm_mday = 1;
432        } else if tm.tm_mday < 0 || tm.tm_mday > 31 {
433            return Err(vm.new_value_error("day of month out of range"));
434        }
435        if tm.tm_hour < 0 || tm.tm_hour > 23 {
436            return Err(vm.new_value_error("hour out of range"));
437        }
438        if tm.tm_min < 0 || tm.tm_min > 59 {
439            return Err(vm.new_value_error("minute out of range"));
440        }
441        if tm.tm_sec < 0 || tm.tm_sec > 61 {
442            return Err(vm.new_value_error("seconds out of range"));
443        }
444        if tm.tm_wday < 0 {
445            return Err(vm.new_value_error("day of week out of range"));
446        }
447        if tm.tm_yday == -1 {
448            tm.tm_yday = 0;
449        } else if tm.tm_yday < 0 || tm.tm_yday > 365 {
450            return Err(vm.new_value_error("day of year out of range"));
451        }
452
453        #[cfg(unix)]
454        {
455            use crate::builtins::PyUtf8StrRef;
456            let zone = if t.tm_zone.is(&vm.ctx.none) {
457                None
458            } else {
459                let zone: PyUtf8StrRef = t
460                    .tm_zone
461                    .clone()
462                    .try_into_value(vm)
463                    .map_err(|_| invalid_tuple())?;
464                Some(
465                    CString::new(zone.as_str())
466                        .map_err(|_| vm.new_value_error("embedded null character"))?,
467                )
468            };
469            if let Some(zone) = &zone {
470                tm.tm_zone = zone.as_ptr().cast_mut();
471            }
472            if !t.tm_gmtoff.is(&vm.ctx.none) {
473                let gmtoff: i64 = t
474                    .tm_gmtoff
475                    .clone()
476                    .try_into_value(vm)
477                    .map_err(|_| invalid_tuple())?;
478                tm.tm_gmtoff = gmtoff as _;
479            }
480
481            Ok(CheckedTm { tm, zone })
482        }
483        #[cfg(windows)]
484        {
485            Ok(CheckedTm { tm })
486        }
487    }
488
489    #[cfg(any(unix, windows))]
490    fn asctime_from_tm(tm: &libc::tm) -> String {
491        const WDAY_NAME: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
492        const MON_NAME: [&str; 12] = [
493            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
494        ];
495        format!(
496            "{} {}{:>3} {:02}:{:02}:{:02} {}",
497            WDAY_NAME[tm.tm_wday as usize],
498            MON_NAME[tm.tm_mon as usize],
499            tm.tm_mday,
500            tm.tm_hour,
501            tm.tm_min,
502            tm.tm_sec,
503            tm.tm_year + 1900
504        )
505    }
506
507    #[cfg(not(any(unix, windows)))]
508    impl OptionalArg<StructTimeData> {
509        fn naive_or_local(self, vm: &VirtualMachine) -> PyResult<NaiveDateTime> {
510            Ok(match self {
511                Self::Present(t) => t.to_date_time(vm)?,
512                Self::Missing => chrono::offset::Local::now().naive_local(),
513            })
514        }
515    }
516
517    /// https://docs.python.org/3/library/time.html?highlight=gmtime#time.gmtime
518    #[pyfunction]
519    fn gmtime(
520        secs: OptionalArg<Option<Either<f64, i64>>>,
521        vm: &VirtualMachine,
522    ) -> PyResult<StructTimeData> {
523        #[cfg(any(unix, windows))]
524        {
525            let ts = match secs {
526                OptionalArg::Present(Some(value)) => pyobj_to_time_t(value, vm)?,
527                OptionalArg::Present(None) | OptionalArg::Missing => current_time_t(),
528            };
529            gmtime_from_timestamp(ts, vm)
530        }
531
532        #[cfg(not(any(unix, windows)))]
533        {
534            let instant = match secs {
535                OptionalArg::Present(Some(secs)) => pyobj_to_date_time(secs, vm)?.naive_utc(),
536                OptionalArg::Present(None) | OptionalArg::Missing => {
537                    chrono::offset::Utc::now().naive_utc()
538                }
539            };
540            Ok(StructTimeData::new_utc(vm, instant))
541        }
542    }
543
544    #[pyfunction]
545    fn localtime(
546        secs: OptionalArg<Option<Either<f64, i64>>>,
547        vm: &VirtualMachine,
548    ) -> PyResult<StructTimeData> {
549        #[cfg(any(unix, windows))]
550        {
551            let ts = match secs {
552                OptionalArg::Present(Some(value)) => pyobj_to_time_t(value, vm)?,
553                OptionalArg::Present(None) | OptionalArg::Missing => current_time_t(),
554            };
555            localtime_from_timestamp(ts, vm)
556        }
557
558        #[cfg(not(any(unix, windows)))]
559        let instant = secs.naive_or_local(vm)?;
560        #[cfg(not(any(unix, windows)))]
561        {
562            Ok(StructTimeData::new_local(vm, instant, 0))
563        }
564    }
565
566    #[pyfunction]
567    fn mktime(t: StructTimeData, vm: &VirtualMachine) -> PyResult<f64> {
568        #[cfg(unix)]
569        {
570            unix_mktime(&t, vm)
571        }
572
573        #[cfg(windows)]
574        {
575            win_mktime(&t, vm)
576        }
577
578        #[cfg(not(any(unix, windows)))]
579        {
580            let datetime = t.to_date_time(vm)?;
581            // mktime interprets struct_time as local time
582            let local_dt = chrono::Local
583                .from_local_datetime(&datetime)
584                .single()
585                .ok_or_else(|| vm.new_overflow_error("mktime argument out of range"))?;
586            let seconds_since_epoch = local_dt.timestamp() as f64;
587            Ok(seconds_since_epoch)
588        }
589    }
590
591    #[cfg(not(any(unix, windows)))]
592    const CFMT: &str = "%a %b %e %H:%M:%S %Y";
593
594    #[pyfunction]
595    fn asctime(t: OptionalArg<StructTimeData>, vm: &VirtualMachine) -> PyResult {
596        #[cfg(any(unix, windows))]
597        {
598            let tm = match t {
599                OptionalArg::Present(value) => {
600                    checked_tm_from_struct_time(&value, vm, "asctime")?.tm
601                }
602                OptionalArg::Missing => {
603                    let now = current_time_t();
604                    let local = localtime_from_timestamp(now, vm)?;
605                    checked_tm_from_struct_time(&local, vm, "asctime")?.tm
606                }
607            };
608            Ok(vm.ctx.new_str(asctime_from_tm(&tm)).into())
609        }
610
611        #[cfg(not(any(unix, windows)))]
612        {
613            let instant = t.naive_or_local(vm)?;
614            let formatted_time = instant.format(CFMT).to_string();
615            Ok(vm.ctx.new_str(formatted_time).into())
616        }
617    }
618
619    #[pyfunction]
620    fn ctime(secs: OptionalArg<Option<Either<f64, i64>>>, vm: &VirtualMachine) -> PyResult<String> {
621        #[cfg(any(unix, windows))]
622        {
623            let ts = match secs {
624                OptionalArg::Present(Some(value)) => pyobj_to_time_t(value, vm)?,
625                OptionalArg::Present(None) | OptionalArg::Missing => current_time_t(),
626            };
627            let local = localtime_from_timestamp(ts, vm)?;
628            let tm = checked_tm_from_struct_time(&local, vm, "asctime")?.tm;
629            Ok(asctime_from_tm(&tm))
630        }
631
632        #[cfg(not(any(unix, windows)))]
633        {
634            let instant = secs.naive_or_local(vm)?;
635            Ok(instant.format(CFMT).to_string())
636        }
637    }
638
639    #[cfg(any(unix, windows))]
640    fn strftime_crt(format: &PyStrRef, checked_tm: CheckedTm, vm: &VirtualMachine) -> PyResult {
641        #[cfg(unix)]
642        let _keep_zone_alive = &checked_tm.zone;
643        let mut tm = checked_tm.tm;
644        tm.tm_isdst = tm.tm_isdst.clamp(-1, 1);
645
646        // MSVC strftime requires year in [1; 9999]
647        #[cfg(windows)]
648        {
649            let year = tm.tm_year + 1900;
650            if !(1..=9999).contains(&year) {
651                return Err(vm.new_value_error("strftime() requires year in [1; 9999]"));
652            }
653        }
654
655        #[cfg(unix)]
656        fn strftime_ascii(fmt: &str, tm: &libc::tm, vm: &VirtualMachine) -> PyResult<String> {
657            let fmt_c =
658                CString::new(fmt).map_err(|_| vm.new_value_error("embedded null character"))?;
659            let mut size = 1024usize;
660            let max_scale = 256usize.saturating_mul(fmt.len().max(1));
661            loop {
662                let mut out = vec![0u8; size];
663                let written = unsafe {
664                    libc::strftime(
665                        out.as_mut_ptr().cast(),
666                        out.len(),
667                        fmt_c.as_ptr(),
668                        tm as *const libc::tm,
669                    )
670                };
671                if written > 0 || size >= max_scale {
672                    return Ok(String::from_utf8_lossy(&out[..written]).into_owned());
673                }
674                size = size.saturating_mul(2);
675            }
676        }
677
678        #[cfg(windows)]
679        fn strftime_ascii(fmt: &str, tm: &libc::tm, vm: &VirtualMachine) -> PyResult<String> {
680            if fmt.contains('\0') {
681                return Err(vm.new_value_error("embedded null character"));
682            }
683            // Use wcsftime for proper Unicode output (e.g. %Z timezone names)
684            let fmt_wide: Vec<u16> = fmt.encode_utf16().chain(core::iter::once(0)).collect();
685            let mut size = 1024usize;
686            let max_scale = 256usize.saturating_mul(fmt.len().max(1));
687            loop {
688                let mut out = vec![0u16; size];
689                let written = unsafe {
690                    rustpython_common::suppress_iph!(wcsftime(
691                        out.as_mut_ptr(),
692                        out.len(),
693                        fmt_wide.as_ptr(),
694                        tm as *const libc::tm,
695                    ))
696                };
697                if written > 0 || size >= max_scale {
698                    return Ok(String::from_utf16_lossy(&out[..written]));
699                }
700                size = size.saturating_mul(2);
701            }
702        }
703
704        let mut out = Wtf8Buf::new();
705        let mut ascii = String::new();
706
707        for codepoint in format.as_wtf8().code_points() {
708            if codepoint.to_u32() == 0 {
709                if !ascii.is_empty() {
710                    let part = strftime_ascii(&ascii, &tm, vm)?;
711                    out.extend(part.chars());
712                    ascii.clear();
713                }
714                out.push(codepoint);
715                continue;
716            }
717            if let Some(ch) = codepoint.to_char()
718                && ch.is_ascii()
719            {
720                ascii.push(ch);
721                continue;
722            }
723
724            if !ascii.is_empty() {
725                let part = strftime_ascii(&ascii, &tm, vm)?;
726                out.extend(part.chars());
727                ascii.clear();
728            }
729            out.push(codepoint);
730        }
731        if !ascii.is_empty() {
732            let part = strftime_ascii(&ascii, &tm, vm)?;
733            out.extend(part.chars());
734        }
735        Ok(out.to_pyobject(vm))
736    }
737
738    #[pyfunction]
739    fn strftime(format: PyStrRef, t: OptionalArg<StructTimeData>, vm: &VirtualMachine) -> PyResult {
740        #[cfg(any(unix, windows))]
741        {
742            let checked_tm = match t {
743                OptionalArg::Present(value) => checked_tm_from_struct_time(&value, vm, "strftime")?,
744                OptionalArg::Missing => {
745                    let now = current_time_t();
746                    let local = localtime_from_timestamp(now, vm)?;
747                    checked_tm_from_struct_time(&local, vm, "strftime")?
748                }
749            };
750            strftime_crt(&format, checked_tm, vm)
751        }
752
753        #[cfg(not(any(unix, windows)))]
754        {
755            use core::fmt::Write;
756
757            let fmt_lossy = format.to_string_lossy();
758
759            // If the struct_time can't be represented as NaiveDateTime
760            // (e.g. month=0), return the format string as-is, matching
761            // the fallback behavior for unsupported chrono formats.
762            let instant = match t.naive_or_local(vm) {
763                Ok(dt) => dt,
764                Err(_) => return Ok(vm.ctx.new_str(fmt_lossy.into_owned()).into()),
765            };
766
767            let mut formatted_time = String::new();
768            write!(&mut formatted_time, "{}", instant.format(&fmt_lossy))
769                .unwrap_or_else(|_| formatted_time = format.to_string());
770            Ok(vm.ctx.new_str(formatted_time).into())
771        }
772    }
773
774    #[pyfunction]
775    fn strptime(string: PyStrRef, format: OptionalArg<PyStrRef>, vm: &VirtualMachine) -> PyResult {
776        // Call _strptime._strptime_time like CPython does
777        let strptime_module = vm.import("_strptime", 0)?;
778        let strptime_func = strptime_module.get_attr("_strptime_time", vm)?;
779
780        // Call with positional arguments
781        match format.into_option() {
782            Some(fmt) => strptime_func.call((string, fmt), vm),
783            None => strptime_func.call((string,), vm),
784        }
785    }
786
787    #[cfg(not(any(
788        windows,
789        target_vendor = "apple",
790        target_os = "android",
791        target_os = "dragonfly",
792        target_os = "freebsd",
793        target_os = "linux",
794        target_os = "fuchsia",
795        target_os = "emscripten",
796    )))]
797    fn get_thread_time(vm: &VirtualMachine) -> PyResult<Duration> {
798        Err(vm.new_not_implemented_error("thread time unsupported in this system"))
799    }
800
801    #[pyfunction]
802    fn thread_time(vm: &VirtualMachine) -> PyResult<f64> {
803        Ok(get_thread_time(vm)?.as_secs_f64())
804    }
805
806    #[pyfunction]
807    fn thread_time_ns(vm: &VirtualMachine) -> PyResult<u64> {
808        Ok(get_thread_time(vm)?.as_nanos() as u64)
809    }
810
811    #[cfg(any(windows, all(target_arch = "wasm32", target_os = "emscripten")))]
812    pub(super) fn time_muldiv(ticks: i64, mul: i64, div: i64) -> u64 {
813        let int_part = ticks / div;
814        let ticks = ticks % div;
815        let remaining = (ticks * mul) / div;
816        (int_part * mul + remaining) as u64
817    }
818
819    #[cfg(all(target_arch = "wasm32", target_os = "emscripten"))]
820    fn get_process_time(vm: &VirtualMachine) -> PyResult<Duration> {
821        let t: libc::tms = unsafe {
822            let mut t = core::mem::MaybeUninit::uninit();
823            if libc::times(t.as_mut_ptr()) == -1 {
824                return Err(vm.new_os_error("Failed to get clock time".to_owned()));
825            }
826            t.assume_init()
827        };
828        let freq = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
829
830        Ok(Duration::from_nanos(
831            time_muldiv(t.tms_utime, SEC_TO_NS, freq) + time_muldiv(t.tms_stime, SEC_TO_NS, freq),
832        ))
833    }
834
835    #[cfg(not(any(
836        windows,
837        target_os = "macos",
838        target_os = "android",
839        target_os = "dragonfly",
840        target_os = "freebsd",
841        target_os = "linux",
842        target_os = "illumos",
843        target_os = "netbsd",
844        target_os = "solaris",
845        target_os = "openbsd",
846        target_os = "redox",
847        all(target_arch = "wasm32", target_os = "emscripten")
848    )))]
849    fn get_process_time(vm: &VirtualMachine) -> PyResult<Duration> {
850        Err(vm.new_not_implemented_error("process time unsupported in this system"))
851    }
852
853    #[pyfunction]
854    fn process_time(vm: &VirtualMachine) -> PyResult<f64> {
855        Ok(get_process_time(vm)?.as_secs_f64())
856    }
857
858    #[pyfunction]
859    fn process_time_ns(vm: &VirtualMachine) -> PyResult<u64> {
860        Ok(get_process_time(vm)?.as_nanos() as u64)
861    }
862
863    /// Data struct for struct_time
864    #[pystruct_sequence_data(try_from_object)]
865    pub struct StructTimeData {
866        pub tm_year: PyObjectRef,
867        pub tm_mon: PyObjectRef,
868        pub tm_mday: PyObjectRef,
869        pub tm_hour: PyObjectRef,
870        pub tm_min: PyObjectRef,
871        pub tm_sec: PyObjectRef,
872        pub tm_wday: PyObjectRef,
873        pub tm_yday: PyObjectRef,
874        pub tm_isdst: PyObjectRef,
875        #[pystruct_sequence(skip)]
876        pub tm_zone: PyObjectRef,
877        #[pystruct_sequence(skip)]
878        pub tm_gmtoff: PyObjectRef,
879    }
880
881    impl core::fmt::Debug for StructTimeData {
882        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
883            write!(f, "struct_time()")
884        }
885    }
886
887    impl StructTimeData {
888        #[cfg(not(any(unix, windows)))]
889        fn new_inner(
890            vm: &VirtualMachine,
891            tm: NaiveDateTime,
892            isdst: i32,
893            gmtoff: i32,
894            zone: &str,
895        ) -> Self {
896            Self {
897                tm_year: vm.ctx.new_int(tm.year()).into(),
898                tm_mon: vm.ctx.new_int(tm.month()).into(),
899                tm_mday: vm.ctx.new_int(tm.day()).into(),
900                tm_hour: vm.ctx.new_int(tm.hour()).into(),
901                tm_min: vm.ctx.new_int(tm.minute()).into(),
902                tm_sec: vm.ctx.new_int(tm.second()).into(),
903                tm_wday: vm.ctx.new_int(tm.weekday().num_days_from_monday()).into(),
904                tm_yday: vm.ctx.new_int(tm.ordinal()).into(),
905                tm_isdst: vm.ctx.new_int(isdst).into(),
906                tm_zone: vm.ctx.new_str(zone).into(),
907                tm_gmtoff: vm.ctx.new_int(gmtoff).into(),
908            }
909        }
910
911        /// Create struct_time for UTC (gmtime)
912        #[cfg(not(any(unix, windows)))]
913        fn new_utc(vm: &VirtualMachine, tm: NaiveDateTime) -> Self {
914            Self::new_inner(vm, tm, 0, 0, "UTC")
915        }
916
917        /// Create struct_time for local timezone (localtime)
918        #[cfg(not(any(unix, windows)))]
919        fn new_local(vm: &VirtualMachine, tm: NaiveDateTime, isdst: i32) -> Self {
920            let local_time = chrono::Local.from_local_datetime(&tm).unwrap();
921            let offset_seconds = local_time.offset().local_minus_utc();
922            let tz_abbr = local_time.format("%Z").to_string();
923            Self::new_inner(vm, tm, isdst, offset_seconds, &tz_abbr)
924        }
925
926        #[cfg(not(any(unix, windows)))]
927        fn to_date_time(&self, vm: &VirtualMachine) -> PyResult<NaiveDateTime> {
928            let invalid_overflow = || vm.new_overflow_error("mktime argument out of range");
929            let invalid_value = || vm.new_value_error("invalid struct_time parameter");
930
931            macro_rules! field {
932                ($field:ident) => {
933                    self.$field.clone().try_into_value(vm)?
934                };
935            }
936            let dt = NaiveDateTime::new(
937                NaiveDate::from_ymd_opt(field!(tm_year), field!(tm_mon), field!(tm_mday))
938                    .ok_or_else(invalid_value)?,
939                NaiveTime::from_hms_opt(field!(tm_hour), field!(tm_min), field!(tm_sec))
940                    .ok_or_else(invalid_overflow)?,
941            );
942            Ok(dt)
943        }
944    }
945
946    #[pyattr]
947    #[pystruct_sequence(name = "struct_time", module = "time", data = "StructTimeData")]
948    pub struct PyStructTime;
949
950    #[pyclass(with(PyStructSequence))]
951    impl PyStructTime {
952        #[pyslot]
953        fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
954            let (seq, _dict): (PyObjectRef, OptionalArg<PyObjectRef>) = args.bind(vm)?;
955            struct_sequence_new(cls, seq, vm)
956        }
957    }
958
959    /// Extract fields from StructTimeData into a libc::tm for mktime.
960    #[cfg(any(unix, windows))]
961    pub(super) fn tm_from_struct_time(
962        t: &StructTimeData,
963        vm: &VirtualMachine,
964    ) -> PyResult<libc::tm> {
965        let invalid_tuple = || vm.new_type_error("mktime(): illegal time tuple argument");
966        let year: i32 = t
967            .tm_year
968            .clone()
969            .try_into_value(vm)
970            .map_err(|_| invalid_tuple())?;
971        if year < i32::MIN + 1900 {
972            return Err(vm.new_overflow_error("year out of range"));
973        }
974
975        let mut tm: libc::tm = unsafe { core::mem::zeroed() };
976        tm.tm_sec = t
977            .tm_sec
978            .clone()
979            .try_into_value(vm)
980            .map_err(|_| invalid_tuple())?;
981        tm.tm_min = t
982            .tm_min
983            .clone()
984            .try_into_value(vm)
985            .map_err(|_| invalid_tuple())?;
986        tm.tm_hour = t
987            .tm_hour
988            .clone()
989            .try_into_value(vm)
990            .map_err(|_| invalid_tuple())?;
991        tm.tm_mday = t
992            .tm_mday
993            .clone()
994            .try_into_value(vm)
995            .map_err(|_| invalid_tuple())?;
996        tm.tm_mon = t
997            .tm_mon
998            .clone()
999            .try_into_value::<i32>(vm)
1000            .map_err(|_| invalid_tuple())?
1001            - 1;
1002        tm.tm_year = year - 1900;
1003        tm.tm_wday = -1;
1004        tm.tm_yday = t
1005            .tm_yday
1006            .clone()
1007            .try_into_value::<i32>(vm)
1008            .map_err(|_| invalid_tuple())?
1009            - 1;
1010        tm.tm_isdst = t
1011            .tm_isdst
1012            .clone()
1013            .try_into_value(vm)
1014            .map_err(|_| invalid_tuple())?;
1015        Ok(tm)
1016    }
1017
1018    #[cfg(any(unix, windows))]
1019    #[cfg_attr(target_env = "musl", allow(deprecated))]
1020    fn pyobj_to_time_t(value: Either<f64, i64>, vm: &VirtualMachine) -> PyResult<libc::time_t> {
1021        match value {
1022            Either::A(float) => {
1023                if !float.is_finite() {
1024                    return Err(vm.new_value_error("Invalid value for timestamp"));
1025                }
1026                let secs = float.floor();
1027                #[cfg_attr(target_env = "musl", allow(deprecated))]
1028                if secs < libc::time_t::MIN as f64 || secs > libc::time_t::MAX as f64 {
1029                    return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
1030                }
1031                #[cfg_attr(target_env = "musl", allow(deprecated))]
1032                Ok(secs as libc::time_t)
1033            }
1034            Either::B(int) => {
1035                // try_into is needed on 32-bit platforms where time_t != i64
1036                #[allow(clippy::useless_conversion)]
1037                #[cfg_attr(target_env = "musl", allow(deprecated))]
1038                let ts: libc::time_t = int.try_into().map_err(|_| {
1039                    vm.new_overflow_error("timestamp out of range for platform time_t")
1040                })?;
1041                Ok(ts)
1042            }
1043        }
1044    }
1045
1046    #[cfg(any(unix, windows))]
1047    #[allow(unused_imports)]
1048    use super::platform::*;
1049
1050    pub(crate) fn module_exec(
1051        vm: &VirtualMachine,
1052        module: &Py<crate::builtins::PyModule>,
1053    ) -> PyResult<()> {
1054        #[cfg(not(target_env = "msvc"))]
1055        #[cfg(not(target_arch = "wasm32"))]
1056        unsafe {
1057            super::c_tzset()
1058        };
1059
1060        __module_exec(vm, module);
1061        Ok(())
1062    }
1063}
1064
1065#[cfg(unix)]
1066#[pymodule(sub)]
1067mod platform {
1068    #[allow(unused_imports)]
1069    use super::decl::{SEC_TO_NS, StructTimeData, US_TO_NS};
1070    #[cfg_attr(target_os = "macos", allow(unused_imports))]
1071    use crate::{
1072        PyObject, PyRef, PyResult, TryFromBorrowedObject, VirtualMachine,
1073        builtins::{PyNamespace, PyUtf8StrRef},
1074        convert::IntoPyException,
1075    };
1076    use core::time::Duration;
1077    #[cfg_attr(target_env = "musl", allow(deprecated))]
1078    use libc::time_t;
1079    use nix::{sys::time::TimeSpec, time::ClockId};
1080
1081    #[cfg(target_os = "solaris")]
1082    #[pyattr]
1083    use libc::CLOCK_HIGHRES;
1084    #[cfg(not(any(
1085        target_os = "illumos",
1086        target_os = "netbsd",
1087        target_os = "solaris",
1088        target_os = "openbsd",
1089        target_os = "wasi",
1090    )))]
1091    #[pyattr]
1092    use libc::CLOCK_PROCESS_CPUTIME_ID;
1093    #[cfg(not(any(
1094        target_os = "illumos",
1095        target_os = "netbsd",
1096        target_os = "solaris",
1097        target_os = "openbsd",
1098        target_os = "redox",
1099    )))]
1100    #[pyattr]
1101    use libc::CLOCK_THREAD_CPUTIME_ID;
1102    #[cfg(target_os = "linux")]
1103    #[pyattr]
1104    use libc::{CLOCK_BOOTTIME, CLOCK_MONOTONIC_RAW, CLOCK_TAI};
1105    #[pyattr]
1106    use libc::{CLOCK_MONOTONIC, CLOCK_REALTIME};
1107    #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly"))]
1108    #[pyattr]
1109    use libc::{CLOCK_PROF, CLOCK_UPTIME};
1110
1111    impl<'a> TryFromBorrowedObject<'a> for ClockId {
1112        fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult<Self> {
1113            obj.try_to_value(vm).map(Self::from_raw)
1114        }
1115    }
1116
1117    fn struct_time_from_tm(vm: &VirtualMachine, tm: libc::tm) -> StructTimeData {
1118        let zone = unsafe {
1119            if tm.tm_zone.is_null() {
1120                String::new()
1121            } else {
1122                core::ffi::CStr::from_ptr(tm.tm_zone)
1123                    .to_string_lossy()
1124                    .into_owned()
1125            }
1126        };
1127        StructTimeData {
1128            tm_year: vm.ctx.new_int(tm.tm_year + 1900).into(),
1129            tm_mon: vm.ctx.new_int(tm.tm_mon + 1).into(),
1130            tm_mday: vm.ctx.new_int(tm.tm_mday).into(),
1131            tm_hour: vm.ctx.new_int(tm.tm_hour).into(),
1132            tm_min: vm.ctx.new_int(tm.tm_min).into(),
1133            tm_sec: vm.ctx.new_int(tm.tm_sec).into(),
1134            tm_wday: vm.ctx.new_int((tm.tm_wday + 6) % 7).into(),
1135            tm_yday: vm.ctx.new_int(tm.tm_yday + 1).into(),
1136            tm_isdst: vm.ctx.new_int(tm.tm_isdst).into(),
1137            tm_zone: vm.ctx.new_str(zone).into(),
1138            tm_gmtoff: vm.ctx.new_int(tm.tm_gmtoff).into(),
1139        }
1140    }
1141
1142    #[cfg_attr(target_env = "musl", allow(deprecated))]
1143    pub(super) fn current_time_t() -> time_t {
1144        unsafe { libc::time(core::ptr::null_mut()) }
1145    }
1146
1147    #[cfg_attr(target_env = "musl", allow(deprecated))]
1148    pub(super) fn gmtime_from_timestamp(
1149        when: time_t,
1150        vm: &VirtualMachine,
1151    ) -> PyResult<StructTimeData> {
1152        let mut out = core::mem::MaybeUninit::<libc::tm>::uninit();
1153        let ret = unsafe { libc::gmtime_r(&when, out.as_mut_ptr()) };
1154        if ret.is_null() {
1155            return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
1156        }
1157        Ok(struct_time_from_tm(vm, unsafe { out.assume_init() }))
1158    }
1159
1160    #[cfg_attr(target_env = "musl", allow(deprecated))]
1161    pub(super) fn localtime_from_timestamp(
1162        when: time_t,
1163        vm: &VirtualMachine,
1164    ) -> PyResult<StructTimeData> {
1165        let mut out = core::mem::MaybeUninit::<libc::tm>::uninit();
1166        let ret = unsafe { libc::localtime_r(&when, out.as_mut_ptr()) };
1167        if ret.is_null() {
1168            return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
1169        }
1170        Ok(struct_time_from_tm(vm, unsafe { out.assume_init() }))
1171    }
1172
1173    pub(super) fn unix_mktime(t: &StructTimeData, vm: &VirtualMachine) -> PyResult<f64> {
1174        let mut tm = super::decl::tm_from_struct_time(t, vm)?;
1175        let timestamp = unsafe { libc::mktime(&mut tm) };
1176        if timestamp == -1 && tm.tm_wday == -1 {
1177            return Err(vm.new_overflow_error("mktime argument out of range"));
1178        }
1179        Ok(timestamp as f64)
1180    }
1181
1182    fn get_clock_time(clk_id: ClockId, vm: &VirtualMachine) -> PyResult<Duration> {
1183        let ts = nix::time::clock_gettime(clk_id).map_err(|e| e.into_pyexception(vm))?;
1184        Ok(ts.into())
1185    }
1186
1187    #[pyfunction]
1188    fn clock_gettime(clk_id: ClockId, vm: &VirtualMachine) -> PyResult<f64> {
1189        get_clock_time(clk_id, vm).map(|d| d.as_secs_f64())
1190    }
1191
1192    #[pyfunction]
1193    fn clock_gettime_ns(clk_id: ClockId, vm: &VirtualMachine) -> PyResult<u128> {
1194        get_clock_time(clk_id, vm).map(|d| d.as_nanos())
1195    }
1196
1197    #[cfg(not(target_os = "redox"))]
1198    #[pyfunction]
1199    fn clock_getres(clk_id: ClockId, vm: &VirtualMachine) -> PyResult<f64> {
1200        let ts = nix::time::clock_getres(clk_id).map_err(|e| e.into_pyexception(vm))?;
1201        Ok(Duration::from(ts).as_secs_f64())
1202    }
1203
1204    #[cfg(not(target_os = "redox"))]
1205    #[cfg(not(target_vendor = "apple"))]
1206    fn set_clock_time(clk_id: ClockId, timespec: TimeSpec, vm: &VirtualMachine) -> PyResult<()> {
1207        nix::time::clock_settime(clk_id, timespec).map_err(|e| e.into_pyexception(vm))
1208    }
1209
1210    #[cfg(not(target_os = "redox"))]
1211    #[cfg(target_os = "macos")]
1212    fn set_clock_time(clk_id: ClockId, timespec: TimeSpec, vm: &VirtualMachine) -> PyResult<()> {
1213        // idk why nix disables clock_settime on macos
1214        let ret = unsafe { libc::clock_settime(clk_id.as_raw(), timespec.as_ref()) };
1215        nix::Error::result(ret)
1216            .map(drop)
1217            .map_err(|e| e.into_pyexception(vm))
1218    }
1219
1220    #[cfg(not(target_os = "redox"))]
1221    #[cfg(any(not(target_vendor = "apple"), target_os = "macos"))]
1222    #[pyfunction]
1223    fn clock_settime(clk_id: ClockId, time: Duration, vm: &VirtualMachine) -> PyResult<()> {
1224        set_clock_time(clk_id, time.into(), vm)
1225    }
1226
1227    #[cfg(not(target_os = "redox"))]
1228    #[cfg(any(not(target_vendor = "apple"), target_os = "macos"))]
1229    #[cfg_attr(target_env = "musl", allow(deprecated))]
1230    #[pyfunction]
1231    fn clock_settime_ns(clk_id: ClockId, time: libc::time_t, vm: &VirtualMachine) -> PyResult<()> {
1232        let ts = Duration::from_nanos(time as _).into();
1233        set_clock_time(clk_id, ts, vm)
1234    }
1235
1236    // Requires all CLOCK constants available and clock_getres
1237    #[cfg(any(
1238        target_os = "macos",
1239        target_os = "android",
1240        target_os = "dragonfly",
1241        target_os = "freebsd",
1242        target_os = "fuchsia",
1243        target_os = "emscripten",
1244        target_os = "linux",
1245    ))]
1246    #[pyfunction]
1247    fn get_clock_info(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<PyRef<PyNamespace>> {
1248        let (adj, imp, mono, res) = match name.as_str() {
1249            "monotonic" | "perf_counter" => (
1250                false,
1251                "time.clock_gettime(CLOCK_MONOTONIC)",
1252                true,
1253                clock_getres(ClockId::CLOCK_MONOTONIC, vm)?,
1254            ),
1255            "process_time" => (
1256                false,
1257                "time.clock_gettime(CLOCK_PROCESS_CPUTIME_ID)",
1258                true,
1259                clock_getres(ClockId::CLOCK_PROCESS_CPUTIME_ID, vm)?,
1260            ),
1261            "thread_time" => (
1262                false,
1263                "time.clock_gettime(CLOCK_THREAD_CPUTIME_ID)",
1264                true,
1265                clock_getres(ClockId::CLOCK_THREAD_CPUTIME_ID, vm)?,
1266            ),
1267            "time" => (
1268                true,
1269                "time.clock_gettime(CLOCK_REALTIME)",
1270                false,
1271                clock_getres(ClockId::CLOCK_REALTIME, vm)?,
1272            ),
1273            _ => return Err(vm.new_value_error("unknown clock")),
1274        };
1275
1276        Ok(py_namespace!(vm, {
1277            "implementation" => vm.new_pyobj(imp),
1278            "monotonic" => vm.ctx.new_bool(mono),
1279            "adjustable" => vm.ctx.new_bool(adj),
1280            "resolution" => vm.ctx.new_float(res),
1281        }))
1282    }
1283
1284    #[cfg(not(any(
1285        target_os = "macos",
1286        target_os = "android",
1287        target_os = "dragonfly",
1288        target_os = "freebsd",
1289        target_os = "fuchsia",
1290        target_os = "emscripten",
1291        target_os = "linux",
1292    )))]
1293    #[pyfunction]
1294    fn get_clock_info(_name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<PyRef<PyNamespace>> {
1295        Err(vm.new_not_implemented_error("get_clock_info unsupported on this system"))
1296    }
1297
1298    pub(super) fn get_monotonic_time(vm: &VirtualMachine) -> PyResult<Duration> {
1299        get_clock_time(ClockId::CLOCK_MONOTONIC, vm)
1300    }
1301
1302    pub(super) fn get_perf_time(vm: &VirtualMachine) -> PyResult<Duration> {
1303        get_clock_time(ClockId::CLOCK_MONOTONIC, vm)
1304    }
1305
1306    #[cfg(not(any(
1307        target_os = "illumos",
1308        target_os = "netbsd",
1309        target_os = "openbsd",
1310        target_os = "redox"
1311    )))]
1312    pub(super) fn get_thread_time(vm: &VirtualMachine) -> PyResult<Duration> {
1313        get_clock_time(ClockId::CLOCK_THREAD_CPUTIME_ID, vm)
1314    }
1315
1316    #[cfg(target_os = "solaris")]
1317    pub(super) fn get_thread_time(vm: &VirtualMachine) -> PyResult<Duration> {
1318        Ok(Duration::from_nanos(unsafe { libc::gethrvtime() }))
1319    }
1320
1321    #[cfg(not(any(
1322        target_os = "illumos",
1323        target_os = "netbsd",
1324        target_os = "solaris",
1325        target_os = "openbsd",
1326    )))]
1327    pub(super) fn get_process_time(vm: &VirtualMachine) -> PyResult<Duration> {
1328        get_clock_time(ClockId::CLOCK_PROCESS_CPUTIME_ID, vm)
1329    }
1330
1331    #[cfg(any(
1332        target_os = "illumos",
1333        target_os = "netbsd",
1334        target_os = "solaris",
1335        target_os = "openbsd",
1336    ))]
1337    pub(super) fn get_process_time(vm: &VirtualMachine) -> PyResult<Duration> {
1338        use nix::sys::resource::{UsageWho, getrusage};
1339        fn from_timeval(tv: libc::timeval, vm: &VirtualMachine) -> PyResult<i64> {
1340            (|tv: libc::timeval| {
1341                let t = tv.tv_sec.checked_mul(SEC_TO_NS)?;
1342                let u = (tv.tv_usec as i64).checked_mul(US_TO_NS)?;
1343                t.checked_add(u)
1344            })(tv)
1345            .ok_or_else(|| vm.new_overflow_error("timestamp too large to convert to i64"))
1346        }
1347        let ru = getrusage(UsageWho::RUSAGE_SELF).map_err(|e| e.into_pyexception(vm))?;
1348        let utime = from_timeval(ru.user_time().into(), vm)?;
1349        let stime = from_timeval(ru.system_time().into(), vm)?;
1350
1351        Ok(Duration::from_nanos((utime + stime) as u64))
1352    }
1353}
1354
1355#[cfg(windows)]
1356#[pymodule(sub)]
1357mod platform {
1358    use super::decl::{MS_TO_NS, SEC_TO_NS, StructTimeData, get_tz_info, time_muldiv};
1359    use crate::{
1360        PyRef, PyResult, VirtualMachine,
1361        builtins::{PyNamespace, PyUtf8StrRef},
1362    };
1363    use core::time::Duration;
1364    use windows_sys::Win32::{
1365        Foundation::FILETIME,
1366        System::Performance::{QueryPerformanceCounter, QueryPerformanceFrequency},
1367        System::SystemInformation::{GetSystemTimeAdjustment, GetTickCount64},
1368        System::Threading::{GetCurrentProcess, GetCurrentThread, GetProcessTimes, GetThreadTimes},
1369    };
1370
1371    unsafe extern "C" {
1372        fn _gmtime64_s(tm: *mut libc::tm, time: *const libc::time_t) -> libc::c_int;
1373        fn _localtime64_s(tm: *mut libc::tm, time: *const libc::time_t) -> libc::c_int;
1374        #[link_name = "_mktime64"]
1375        fn c_mktime(tm: *mut libc::tm) -> libc::time_t;
1376    }
1377
1378    fn struct_time_from_tm(
1379        vm: &VirtualMachine,
1380        tm: libc::tm,
1381        zone: &str,
1382        gmtoff: i32,
1383    ) -> StructTimeData {
1384        StructTimeData {
1385            tm_year: vm.ctx.new_int(tm.tm_year + 1900).into(),
1386            tm_mon: vm.ctx.new_int(tm.tm_mon + 1).into(),
1387            tm_mday: vm.ctx.new_int(tm.tm_mday).into(),
1388            tm_hour: vm.ctx.new_int(tm.tm_hour).into(),
1389            tm_min: vm.ctx.new_int(tm.tm_min).into(),
1390            tm_sec: vm.ctx.new_int(tm.tm_sec).into(),
1391            tm_wday: vm.ctx.new_int((tm.tm_wday + 6) % 7).into(),
1392            tm_yday: vm.ctx.new_int(tm.tm_yday + 1).into(),
1393            tm_isdst: vm.ctx.new_int(tm.tm_isdst).into(),
1394            tm_zone: vm.ctx.new_str(zone).into(),
1395            tm_gmtoff: vm.ctx.new_int(gmtoff).into(),
1396        }
1397    }
1398
1399    #[cfg_attr(target_env = "musl", allow(deprecated))]
1400    pub(super) fn current_time_t() -> libc::time_t {
1401        unsafe { libc::time(core::ptr::null_mut()) }
1402    }
1403
1404    #[cfg_attr(target_env = "musl", allow(deprecated))]
1405    pub(super) fn gmtime_from_timestamp(
1406        when: libc::time_t,
1407        vm: &VirtualMachine,
1408    ) -> PyResult<StructTimeData> {
1409        let mut out = core::mem::MaybeUninit::<libc::tm>::uninit();
1410        let err = unsafe { _gmtime64_s(out.as_mut_ptr(), &when) };
1411        if err != 0 {
1412            return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
1413        }
1414        Ok(struct_time_from_tm(
1415            vm,
1416            unsafe { out.assume_init() },
1417            "UTC",
1418            0,
1419        ))
1420    }
1421
1422    #[cfg_attr(target_env = "musl", allow(deprecated))]
1423    pub(super) fn localtime_from_timestamp(
1424        when: libc::time_t,
1425        vm: &VirtualMachine,
1426    ) -> PyResult<StructTimeData> {
1427        let mut out = core::mem::MaybeUninit::<libc::tm>::uninit();
1428        let err = unsafe { _localtime64_s(out.as_mut_ptr(), &when) };
1429        if err != 0 {
1430            return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
1431        }
1432        let tm = unsafe { out.assume_init() };
1433
1434        // Get timezone info from Windows API
1435        let info = get_tz_info();
1436        let (bias, name) = if tm.tm_isdst > 0 {
1437            (info.DaylightBias, &info.DaylightName)
1438        } else {
1439            (info.StandardBias, &info.StandardName)
1440        };
1441        let zone = widestring::decode_utf16_lossy(name.iter().copied())
1442            .take_while(|&c| c != '\0')
1443            .collect::<String>();
1444        let gmtoff = -((info.Bias + bias) as i32) * 60;
1445
1446        Ok(struct_time_from_tm(vm, tm, &zone, gmtoff))
1447    }
1448
1449    pub(super) fn win_mktime(t: &StructTimeData, vm: &VirtualMachine) -> PyResult<f64> {
1450        let mut tm = super::decl::tm_from_struct_time(t, vm)?;
1451        let timestamp = unsafe { rustpython_common::suppress_iph!(c_mktime(&mut tm)) };
1452        if timestamp == -1 && tm.tm_wday == -1 {
1453            return Err(vm.new_overflow_error("mktime argument out of range"));
1454        }
1455        Ok(timestamp as f64)
1456    }
1457
1458    fn u64_from_filetime(time: FILETIME) -> u64 {
1459        let large: [u32; 2] = [time.dwLowDateTime, time.dwHighDateTime];
1460        unsafe { core::mem::transmute(large) }
1461    }
1462
1463    fn win_perf_counter_frequency(vm: &VirtualMachine) -> PyResult<i64> {
1464        let frequency = unsafe {
1465            let mut freq = core::mem::MaybeUninit::uninit();
1466            if QueryPerformanceFrequency(freq.as_mut_ptr()) == 0 {
1467                return Err(vm.new_last_os_error());
1468            }
1469            freq.assume_init()
1470        };
1471
1472        if frequency < 1 {
1473            Err(vm.new_runtime_error("invalid QueryPerformanceFrequency"))
1474        } else if frequency > i64::MAX / SEC_TO_NS {
1475            Err(vm.new_overflow_error("QueryPerformanceFrequency is too large"))
1476        } else {
1477            Ok(frequency)
1478        }
1479    }
1480
1481    fn global_frequency(vm: &VirtualMachine) -> PyResult<i64> {
1482        rustpython_common::static_cell! {
1483            static FREQUENCY: PyResult<i64>;
1484        };
1485        FREQUENCY
1486            .get_or_init(|| win_perf_counter_frequency(vm))
1487            .clone()
1488    }
1489
1490    pub(super) fn get_perf_time(vm: &VirtualMachine) -> PyResult<Duration> {
1491        let ticks = unsafe {
1492            let mut performance_count = core::mem::MaybeUninit::uninit();
1493            QueryPerformanceCounter(performance_count.as_mut_ptr());
1494            performance_count.assume_init()
1495        };
1496
1497        Ok(Duration::from_nanos(time_muldiv(
1498            ticks,
1499            SEC_TO_NS,
1500            global_frequency(vm)?,
1501        )))
1502    }
1503
1504    fn get_system_time_adjustment(vm: &VirtualMachine) -> PyResult<u32> {
1505        let mut _time_adjustment = core::mem::MaybeUninit::uninit();
1506        let mut time_increment = core::mem::MaybeUninit::uninit();
1507        let mut _is_time_adjustment_disabled = core::mem::MaybeUninit::uninit();
1508        let time_increment = unsafe {
1509            if GetSystemTimeAdjustment(
1510                _time_adjustment.as_mut_ptr(),
1511                time_increment.as_mut_ptr(),
1512                _is_time_adjustment_disabled.as_mut_ptr(),
1513            ) == 0
1514            {
1515                return Err(vm.new_last_os_error());
1516            }
1517            time_increment.assume_init()
1518        };
1519        Ok(time_increment)
1520    }
1521
1522    pub(super) fn get_monotonic_time(vm: &VirtualMachine) -> PyResult<Duration> {
1523        let ticks = unsafe { GetTickCount64() };
1524
1525        Ok(Duration::from_nanos(
1526            (ticks as i64)
1527                .checked_mul(MS_TO_NS)
1528                .ok_or_else(|| vm.new_overflow_error("timestamp too large to convert to i64"))?
1529                as u64,
1530        ))
1531    }
1532
1533    #[pyfunction]
1534    fn get_clock_info(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<PyRef<PyNamespace>> {
1535        let (adj, imp, mono, res) = match name.as_str() {
1536            "monotonic" => (
1537                false,
1538                "GetTickCount64()",
1539                true,
1540                get_system_time_adjustment(vm)? as f64 * 1e-7,
1541            ),
1542            "perf_counter" => (
1543                false,
1544                "QueryPerformanceCounter()",
1545                true,
1546                1.0 / (global_frequency(vm)? as f64),
1547            ),
1548            "process_time" => (false, "GetProcessTimes()", true, 1e-7),
1549            "thread_time" => (false, "GetThreadTimes()", true, 1e-7),
1550            "time" => (
1551                true,
1552                "GetSystemTimeAsFileTime()",
1553                false,
1554                get_system_time_adjustment(vm)? as f64 * 1e-7,
1555            ),
1556            _ => return Err(vm.new_value_error("unknown clock")),
1557        };
1558
1559        Ok(py_namespace!(vm, {
1560            "implementation" => vm.new_pyobj(imp),
1561            "monotonic" => vm.ctx.new_bool(mono),
1562            "adjustable" => vm.ctx.new_bool(adj),
1563            "resolution" => vm.ctx.new_float(res),
1564        }))
1565    }
1566
1567    pub(super) fn get_thread_time(vm: &VirtualMachine) -> PyResult<Duration> {
1568        let (kernel_time, user_time) = unsafe {
1569            let mut _creation_time = core::mem::MaybeUninit::uninit();
1570            let mut _exit_time = core::mem::MaybeUninit::uninit();
1571            let mut kernel_time = core::mem::MaybeUninit::uninit();
1572            let mut user_time = core::mem::MaybeUninit::uninit();
1573
1574            let thread = GetCurrentThread();
1575            if GetThreadTimes(
1576                thread,
1577                _creation_time.as_mut_ptr(),
1578                _exit_time.as_mut_ptr(),
1579                kernel_time.as_mut_ptr(),
1580                user_time.as_mut_ptr(),
1581            ) == 0
1582            {
1583                return Err(vm.new_os_error("Failed to get clock time".to_owned()));
1584            }
1585            (kernel_time.assume_init(), user_time.assume_init())
1586        };
1587        let k_time = u64_from_filetime(kernel_time);
1588        let u_time = u64_from_filetime(user_time);
1589        Ok(Duration::from_nanos((k_time + u_time) * 100))
1590    }
1591
1592    pub(super) fn get_process_time(vm: &VirtualMachine) -> PyResult<Duration> {
1593        let (kernel_time, user_time) = unsafe {
1594            let mut _creation_time = core::mem::MaybeUninit::uninit();
1595            let mut _exit_time = core::mem::MaybeUninit::uninit();
1596            let mut kernel_time = core::mem::MaybeUninit::uninit();
1597            let mut user_time = core::mem::MaybeUninit::uninit();
1598
1599            let process = GetCurrentProcess();
1600            if GetProcessTimes(
1601                process,
1602                _creation_time.as_mut_ptr(),
1603                _exit_time.as_mut_ptr(),
1604                kernel_time.as_mut_ptr(),
1605                user_time.as_mut_ptr(),
1606            ) == 0
1607            {
1608                return Err(vm.new_os_error("Failed to get clock time".to_owned()));
1609            }
1610            (kernel_time.assume_init(), user_time.assume_init())
1611        };
1612        let k_time = u64_from_filetime(kernel_time);
1613        let u_time = u64_from_filetime(user_time);
1614        Ok(Duration::from_nanos((k_time + u_time) * 100))
1615    }
1616}