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}