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