zsh/ported/modules/stat.rs
1//! File stat module — port of `Src/Modules/stat.c`.
2//!
3//! C source has 2 enums (`statnum`, `statflags`) — both anonymous-
4//! valued integer-constant tables. Rust port mirrors as constants
5//! to avoid Rust-only type wrappers.
6//!
7//! Functions (matching C 1:1):
8//! - statmodeprint `[c:47]`
9//! - statuidprint `[c:132]`
10//! - statgidprint `[c:161]`
11//! - stattimeprint `[c:191]`
12//! - statulprint `[c:211]`
13//! - statlinkprint `[c:219]`
14//! - statprint `[c:234]`
15//! - bin_stat `[c:368]`
16//! - 6 module loaders
17
18use crate::ported::params::{setaparam, sethparam, setsparam};
19use crate::ported::utils::{zstrtol, ztrftime, zwarnnam};
20use crate::ported::zsh_h::{features, module, options};
21use std::fs;
22use std::os::unix::fs::MetadataExt;
23use std::sync::{Mutex, OnceLock};
24
25// ============================================================
26// Port of `enum statnum` from `Src/Modules/stat.c:33-35`.
27// Anonymous integer constants for the per-element index passed
28// to `statprint(..., iwhich, ...)`.
29// ============================================================
30/// Port of `HNAMEKEY` from `Src/Modules/stat.c:43`. Hash key the
31/// `zstat -H` mode uses to store the file name in the result assoc.
32pub const HNAMEKEY: &str = "name"; // c:43
33
34/// Port of `statmodeprint(mode_t mode, char *outbuf, int flags)` from `Src/Modules/stat.c:47`. Renders
35/// a Unix mode word per the STF_RAW / STF_OCTAL / STF_STRING flag
36/// combination — raw octal/decimal, "ls -l"-style permission
37/// string, or both with the raw form parenthesised.
38///
39/// C signature: `static void statmodeprint(mode_t mode, char *outbuf, int flags)`.
40/// Rust port returns the formatted string (caller writes to its
41/// own buffer) — same observable output for a given flag set.
42/// WARNING: param names don't match C — Rust=(mode, flags) vs C=(mode, outbuf, flags)
43pub fn statmodeprint(mode: u32, flags: i32) -> String {
44 // c:47
45 let mut out = String::new();
46 if (flags & STF_RAW) != 0 {
47 // c:50
48 if (flags & STF_OCTAL) != 0 {
49 // c:51
50 out.push_str(&format!("0{:o}", mode));
51 } else {
52 out.push_str(&format!("{}", mode));
53 }
54 if (flags & STF_STRING) != 0 {
55 // c:53
56 out.push_str(" ("); // c:54
57 }
58 }
59 if (flags & STF_STRING) != 0 {
60 // c:56
61 let modes = b"?rwxrwxrwx";
62 let mut pm = [b'-'; 10];
63 // c:84-103 — file-type char.
64 let ifmt = mode & 0o170_000; // S_IFMT
65 pm[0] = match ifmt {
66 0o020_000 => b'c', // S_ISCHR
67 0o040_000 => b'd', // S_ISDIR
68 0o060_000 => b'b', // S_ISBLK
69 0o100_000 => b'-', // S_ISREG
70 0o120_000 => b'l', // S_ISLNK
71 0o140_000 => b's', // S_ISSOCK
72 0o010_000 => b'p', // S_ISFIFO
73 _ => b'?',
74 };
75 // c:105-107 — owner/group/other rwx bits.
76 let bits = [
77 0o0400, 0o0200, 0o0100, 0o0040, 0o0020, 0o0010, 0o0004, 0o0002, 0o0001,
78 ];
79 for i in 0..9 {
80 pm[i + 1] = if (mode & bits[i]) != 0 {
81 modes[i + 1]
82 } else {
83 b'-'
84 };
85 }
86 // c:111-115 — setuid / setgid / sticky.
87 if (mode & 0o4000) != 0 {
88 // S_ISUID
89 pm[3] = if (mode & 0o0100) != 0 { b's' } else { b'S' };
90 }
91 if (mode & 0o2000) != 0 {
92 // S_ISGID
93 pm[6] = if (mode & 0o0010) != 0 { b's' } else { b'S' };
94 }
95 if (mode & 0o1000) != 0 {
96 // S_ISVTX
97 pm[9] = if (mode & 0o0001) != 0 { b't' } else { b'T' };
98 }
99 out.push_str(std::str::from_utf8(&pm).unwrap_or(""));
100 if (flags & STF_RAW) != 0 {
101 // c:132
102 out.push(')'); // c:132
103 }
104 }
105 out
106}
107
108/// Port of `statuidprint(uid_t uid, char *outbuf, int flags)` from `Src/Modules/stat.c:132`. Renders
109/// a uid in raw form (decimal), string form (user name via
110/// `getpwuid`), or both.
111/// WARNING: param names don't match C — Rust=(uid, flags) vs C=(uid, outbuf, flags)
112pub fn statuidprint(uid: u32, flags: i32) -> String {
113 // c:132
114 let mut out = String::new();
115 if (flags & STF_RAW) != 0 {
116 // c:135
117 out.push_str(&format!("{}", uid));
118 if (flags & STF_STRING) != 0 {
119 // c:137
120 out.push_str(" (");
121 }
122 }
123 if (flags & STF_STRING) != 0 {
124 // c:140
125 let name = unsafe {
126 // c:142 — `pwd = getpwuid(uid);`
127 let p = libc::getpwuid(uid);
128 if p.is_null() {
129 String::new()
130 } else {
131 let nm = (*p).pw_name;
132 if nm.is_null() {
133 String::new()
134 } else {
135 std::ffi::CStr::from_ptr(nm).to_string_lossy().into_owned()
136 }
137 }
138 };
139 if name.is_empty() {
140 // c:148 numeric fallback
141 out.push_str(&format!("{}", uid));
142 } else {
143 out.push_str(&name); // c:161 pwd->pw_name
144 }
145 if (flags & STF_RAW) != 0 {
146 // c:161
147 out.push(')');
148 }
149 }
150 out
151}
152
153/// Port of `statgidprint(gid_t gid, char *outbuf, int flags)` from `Src/Modules/stat.c:161`. Symmetric
154/// with `statuidprint` for gid via `getgrgid`.
155/// WARNING: param names don't match C — Rust=(gid, flags) vs C=(gid, outbuf, flags)
156pub fn statgidprint(gid: u32, flags: i32) -> String {
157 // c:161
158 let mut out = String::new();
159 if (flags & STF_RAW) != 0 {
160 // c:164
161 out.push_str(&format!("{}", gid));
162 if (flags & STF_STRING) != 0 {
163 // c:166
164 out.push_str(" (");
165 }
166 }
167 if (flags & STF_STRING) != 0 {
168 // c:169
169 let name = unsafe {
170 let g = libc::getgrgid(gid); // c:171
171 if g.is_null() {
172 String::new()
173 } else {
174 let nm = (*g).gr_name;
175 if nm.is_null() {
176 String::new()
177 } else {
178 std::ffi::CStr::from_ptr(nm).to_string_lossy().into_owned()
179 }
180 }
181 };
182 if name.is_empty() {
183 out.push_str(&format!("{}", gid)); // c:184
184 } else {
185 out.push_str(&name); // c:178
186 }
187 if (flags & STF_RAW) != 0 {
188 // c:191
189 out.push(')');
190 }
191 }
192 out
193}
194
195/// Port of `static char *timefmt;` from `Src/Modules/stat.c:187`. C uses a
196/// module-static global initialized to the ctime-like default at the top of
197/// `bin_stat` (c:376) and overwritten by `-F FMT`. Rust mirrors with a
198/// `Mutex<String>` so `stattimeprint` (c:201) can read the same global.
199/// Default constant lives next to the static; callers read/write the lock
200/// directly (no accessor helpers — those would be Rust-only fns).
201const TIMEFMT_DEFAULT: &str = "%a %b %e %k:%M:%S %Z %Y"; // c:376
202
203static TIMEFMT: std::sync::OnceLock<std::sync::Mutex<String>> = std::sync::OnceLock::new();
204
205/// Port of `stattimeprint(time_t tim, long nsecs, char *outbuf, int flags)` from `Src/Modules/stat.c:191`. Renders
206/// a Unix timestamp + nsec offset: raw form is integer seconds;
207/// string form is `ctime(3)` (or strftime via the timefmt global).
208/// WARNING: param names don't match C — Rust=(tim, _nsecs, flags) vs C=(tim, nsecs, outbuf, flags)
209pub fn stattimeprint(tim: i64, nsecs: i64, flags: i32) -> String {
210 // c:191
211 let mut out = String::new();
212 if (flags & STF_RAW) != 0 {
213 // c:194
214 out.push_str(&format!("{}", tim));
215 if (flags & STF_STRING) != 0 {
216 // c:196
217 out.push_str(" (");
218 }
219 }
220 if (flags & STF_STRING) != 0 {
221 // c:199
222 // c:201 — `ztrftime(oend, 40, timefmt,
223 // (flags & STF_GMT) ? gmtime(&tim) : localtime(&tim), nsecs);`
224 // C reads the module-static `timefmt` here (initialized to the
225 // ctime default at bin_stat entry, possibly overwritten by -F).
226 // The GMT vs local choice comes from the STF_GMT flag (`stat -g`).
227 // c:201 — `nsecs` is the sub-second component (GET_ST_*_NSEC);
228 // pack into Duration::new(secs, nsec) so ztrftime sees the
229 // fractional digits for the `%.` / `%N.` zsh-extension.
230 let nsec_u32 = nsecs.clamp(0, 999_999_999) as u32;
231 let st = std::time::UNIX_EPOCH + std::time::Duration::new(tim.max(0) as u64, nsec_u32);
232 let fmt: String = TIMEFMT
233 .get_or_init(|| std::sync::Mutex::new(TIMEFMT_DEFAULT.to_string()))
234 .lock()
235 .map(|g| g.clone())
236 .unwrap_or_else(|_| TIMEFMT_DEFAULT.to_string());
237 let use_gmt = (flags & STF_GMT) != 0; // c:201 — picks gmtime(&tim)
238 let formatted = ztrftime(&fmt, st, use_gmt);
239 out.push_str(&formatted);
240 if (flags & STF_RAW) != 0 {
241 // c:211
242 out.push(')');
243 }
244 }
245 out
246}
247
248/// Port of `statulprint(unsigned long num, char *outbuf)` from `Src/Modules/stat.c:211`. Renders an
249/// unsigned-long stat field as decimal (always raw, no STF_STRING
250/// branch).
251/// WARNING: param names don't match C — Rust=(num) vs C=(num, outbuf)
252pub fn statulprint(num: u64) -> String {
253 // c:211
254 format!("{}", num) // c:219
255}
256
257/// Port of `statlinkprint(struct stat *sbuf, char *outbuf, char *fname)` from `Src/Modules/stat.c:219`. For
258/// symlinks, renders the link target via `readlink(2)`; otherwise
259/// returns empty.
260/// WARNING: param names don't match C — Rust=(sbuf_mode, fname) vs C=(sbuf, outbuf, fname)
261pub fn statlinkprint(sbuf_mode: u32, fname: &str) -> String {
262 // c:219
263 if (sbuf_mode & 0o170_000) != 0o120_000 {
264 // c:219 S_ISLNK
265 return String::new();
266 }
267 fs::read_link(fname) // c:226 readlink
268 .map(|p| p.to_string_lossy().into_owned())
269 .unwrap_or_default()
270}
271
272/// Port of `statprint(struct stat *sbuf, char *outbuf, char *fname, int iwhich, int flags)` from `Src/Modules/stat.c:234`. The unified
273/// per-field dispatcher: given a stat metadata, file name, the
274/// `iwhich` index from `STATELTS`, and a flag word, produce the
275/// formatted value string.
276///
277/// C signature: `static void statprint(struct stat *sbuf, char *outbuf,
278/// char *fname, int iwhich, int flags)`.
279/// WARNING: param names don't match C — Rust=(meta, fname, iwhich, flags) vs C=(sbuf, outbuf, fname, iwhich, flags)
280pub fn statprint(meta: &fs::Metadata, fname: &str, iwhich: i32, flags: i32) -> String {
281 // c:234
282 // c:234-241 — `if (flags & STF_NAME)` prefix with `name<space>`.
283 // `%-8s` left-justifies the name to 8 chars when not PICK/ARRAY,
284 // `%s ` otherwise.
285 let name_prefix = if (flags & STF_NAME) != 0 {
286 let n = STATELTS.get(iwhich as usize).copied().unwrap_or("");
287 if (flags & (STF_PICK | STF_ARRAY)) != 0 {
288 format!("{} ", n) // c:239
289 } else {
290 format!("{:<8}", n) // c:240
291 }
292 } else {
293 String::new()
294 };
295 let val = match iwhich {
296 ST_DEV => format!("{}", meta.dev()), // c:240
297 ST_INO => format!("{}", meta.ino()), // c:241
298 ST_MODE => statmodeprint(meta.mode(), flags), // c:242
299 ST_NLINK => format!("{}", meta.nlink()), // c:243
300 ST_UID => statuidprint(meta.uid(), flags), // c:244
301 ST_GID => statgidprint(meta.gid(), flags), // c:245
302 ST_RDEV => format!("{}", meta.rdev()), // c:246
303 ST_SIZE => statulprint(meta.size()), // c:247
304 // c:290-296 — GET_ST_ATIME_NSEC(*sbuf): the per-platform macro
305 // pulls the sub-second component out of struct stat
306 // (st_atim.tv_nsec on POSIX 2008, st_atimespec.tv_nsec on
307 // BSD-derived systems, etc.). std::fs::Metadata's MetadataExt
308 // exposes the same value via atime_nsec() / mtime_nsec() /
309 // ctime_nsec(). Passing zero (as the prior port did) broke the
310 // ztrftime `%.` / `%N.` fractional-seconds specifier — every
311 // formatted timestamp printed `000` regardless of the actual
312 // sub-second value.
313 ST_ATIM => stattimeprint(meta.atime(), meta.atime_nsec(), flags), // c:292
314 ST_MTIM => stattimeprint(meta.mtime(), meta.mtime_nsec(), flags), // c:300
315 ST_CTIM => stattimeprint(meta.ctime(), meta.ctime_nsec(), flags), // c:308
316 ST_BLKSIZE => statulprint(meta.blksize()), // c:251
317 ST_BLOCKS => statulprint(meta.blocks()), // c:252
318 ST_READLINK => statlinkprint(meta.mode(), fname), // c:253
319 _ => String::new(),
320 };
321 format!("{}{}", name_prefix, val)
322}
323
324/// Port of `bin_stat(char *name, char **args, Options ops, UNUSED(int func))` from `Src/Modules/stat.c:368`. The `zstat`
325/// builtin entry. Parses the `+ELEMENT` / `-flag` / `-A NAME` /
326/// `-H NAME` / `-f FD` / `-F FORMAT` arg syntax, then calls
327/// `lstat`/`stat`/`fstat` per file, dispatching `statprint` for
328/// each requested element.
329///
330/// C signature: `static int bin_stat(char *name, char **args,
331/// Options ops, int func)`.
332/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(name, args, ops, func)
333pub fn bin_stat(
334 nam: &str,
335 args: &[String], // c:368
336 _ops_unused: &options,
337 _func: i32,
338) -> i32 {
339 // c:370-374 — locals.
340 let mut iwhich: i32 = -1; // c:373
341 let mut flags: i32 = 0;
342 let mut found = 0i32; // c:375
343 let mut arrnam: Option<String> = None;
344 let mut hashnam: Option<String> = None;
345 let mut fd: i32 = 0;
346 // c:376 — `timefmt = "%a %b %e %k:%M:%S %Z %Y";`. Reset the module-
347 // static every entry so a prior `-F` doesn't leak into a fresh
348 // invocation without `-F`.
349 if let Ok(mut g) = TIMEFMT
350 .get_or_init(|| std::sync::Mutex::new(TIMEFMT_DEFAULT.to_string()))
351 .lock()
352 {
353 g.clear();
354 g.push_str(TIMEFMT_DEFAULT);
355 }
356 // The C `Options ops` bitmap is parsed inline by this fn (the
357 // BUILTIN spec at c:637 is `NULL`, so the framework doesn't pre-
358 // parse). Per PORT_CHECKLIST.md rule 3 we keep `ops` as a local
359 // 256-entry bitmap rather than introducing a Rust-only struct.
360 let mut ops = [false; 256];
361 let args: Vec<&str> = args.iter().map(String::as_str).collect();
362 let mut argv: Vec<&str> = Vec::with_capacity(args.len());
363 let mut i = 0;
364 // c:381 — arg loop.
365 while i < args.len() && (args[i].starts_with('+') || args[i].starts_with('-')) {
366 let arg = &args[i][1..];
367 if arg.is_empty() || arg.starts_with('-') || arg.starts_with('+') {
368 i += 1;
369 break; // c:386
370 }
371 if args[i].starts_with('+') {
372 // c:389
373 if found != 0 {
374 break;
375 }
376 for (idx, name) in STATELTS.iter().enumerate() {
377 // c:392
378 if name.starts_with(arg) {
379 found += 1;
380 iwhich = idx as i32;
381 }
382 }
383 if found > 1 {
384 // c:397
385 zwarnnam(nam, &format!("{}: ambiguous stat element", arg));
386 return 1;
387 } else if found == 0 {
388 // c:400
389 zwarnnam(nam, &format!("{}: no such stat element", arg));
390 return 1;
391 }
392 // c:404 — `if (iwhich == ST_READLINK) ops->ind['L'] = 1;`
393 if iwhich == ST_READLINK {
394 ops[b'L' as usize] = true;
395 }
396 flags |= STF_PICK; // c:406
397 } else {
398 // c:407 - flag arm
399 for (pos, ch) in arg.char_indices() {
400 // c:410-411 — `rest` is `arg+1` for an option that takes a value.
401 let rest: &str = &arg[pos + 1..];
402 match ch {
403 'g' | 'l' | 'L' | 'n' | 'N' | 'o' | 'r' | 's' | 't' | 'T' => {
404 ops[ch as u8 as usize] = true; // c:411
405 }
406 'A' => {
407 // c:412-418 — `if (arg[1]) arrnam = arg+1; else if
408 // (!(arrnam = *++args)) { missing parameter name }`.
409 if !rest.is_empty() {
410 arrnam = Some(rest.to_string()); // c:414
411 } else {
412 i += 1;
413 if i >= args.len() {
414 zwarnnam(nam, "missing parameter name"); // c:416
415 return 1;
416 }
417 arrnam = Some(args[i].to_string()); // c:415
418 }
419 flags |= STF_ARRAY;
420 break;
421 }
422 'H' => {
423 // c:421-427 — same attached-or-next rule as -A.
424 if !rest.is_empty() {
425 hashnam = Some(rest.to_string()); // c:422
426 } else {
427 i += 1;
428 if i >= args.len() {
429 zwarnnam(nam, "missing parameter name"); // c:424
430 return 1;
431 }
432 hashnam = Some(args[i].to_string()); // c:423
433 }
434 flags |= STF_HASH;
435 break;
436 }
437 'f' => {
438 ops[b'f' as usize] = true; // c:429
439 // c:430-435 — `if (arg[1]) sfd = arg+1; else if
440 // (!(sfd = *++args)) { missing file descriptor }`.
441 let sfd: &str = if !rest.is_empty() {
442 rest // c:431
443 } else {
444 i += 1;
445 if i >= args.len() {
446 zwarnnam(nam, "missing file descriptor"); // c:433
447 return 1;
448 }
449 args[i] // c:432
450 };
451 let (val, endptr) = zstrtol(sfd, 10); // c:436
452 if !endptr.is_empty() {
453 zwarnnam(nam, "bad file descriptor");
454 return 1;
455 }
456 fd = val as i32;
457 break;
458 }
459 'F' => {
460 // c:442-451 — `-F FMT`. If the same arg has chars
461 // after 'F' (e.g. `-F%Y`), use those; else consume
462 // the next argv entry. Force STF_STRING via -s so
463 // the format actually gets used (c:449-450).
464 let inline: &str = rest;
465 let fmt: &str = if !inline.is_empty() {
466 // c:443-444
467 inline
468 } else {
469 i += 1;
470 if i >= args.len() {
471 // c:446
472 zwarnnam(nam, "missing time format");
473 return 1;
474 }
475 args[i]
476 };
477 // c:444 — `timefmt = arg+1;` / c:445 — `timefmt = *++args;`
478 if let Ok(mut g) = TIMEFMT
479 .get_or_init(|| std::sync::Mutex::new(TIMEFMT_DEFAULT.to_string()))
480 .lock()
481 {
482 g.clear();
483 g.push_str(fmt);
484 }
485 ops[b's' as usize] = true; // c:450 — force STF_STRING.
486 break;
487 }
488 _ => {
489 zwarnnam(nam, &format!("bad option: -{}", ch));
490 return 1;
491 }
492 }
493 }
494 }
495 i += 1;
496 }
497 while i < args.len() {
498 argv.push(args[i]);
499 i += 1;
500 }
501
502 if (flags & STF_ARRAY) != 0 && (flags & STF_HASH) != 0 {
503 // c:459
504 zwarnnam(nam, "both array and hash requested");
505 return 1;
506 }
507
508 if ops[b'l' as usize] {
509 // c:467
510 // List elements + return.
511 if let Some(ref name) = arrnam {
512 // c:469
513 // c:485 — `setaparam(arrnam, array);` — REAL array of
514 // element names. Prior port colon-joined into a scalar
515 // via setsparam — `$names[1]` returned the first CHAR of
516 // "device:inode:mode:..." instead of "device", and
517 // ${(t)names} reported scalar where zsh reports array.
518 let names: Vec<String> = STATELTS.iter().map(|s| s.to_string()).collect();
519 setaparam(name, names); // c:485
520 } else {
521 let joined: Vec<&str> = STATELTS.iter().copied().collect();
522 println!("{}", joined.join(" ")); // c:478 putchar
523 }
524 return 0; // c:489
525 }
526
527 if argv.is_empty() && !ops[b'f' as usize] {
528 // c:491
529 zwarnnam(nam, "no files given");
530 return 1;
531 } else if !argv.is_empty() && ops[b'f' as usize] {
532 // c:493
533 zwarnnam(nam, "no files allowed with -f");
534 return 1;
535 }
536
537 // c:496+ — per-file stat + dispatch loop.
538 let use_lstat = ops[b'L' as usize];
539 let mut hash_out: Vec<(String, String)> = Vec::new();
540 let mut array_out: Vec<String> = Vec::new();
541 let show_type = ops[b't' as usize]; // c: -t
542 let mut local_flags = flags;
543 // c:510-513 — `if (OPT_ISSET(ops,'g')) { flags |= STF_GMT;
544 // ops->ind['s'] = 1; }`
545 // -g picks gmtime over localtime (consumed by stattimeprint at c:201)
546 // AND auto-enables string formatting since raw epoch seconds carry
547 // no timezone semantics.
548 if ops[b'g' as usize] {
549 local_flags |= STF_GMT; // c:511
550 ops[b's' as usize] = true; // c:512
551 }
552 // c:514-515 — `if (OPT_ISSET(ops,'s') || OPT_ISSET(ops,'r'))` STF_STRING.
553 if ops[b's' as usize] || ops[b'r' as usize] {
554 local_flags |= STF_STRING;
555 }
556 // c:516 — `if (OPT_ISSET(ops,'r') || !OPT_ISSET(ops,'s'))` STF_RAW.
557 if ops[b'r' as usize] || !ops[b's' as usize] {
558 // c:516
559 local_flags |= STF_RAW;
560 }
561 // c:518-519 — `-n` → STF_FILE (filename prefix).
562 if ops[b'n' as usize] {
563 local_flags |= STF_FILE;
564 } // c:519
565 // c:520-521 — `-o` → STF_OCTAL.
566 if ops[b'o' as usize] {
567 local_flags |= STF_OCTAL;
568 } // c:521
569 // c:522-523 — `-t` → STF_NAME explicit.
570 if ops[b't' as usize] {
571 local_flags |= STF_NAME;
572 } // c:523
573 // c:525-530 — default STF_NAME when neither -A nor -H and no
574 // single-element pick: every line gets a `name<sp>` prefix so
575 // `zstat /etc/hosts` looks like `mode 33188` etc.
576 if arrnam.is_none() && hashnam.is_none() {
577 if argv.len() > 1 {
578 local_flags |= STF_FILE;
579 } // c:527
580 if (local_flags & STF_PICK) == 0 {
581 // c:528
582 local_flags |= STF_NAME; // c:529
583 }
584 }
585 // c:532-535 — explicit -N / -f turn off STF_FILE; -T / -H turn off
586 // STF_NAME (suppress prefix for `read` / hash use).
587 if ops[b'N' as usize] || ops[b'f' as usize] {
588 // c:532
589 local_flags &= !STF_FILE;
590 }
591 if ops[b'T' as usize] || ops[b'H' as usize] {
592 // c:534
593 local_flags &= !STF_NAME;
594 }
595 let _ = show_type;
596
597 // c:Src/Modules/stat.c:bin_stat — C tracks per-file stat(2) failures
598 // and propagates a non-zero rc when any path errored (`ret = 1` at
599 // c:560-565 inside the per-path loop). The Rust port previously
600 // skipped the path on error via `continue` but unconditionally
601 // returned 0 at the end, masking ENOENT (and any other stat error)
602 // as success. Track the first failure rc and return it after the
603 // loop.
604 let mut rc: i32 = 0;
605 // c:557 — `for (; OPT_ISSET(ops,'f') || *args; args++)`: with -f there
606 // are no file arguments (c:493) and the single pass fstat()s `fd`; the
607 // c:563 diagnostic then names the descriptor (`sprintf(outbuf, "%d", fd)`).
608 let fd_label = fd.to_string();
609 let targets: Vec<&str> = if ops[b'f' as usize] { vec![fd_label.as_str()] } else { argv.clone() };
610 for path in &targets {
611 let meta = if ops[b'f' as usize] {
612 // c:559 — `fstat(fd, &statbuf)`. The descriptor is borrowed,
613 // never closed: ManuallyDrop keeps File's Drop off it.
614 use std::os::unix::io::FromRawFd;
615 let file = std::mem::ManuallyDrop::new(unsafe { fs::File::from_raw_fd(fd) });
616 file.metadata()
617 } else if use_lstat {
618 fs::symlink_metadata(path) // c:560
619 } else {
620 fs::metadata(path) // c:561
621 };
622 let meta = match meta {
623 Ok(m) => m,
624 Err(e) => {
625 // Bug #112 — strip Rust's " (os error N)" suffix by
626 // routing the errno through the canonical strerror
627 // port (Src/compat.c:194).
628 let errno = e.raw_os_error().unwrap_or(0);
629 let raw = crate::ported::compat::strerror(errno);
630 // c:Src/stat.c:564 — `zwarnnam(name, "%s: %e", ...)`.
631 // The `%e` directive (utils.c:360-367) uncapitalizes the
632 // first letter of strerror unless errno==EIO, so zsh
633 // prints "no such file or directory", not "No such…".
634 const EIO: i32 = 5;
635 let msg = if errno == EIO {
636 raw
637 } else {
638 let mut cs = raw.chars();
639 match cs.next() {
640 Some(f) => f.to_lowercase().collect::<String>() + cs.as_str(),
641 None => raw,
642 }
643 };
644 zwarnnam(nam, &format!("{}: {}", path, msg));
645 rc = 1;
646 // c:567-568 — `if (OPT_ISSET(ops,'f') || arrnam) break; else continue;`
647 // With -A (or -f) the whole result is a single aggregate, so the
648 // first failure abandons the loop; without one, the remaining
649 // files are still reported.
650 if arrnam.is_some() || ops[b'f' as usize] {
651 break;
652 }
653 continue;
654 }
655 };
656
657 // c:574-582 — `STF_FILE`: the file name leads this file's entries —
658 // an array element for `-A`, the HNAMEKEY pair for `-H`, a printed
659 // prefix otherwise.
660 if (local_flags & STF_FILE) != 0 {
661 if arrnam.is_some() {
662 array_out.push(path.to_string()); // c:576 ztrdup_metafy(*args)
663 } else if hashnam.is_some() {
664 hash_out.push((HNAMEKEY.to_string(), path.to_string())); // c:578-579
665 } else if (local_flags & STF_PICK) != 0 {
666 print!("{} ", path); // c:581
667 } else {
668 println!("{}:", path); // c:581
669 }
670 }
671 if iwhich >= 0 {
672 // -E single element.
673 let val = statprint(&meta, path, iwhich, local_flags);
674 if let Some(ref aname) = arrnam {
675 array_out.push(val);
676 let _ = aname;
677 } else if let Some(ref hname) = hashnam {
678 hash_out.push((STATELTS[iwhich as usize].to_string(), val));
679 let _ = hname;
680 } else {
681 println!("{}", val); // c:591
682 }
683 } else {
684 // All elements.
685 for idx in 0..STATELTS.len() {
686 let val = statprint(&meta, path, idx as i32, local_flags);
687 if let Some(_) = &arrnam {
688 array_out.push(val);
689 } else if let Some(_) = &hashnam {
690 hash_out.push((STATELTS[idx].to_string(), val));
691 } else {
692 println!("{}", val); // c:603
693 }
694 }
695 }
696 }
697
698 // c:613-631 — `if (ret) freearray(array); else setaparam(...)`. The result is
699 // accumulated into a LOCAL buffer and only published to the parameter when
700 // every file stat'd cleanly. On any failure the buffer is dropped and the
701 // target parameter keeps whatever it already held — so a failing
702 // `zstat -H h <dangling-link>` must not wipe the assoc a previous successful
703 // zstat left in `h`.
704 if rc == 0 {
705 if let Some(name) = arrnam {
706 // c — `setaparam(name, zarrdup(array_out));` — real indexed array.
707 setaparam(&name, array_out); // c:params.c:3595
708 }
709 if let Some(name) = hashnam {
710 // c — `sethparam(name, ...);` — real assoc array. Flatten
711 // hash_out into alternating [k,v,k,v,...].
712 let mut flat: Vec<String> = Vec::with_capacity(hash_out.len() * 2);
713 for (k, v) in hash_out {
714 flat.push(k);
715 flat.push(v);
716 }
717 sethparam(&name, flat); // c:params.c:3602
718 }
719 }
720 rc
721}
722
723// `bintab` — port of `static struct builtin bintab[]` (stat.c:638).
724
725// `module_features` — port of `static struct features module_features`
726// from stat.c:642.
727
728/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/stat.c:651`.
729#[allow(unused_variables)]
730pub fn setup_(m: *const module) -> i32 {
731 // c:651
732 // C body c:653-654 — `return 0`. Faithful empty-body port.
733 0
734}
735
736/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/stat.c:658`.
737/// C body: `*features = featuresarray(m, &module_features); return 0;`
738pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
739 // c:658
740 *features = featuresarray(m, module_features());
741 0 // c:673
742}
743
744/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/stat.c:666`.
745/// C body: `return handlefeatures(m, &module_features, enables);`
746pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
747 // c:666
748 handlefeatures(m, module_features(), enables) // c:673
749}
750
751/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/stat.c:673`.
752#[allow(unused_variables)]
753pub fn boot_(m: *const module) -> i32 {
754 // c:673
755 // C body c:675-676 — `return 0`. Faithful empty-body port; the
756 // zstat builtin registers via the bn_list dispatch.
757 0
758}
759
760/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/stat.c:680`.
761/// C body: `return setfeatureenables(m, &module_features, NULL);`
762pub fn cleanup_(m: *const module) -> i32 {
763 // c:680
764 setfeatureenables(m, module_features(), None) // c:687
765}
766
767/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/stat.c:687`.
768#[allow(unused_variables)]
769pub fn finish_(m: *const module) -> i32 {
770 // c:687
771 // C body c:689-690 — `return 0`. Faithful empty-body port; the
772 // zstat builtin unregisters via cleanup_'s setfeatureenables.
773 0
774}
775/// `ST_DEV` constant.
776pub const ST_DEV: i32 = 0; // c:33
777/// `ST_INO` constant.
778pub const ST_INO: i32 = 1;
779/// `ST_MODE` constant.
780pub const ST_MODE: i32 = 2;
781/// `ST_NLINK` constant.
782pub const ST_NLINK: i32 = 3;
783/// `ST_UID` constant.
784pub const ST_UID: i32 = 4;
785/// `ST_GID` constant.
786pub const ST_GID: i32 = 5;
787/// `ST_RDEV` constant.
788pub const ST_RDEV: i32 = 6;
789/// `ST_SIZE` constant.
790pub const ST_SIZE: i32 = 7;
791/// `ST_ATIM` constant.
792pub const ST_ATIM: i32 = 8;
793/// `ST_MTIM` constant.
794pub const ST_MTIM: i32 = 9;
795/// `ST_CTIM` constant.
796pub const ST_CTIM: i32 = 10;
797/// `ST_BLKSIZE` constant.
798pub const ST_BLKSIZE: i32 = 11;
799/// `ST_BLOCKS` constant.
800pub const ST_BLOCKS: i32 = 12;
801/// `ST_READLINK` constant.
802pub const ST_READLINK: i32 = 13;
803/// `ST_COUNT` constant.
804pub const ST_COUNT: i32 = 14; // c:34
805
806// ============================================================
807// Port of `enum statflags` from `Src/Modules/stat.c:36-38`.
808// Bitmask flags passed to the print ported + bin_stat dispatch.
809// ============================================================
810/// `STF_NAME` constant.
811pub const STF_NAME: i32 = 1; // c:36
812/// `STF_FILE` constant.
813pub const STF_FILE: i32 = 2;
814/// `STF_STRING` constant.
815pub const STF_STRING: i32 = 4;
816/// `STF_RAW` constant.
817pub const STF_RAW: i32 = 8;
818
819// =====================================================================
820// static struct builtin bintab[] c:638
821// static struct features module_features c:642
822// =====================================================================
823/// `STF_PICK` constant.
824pub const STF_PICK: i32 = 16;
825/// `STF_ARRAY` constant.
826pub const STF_ARRAY: i32 = 32;
827/// `STF_GMT` constant.
828pub const STF_GMT: i32 = 64;
829/// `STF_HASH` constant.
830pub const STF_HASH: i32 = 128;
831/// `STF_OCTAL` constant.
832pub const STF_OCTAL: i32 = 256; // c:38
833
834/// Port of `statelts[]` from `Src/Modules/stat.c:39`. Names of the
835/// 14 stat-elements, indexed by the `ST_*` constants above.
836pub static STATELTS: &[&str] = &[
837 // c:39
838 "device", "inode", "mode", "nlink", "uid", "gid", "rdev", "size", "atime", "mtime", "ctime",
839 "blksize", "blocks", "link",
840];
841
842static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
843
844// Local stubs for the per-module entry points. C uses generic
845// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
846// 3275/3370/3445) but those take `Builtin` + `Features` pointer
847// fields the Rust port doesn't carry. The hardcoded descriptor
848// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
849// WARNING: NOT IN STAT.C — Rust-only module-framework shim.
850// C uses generic featuresarray/handlefeatures/setfeatureenables from
851// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
852// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
853fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
854 vec!["b:stat".to_string(), "b:zstat".to_string()]
855}
856
857// WARNING: NOT IN STAT.C — Rust-only module-framework shim.
858// C uses generic featuresarray/handlefeatures/setfeatureenables from
859// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
860// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
861fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
862 // c:3392 — the name-keyed variant in src/ported/module.rs; this
863 // module ships no `Features` descriptor tables for the per-feature
864 // ADDED bit to live on (see MODULE_FEATURE_ENABLES there).
865 crate::ported::module::handlefeatures("zsh/stat", &featuresarray(m, f), enables)
866}
867
868// WARNING: NOT IN STAT.C — Rust-only module-framework shim.
869// C uses generic featuresarray/handlefeatures/setfeatureenables from
870// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
871// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
872fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
873 0
874}
875
876// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
877// ─── RUST-ONLY ACCESSORS ───
878//
879// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
880// RwLock<T>>` globals declared above. C zsh uses direct global
881// access; Rust needs these wrappers because `OnceLock::get_or_init`
882// is the only way to lazily construct shared state. These ported sit
883// here so the body of this file reads in C source order without
884// the accessor wrappers interleaved between real port ported.
885// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
886
887// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
888// ─── RUST-ONLY ACCESSORS ───
889//
890// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
891// RwLock<T>>` globals declared above. C zsh uses direct global
892// access; Rust needs these wrappers because `OnceLock::get_or_init`
893// is the only way to lazily construct shared state. These ported sit
894// here so the body of this file reads in C source order without
895// the accessor wrappers interleaved between real port ported.
896// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
897
898// WARNING: NOT IN STAT.C — Rust-only module-framework shim.
899// C uses generic featuresarray/handlefeatures/setfeatureenables from
900// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
901// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
902fn module_features() -> &'static Mutex<features> {
903 MODULE_FEATURES.get_or_init(|| {
904 Mutex::new(features {
905 bn_list: None,
906 bn_size: 2,
907 cd_list: None,
908 cd_size: 0,
909 mf_list: None,
910 mf_size: 0,
911 pd_list: None,
912 pd_size: 0,
913 n_abstract: 0,
914 })
915 })
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921 use std::fs::File;
922 use std::io::Write;
923
924 #[test]
925 fn statelts_count_matches_st_count() {
926 let _g = crate::test_util::global_state_lock();
927 assert_eq!(STATELTS.len() as i32, ST_COUNT);
928 }
929
930 #[test]
931 fn statmodeprint_octal_only() {
932 let _g = crate::test_util::global_state_lock();
933 let s = statmodeprint(0o100644, STF_RAW | STF_OCTAL);
934 assert!(s.starts_with('0'));
935 assert!(s.contains("644"));
936 }
937
938 #[test]
939 fn statmodeprint_string_only() {
940 let _g = crate::test_util::global_state_lock();
941 let s = statmodeprint(0o100644, STF_STRING);
942 assert_eq!(s.len(), 10);
943 assert_eq!(&s[..1], "-");
944 }
945
946 #[test]
947 fn statmodeprint_directory() {
948 let _g = crate::test_util::global_state_lock();
949 let s = statmodeprint(0o040755, STF_STRING);
950 assert_eq!(&s[..1], "d");
951 }
952
953 #[test]
954 fn statulprint_decimal() {
955 let _g = crate::test_util::global_state_lock();
956 assert_eq!(statulprint(12345), "12345");
957 }
958
959 #[test]
960 fn statprint_size_via_index() {
961 let _g = crate::test_util::global_state_lock();
962 let dir = tempfile::TempDir::new().unwrap();
963 let path = dir.path().join("x.txt");
964 File::create(&path).unwrap().write_all(b"hello").unwrap();
965 let meta = fs::metadata(&path).unwrap();
966 let s = statprint(&meta, path.to_str().unwrap(), ST_SIZE, 0);
967 assert_eq!(s, "5");
968 }
969
970 /// c:47 — `statmodeprint` for the BLOCK device file type. The
971 /// `pm[0]` lookup at c:84-103 must yield 'b'. Pin all file-type
972 /// chars so a regen that swaps `0o060_000` and `0o020_000`
973 /// silently renders block devices as char devices.
974 #[test]
975 fn statmodeprint_file_type_chars_match_ls_output() {
976 let _g = crate::test_util::global_state_lock();
977 // S_IFREG (regular file)
978 assert_eq!(&statmodeprint(0o100_644, STF_STRING)[..1], "-");
979 // S_IFDIR
980 assert_eq!(&statmodeprint(0o040_755, STF_STRING)[..1], "d");
981 // S_IFLNK (symlink)
982 assert_eq!(&statmodeprint(0o120_777, STF_STRING)[..1], "l");
983 // S_IFCHR (char device)
984 assert_eq!(&statmodeprint(0o020_644, STF_STRING)[..1], "c");
985 // S_IFBLK (block device)
986 assert_eq!(&statmodeprint(0o060_644, STF_STRING)[..1], "b");
987 // S_IFIFO (named pipe)
988 assert_eq!(&statmodeprint(0o010_644, STF_STRING)[..1], "p");
989 // S_IFSOCK
990 assert_eq!(&statmodeprint(0o140_644, STF_STRING)[..1], "s");
991 }
992
993 /// c:111-115 — setuid bit renders as 's' in the user-execute
994 /// slot when execute is set, 'S' when not. Pin both polarities
995 /// for setuid, setgid, and sticky so a regen flipping the
996 /// uppercase/lowercase dispatch gets caught.
997 #[test]
998 fn statmodeprint_setuid_setgid_sticky_render_correctly() {
999 let _g = crate::test_util::global_state_lock();
1000 // 4755 = setuid + executable → 's' in user slot
1001 let s = statmodeprint(0o104_755, STF_STRING);
1002 assert_eq!(
1003 s.chars().nth(3),
1004 Some('s'),
1005 "setuid+x must render as 's' in user-execute slot"
1006 );
1007
1008 // 4644 = setuid + NOT executable → 'S' in user slot
1009 let s = statmodeprint(0o104_644, STF_STRING);
1010 assert_eq!(
1011 s.chars().nth(3),
1012 Some('S'),
1013 "setuid without x must render as 'S' (uppercase)"
1014 );
1015
1016 // 2755 = setgid + group-x → 's' in group-execute slot
1017 let s = statmodeprint(0o102_755, STF_STRING);
1018 assert_eq!(
1019 s.chars().nth(6),
1020 Some('s'),
1021 "setgid+gx must render as 's' in group-execute slot"
1022 );
1023
1024 // 2644 = setgid without group-x → 'S'
1025 let s = statmodeprint(0o102_644, STF_STRING);
1026 assert_eq!(
1027 s.chars().nth(6),
1028 Some('S'),
1029 "setgid without gx must render as 'S'"
1030 );
1031
1032 // 1755 = sticky + world-x → 't' in other-execute slot
1033 let s = statmodeprint(0o101_755, STF_STRING);
1034 assert_eq!(
1035 s.chars().nth(9),
1036 Some('t'),
1037 "sticky+ox must render as 't' in other-execute slot"
1038 );
1039
1040 // 1644 = sticky without world-x → 'T'
1041 let s = statmodeprint(0o101_644, STF_STRING);
1042 assert_eq!(
1043 s.chars().nth(9),
1044 Some('T'),
1045 "sticky without ox must render as 'T'"
1046 );
1047 }
1048
1049 /// c:47-93 — `STF_RAW | STF_STRING` produces "raw (string)" with
1050 /// the raw form OUTSIDE parens and the string form inside.
1051 /// Pin the format because user scripts grep for "^(0?[0-9]+) \("
1052 /// to split the two halves.
1053 #[test]
1054 fn statmodeprint_raw_and_string_renders_with_parens() {
1055 let _g = crate::test_util::global_state_lock();
1056 let s = statmodeprint(0o100_644, STF_RAW | STF_STRING);
1057 // Decimal raw form, space, paren, 10-char string, close paren
1058 assert!(s.contains(" ("), "missing ' (' separator: {}", s);
1059 assert!(s.ends_with(')'), "missing closing ')': {}", s);
1060 // The closing paren must come right after the 10-char ls form
1061 let open = s.find('(').unwrap();
1062 let close = s.rfind(')').unwrap();
1063 assert_eq!(
1064 close - open - 1,
1065 10,
1066 "expected 10-char ls-mode between parens, got: {:?}",
1067 &s[open + 1..close]
1068 );
1069 }
1070
1071 /// c:47-93 — `statmodeprint(0)` with STF_STRING renders all dashes
1072 /// after the file-type indicator. Edge case: zero permissions on
1073 /// a "no file-type" mode falls through to '?'.
1074 #[test]
1075 fn statmodeprint_zero_mode_renders_unknown_type_no_perms() {
1076 let _g = crate::test_util::global_state_lock();
1077 let s = statmodeprint(0, STF_STRING);
1078 assert_eq!(s.len(), 10);
1079 assert_eq!(&s[..1], "?", "mode with no S_IFMT bits → unknown");
1080 assert_eq!(&s[1..], "---------", "no permission bits → all dashes");
1081 }
1082
1083 /// c:132 — `statuidprint` raw form is just the decimal uid;
1084 /// pin the no-leading-zeros, no-prefix shape so a regen that
1085 /// renders octal silently breaks `${(t)f[uid]}`.
1086 #[test]
1087 fn statuidprint_raw_is_decimal() {
1088 let _g = crate::test_util::global_state_lock();
1089 let s = statuidprint(1000, STF_RAW);
1090 assert_eq!(s, "1000");
1091 }
1092
1093 /// c:132 — `statuidprint` for uid 0 must include "root" in the
1094 /// string form (every Unix has uid 0 = root). Pin the well-known
1095 /// case so a regen that breaks the getpwuid path doesn't silently
1096 /// fall back to numeric.
1097 #[test]
1098 fn statuidprint_uid_zero_resolves_to_root() {
1099 let _g = crate::test_util::global_state_lock();
1100 let s = statuidprint(0, STF_STRING);
1101 // Some hardened systems map uid 0 to a different name, but
1102 // it MUST resolve to non-numeric.
1103 assert!(
1104 !s.parse::<u32>().is_ok(),
1105 "uid 0 fell back to numeric form: {}",
1106 s
1107 );
1108 assert!(!s.is_empty());
1109 }
1110
1111 /// c:161 — `statgidprint` raw form is decimal.
1112 #[test]
1113 fn statgidprint_raw_is_decimal() {
1114 let _g = crate::test_util::global_state_lock();
1115 let s = statgidprint(100, STF_RAW);
1116 assert_eq!(s, "100");
1117 }
1118
1119 /// c:211 — `statulprint` for zero must render "0". A regression
1120 /// that prints "" or "0x0" silently breaks numeric script
1121 /// comparisons.
1122 #[test]
1123 fn statulprint_zero_renders_as_zero_digit() {
1124 let _g = crate::test_util::global_state_lock();
1125 assert_eq!(statulprint(0), "0");
1126 }
1127
1128 /// c:211 — `statulprint` for u64::MAX renders the full decimal
1129 /// digit string. Pin the no-overflow / no-truncation behavior.
1130 #[test]
1131 fn statulprint_u64_max_renders_full_digits() {
1132 let _g = crate::test_util::global_state_lock();
1133 let s = statulprint(u64::MAX);
1134 assert_eq!(s, "18446744073709551615");
1135 }
1136
1137 /// c:36-38 — STF_* flag values must each occupy a unique single
1138 /// bit, AND must not overlap with each other (so they can be
1139 /// OR'd together). Pin the bit-distinctness because the flags
1140 /// are AND-tested individually throughout statprint.
1141 #[test]
1142 fn stf_flag_values_are_distinct_single_bits() {
1143 let _g = crate::test_util::global_state_lock();
1144 for f in [
1145 STF_NAME, STF_FILE, STF_STRING, STF_RAW, STF_PICK, STF_ARRAY, STF_GMT, STF_HASH,
1146 STF_OCTAL,
1147 ] {
1148 assert!(f > 0, "STF_* flag {} must be positive", f);
1149 assert_eq!(
1150 (f as u32).count_ones(),
1151 1,
1152 "STF_* flag {} = 0b{:b} must be a single bit",
1153 f,
1154 f
1155 );
1156 }
1157 // Pairwise: no two flags share a bit
1158 let flags = [
1159 STF_NAME, STF_FILE, STF_STRING, STF_RAW, STF_PICK, STF_ARRAY, STF_GMT, STF_HASH,
1160 STF_OCTAL,
1161 ];
1162 for (i, &a) in flags.iter().enumerate() {
1163 for &b in &flags[i + 1..] {
1164 assert_eq!(a & b, 0, "STF flags {} and {} overlap", a, b);
1165 }
1166 }
1167 }
1168
1169 /// c:651-690 — module-lifecycle stubs all return 0 in C.
1170 #[test]
1171 fn module_lifecycle_shims_all_return_zero() {
1172 let _g = crate::test_util::global_state_lock();
1173 let m: *const module = std::ptr::null();
1174 assert_eq!(setup_(m), 0);
1175 assert_eq!(boot_(m), 0);
1176 assert_eq!(cleanup_(m), 0);
1177 assert_eq!(finish_(m), 0);
1178 }
1179
1180 // ─── zsh-corpus pins for statmodeprint ─────────────────────────
1181
1182 /// Regular file 0644 → "-rw-r--r--" string form.
1183 #[test]
1184 fn stat_corpus_modeprint_regular_644() {
1185 let mode = 0o100_644; // S_IFREG | 0644
1186 let r = statmodeprint(mode, STF_STRING);
1187 assert_eq!(r, "-rw-r--r--", "regular 0644 → -rw-r--r--, got {r:?}");
1188 }
1189
1190 /// Regular file 0755 → "-rwxr-xr-x".
1191 #[test]
1192 fn stat_corpus_modeprint_regular_755() {
1193 let mode = 0o100_755;
1194 let r = statmodeprint(mode, STF_STRING);
1195 assert_eq!(r, "-rwxr-xr-x");
1196 }
1197
1198 /// Directory 0755 → "drwxr-xr-x".
1199 #[test]
1200 fn stat_corpus_modeprint_directory() {
1201 let mode = 0o040_755; // S_IFDIR | 0755
1202 let r = statmodeprint(mode, STF_STRING);
1203 assert_eq!(r, "drwxr-xr-x");
1204 }
1205
1206 /// Symlink → 'l' file-type prefix.
1207 #[test]
1208 fn stat_corpus_modeprint_symlink_prefix() {
1209 let mode = 0o120_777; // S_IFLNK | 0777
1210 let r = statmodeprint(mode, STF_STRING);
1211 assert!(
1212 r.starts_with('l'),
1213 "symlink mode starts with 'l', got {r:?}"
1214 );
1215 }
1216
1217 /// FIFO → 'p' file-type prefix.
1218 #[test]
1219 fn stat_corpus_modeprint_fifo_prefix() {
1220 let mode = 0o010_644;
1221 let r = statmodeprint(mode, STF_STRING);
1222 assert!(r.starts_with('p'), "FIFO mode starts with 'p', got {r:?}");
1223 }
1224
1225 /// Socket → 's' file-type prefix.
1226 #[test]
1227 fn stat_corpus_modeprint_socket_prefix() {
1228 let mode = 0o140_644;
1229 let r = statmodeprint(mode, STF_STRING);
1230 assert!(r.starts_with('s'));
1231 }
1232
1233 /// `STF_RAW | STF_OCTAL` → numeric octal form.
1234 #[test]
1235 fn stat_corpus_modeprint_raw_octal() {
1236 let mode = 0o100_644;
1237 let r = statmodeprint(mode, STF_RAW | STF_OCTAL);
1238 assert!(
1239 r.starts_with('0'),
1240 "octal raw form starts with leading 0, got {r:?}"
1241 );
1242 assert!(
1243 r.contains("100644") || r.contains("644"),
1244 "octal repr contains 100644 or 644, got {r:?}"
1245 );
1246 }
1247
1248 /// `STF_RAW` alone → decimal numeric.
1249 #[test]
1250 fn stat_corpus_modeprint_raw_decimal() {
1251 let mode = 0o100_644;
1252 let r = statmodeprint(mode, STF_RAW);
1253 // Decimal form of 0o100644 = 33188.
1254 assert_eq!(r, "33188", "raw decimal form, got {r:?}");
1255 }
1256
1257 /// Empty flags → empty output.
1258 #[test]
1259 fn stat_corpus_modeprint_no_flags_empty() {
1260 let r = statmodeprint(0o100_644, 0);
1261 assert_eq!(r, "", "no flags → empty output");
1262 }
1263
1264 // ═══════════════════════════════════════════════════════════════════
1265 // Additional C-parity tests for Src/Modules/stat.c statmodeprint
1266 // file-type + rwx bit dispatch.
1267 // ═══════════════════════════════════════════════════════════════════
1268
1269 /// c:84-103 — regular file (S_IFREG=0o100000) → '-' first char.
1270 #[test]
1271 fn statmodeprint_regular_file_starts_with_dash() {
1272 let r = statmodeprint(0o100_644, STF_STRING);
1273 assert!(r.starts_with('-'), "regular file → '-', got {:?}", r);
1274 }
1275
1276 /// c:84-103 — directory (S_IFDIR=0o040000) → 'd' first char.
1277 #[test]
1278 fn statmodeprint_directory_starts_with_d() {
1279 let r = statmodeprint(0o040_755, STF_STRING);
1280 assert!(r.starts_with('d'), "directory → 'd', got {:?}", r);
1281 }
1282
1283 /// c:84-103 — symlink (S_IFLNK=0o120000) → 'l' first char.
1284 #[test]
1285 fn statmodeprint_symlink_starts_with_l() {
1286 let r = statmodeprint(0o120_777, STF_STRING);
1287 assert!(r.starts_with('l'), "symlink → 'l', got {:?}", r);
1288 }
1289
1290 /// c:84-103 — char device (S_IFCHR=0o020000) → 'c' first char.
1291 #[test]
1292 fn statmodeprint_char_device_starts_with_c() {
1293 let r = statmodeprint(0o020_644, STF_STRING);
1294 assert!(r.starts_with('c'), "char device → 'c', got {:?}", r);
1295 }
1296
1297 /// c:84-103 — block device (S_IFBLK=0o060000) → 'b' first char.
1298 #[test]
1299 fn statmodeprint_block_device_starts_with_b() {
1300 let r = statmodeprint(0o060_644, STF_STRING);
1301 assert!(r.starts_with('b'), "block device → 'b', got {:?}", r);
1302 }
1303
1304 /// c:84-103 — socket (S_IFSOCK=0o140000) → 's' first char.
1305 #[test]
1306 fn statmodeprint_socket_starts_with_s() {
1307 let r = statmodeprint(0o140_777, STF_STRING);
1308 assert!(r.starts_with('s'), "socket → 's', got {:?}", r);
1309 }
1310
1311 /// c:84-103 — FIFO (S_IFIFO=0o010000) → 'p' first char.
1312 #[test]
1313 fn statmodeprint_fifo_starts_with_p() {
1314 let r = statmodeprint(0o010_644, STF_STRING);
1315 assert!(r.starts_with('p'), "FIFO → 'p', got {:?}", r);
1316 }
1317
1318 /// c:84-103 — unknown ifmt → '?' fallback.
1319 #[test]
1320 fn statmodeprint_unknown_ifmt_returns_question_mark() {
1321 let r = statmodeprint(0o070_000, STF_STRING);
1322 assert!(r.starts_with('?'), "unknown ifmt → '?', got {:?}", r);
1323 }
1324
1325 /// c:111 — setuid sticky 's' when execute bit set on user.
1326 #[test]
1327 fn statmodeprint_setuid_with_exec_shows_lowercase_s() {
1328 // 0o4755 = setuid + rwxr-xr-x
1329 let r = statmodeprint(0o104_755, STF_STRING);
1330 // Position 3 (user exec) should be 's' (setuid + x).
1331 assert_eq!(r.as_bytes()[3], b's', "setuid+x → 's', got {:?}", r);
1332 }
1333
1334 /// c:111 — setuid uppercase 'S' when execute bit NOT set on user.
1335 #[test]
1336 fn statmodeprint_setuid_without_exec_shows_uppercase_S() {
1337 // 0o4644 = setuid + rw-r--r--
1338 let r = statmodeprint(0o104_644, STF_STRING);
1339 assert_eq!(r.as_bytes()[3], b'S', "setuid-no-x → 'S', got {:?}", r);
1340 }
1341
1342 /// c:115 — sticky bit 't' when other-exec set, 'T' when not.
1343 #[test]
1344 fn statmodeprint_sticky_bit_dispatch() {
1345 // 0o1777 = sticky + rwxrwxrwx
1346 let r1 = statmodeprint(0o101_777, STF_STRING);
1347 assert_eq!(r1.as_bytes()[9], b't', "sticky+other-x → 't'");
1348 // 0o1644 = sticky + rw-r--r--
1349 let r2 = statmodeprint(0o101_644, STF_STRING);
1350 assert_eq!(r2.as_bytes()[9], b'T', "sticky no other-x → 'T'");
1351 }
1352
1353 /// c:228 — `statulprint(N)` formats as decimal.
1354 #[test]
1355 fn statulprint_decimal_format() {
1356 assert_eq!(statulprint(0), "0");
1357 assert_eq!(statulprint(42), "42");
1358 assert_eq!(statulprint(u64::MAX), format!("{}", u64::MAX));
1359 }
1360
1361 // ═══════════════════════════════════════════════════════════════════
1362 // Additional C-parity tests for Src/Modules/stat.c
1363 // c:43 statmodeprint / c:112 statuidprint / c:156 statgidprint /
1364 // c:199 stattimeprint / c:228 statulprint / c:237 statlinkprint / lifecycle
1365 // ═══════════════════════════════════════════════════════════════════
1366
1367 /// c:228 — `statulprint` is pure (deterministic).
1368 #[test]
1369 fn statulprint_is_pure() {
1370 for v in [0u64, 1, 42, 1_000_000, u64::MAX] {
1371 let first = statulprint(v);
1372 for _ in 0..5 {
1373 assert_eq!(statulprint(v), first, "statulprint({}) must be pure", v);
1374 }
1375 }
1376 }
1377
1378 /// c:228 — `statulprint` output is non-empty ASCII digit string.
1379 #[test]
1380 fn statulprint_output_is_ascii_digits() {
1381 for v in [0u64, 1, 42, u32::MAX as u64, u64::MAX] {
1382 let s = statulprint(v);
1383 assert!(!s.is_empty(), "non-empty");
1384 assert!(
1385 s.chars().all(|c| c.is_ascii_digit()),
1386 "all chars must be digits: {:?}",
1387 s
1388 );
1389 }
1390 }
1391
1392 /// c:43 — `statmodeprint` is pure (deterministic for same mode+flags).
1393 #[test]
1394 fn statmodeprint_is_pure() {
1395 let mode = 0o100644u32;
1396 let first = statmodeprint(mode, 0);
1397 for _ in 0..5 {
1398 assert_eq!(statmodeprint(mode, 0), first, "statmodeprint must be pure");
1399 }
1400 }
1401
1402 /// c:43 — `statmodeprint(_, 0)` returns empty (no flags = no output).
1403 #[test]
1404 fn statmodeprint_no_flags_returns_empty() {
1405 assert_eq!(statmodeprint(0o100644, 0), "");
1406 }
1407
1408 /// c:199 — `stattimeprint` is pure.
1409 #[test]
1410 fn stattimeprint_is_pure() {
1411 let t = 1_700_000_000i64;
1412 let first = stattimeprint(t, 0, 0);
1413 for _ in 0..5 {
1414 assert_eq!(stattimeprint(t, 0, 0), first, "stattimeprint must be pure");
1415 }
1416 }
1417
1418 /// c:237 — `statlinkprint` for non-symlink returns empty.
1419 #[test]
1420 fn statlinkprint_non_symlink_returns_empty() {
1421 let r = statlinkprint(0o100644, "/tmp/__not_symlink__");
1422 // Regular file mode bit pattern → empty.
1423 assert!(
1424 r.is_empty() || r == "" || r.starts_with(" "),
1425 "non-symlink should return empty or marker, got {:?}",
1426 r
1427 );
1428 }
1429
1430 /// c:237 — `statlinkprint` empty fname doesn't panic.
1431 #[test]
1432 fn statlinkprint_empty_fname_no_panic() {
1433 let _ = statlinkprint(0o100644, "");
1434 }
1435
1436 /// c:112 — `statuidprint(uid, flags)` is pure (deterministic for same args).
1437 #[test]
1438 fn statuidprint_is_pure() {
1439 for uid in [0u32, 1, 1000, u32::MAX] {
1440 let first = statuidprint(uid, 0);
1441 for _ in 0..5 {
1442 assert_eq!(
1443 statuidprint(uid, 0),
1444 first,
1445 "statuidprint({}, 0) must be pure",
1446 uid
1447 );
1448 }
1449 }
1450 }
1451
1452 /// c:156 — `statgidprint(gid, flags)` is pure.
1453 #[test]
1454 fn statgidprint_is_pure() {
1455 for gid in [0u32, 1, 1000, u32::MAX] {
1456 let first = statgidprint(gid, 0);
1457 for _ in 0..5 {
1458 assert_eq!(
1459 statgidprint(gid, 0),
1460 first,
1461 "statgidprint({}, 0) must be pure",
1462 gid
1463 );
1464 }
1465 }
1466 }
1467
1468 // ═══════════════════════════════════════════════════════════════════
1469 // Additional C-parity tests for Src/Modules/stat.c
1470 // c:43 statmodeprint / c:112 statuidprint / c:156 statgidprint /
1471 // c:199 stattimeprint / c:228 statulprint / c:237 statlinkprint /
1472 // c:300 bin_stat + lifecycle type pins
1473 // ═══════════════════════════════════════════════════════════════════
1474
1475 /// c:43 — `statmodeprint` returns String (compile-time type pin).
1476 #[test]
1477 fn statmodeprint_returns_string_type() {
1478 let _: String = statmodeprint(0o100644, 0);
1479 }
1480
1481 /// c:112 — `statuidprint` returns String.
1482 #[test]
1483 fn statuidprint_returns_string_type() {
1484 let _: String = statuidprint(0, 0);
1485 }
1486
1487 /// c:156 — `statgidprint` returns String.
1488 #[test]
1489 fn statgidprint_returns_string_type() {
1490 let _: String = statgidprint(0, 0);
1491 }
1492
1493 /// c:199 — `stattimeprint` returns String.
1494 #[test]
1495 fn stattimeprint_returns_string_type() {
1496 let _: String = stattimeprint(0, 0, 0);
1497 }
1498
1499 /// c:228 — `statulprint` returns String.
1500 #[test]
1501 fn statulprint_returns_string_type() {
1502 let _: String = statulprint(0);
1503 }
1504
1505 /// c:237 — `statlinkprint` returns String.
1506 #[test]
1507 fn statlinkprint_returns_string_type() {
1508 let _: String = statlinkprint(0o100644, "");
1509 }
1510
1511 /// c:228 — `statulprint(0)` returns "0".
1512 #[test]
1513 fn statulprint_zero_returns_zero_digit() {
1514 assert_eq!(statulprint(0), "0", "0 → \"0\" canonical");
1515 }
1516
1517 /// c:228 — `statulprint(u64::MAX)` returns max decimal repr.
1518 #[test]
1519 fn statulprint_u64_max_returns_canonical_decimal() {
1520 let s = statulprint(u64::MAX);
1521 assert_eq!(s, u64::MAX.to_string(), "u64::MAX matches std decimal");
1522 }
1523
1524 /// c:300 — `bin_stat` returns i32 (compile-time type pin).
1525 #[test]
1526 fn bin_stat_returns_i32_type() {
1527 let _g = crate::test_util::global_state_lock();
1528 let ops = crate::ported::zsh_h::options {
1529 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1530 args: Vec::new(),
1531 argscount: 0,
1532 argsalloc: 0,
1533 };
1534 let _: i32 = bin_stat("stat", &[], &ops, 0);
1535 }
1536
1537 /// c:579 — `setup_` returns i32 (compile-time type pin).
1538 #[test]
1539 fn stat_setup_returns_i32_type() {
1540 let _g = crate::test_util::global_state_lock();
1541 let _: i32 = setup_(std::ptr::null());
1542 }
1543
1544 /// c:587 — features non-empty.
1545 #[test]
1546 fn stat_features_nonempty() {
1547 let _g = crate::test_util::global_state_lock();
1548 let mut feats = Vec::new();
1549 features_(std::ptr::null(), &mut feats);
1550 assert!(!feats.is_empty(), "stat must advertise ≥1 feature");
1551 }
1552
1553 /// c:587 — features use b:/p: prefix per zsh module spec.
1554 #[test]
1555 fn stat_features_use_canonical_prefix() {
1556 let _g = crate::test_util::global_state_lock();
1557 let mut feats = Vec::new();
1558 features_(std::ptr::null(), &mut feats);
1559 for f in &feats {
1560 assert!(
1561 f.starts_with("b:") || f.starts_with("p:"),
1562 "feature {:?} must use b:/p: prefix",
1563 f
1564 );
1565 }
1566 }
1567
1568 /// c:228 — `statulprint` is monotonically non-decreasing-length
1569 /// for monotonically increasing input (10 → 100 → 1000).
1570 #[test]
1571 fn statulprint_length_grows_with_magnitude() {
1572 let a = statulprint(10).len();
1573 let b = statulprint(100).len();
1574 let c = statulprint(1000).len();
1575 assert!(a <= b, "len(10) ≤ len(100), got {} vs {}", a, b);
1576 assert!(b <= c, "len(100) ≤ len(1000), got {} vs {}", b, c);
1577 }
1578
1579 /// c:579-618 — full lifecycle setup→features→enables→boot→cleanup→finish.
1580 #[test]
1581 fn stat_full_lifecycle_returns_zero_for_all() {
1582 let _g = crate::test_util::global_state_lock();
1583 let null = std::ptr::null();
1584 assert_eq!(setup_(null), 0);
1585 let mut feats = Vec::new();
1586 let _ = features_(null, &mut feats);
1587 let mut enables: Option<Vec<i32>> = None;
1588 let _ = enables_(null, &mut enables);
1589 assert_eq!(boot_(null), 0);
1590 assert_eq!(cleanup_(null), 0);
1591 assert_eq!(finish_(null), 0);
1592 }
1593
1594 // ═══════════════════════════════════════════════════════════════════
1595 // Additional C-parity tests for Src/Modules/stat.c
1596 // c:43 statmodeprint / c:112 statuidprint / c:156 statgidprint /
1597 // c:199 stattimeprint / c:228 statulprint / c:300 bin_stat
1598 // ═══════════════════════════════════════════════════════════════════
1599
1600 /// c:43 — `statmodeprint` returns String (compile-time pin, alt).
1601 #[test]
1602 fn statmodeprint_returns_string_pin_alt() {
1603 let _: String = statmodeprint(0o644, 0);
1604 }
1605
1606 /// c:43 — `statmodeprint` is deterministic for any mode.
1607 #[test]
1608 fn statmodeprint_deterministic_for_common_modes() {
1609 for mode in [0o644u32, 0o755, 0o600, 0o777, 0o000] {
1610 let a = statmodeprint(mode, 0);
1611 let b = statmodeprint(mode, 0);
1612 assert_eq!(a, b, "statmodeprint({:o}) must be pure", mode);
1613 }
1614 }
1615
1616 /// c:112 — `statuidprint` returns String (compile-time pin, alt).
1617 #[test]
1618 fn statuidprint_returns_string_pin_alt() {
1619 let _: String = statuidprint(0, 0);
1620 }
1621
1622 /// c:112 — `statuidprint(0, STF_RAW)` returns "0" (raw uid digit).
1623 /// Pin the c:115 STF_RAW arm; flags=0 produces empty by design.
1624 #[test]
1625 fn statuidprint_root_with_raw_flag_returns_zero_digit() {
1626 let s = statuidprint(0, STF_RAW);
1627 assert_eq!(s, "0", "uid 0 + STF_RAW must produce '0'");
1628 }
1629
1630 /// c:156 — `statgidprint` returns String (compile-time pin, alt).
1631 #[test]
1632 fn statgidprint_returns_string_pin_alt() {
1633 let _: String = statgidprint(0, 0);
1634 }
1635
1636 /// c:156 — `statgidprint(0, STF_RAW)` returns "0" (raw gid digit).
1637 /// Pin the c:159 STF_RAW arm; flags=0 produces empty by design.
1638 #[test]
1639 fn statgidprint_zero_with_raw_flag_returns_zero_digit() {
1640 let s = statgidprint(0, STF_RAW);
1641 assert_eq!(s, "0", "gid 0 + STF_RAW must produce '0'");
1642 }
1643
1644 /// c:199 — `stattimeprint` returns String (compile-time pin, alt).
1645 #[test]
1646 fn stattimeprint_returns_string_pin_alt() {
1647 let _: String = stattimeprint(0, 0, 0);
1648 }
1649
1650 /// c:199 — `stattimeprint(0, 0, STF_RAW)` returns "0" (raw epoch).
1651 /// Pin the c:202 STF_RAW arm; flags=0 produces empty by design.
1652 #[test]
1653 fn stattimeprint_epoch_zero_with_raw_flag_returns_zero() {
1654 let s = stattimeprint(0, 0, STF_RAW);
1655 assert_eq!(s, "0", "epoch 0 + STF_RAW must produce '0'");
1656 }
1657
1658 /// c:300 — `bin_stat` exit code is non-negative.
1659 #[test]
1660 fn bin_stat_exit_code_non_negative() {
1661 let _g = crate::test_util::global_state_lock();
1662 let ops = crate::ported::zsh_h::options {
1663 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1664 args: Vec::new(),
1665 argscount: 0,
1666 argsalloc: 0,
1667 };
1668 for argv in [
1669 vec![],
1670 vec!["/tmp".into()],
1671 vec!["/dev/null".into()],
1672 vec!["/__nonexistent_xyz__".into()],
1673 ] {
1674 let r = bin_stat("stat", &argv, &ops, 0);
1675 assert!(
1676 r >= 0,
1677 "exit code must be non-negative, got {} for {:?}",
1678 r,
1679 argv
1680 );
1681 }
1682 }
1683
1684 /// c:300 — `bin_stat` of nonexistent path MUST return nonzero
1685 /// (C uses `stat(2)` which fails ENOENT, then zwarnnam + return 1).
1686 /// In zshrs the port silently returns 0.
1687 #[test]
1688 fn bin_stat_nonexistent_path_returns_nonzero() {
1689 let _g = crate::test_util::global_state_lock();
1690 let ops = crate::ported::zsh_h::options {
1691 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1692 args: Vec::new(),
1693 argscount: 0,
1694 argsalloc: 0,
1695 };
1696 let r = bin_stat("stat", &["/__definitely_no_such_xyz__".into()], &ops, 0);
1697 assert_ne!(r, 0, "nonexistent path → nonzero");
1698 }
1699
1700 /// c:228 — `statulprint` deterministic for powers-of-2.
1701 #[test]
1702 fn statulprint_deterministic_for_powers_of_two() {
1703 for shift in 0..64 {
1704 let v = 1u64 << shift;
1705 let a = statulprint(v);
1706 let b = statulprint(v);
1707 assert_eq!(a, b, "statulprint({}) must be pure", v);
1708 }
1709 }
1710}