Skip to main content

zsh/ported/modules/
datetime.rs

1//! Date/time utilities — port of `Src/Modules/datetime.c`.
2//!
3//! C source has 0 structs/enums. Rust port matches: 0 types.
4//! Functions:
5//!   - `getcurrentsecs`     `[c:206]`
6//!   - `getcurrentrealtime` `[c:212]`
7//!   - `getcurrenttime`     `[c:220]`
8//!   - `reverse_strftime`   `[c:42]`
9//!   - `output_strftime`    `[c:99]`   (the actual builtin entry)
10//!   - `bin_strftime`       `[c:187]`  (TZ-scope wrapper around output_strftime)
11//!   - 6 module loaders
12//!
13//! C uses libc `localtime(3)` + zsh's custom `ztrftime()` (which
14//! extends POSIX strftime with the `%.N` nanosecond syntax). The
15//! Rust port calls `crate::ported::utils::ztrftime()` for the
16//! base format and adds %N extensions on top.
17
18use crate::ported::compat::zgettime;
19use crate::ported::params::{getsparam, isident, setiparam, setsparam};
20use crate::ported::utils::{metafy, zwarnnam};
21use crate::ported::zsh_h::{features, module, options, MAX_OPS, OPT_ARG, OPT_ISSET};
22use crate::ported::zsh_system_h::timespec;
23use once_cell::sync::Lazy;
24use std::sync::{Mutex, OnceLock};
25use std::time::{Duration, SystemTime, UNIX_EPOCH};
26
27/// Port of `reverse_strftime(char *nam, char **argv, char *scalar, int quiet)` from `Src/Modules/datetime.c:42`.
28/// Parses a time string per the format string and assigns the
29/// resulting epoch seconds to `scalar` (or stdout if NULL).
30///
31/// C signature: `static int reverse_strftime(char *nam, char **argv,
32///                                            char *scalar, int quiet)`.
33/// WARNING: param names don't match C — Rust=(nam, argv, quiet) vs C=(nam, argv, scalar, quiet)
34pub fn reverse_strftime(
35    nam: &str,
36    argv: &[&str], // c:42
37    scalar: Option<&str>,
38    quiet: i32,
39) -> i32 {
40    if argv.len() < 2 {
41        // c:54 timestring expected
42        zwarnnam(nam, "timestring expected");
43        return 1;
44    }
45    // c:59-61 — `memset(&tm, 0, sizeof(tm)); tm.tm_isdst = -1;
46    // tm.tm_mday = 1;`. tm_year stays 0 (1900), so a partial format
47    // such as `%H:%M` lands on 1900-01-01.
48    //
49    // The conversion is libc's strptime(3) + mktime(3), exactly as in C.
50    // A chrono re-implementation used to stand here; chrono's `Local`
51    // caches the zone it first resolved, so a later `TZ=UTC strftime -r …`
52    // (or a function run under a TZ prefix assignment, as
53    // Functions/Calendar/calendar_scandate does) kept converting in the
54    // zone of the first call. mktime(3) consults TZ on every call.
55    // A C string ends at its first NUL, so an embedded one truncates.
56    let cstr = |s: &str| std::ffi::CString::new(s.split('\0').next().unwrap_or("")).unwrap_or_default();
57    let (c_input, c_format) = (cstr(argv[1]), cstr(argv[0]));
58    let mut tm: libc::tm = unsafe { std::mem::zeroed() }; // c:59
59    tm.tm_isdst = -1; // c:60
60    tm.tm_mday = 1; // c:61
61    // c:62 — `endp = strptime(argv[1], argv[0], &tm);`
62    let endp = unsafe { libc::strptime(c_input.as_ptr(), c_format.as_ptr(), &mut tm) };
63    if endp.is_null() {
64        // c:64-69
65        if quiet == 0 {
66            zwarnnam(nam, "format not matched"); // c:67
67        }
68        return 1; // c:68
69    }
70    // c:71 — `mytime = (zlong)mktime(&tm);`
71    let mytime = unsafe { libc::mktime(&mut tm) } as i64;
72    if let Some(name) = scalar {
73        // c:73-74 — `if (scalar) setiparam(scalar, mytime);`
74        setiparam(name, mytime); // c:74 setiparam
75    } else {
76        // c:75-79 — `convbase(buf, mytime, 10); printf("%s\n", buf);`
77        println!("{}", mytime);
78    }
79    // c:80-87 — `if (*endp && !quiet) zwarnnam(nam, "warning: input string
80    // not completely matched");`
81    if unsafe { *endp } != 0 && quiet == 0 {
82        zwarnnam(nam, "warning: input string not completely matched"); // c:86
83    }
84    0 // c:90
85}
86
87/// Port of `output_strftime(char *nam, char **argv, Options ops, UNUSED(int func))` from `Src/Modules/datetime.c:99`.
88/// The `output_strftime` builtin entry. Parses argv (format,
89/// timestamp, nanoseconds), calls `localtime(3)` to convert,
90/// formats via `ztrftime()` with retry-on-overflow, then writes
91/// the result to stdout (or `setsparam` to the `-s NAME` scalar).
92///
93/// C signature: `static int output_strftime(char *nam, char **argv,
94///                                           Options ops, int func)`.
95/// WARNING: param names don't match C — Rust=(nam, argv, _func) vs C=(nam, argv, ops, func)
96pub fn output_strftime(
97    nam: &str,
98    argv: &[&str], // c:99
99    ops: &options,
100    _func: i32,
101) -> i32 {
102    // c:107 — `if (OPT_ISSET(ops,'s'))`
103    let scalar: Option<&str> = if OPT_ISSET(ops, b's') {
104        Some(OPT_ARG(ops, b's').unwrap_or(""))
105    } else {
106        None
107    };
108    if let Some(name) = scalar {
109        if !isident(name) {
110            // c:110 isident check
111            zwarnnam(nam, &format!("not an identifier: {}", name)); // c:111
112            return 1; // c:112
113        }
114    }
115
116    // c:113-119 — `if (OPT_ISSET(ops, 'r'))` reverse path.
117    // C body:
118    //   if (OPT_ISSET(ops, 'r')) {
119    //       if (!argv[1]) {
120    //           zwarnnam(nam, "timestring expected");
121    //           return 1;
122    //       }
123    //       return reverse_strftime(nam, argv, scalar,
124    //                               OPT_ISSET(ops, 'q'));
125    //   }
126    //
127    // Prior port skipped the !argv[1] guard so `strftime -r %s`
128    // (format with no timestring) fell through to reverse_strftime
129    // which would either crash or emit a non-canonical diagnostic.
130    if OPT_ISSET(ops, b'r') {
131        if argv.len() < 2 {
132            // c:114-117 — `if (!argv[1])` then 'timestring expected'.
133            zwarnnam(nam, "timestring expected"); // c:115
134            return 1; // c:116
135        }
136        let quiet = if OPT_ISSET(ops, b'q') { 1 } else { 0 };
137        return reverse_strftime(nam, argv, scalar, quiet); // c:118
138    }
139
140    if argv.is_empty() {
141        zwarnnam(nam, "format expected");
142        return 1;
143    }
144
145    // c:122 — parse argv[1] as timestamp, or use current time.
146    let (secs, nsec) = if argv.len() < 2 {
147        let now = SystemTime::now()
148            .duration_since(UNIX_EPOCH)
149            .unwrap_or(Duration::ZERO);
150        (now.as_secs() as i64, now.subsec_nanos() as i64) // c:124-125 zgettime
151    } else {
152        // c:126 — `errno = 0;`
153        // c:128 — `ts.tv_sec = (time_t)strtoul(argv[1], &endptr, 10);`
154        // libc strtoul, not a Rust parse: it skips leading blanks, takes
155        // a sign, and reports failures through errno (ERANGE on
156        // overflow; EINVAL for "no digits" on BSD/macOS libc), which
157        // c:129-131 prints with `%e`.
158        let arg1 = std::ffi::CString::new(argv[1]).unwrap_or_default();
159        let mut endptr: *mut libc::c_char = std::ptr::null_mut();
160        nix::errno::Errno::clear(); // c:126
161        let secs = unsafe { libc::strtoul(arg1.as_ptr(), &mut endptr, 10) } as libc::time_t as i64; // c:128
162        let err = nix::errno::Errno::last_raw();
163        if err != 0 {
164            // c:129-131
165            zwarnnam(
166                nam,
167                &format!("{}: {}", argv[1], crate::ported::utils::zsh_errno_msg(err)),
168            );
169            return 1; // c:131
170        } else if argv[1].is_empty() || unsafe { *endptr } != 0 {
171            // c:132-134
172            zwarnnam(nam, &format!("{}: invalid decimal number", argv[1]));
173            return 1; // c:134
174        }
175        // c:144 — argv[2] nanoseconds (optional).
176        let nsec = if argv.len() > 2 {
177            match argv[2].parse::<i64>() {
178                Ok(v) if (0..=999_999_999).contains(&v) => v, // c:151
179                Ok(_) => {
180                    zwarnnam(nam, &format!("{}: invalid nanosecond value", argv[2]));
181                    return 1; // c:153
182                }
183                Err(_) => {
184                    zwarnnam(nam, &format!("{}: invalid decimal number", argv[2]));
185                    return 1;
186                }
187            }
188        } else {
189            0
190        };
191        (secs, nsec)
192    };
193
194    // c:136-140 — `tm = localtime(&ts.tv_sec); if (!tm) {
195    //               zwarnnam(nam, "%s: unable to convert to time", ...);
196    //               return 1; }`
197    let format = argv[0];
198    #[cfg(unix)]
199    {
200        let secs_c: libc::time_t = secs as libc::time_t;
201        if unsafe { libc::localtime(&secs_c) }.is_null() {
202            // c:137-139
203            let what = argv.get(1).copied().unwrap_or("");
204            zwarnnam(nam, &format!("{}: unable to convert to time", what));
205            return 1;
206        }
207    }
208    // c:158-167 — `bufsize = strlen(argv[0]) * 8; buffer = zalloc(bufsize);
209    //              for (x=0; x < 4; x++) {
210    //                  if ((len = ztrftime(buffer, bufsize, argv[0], tm,
211    //                                      ts.tv_nsec)) >= 0 || x==3) break;
212    //                  buffer = zrealloc(buffer, bufsize *= 2); }`
213    // Route through the canonical ztrftime port (utils.rs:4231) — it
214    // implements zsh's strftime extensions per Src/utils.c ztrftime:
215    // %. (fractional seconds, optional digit-count prefix), %f, %e,
216    // %K/%k, %L/%l. The prior chrono-based path missed all of those
217    // AND invented a `%N` substitution that exists in neither zsh's
218    // ztrftime specifier set nor POSIX strftime (zsh passes %N through
219    // to the system strftime, which emits it literally). The bufsize
220    // retry loop is C buffer management; the port returns an owned
221    // String. Nanoseconds travel inside the SystemTime (ztrftime reads
222    // subsec_nanos for the %. specifier, mirroring the C `usec` arg).
223    let st = if secs >= 0 {
224        UNIX_EPOCH + Duration::new(secs as u64, nsec as u32)
225    } else {
226        // pre-epoch: subtract whole seconds, then add the positive
227        // nanosecond part back (tm convention: nsec ∈ [0, 1e9)).
228        UNIX_EPOCH - Duration::from_secs(secs.unsigned_abs()) + Duration::from_nanos(nsec as u64)
229    };
230    let formatted = crate::ported::utils::ztrftime(format, st, false); // c:163
231
232    // c:178 — `if (scalar) { setsparam(scalar, metafy(buffer, len, META_DUP)); }`
233    if let Some(name) = scalar {
234        setsparam(name, &metafy(&formatted));
235        // c:178
236    } else {
237        // c:180-183 — fwrite + putchar('\n') unless -n
238        print!("{}", formatted); // c:181 fwrite
239        if !OPT_ISSET(ops, b'n') {
240            // c:182 !OPT_ISSET(ops,'n')
241            println!(); // c:183 putchar('\n')
242        }
243    }
244
245    0 // c:187
246}
247
248/// Port of `bin_strftime(char *nam, char **argv, Options ops, int func)` from `Src/Modules/datetime.c:187`. The
249/// `strftime` builtin entry — wraps `output_strftime` in a local
250/// param-scope that copies `$TZ` so `output_strftime`'s
251/// `localtime(3)` calls see the user's timezone even if a function
252/// scope has shadowed it.
253///
254/// C signature: `static int bin_strftime(char *nam, char **argv,
255///                                         Options ops, int func)`.
256/// WARNING: param names don't match C — Rust=(nam, argv, func) vs C=(nam, argv, ops, func)
257pub fn bin_strftime(
258    nam: &str,
259    argv: &[String], // c:187
260    ops: &options,
261    func: i32,
262) -> i32 {
263    // c:190-191 — `int result = 1; char *tz = getsparam("TZ");`
264    let tz_saved = getsparam("TZ"); // c:190
265                                    // c:192-198 — `startparamscope();
266                                    //              if (tz) {
267                                    //                  Param pm = createparam("TZ", PM_LOCAL|...);
268                                    //                  if (pm) pm->level = locallevel;
269                                    //                  setsparam("TZ", ztrdup(tz));
270                                    //              }`
271                                    //
272                                    // C's PM_LOCAL gives the TZ override function-scope so
273                                    // endparamscope at c:200 automatically restores the prior TZ.
274                                    // Rust port pushes via env::set_var so libc's strftime sees
275                                    // the new zone — paramtab setsparam alone doesn't propagate
276                                    // to the libc-level zone (libc reads $TZ from getenv at
277                                    // strftime time, not from zsh's paramtab).
278                                    //
279                                    // Capture the ORIGINAL env::TZ here so we can restore it on
280                                    // exit. Prior port re-set env::TZ to the same value on exit
281                                    // instead of restoring — leaving a stale env::TZ override
282                                    // after bin_strftime returned. Real-world: user has env::TZ
283                                    // = "UTC" but $TZ paramtab = "America/Chicago"; after
284                                    // strftime returns, env::TZ silently became "America/Chicago"
285                                    // and stayed there for the rest of the session.
286    let env_tz_saved = std::env::var_os("TZ"); // capture before overwrite
287    if let Some(ref tz) = tz_saved {
288        std::env::set_var("TZ", tz); // c:197 setsparam
289    }
290    // Convert &[String] → &[&str] for the internal helper which
291    // takes the narrower view.
292    let argv_views: Vec<&str> = argv.iter().map(String::as_str).collect();
293    let result = output_strftime(nam, &argv_views, ops, func); // c:199
294                                                               // c:200 — `endparamscope();` — restore prior env::TZ.
295    match env_tz_saved {
296        Some(prev) => std::env::set_var("TZ", prev),
297        None => std::env::remove_var("TZ"),
298    }
299    result // c:202
300}
301
302/// Port of `getcurrentsecs(UNUSED(Param pm))` from `Src/Modules/datetime.c:206`.
303/// Returns the current epoch seconds — backs `$EPOCHSECONDS`.
304/// C body: `return (zlong) time(NULL);`
305/// WARNING: param names don't match C — Rust=() vs C=(pm)
306pub fn getcurrentsecs() -> i64 {
307    // c:206
308    // c:206 — `return (zlong) time(NULL);`
309    unsafe { libc::time(std::ptr::null_mut()) as i64 }
310}
311
312/// Port of `getcurrentrealtime(UNUSED(Param pm))` from `Src/Modules/datetime.c:212`.
313/// Returns the current high-resolution epoch time as f64 — backs
314/// `$EPOCHREALTIME`.
315///
316/// C body:
317/// ```c
318/// struct timespec now;
319/// zgettime(&now);
320/// return (double)now.tv_sec + (double)now.tv_nsec * 1e-9;
321/// ```
322/// WARNING: param names don't match C — Rust=() vs C=(pm)
323pub fn getcurrentrealtime() -> f64 {
324    // c:212
325    let mut now: timespec = unsafe { std::mem::zeroed() }; // c:212
326    zgettime(&mut now); // c:215
327    (now.tv_sec as f64) + (now.tv_nsec as f64) * 1e-9 // c:216
328}
329
330/// Port of `getcurrenttime(UNUSED(Param pm))` from `Src/Modules/datetime.c:220`.
331/// Returns the current epoch as `(secs, nanos)` — backs the
332/// `$epochtime` two-element array param.
333///
334/// C body:
335/// ```c
336/// struct timespec now;
337/// zgettime(&now);
338/// arr[0] = sprintf "%ld" now.tv_sec
339/// arr[1] = sprintf "%ld" now.tv_nsec
340/// return arr;
341/// ```
342/// WARNING: param names don't match C — Rust=() vs C=(pm)
343pub fn getcurrenttime() -> Vec<String> {
344    // c:220
345    // c:222-224 — `char **arr; char buf[DIGBUFSIZE]; struct timespec now;`
346    let mut arr: Vec<String> = Vec::with_capacity(2); // c:228 zhalloc(3 * sizeof(*arr))
347    let mut now: timespec = unsafe { std::mem::zeroed() }; // c:224
348                                                           // c:226 — `zgettime(&now);`
349    zgettime(&mut now);
350    // c:229 — `sprintf(buf, "%ld", (long)now.tv_sec);`
351    let buf = format!("{}", now.tv_sec as i64);
352    arr.push(buf); // c:230 arr[0] = dupstring(buf)
353                   // c:231 — `sprintf(buf, "%ld", (long)now.tv_nsec);`
354    let buf = format!("{}", now.tv_nsec as i64);
355    arr.push(buf); // c:232 arr[1] = dupstring(buf)
356                   // c:233 — `arr[2] = NULL;` (collapsed: Vec's length is the terminator)
357    arr // c:235
358}
359
360// `bintab` — port of `static struct builtin bintab[]` (datetime.c:255).
361
362// `patab` — port of `static struct paramdef patab[]` (datetime.c).
363
364// `module_features` — port of `static struct features module_features`
365// from datetime.c:262.
366
367/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/datetime.c:270`.
368#[allow(unused_variables)]
369pub fn setup_(m: *const module) -> i32 {
370    // c:270
371    // C body c:272-273 — `return 0`. Faithful empty-body port.
372    0
373}
374
375// =====================================================================
376// static struct builtin bintab[]                                    c:255
377// static struct features module_features                            c:262
378// =====================================================================
379
380/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/datetime.c:277`.
381/// C body: `*features = featuresarray(m, &module_features); return 0;`
382pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
383    // c:277
384    *features = featuresarray(m, module_features());
385    0 // c:292
386}
387
388/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/datetime.c:285`.
389/// C body: `return handlefeatures(m, &module_features, enables);`
390pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
391    // c:285
392    handlefeatures(m, module_features(), enables) // c:292
393}
394
395/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/datetime.c:292`.
396#[allow(unused_variables)]
397pub fn boot_(m: *const module) -> i32 {
398    // c:292
399    // C body c:294-295 — `return 0`. The parameter registration happens
400    // through the `pd_list` feature descriptor (`patab`, c:251-258): the
401    // `p:` enable bits reach `setparamdefs` (c:1169) via
402    // `setfeatureenables` (c:3445), which is what `do_module_features`
403    // (c:1998) drives on every load and on every `zmodload -F`.
404    //
405    // An earlier revision registered the three params here instead,
406    // because the Rust `handlefeatures`/`setfeatureenables` shims were
407    // no-ops (Bug #512). They are real now, so doing it here as well
408    // would resurrect a disabled `p:` feature the moment boot_ ran —
409    // `zmodload -F zsh/datetime` (enable nothing) must leave
410    // `${+EPOCHSECONDS}` at 0.
411    0
412}
413
414/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/datetime.c:299`.
415/// C body: `return setfeatureenables(m, &module_features, NULL);`
416pub fn cleanup_(m: *const module) -> i32 {
417    // c:299
418    setfeatureenables(m, module_features(), None) // c:306
419}
420
421/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/datetime.c:306`.
422#[allow(unused_variables)]
423pub fn finish_(m: *const module) -> i32 {
424    // c:306
425    // C body c:308-309 — `return 0`. Faithful empty-body port; the
426    //                    strftime builtin + EPOCHREALTIME unregister
427    //                    via cleanup_'s setfeatureenables(...).
428    0
429}
430
431static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
432
433// Local stubs for the per-module entry points. C uses generic
434// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
435// 3275/3370/3445) but those take `Builtin` + `Features` pointer
436// fields the Rust port doesn't carry. The hardcoded descriptor
437// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
438// `static struct builtin bintab[]` — port of `Src/Modules/datetime.c:238`:
439//   BUILTIN("strftime", 0, bin_strftime, 1, 3, 0, "nqrs:", NULL)
440//
441// !!! RUST-ONLY STORAGE SHAPE !!! C declares this file-static and reaches
442// it through `module_features.bn_list` (`struct features`, Src/zsh.h:1555).
443// The Rust `features` port carries only the SIZES, so the array lives in
444// its own static. The FIELD that matters is `node.flags & BINF_ADDED`:
445// that bit IS C's record of "this feature is currently enabled"
446// (`setbuiltins`, c:Src/module.c:507-520 / `getfeatureenables`, c:3331).
447static BINTAB: Lazy<Mutex<Vec<crate::ported::zsh_h::builtin>>> = Lazy::new(|| {
448    // c:239 — BUILTIN("strftime", 0, bin_strftime, 1, 3, 0, "nqrs:", NULL)
449    Mutex::new(vec![crate::ported::zsh_h::builtin {
450        node: crate::ported::zsh_h::hashnode {
451            next: None,
452            nam: "strftime".to_string(),
453            flags: 0,
454        },
455        handlerfunc: None,
456        minargs: 1,
457        maxargs: 3,
458        funcid: 0,
459        optstr: Some("nqrs:".to_string()),
460        defopts: None,
461    }])
462}); // c:238
463
464// `static struct paramdef patab[]` — port of `Src/Modules/datetime.c:251`.
465// `SPECIALPMDEF` (Src/zsh.h) folds PM_SPECIAL|PM_HIDE|PM_HIDEVAL into the
466// declared flags. `pm != NULL` is C's per-parameter enable bit
467// (`setparamdefs`, c:Src/module.c:1169-1190 / `getfeatureenables`, c:3337).
468static PATAB: Lazy<Mutex<Vec<crate::ported::zsh_h::paramdef>>> = Lazy::new(|| {
469    use crate::ported::zsh_h::{
470        PM_ARRAY, PM_FFLOAT, PM_HIDE, PM_HIDEVAL, PM_INTEGER, PM_READONLY, PM_SPECIAL,
471    };
472    // c:252-257 — SPECIALPMDEF(name, flags, gsu, getfn, scantfn); the macro
473    // ORs in PM_SPECIAL|PM_HIDE|PM_HIDEVAL.
474    let special = (PM_SPECIAL | PM_HIDE | PM_HIDEVAL) as i32;
475    Mutex::new(
476        [
477            ("EPOCHSECONDS", (PM_INTEGER | PM_READONLY) as i32), // c:252
478            ("EPOCHREALTIME", (PM_FFLOAT | PM_READONLY) as i32), // c:254
479            ("epochtime", (PM_ARRAY | PM_READONLY) as i32),      // c:256
480        ]
481        .iter()
482        .map(|(name, flags)| crate::ported::zsh_h::paramdef {
483            name: name.to_string(),
484            flags: flags | special,
485            var: 0,
486            gsu: 0,
487            getnfn: None,
488            scantfn: None,
489            pm: None,
490        })
491        .collect(),
492    )
493}); // c:251
494
495// WARNING: NOT IN DATETIME.C — the C file calls the generic
496// `featuresarray(m, &module_features)` (Src/module.c:3284) directly. This
497// thin wrapper only unpacks the Rust-side bintab/patab statics into the
498// slices that generic fn takes.
499fn featuresarray(m: *const module, _f: &Mutex<features>) -> Vec<String> {
500    let bn = BINTAB.lock().unwrap();
501    let pd = PATAB.lock().unwrap();
502    crate::ported::module::featuresarray(m, &bn, &[], &[], &pd, 0) // c:279
503}
504
505/// Port of `mod_export int handlefeatures(Module m, Features f, int **enables)`
506/// from `Src/module.c:3370`.
507///
508/// C body:
509/// ```c
510/// if (!enables || *enables)
511///     return setfeatureenables(m, f, enables ? *enables : NULL);
512/// *enables = getfeatureenables(m, f);
513/// return 0;
514/// ```
515///
516/// The Rust `enables` is `&mut Option<Vec<i32>>`, so C's `enables` (the
517/// `int **`) is never NULL here; `*enables` non-NULL maps to `Some`. That
518/// makes this the same two-way door C has: `Some` = SET the given bitmap,
519/// `None` = GET the live one.
520fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
521    // c:3370
522    if let Some(e) = enables.as_ref() {
523        // c:3372-3373
524        let e = e.clone();
525        return setfeatureenables(m, f, Some(&e));
526    }
527    // c:3374 — `*enables = getfeatureenables(m, f);`
528    let bn = BINTAB.lock().unwrap();
529    let pd = PATAB.lock().unwrap();
530    *enables = Some(crate::ported::module::getfeatureenables(
531        m,
532        &bn,
533        &[],
534        &[],
535        &pd,
536        0,
537    ));
538    0 // c:3375
539}
540
541/// Port of `mod_export int setfeatureenables(Module m, Features f, int *e)`
542/// from `Src/module.c:3445`. Walks the four per-kind tables in the fixed
543/// order `bn`, `cd`, `mf`, `pd`, advancing `e` past each block. datetime
544/// has only `bn_size`(1) and `pd_size`(3), so the `cd`/`mf` arms are
545/// absent (C skips them too — `if (f->cd_size)` is false).
546fn setfeatureenables(_m: *const module, _f: &Mutex<features>, e: Option<&[i32]>) -> i32 {
547    // c:3445
548    let mut ret = 0; // c:3448
549    {
550        // c:3450-3455 — `if (f->bn_size) { setbuiltins(...); e += f->bn_size; }`
551        let mut bn = BINTAB.lock().unwrap();
552        if crate::ported::module::setbuiltins("zsh/datetime", &mut bn, e.map(|a| &a[..1])) != 0
553        {
554            ret = 1; // c:3452
555        }
556    }
557    {
558        // c:3465-3470 — `if (f->pd_size) { setparamdefs(...); }`
559        let mut pd = PATAB.lock().unwrap();
560        if crate::ported::module::setparamdefs("zsh/datetime", &mut pd, e.map(|a| &a[1..4]))
561            != 0
562        {
563            ret = 1; // c:3467
564        }
565    }
566    ret // c:3472
567}
568
569// `setbuiltins` (`Src/module.c:501`) and `setparamdefs`
570// (`Src/module.c:1169`) are module.c functions, so they live in the
571// Rust mirror of that file, `src/ported/module.rs`. `setfeatureenables`
572// below calls them exactly as C's does at c:3450-3470.
573
574
575// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
576// ─── RUST-ONLY ACCESSORS ───
577//
578// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
579// RwLock<T>>` globals declared above. C zsh uses direct global
580// access; Rust needs these wrappers because `OnceLock::get_or_init`
581// is the only way to lazily construct shared state. These ported sit
582// here so the body of this file reads in C source order without
583// the accessor wrappers interleaved between real port ported.
584// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
585
586// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
587// ─── RUST-ONLY ACCESSORS ───
588//
589// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
590// RwLock<T>>` globals declared above. C zsh uses direct global
591// access; Rust needs these wrappers because `OnceLock::get_or_init`
592// is the only way to lazily construct shared state. These ported sit
593// here so the body of this file reads in C source order without
594// the accessor wrappers interleaved between real port ported.
595// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
596
597// WARNING: NOT IN DATETIME.C — Rust-only module-framework shim.
598// C uses generic featuresarray/handlefeatures/setfeatureenables from
599// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
600// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
601fn module_features() -> &'static Mutex<features> {
602    MODULE_FEATURES.get_or_init(|| {
603        Mutex::new(features {
604            bn_list: None,
605            bn_size: 1,
606            cd_list: None,
607            cd_size: 0,
608            mf_list: None,
609            mf_size: 0,
610            pd_list: None,
611            pd_size: 3,
612            n_abstract: 0,
613        })
614    })
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620
621    #[test]
622    fn test_epoch_seconds() {
623        let _g = crate::test_util::global_state_lock();
624        let secs = getcurrentsecs();
625        assert!(secs > 1700000000);
626    }
627
628    #[test]
629    fn test_epoch_realtime() {
630        let _g = crate::test_util::global_state_lock();
631        let rt = getcurrentrealtime();
632        assert!(rt > 1700000000.0);
633        let arr = getcurrenttime();
634        let secs: i64 = arr[0].parse().unwrap();
635        assert!((rt - secs as f64).abs() < 1.0);
636    }
637
638    #[test]
639    fn test_epoch_time() {
640        let _g = crate::test_util::global_state_lock();
641        let arr = getcurrenttime();
642        let secs: i64 = arr[0].parse().unwrap();
643        let nanos: i64 = arr[1].parse().unwrap();
644        assert!(secs > 1700000000);
645        assert!((0..1_000_000_000).contains(&nanos));
646    }
647
648    /// Build an `Options` struct populated for the canonical
649    /// `output_strftime(name, argv, ops, func)` signature, with
650    /// flag `flag` set and (optionally) -s SCALAR slot encoded.
651    fn ops_for(flags: &[u8], scalar: Option<&str>) -> options {
652        let mut ops = options {
653            ind: [0u8; MAX_OPS],
654            args: Vec::new(),
655            argscount: 0,
656            argsalloc: 0,
657        };
658        for f in flags {
659            ops.ind[*f as usize] = 1;
660        }
661        if let Some(s) = scalar {
662            ops.ind[b's' as usize] = 4;
663            ops.args.push(s.to_string());
664            ops.argscount = 1;
665            ops.argsalloc = 1;
666        }
667        ops
668    }
669
670    /// Reads a scalar from the canonical paramtab — used by tests
671    /// to assert side-effects of params::setsparam writes.
672    fn pt_get(name: &str) -> Option<String> {
673        crate::ported::params::paramtab()
674            .read()
675            .ok()
676            .and_then(|t| t.get(name).and_then(|p| p.u_str.clone()))
677    }
678
679    /// Src/utils.c:3491-3496 — `%N` is nanoseconds, ALWAYS nine digits
680    /// (`sprintf(buf, "%09ld", nsec)`); there is no digit-count variant.
681    /// A `%<n>N` form is therefore not a zsh specifier: ztrftime hands it to
682    /// the host `strftime(3)` (c:Src/utils.c:3398-3406), so the result is the
683    /// C library's. Master oracle 8cc5ead,
684    /// `strftime "[%N][%3N][%Q]" 1700000000 42`:
685    ///   macOS libc → `[000000042][3N][Q]`
686    ///   glibc      → `[000000042][%3N][%Q]` (Ubuntu's zsh 5.9 agrees)
687    #[test]
688    fn test_output_strftime_nanoseconds() {
689        let _g = crate::test_util::global_state_lock();
690        let ops = ops_for(&[b'n'], Some("OUT"));
691        let r = output_strftime("strftime", &["%N", "1700000000", "123456789"], &ops, 0);
692        assert_eq!(r, 0);
693        assert_eq!(pt_get("OUT").as_deref(), Some("123456789"));
694        // Zero-padded to the full nine digits, never truncated.
695        let r = output_strftime("strftime", &["%N", "1700000000", "42"], &ops, 0);
696        assert_eq!(r, 0);
697        assert_eq!(pt_get("OUT").as_deref(), Some("000000042"));
698        // Digit prefix is not part of the specifier — the host strftime(3)
699        // renders the unknown conversion.
700        let r = output_strftime("strftime", &["%3N", "1700000000", "123456789"], &ops, 0);
701        assert_eq!(r, 0);
702        let want = if cfg!(target_os = "linux") { "%3N" } else { "3N" };
703        assert_eq!(pt_get("OUT").as_deref(), Some(want));
704    }
705
706    #[test]
707    fn test_output_strftime_to_scalar() {
708        let _g = crate::test_util::global_state_lock();
709        let ops = ops_for(&[b'n'], Some("OUT2"));
710        let r = output_strftime("strftime", &["%s", "1700000000"], &ops, 0);
711        assert_eq!(r, 0);
712        assert_eq!(pt_get("OUT2").as_deref(), Some("1700000000"));
713    }
714
715    #[test]
716    fn test_output_strftime_format_required() {
717        let _g = crate::test_util::global_state_lock();
718        let ops = ops_for(&[], None);
719        let r = output_strftime("strftime", &[], &ops, 0);
720        assert_eq!(r, 1);
721    }
722
723    /// c:206 — `getcurrentsecs` matches `time(NULL)` within 1 second.
724    /// Pinning the libc-passthrough so a regression that adds an
725    /// offset (timezone, monotonic-vs-realtime confusion) gets caught.
726    #[test]
727    fn getcurrentsecs_matches_libc_time() {
728        let _g = crate::test_util::global_state_lock();
729        let libc_now = unsafe { libc::time(std::ptr::null_mut()) } as i64;
730        let our_now = getcurrentsecs();
731        assert!(
732            (our_now - libc_now).abs() <= 1,
733            "getcurrentsecs {} drifted from libc::time {}",
734            our_now,
735            libc_now
736        );
737    }
738
739    /// c:212 — `getcurrentrealtime` returns a value with nonzero
740    /// sub-second precision over a small sample. Catches a regression
741    /// that truncates to whole seconds (e.g. wrong tv_nsec scaling).
742    #[test]
743    fn getcurrentrealtime_carries_subsecond_precision() {
744        let _g = crate::test_util::global_state_lock();
745        let mut saw_fractional = false;
746        for _ in 0..10 {
747            let rt = getcurrentrealtime();
748            if rt.fract().abs() > 1e-9 {
749                saw_fractional = true;
750                break;
751            }
752        }
753        assert!(
754            saw_fractional,
755            "getcurrentrealtime over 10 samples never produced a fractional part"
756        );
757    }
758
759    /// c:220 — `getcurrenttime` returns `(secs, nanos)` with
760    /// nanos < 1_000_000_000. Pinning the nanos invariant catches
761    /// a regression that returns microseconds (cap 1e6) or the raw
762    /// time-spec value without modulo.
763    #[test]
764    fn getcurrenttime_nanos_under_one_billion() {
765        let _g = crate::test_util::global_state_lock();
766        for _ in 0..5 {
767            let arr = getcurrenttime();
768            let nanos: i64 = arr[1].parse().unwrap();
769            assert!(
770                nanos < 1_000_000_000,
771                "nanos {} >= 1e9 — unit confusion in c:220 port",
772                nanos
773            );
774            assert!(nanos >= 0, "nanos {} negative", nanos);
775        }
776    }
777
778    /// c:212 — Wall-clock advances monotonically forward between two
779    /// `getcurrentrealtime` calls. Captures a "clock went backward"
780    /// regression that would break `$EPOCHREALTIME` script timing.
781    #[test]
782    fn getcurrentrealtime_advances_forward() {
783        let _g = crate::test_util::global_state_lock();
784        let a = getcurrentrealtime();
785        std::thread::sleep(Duration::from_millis(10));
786        let b = getcurrentrealtime();
787        assert!(b >= a, "realtime went backward: {} -> {}", a, b);
788        assert!(
789            b - a < 5.0,
790            "realtime jumped {} seconds in 10ms sleep",
791            b - a
792        );
793    }
794
795    /// c:206 — `getcurrentsecs` advances monotonically across sleeps.
796    /// Same as above but for the integer-second accessor.
797    #[test]
798    fn getcurrentsecs_advances_or_stays_equal() {
799        let _g = crate::test_util::global_state_lock();
800        let a = getcurrentsecs();
801        std::thread::sleep(Duration::from_millis(20));
802        let b = getcurrentsecs();
803        assert!(b >= a, "seconds went backward: {} -> {}", a, b);
804    }
805
806    /// c:99 — `output_strftime` with `%s` (epoch seconds) and `%n`
807    /// flag must print the exact epoch back. Pinning the
808    /// non-side-effect path catches a regression that mangles the
809    /// `-n` (no-newline) shortcut.
810    #[test]
811    fn output_strftime_percent_s_round_trips() {
812        let _g = crate::test_util::global_state_lock();
813        let ops = ops_for(&[b'n'], Some("EPOCH_ROUND_TRIP"));
814        let r = output_strftime("strftime", &["%s", "1234567890"], &ops, 0);
815        assert_eq!(r, 0);
816        assert_eq!(pt_get("EPOCH_ROUND_TRIP").as_deref(), Some("1234567890"));
817    }
818
819    /// c:42-99 — `output_strftime` with an unparseable epoch input
820    /// must return nonzero. Catches a regression that silently
821    /// produces "Wed Dec 31 ..." (epoch 0) on garbage input.
822    #[test]
823    fn output_strftime_invalid_epoch_returns_nonzero() {
824        let _g = crate::test_util::global_state_lock();
825        let ops = ops_for(&[b'n'], Some("BAD"));
826        let r = output_strftime("strftime", &["%s", "not-a-number"], &ops, 0);
827        assert_ne!(r, 0, "garbage epoch must be rejected");
828    }
829
830    /// c:270-307 — module-lifecycle stubs all return 0 in C.
831    #[test]
832    fn module_lifecycle_shims_all_return_zero() {
833        let _g = crate::test_util::global_state_lock();
834        let m: *const module = std::ptr::null();
835        assert_eq!(setup_(m), 0);
836        assert_eq!(boot_(m), 0);
837        assert_eq!(cleanup_(m), 0);
838        assert_eq!(finish_(m), 0);
839    }
840
841    /// c:285 — `enables_` returns 0 and populates `enables` to
842    /// non-None (the module always advertises ≥ 1 feature). A None
843    /// return would mean "no features" and `zmodload -F zsh/datetime`
844    /// would silently disable strftime.
845    #[test]
846    fn enables_populates_some_vec() {
847        let _g = crate::test_util::global_state_lock();
848        let m: *const module = std::ptr::null();
849        let mut enables: Option<Vec<i32>> = None;
850        assert_eq!(enables_(m, &mut enables), 0);
851        assert!(enables.is_some(), "enables must be Some after enables_");
852    }
853
854    // ═══════════════════════════════════════════════════════════════════
855    // getcurrentsecs / getcurrentrealtime / getcurrenttime — back the
856    // $EPOCHSECONDS / $EPOCHREALTIME / $epochtime parameters. Test that
857    // they return monotonic / sane values and the right structure.
858    // ═══════════════════════════════════════════════════════════════════
859
860    /// getcurrentsecs returns a positive epoch time (after year 2020).
861    #[test]
862    fn getcurrentsecs_returns_post_2020_epoch() {
863        let _g = crate::test_util::global_state_lock();
864        let s = getcurrentsecs();
865        // Year 2020-01-01 = epoch 1577836800. Any current time is > this.
866        assert!(s > 1_577_836_800, "epoch must be after 2020-01-01; got {s}");
867        // And < year 2100 (4102444800) — sanity bound.
868        assert!(
869            s < 4_102_444_800,
870            "epoch must be before 2100-01-01; got {s}"
871        );
872    }
873
874    /// Two successive calls must be monotonically non-decreasing.
875    #[test]
876    fn getcurrentsecs_is_monotonic_non_decreasing() {
877        let _g = crate::test_util::global_state_lock();
878        let a = getcurrentsecs();
879        let b = getcurrentsecs();
880        assert!(b >= a, "time went backwards: {a} → {b}");
881    }
882
883    /// getcurrentrealtime returns a positive f64 with sub-second precision.
884    #[test]
885    fn getcurrentrealtime_returns_positive_float() {
886        let _g = crate::test_util::global_state_lock();
887        let t = getcurrentrealtime();
888        assert!(t > 1_577_836_800.0, "realtime must be after 2020; got {t}");
889    }
890
891    /// getcurrentrealtime is close to getcurrentsecs (within 1 second).
892    #[test]
893    fn getcurrentrealtime_matches_getcurrentsecs_within_a_second() {
894        let _g = crate::test_util::global_state_lock();
895        let secs = getcurrentsecs() as f64;
896        let real = getcurrentrealtime();
897        let diff = (real - secs).abs();
898        assert!(diff < 2.0, "realtime/secs diverge by {diff} seconds");
899    }
900
901    /// getcurrenttime returns a 2-element array [secs, nanos].
902    #[test]
903    fn getcurrenttime_returns_two_element_array() {
904        let _g = crate::test_util::global_state_lock();
905        let arr = getcurrenttime();
906        assert_eq!(arr.len(), 2, "must be exactly 2 elements [secs, nanos]");
907    }
908
909    /// First element of getcurrenttime is the epoch seconds (parseable).
910    #[test]
911    fn getcurrenttime_first_element_is_parseable_epoch_seconds() {
912        let _g = crate::test_util::global_state_lock();
913        let arr = getcurrenttime();
914        let secs: i64 = arr[0].parse().expect("element 0 must be valid i64");
915        assert!(secs > 1_577_836_800, "secs must be post-2020");
916    }
917
918    /// Second element of getcurrenttime is nanoseconds (0..1_000_000_000).
919    #[test]
920    fn getcurrenttime_second_element_is_valid_nanoseconds() {
921        let _g = crate::test_util::global_state_lock();
922        let arr = getcurrenttime();
923        let nanos: i64 = arr[1].parse().expect("element 1 must be valid i64");
924        assert!(nanos >= 0, "nanos must be non-negative");
925        assert!(nanos < 1_000_000_000, "nanos must be < 1e9; got {nanos}");
926    }
927
928    /// Successive getcurrenttime calls have non-decreasing secs.
929    #[test]
930    fn getcurrenttime_monotonic_seconds() {
931        let _g = crate::test_util::global_state_lock();
932        let a: i64 = getcurrenttime()[0].parse().unwrap();
933        let b: i64 = getcurrenttime()[0].parse().unwrap();
934        assert!(b >= a, "time went backwards: {a} → {b}");
935    }
936
937    // ─── zsh-corpus pins for datetime helpers ──────────────────────
938
939    /// `getcurrentsecs` returns positive epoch seconds.
940    #[test]
941    fn datetime_corpus_getcurrentsecs_positive() {
942        let _g = crate::test_util::global_state_lock();
943        let s = getcurrentsecs();
944        assert!(s > 1_577_836_800, "post-2020 epoch, got {s}");
945    }
946
947    /// `getcurrentsecs` is monotonically non-decreasing across calls.
948    #[test]
949    fn datetime_corpus_getcurrentsecs_monotonic() {
950        let _g = crate::test_util::global_state_lock();
951        let a = getcurrentsecs();
952        let b = getcurrentsecs();
953        assert!(b >= a, "time went backwards: {a} → {b}");
954    }
955
956    /// `getcurrentrealtime` returns positive f64 in epoch seconds.
957    #[test]
958    fn datetime_corpus_getcurrentrealtime_positive() {
959        let _g = crate::test_util::global_state_lock();
960        let r = getcurrentrealtime();
961        assert!(r > 1_577_836_800.0, "post-2020 epoch, got {r}");
962    }
963
964    /// `getcurrentrealtime` has sub-second precision (probably).
965    /// Pin: returns f64, not just integer-valued.
966    #[test]
967    fn datetime_corpus_getcurrentrealtime_returns_f64() {
968        let _g = crate::test_util::global_state_lock();
969        let r = getcurrentrealtime();
970        // It's a valid float that's finite
971        assert!(r.is_finite(), "must be finite, got {r}");
972    }
973
974    /// `getcurrenttime` returns exactly 2 elements (secs, nanos).
975    #[test]
976    fn datetime_corpus_getcurrenttime_two_elements() {
977        let _g = crate::test_util::global_state_lock();
978        let arr = getcurrenttime();
979        assert_eq!(arr.len(), 2, "exactly 2 elements (secs, nanos)");
980    }
981
982    /// `getcurrenttime` element types: both parse as i64.
983    #[test]
984    fn datetime_corpus_getcurrenttime_both_parse_as_i64() {
985        let _g = crate::test_util::global_state_lock();
986        let arr = getcurrenttime();
987        let _: i64 = arr[0].parse().expect("secs parses");
988        let _: i64 = arr[1].parse().expect("nanos parses");
989    }
990
991    /// `getcurrentsecs` and `getcurrenttime[0]` agree within a couple seconds.
992    #[test]
993    fn datetime_corpus_getcurrentsecs_matches_getcurrenttime_secs() {
994        let _g = crate::test_util::global_state_lock();
995        let a = getcurrentsecs();
996        let b: i64 = getcurrenttime()[0].parse().unwrap();
997        // Should differ by at most a couple seconds.
998        assert!(
999            (a - b).abs() <= 2,
1000            "getcurrentsecs={a} should match getcurrenttime[0]={b} within ~2s"
1001        );
1002    }
1003
1004    // ═══════════════════════════════════════════════════════════════════
1005    // Additional C-parity tests for Src/Modules/datetime.c.
1006    // ═══════════════════════════════════════════════════════════════════
1007
1008    /// c:266 — `getcurrentsecs` returns positive epoch (post-Y2000).
1009    #[test]
1010    fn getcurrentsecs_returns_positive_epoch() {
1011        let _g = crate::test_util::global_state_lock();
1012        let s = getcurrentsecs();
1013        assert!(s > 0, "epoch must be positive, got {}", s);
1014        assert!(s > 946_684_800, "must be after Y2000");
1015    }
1016
1017    /// c:266 — monotonic non-decreasing across 3 immediate calls.
1018    #[test]
1019    fn getcurrentsecs_is_monotonic_non_decreasing_pin() {
1020        let _g = crate::test_util::global_state_lock();
1021        let a = getcurrentsecs();
1022        let b = getcurrentsecs();
1023        let c = getcurrentsecs();
1024        assert!(b >= a, "{} -> {} went backwards", a, b);
1025        assert!(c >= b, "{} -> {} went backwards", b, c);
1026    }
1027
1028    /// c:283 — `getcurrentrealtime` returns positive double.
1029    #[test]
1030    fn getcurrentrealtime_returns_positive() {
1031        let _g = crate::test_util::global_state_lock();
1032        let t = getcurrentrealtime();
1033        assert!(t > 0.0);
1034    }
1035
1036    /// c:283 — not NaN, finite.
1037    #[test]
1038    fn getcurrentrealtime_is_finite() {
1039        let _g = crate::test_util::global_state_lock();
1040        let t = getcurrentrealtime();
1041        assert!(!t.is_nan());
1042        assert!(t.is_finite());
1043    }
1044
1045    /// c:283 — agrees with getcurrentsecs ±5s.
1046    #[test]
1047    fn getcurrentrealtime_agrees_with_secs() {
1048        let _g = crate::test_util::global_state_lock();
1049        let real = getcurrentrealtime();
1050        let secs = getcurrentsecs() as f64;
1051        assert!((real - secs).abs() < 5.0);
1052    }
1053
1054    /// c:303 — returns exactly 2 elements [sec, nsec].
1055    #[test]
1056    fn getcurrenttime_returns_two_elements_pin() {
1057        let _g = crate::test_util::global_state_lock();
1058        let arr = getcurrenttime();
1059        assert_eq!(arr.len(), 2);
1060    }
1061
1062    /// c:303 — nsec in [0, 1B).
1063    #[test]
1064    fn getcurrenttime_nsec_in_valid_range() {
1065        let _g = crate::test_util::global_state_lock();
1066        let arr = getcurrenttime();
1067        let nsec: i64 = arr[1].parse().expect("nsec parses");
1068        assert!(
1069            nsec >= 0 && nsec < 1_000_000_000,
1070            "nsec out of range: {}",
1071            nsec
1072        );
1073    }
1074
1075    /// c:233 — `bin_strftime` with no args returns nonzero (usage).
1076    #[test]
1077    fn bin_strftime_no_args_returns_nonzero() {
1078        let _g = crate::test_util::global_state_lock();
1079        let ops = crate::ported::zsh_h::options {
1080            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1081            args: Vec::new(),
1082            argscount: 0,
1083            argsalloc: 0,
1084        };
1085        let r = bin_strftime("strftime", &[], &ops, 0);
1086        assert_ne!(r, 0, "no args → usage error");
1087    }
1088
1089    /// Lifecycle (c:329/357) split per-hook.
1090    #[test]
1091    fn datetime_setup_returns_zero_pin() {
1092        let _g = crate::test_util::global_state_lock();
1093        assert_eq!(setup_(std::ptr::null()), 0);
1094    }
1095
1096    /// c:357 — boot_(NULL) = 0.
1097    #[test]
1098    fn datetime_boot_returns_zero_pin() {
1099        let _g = crate::test_util::global_state_lock();
1100        assert_eq!(boot_(std::ptr::null()), 0);
1101    }
1102
1103    // ═══════════════════════════════════════════════════════════════════
1104    // Additional C-parity tests for Src/Modules/datetime.c
1105    // c:266 getcurrentsecs / c:283 getcurrentrealtime / c:303 getcurrenttime
1106    // c:233 bin_strftime / c:329-374 lifecycle
1107    // ═══════════════════════════════════════════════════════════════════
1108
1109    /// c:266 — `getcurrentsecs` returns i64.
1110    #[test]
1111    fn getcurrentsecs_returns_i64_type() {
1112        let _: i64 = getcurrentsecs();
1113    }
1114
1115    /// c:266 — `getcurrentsecs` is monotonically non-decreasing across
1116    /// many calls (no time-travel backward).
1117    #[test]
1118    fn getcurrentsecs_monotonic_across_many_calls() {
1119        let mut prev = getcurrentsecs();
1120        for _ in 0..100 {
1121            let cur = getcurrentsecs();
1122            assert!(cur >= prev, "secs went backwards: {} < {}", cur, prev);
1123            prev = cur;
1124        }
1125    }
1126
1127    /// c:266 — `getcurrentsecs` is after Unix epoch (positive).
1128    #[test]
1129    fn getcurrentsecs_after_epoch() {
1130        assert!(getcurrentsecs() > 0, "must be after 1970-01-01");
1131    }
1132
1133    /// c:283 — `getcurrentrealtime` returns f64.
1134    #[test]
1135    fn getcurrentrealtime_returns_f64_type() {
1136        let _: f64 = getcurrentrealtime();
1137    }
1138
1139    /// c:283 — `getcurrentrealtime` non-negative.
1140    #[test]
1141    fn getcurrentrealtime_non_negative() {
1142        assert!(getcurrentrealtime() >= 0.0);
1143    }
1144
1145    /// c:303 — `getcurrenttime` returns Vec<String>.
1146    #[test]
1147    fn getcurrenttime_returns_vec_string() {
1148        let _: Vec<String> = getcurrenttime();
1149    }
1150
1151    /// c:303 — `getcurrenttime` first element parses as integer (seconds).
1152    #[test]
1153    fn getcurrenttime_first_is_numeric() {
1154        let v = getcurrenttime();
1155        assert!(!v.is_empty(), "must have ≥ 1 element");
1156        let parsed: Result<i64, _> = v[0].parse();
1157        assert!(parsed.is_ok(), "first element {:?} must parse as i64", v[0]);
1158    }
1159
1160    /// c:233 — `bin_strftime` return value in u8 exit-code range.
1161    #[test]
1162    fn bin_strftime_return_in_exit_code_range() {
1163        let _g = crate::test_util::global_state_lock();
1164        let ops = crate::ported::zsh_h::options {
1165            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1166            args: Vec::new(),
1167            argscount: 0,
1168            argsalloc: 0,
1169        };
1170        for args in [
1171            vec![],
1172            vec!["%Y".to_string()],
1173            vec!["%Y".to_string(), "1000".to_string()],
1174        ] {
1175            let r = bin_strftime("strftime", &args, &ops, 0);
1176            assert!(
1177                (0..256).contains(&r),
1178                "exit code {} must fit in u8 for {:?}",
1179                r,
1180                args
1181            );
1182        }
1183    }
1184
1185    /// c:329-374 — full lifecycle setup→features→enables→boot→cleanup→finish.
1186    #[test]
1187    fn datetime_full_lifecycle_returns_zero_for_all() {
1188        let _g = crate::test_util::global_state_lock();
1189        let null = std::ptr::null();
1190        assert_eq!(setup_(null), 0);
1191        let mut feats = Vec::new();
1192        let _ = features_(null, &mut feats);
1193        let mut enables: Option<Vec<i32>> = None;
1194        let _ = enables_(null, &mut enables);
1195        assert_eq!(boot_(null), 0);
1196        assert_eq!(cleanup_(null), 0);
1197        assert_eq!(finish_(null), 0);
1198    }
1199
1200    /// c:374 — finish_ idempotent.
1201    #[test]
1202    fn datetime_finish_idempotent() {
1203        let _g = crate::test_util::global_state_lock();
1204        for _ in 0..10 {
1205            assert_eq!(finish_(std::ptr::null()), 0);
1206        }
1207    }
1208
1209    // ═══════════════════════════════════════════════════════════════════
1210    // Additional C-parity tests for Src/Modules/datetime.c
1211    // c:34 reverse_strftime / c:89 output_strftime / c:266 getcurrentsecs
1212    // c:283 getcurrentrealtime / c:303 getcurrenttime
1213    // ═══════════════════════════════════════════════════════════════════
1214
1215    /// c:266 — `getcurrentsecs` is deterministic-ish (monotonic step bound).
1216    #[test]
1217    fn getcurrentsecs_two_calls_close_in_time() {
1218        let _g = crate::test_util::global_state_lock();
1219        let a = getcurrentsecs();
1220        let b = getcurrentsecs();
1221        assert!(
1222            (b - a).abs() <= 2,
1223            "two getcurrentsecs calls must be within 2 seconds"
1224        );
1225    }
1226
1227    /// c:283 — `getcurrentrealtime` returns f64 finite.
1228    #[test]
1229    fn getcurrentrealtime_returns_finite_f64() {
1230        let _g = crate::test_util::global_state_lock();
1231        let v = getcurrentrealtime();
1232        assert!(v.is_finite(), "must be finite");
1233        assert!(v > 0.0, "after epoch");
1234    }
1235
1236    /// c:283 — `getcurrentrealtime` and `getcurrentsecs` agree on whole secs.
1237    #[test]
1238    fn getcurrentrealtime_floor_matches_getcurrentsecs() {
1239        let _g = crate::test_util::global_state_lock();
1240        let secs = getcurrentsecs() as f64;
1241        let real = getcurrentrealtime();
1242        // Allow 2 seconds drift since they may straddle the second boundary.
1243        assert!(
1244            (real - secs).abs() < 2.0,
1245            "realtime {} and secs {} must agree within 2 sec",
1246            real,
1247            secs
1248        );
1249    }
1250
1251    /// c:303 — `getcurrenttime` returns Vec<String> with second element parseable.
1252    #[test]
1253    fn getcurrenttime_second_element_is_nsec() {
1254        let _g = crate::test_util::global_state_lock();
1255        let v = getcurrenttime();
1256        if v.len() >= 2 {
1257            let nsec: Result<i64, _> = v[1].parse();
1258            assert!(nsec.is_ok(), "second element {:?} must parse as i64", v[1]);
1259            if let Ok(n) = nsec {
1260                assert!(n >= 0, "nsec must be ≥ 0");
1261            }
1262        }
1263    }
1264
1265    /// c:303 — `getcurrenttime` Vec length is exactly 2 (secs, nsec).
1266    #[test]
1267    fn getcurrenttime_length_is_two() {
1268        let _g = crate::test_util::global_state_lock();
1269        let v = getcurrenttime();
1270        assert_eq!(v.len(), 2, "secs + nsec = 2 elements");
1271    }
1272
1273    /// c:303 — `getcurrenttime` is deterministic in structure (always 2 elements).
1274    #[test]
1275    fn getcurrenttime_always_two_elements() {
1276        let _g = crate::test_util::global_state_lock();
1277        for _ in 0..5 {
1278            assert_eq!(getcurrenttime().len(), 2);
1279        }
1280    }
1281
1282    /// c:34 — `reverse_strftime` returns i32 (compile-time type pin).
1283    #[test]
1284    fn reverse_strftime_returns_i32_type() {
1285        let _g = crate::test_util::global_state_lock();
1286        let _: i32 = reverse_strftime("strftime", &[], None, 0);
1287    }
1288
1289    /// c:34 — `reverse_strftime` empty argv returns nonzero (usage error).
1290    #[test]
1291    fn reverse_strftime_empty_argv_returns_nonzero() {
1292        let _g = crate::test_util::global_state_lock();
1293        let r = reverse_strftime("strftime", &[], None, 0);
1294        assert_ne!(r, 0, "empty argv → usage error");
1295    }
1296
1297    /// c:266 — `getcurrentsecs` is positive for many calls.
1298    #[test]
1299    fn getcurrentsecs_always_positive() {
1300        let _g = crate::test_util::global_state_lock();
1301        for _ in 0..10 {
1302            assert!(getcurrentsecs() > 0);
1303        }
1304    }
1305
1306    // ═══════════════════════════════════════════════════════════════════
1307    // Additional C-parity tests for Src/Modules/datetime.c
1308    // c:233 bin_strftime / c:266 getcurrentsecs / c:283 getcurrentrealtime /
1309    // c:303 getcurrenttime + lifecycle
1310    // ═══════════════════════════════════════════════════════════════════
1311
1312    /// c:266 — `getcurrentsecs` returns i64 (compile-time pin, alt).
1313    #[test]
1314    fn getcurrentsecs_returns_i64_pin_alt() {
1315        let _g = crate::test_util::global_state_lock();
1316        let _: i64 = getcurrentsecs();
1317    }
1318
1319    /// c:283 — `getcurrentrealtime` returns f64 (compile-time pin, alt).
1320    #[test]
1321    fn getcurrentrealtime_returns_f64_pin_alt() {
1322        let _g = crate::test_util::global_state_lock();
1323        let _: f64 = getcurrentrealtime();
1324    }
1325
1326    /// c:303 — `getcurrenttime` returns Vec<String> (compile-time pin).
1327    #[test]
1328    fn getcurrenttime_returns_vec_string_type() {
1329        let _g = crate::test_util::global_state_lock();
1330        let _: Vec<String> = getcurrenttime();
1331    }
1332
1333    /// c:266 — `getcurrentsecs` is monotonically non-decreasing across
1334    /// rapid consecutive calls (no time-travel within a single test
1335    /// process; system clock could change but on a stable test bed
1336    /// the next call >= prev).
1337    #[test]
1338    fn getcurrentsecs_monotonically_non_decreasing() {
1339        let _g = crate::test_util::global_state_lock();
1340        let mut prev = getcurrentsecs();
1341        for _ in 0..50 {
1342            let now = getcurrentsecs();
1343            assert!(now >= prev, "time went backwards: {} → {}", prev, now);
1344            prev = now;
1345        }
1346    }
1347
1348    /// c:283 — `getcurrentrealtime` is monotonically non-decreasing.
1349    #[test]
1350    fn getcurrentrealtime_monotonically_non_decreasing() {
1351        let _g = crate::test_util::global_state_lock();
1352        let mut prev = getcurrentrealtime();
1353        for _ in 0..50 {
1354            let now = getcurrentrealtime();
1355            assert!(now >= prev, "realtime went backwards: {} → {}", prev, now);
1356            prev = now;
1357        }
1358    }
1359
1360    /// c:266 — `getcurrentsecs` returns plausible current time (after
1361    /// 2020-01-01 epoch = 1577836800, before 2100-01-01 = 4102444800).
1362    #[test]
1363    fn getcurrentsecs_in_plausible_epoch_range() {
1364        let _g = crate::test_util::global_state_lock();
1365        let now = getcurrentsecs();
1366        assert!(
1367            now >= 1_577_836_800,
1368            "current time {} must be after 2020-01-01 epoch",
1369            now
1370        );
1371        assert!(
1372            now <= 4_102_444_800,
1373            "current time {} must be before 2100-01-01 epoch",
1374            now
1375        );
1376    }
1377
1378    /// c:303 — first element of `getcurrenttime` parses as i64 seconds.
1379    #[test]
1380    fn getcurrenttime_first_element_is_secs() {
1381        let _g = crate::test_util::global_state_lock();
1382        let v = getcurrenttime();
1383        assert!(v.len() >= 1, "must have at least one element");
1384        let secs: Result<i64, _> = v[0].parse();
1385        assert!(
1386            secs.is_ok(),
1387            "first element {:?} must parse as i64 secs",
1388            v[0]
1389        );
1390    }
1391
1392    /// c:303 — `getcurrenttime` nsec ≤ 999_999_999 (within one second).
1393    #[test]
1394    fn getcurrenttime_nsec_within_one_second() {
1395        let _g = crate::test_util::global_state_lock();
1396        let v = getcurrenttime();
1397        if v.len() >= 2 {
1398            if let Ok(n) = v[1].parse::<i64>() {
1399                assert!(n <= 999_999_999, "nsec {} must be ≤ 999_999_999", n);
1400            }
1401        }
1402    }
1403
1404    /// c:233 — `bin_strftime` returns i32 (compile-time pin).
1405    #[test]
1406    fn bin_strftime_returns_i32_type() {
1407        let _g = crate::test_util::global_state_lock();
1408        let ops = crate::ported::zsh_h::options {
1409            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1410            args: Vec::new(),
1411            argscount: 0,
1412            argsalloc: 0,
1413        };
1414        let _: i32 = bin_strftime("strftime", &[], &ops, 0);
1415    }
1416
1417    /// c:233 — `bin_strftime` no-args returns nonzero (usage error, alt).
1418    #[test]
1419    fn bin_strftime_no_args_returns_nonzero_pin_alt() {
1420        let _g = crate::test_util::global_state_lock();
1421        let ops = crate::ported::zsh_h::options {
1422            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1423            args: Vec::new(),
1424            argscount: 0,
1425            argsalloc: 0,
1426        };
1427        let r = bin_strftime("strftime", &[], &ops, 0);
1428        assert_ne!(r, 0, "no args → usage error");
1429    }
1430
1431    /// c:329/342/350/357/367/374 — each lifecycle hook returns 0 individually.
1432    #[test]
1433    fn datetime_each_lifecycle_hook_returns_zero_individually() {
1434        let _g = crate::test_util::global_state_lock();
1435        let null = std::ptr::null();
1436        let mut v: Vec<String> = Vec::new();
1437        let mut e: Option<Vec<i32>> = None;
1438        assert_eq!(setup_(null), 0, "c:329 setup_");
1439        assert_eq!(features_(null, &mut v), 0, "c:342 features_");
1440        assert_eq!(enables_(null, &mut e), 0, "c:350 enables_");
1441        assert_eq!(boot_(null), 0, "c:357 boot_");
1442        assert_eq!(cleanup_(null), 0, "c:367 cleanup_");
1443        assert_eq!(finish_(null), 0, "c:374 finish_");
1444    }
1445}