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 ch in arg.chars() {
400 match ch {
401 'g' | 'l' | 'L' | 'n' | 'N' | 'o' | 'r' | 's' | 't' | 'T' => {
402 ops[ch as u8 as usize] = true; // c:411
403 }
404 'A' => {
405 // c:412 — array name follows.
406 i += 1;
407 if i >= args.len() {
408 zwarnnam(nam, "missing parameter name");
409 return 1;
410 }
411 arrnam = Some(args[i].to_string());
412 flags |= STF_ARRAY;
413 break;
414 }
415 'H' => {
416 i += 1;
417 if i >= args.len() {
418 zwarnnam(nam, "missing parameter name");
419 return 1;
420 }
421 hashnam = Some(args[i].to_string());
422 flags |= STF_HASH;
423 break;
424 }
425 'f' => {
426 ops[b'f' as usize] = true;
427 i += 1;
428 if i >= args.len() {
429 zwarnnam(nam, "missing file descriptor");
430 return 1;
431 }
432 let (val, endptr) = zstrtol(args[i], 10);
433 if !endptr.is_empty() {
434 zwarnnam(nam, "bad file descriptor");
435 return 1;
436 }
437 fd = val as i32;
438 break;
439 }
440 'F' => {
441 // c:442-451 — `-F FMT`. If the same arg has chars
442 // after 'F' (e.g. `-F%Y`), use those; else consume
443 // the next argv entry. Force STF_STRING via -s so
444 // the format actually gets used (c:449-450).
445 let inline: &str = &arg[arg.find('F').unwrap() + 1..];
446 let fmt: &str = if !inline.is_empty() {
447 // c:443-444
448 inline
449 } else {
450 i += 1;
451 if i >= args.len() {
452 // c:446
453 zwarnnam(nam, "missing time format");
454 return 1;
455 }
456 args[i]
457 };
458 // c:444 — `timefmt = arg+1;` / c:445 — `timefmt = *++args;`
459 if let Ok(mut g) = TIMEFMT
460 .get_or_init(|| std::sync::Mutex::new(TIMEFMT_DEFAULT.to_string()))
461 .lock()
462 {
463 g.clear();
464 g.push_str(fmt);
465 }
466 ops[b's' as usize] = true; // c:450 — force STF_STRING.
467 break;
468 }
469 _ => {
470 zwarnnam(nam, &format!("bad option: -{}", ch));
471 return 1;
472 }
473 }
474 }
475 }
476 i += 1;
477 }
478 while i < args.len() {
479 argv.push(args[i]);
480 i += 1;
481 }
482 let _ = fd;
483
484 if (flags & STF_ARRAY) != 0 && (flags & STF_HASH) != 0 {
485 // c:459
486 zwarnnam(nam, "both array and hash requested");
487 return 1;
488 }
489
490 if ops[b'l' as usize] {
491 // c:467
492 // List elements + return.
493 if let Some(ref name) = arrnam {
494 // c:469
495 // c:485 — `setaparam(arrnam, array);` — REAL array of
496 // element names. Prior port colon-joined into a scalar
497 // via setsparam — `$names[1]` returned the first CHAR of
498 // "device:inode:mode:..." instead of "device", and
499 // ${(t)names} reported scalar where zsh reports array.
500 let names: Vec<String> = STATELTS.iter().map(|s| s.to_string()).collect();
501 setaparam(name, names); // c:485
502 } else {
503 let joined: Vec<&str> = STATELTS.iter().copied().collect();
504 println!("{}", joined.join(" ")); // c:478 putchar
505 }
506 return 0; // c:489
507 }
508
509 if argv.is_empty() && !ops[b'f' as usize] {
510 // c:491
511 zwarnnam(nam, "no files given");
512 return 1;
513 } else if !argv.is_empty() && ops[b'f' as usize] {
514 // c:493
515 zwarnnam(nam, "no files allowed with -f");
516 return 1;
517 }
518
519 // c:496+ — per-file stat + dispatch loop.
520 let use_lstat = ops[b'L' as usize];
521 let mut hash_out: Vec<(String, String)> = Vec::new();
522 let mut array_out: Vec<String> = Vec::new();
523 let show_type = ops[b't' as usize]; // c: -t
524 let mut local_flags = flags;
525 // c:510-513 — `if (OPT_ISSET(ops,'g')) { flags |= STF_GMT;
526 // ops->ind['s'] = 1; }`
527 // -g picks gmtime over localtime (consumed by stattimeprint at c:201)
528 // AND auto-enables string formatting since raw epoch seconds carry
529 // no timezone semantics.
530 if ops[b'g' as usize] {
531 local_flags |= STF_GMT; // c:511
532 ops[b's' as usize] = true; // c:512
533 }
534 // c:514-515 — `if (OPT_ISSET(ops,'s') || OPT_ISSET(ops,'r'))` STF_STRING.
535 if ops[b's' as usize] || ops[b'r' as usize] {
536 local_flags |= STF_STRING;
537 }
538 // c:516 — `if (OPT_ISSET(ops,'r') || !OPT_ISSET(ops,'s'))` STF_RAW.
539 if ops[b'r' as usize] || !ops[b's' as usize] {
540 // c:516
541 local_flags |= STF_RAW;
542 }
543 // c:518-519 — `-n` → STF_FILE (filename prefix).
544 if ops[b'n' as usize] {
545 local_flags |= STF_FILE;
546 } // c:519
547 // c:520-521 — `-o` → STF_OCTAL.
548 if ops[b'o' as usize] {
549 local_flags |= STF_OCTAL;
550 } // c:521
551 // c:522-523 — `-t` → STF_NAME explicit.
552 if ops[b't' as usize] {
553 local_flags |= STF_NAME;
554 } // c:523
555 // c:525-530 — default STF_NAME when neither -A nor -H and no
556 // single-element pick: every line gets a `name<sp>` prefix so
557 // `zstat /etc/hosts` looks like `mode 33188` etc.
558 if arrnam.is_none() && hashnam.is_none() {
559 if argv.len() > 1 {
560 local_flags |= STF_FILE;
561 } // c:527
562 if (local_flags & STF_PICK) == 0 {
563 // c:528
564 local_flags |= STF_NAME; // c:529
565 }
566 }
567 // c:532-535 — explicit -N / -f turn off STF_FILE; -T / -H turn off
568 // STF_NAME (suppress prefix for `read` / hash use).
569 if ops[b'N' as usize] || ops[b'f' as usize] {
570 // c:532
571 local_flags &= !STF_FILE;
572 }
573 if ops[b'T' as usize] || ops[b'H' as usize] {
574 // c:534
575 local_flags &= !STF_NAME;
576 }
577 let _ = show_type;
578
579 // c:Src/Modules/stat.c:bin_stat — C tracks per-file stat(2) failures
580 // and propagates a non-zero rc when any path errored (`ret = 1` at
581 // c:560-565 inside the per-path loop). The Rust port previously
582 // skipped the path on error via `continue` but unconditionally
583 // returned 0 at the end, masking ENOENT (and any other stat error)
584 // as success. Track the first failure rc and return it after the
585 // loop.
586 let mut rc: i32 = 0;
587 for path in &argv {
588 let meta = if use_lstat {
589 fs::symlink_metadata(path)
590 } else {
591 fs::metadata(path)
592 };
593 let meta = match meta {
594 Ok(m) => m,
595 Err(e) => {
596 // Bug #112 — strip Rust's " (os error N)" suffix by
597 // routing the errno through the canonical strerror
598 // port (Src/compat.c:194).
599 let errno = e.raw_os_error().unwrap_or(0);
600 let raw = crate::ported::compat::strerror(errno);
601 // c:Src/stat.c:564 — `zwarnnam(name, "%s: %e", ...)`.
602 // The `%e` directive (utils.c:360-367) uncapitalizes the
603 // first letter of strerror unless errno==EIO, so zsh
604 // prints "no such file or directory", not "No such…".
605 const EIO: i32 = 5;
606 let msg = if errno == EIO {
607 raw
608 } else {
609 let mut cs = raw.chars();
610 match cs.next() {
611 Some(f) => f.to_lowercase().collect::<String>() + cs.as_str(),
612 None => raw,
613 }
614 };
615 zwarnnam(nam, &format!("{}: {}", path, msg));
616 rc = 1;
617 // c:567-568 — `if (OPT_ISSET(ops,'f') || arrnam) break; else continue;`
618 // With -A (or -f) the whole result is a single aggregate, so the
619 // first failure abandons the loop; without one, the remaining
620 // files are still reported.
621 if arrnam.is_some() || ops[b'f' as usize] {
622 break;
623 }
624 continue;
625 }
626 };
627
628 // c:573-581 — `STF_FILE` prefix the filename per file.
629 if (local_flags & STF_FILE) != 0 && arrnam.is_none() && hashnam.is_none() {
630 if (local_flags & STF_PICK) != 0 {
631 print!("{} ", path); // c:580
632 } else {
633 println!("{}:", path);
634 }
635 }
636 if iwhich >= 0 {
637 // -E single element.
638 let val = statprint(&meta, path, iwhich, local_flags);
639 if let Some(ref aname) = arrnam {
640 array_out.push(val);
641 let _ = aname;
642 } else if let Some(ref hname) = hashnam {
643 hash_out.push((STATELTS[iwhich as usize].to_string(), val));
644 let _ = hname;
645 } else {
646 println!("{}", val); // c:591
647 }
648 } else {
649 // All elements.
650 for idx in 0..STATELTS.len() {
651 let val = statprint(&meta, path, idx as i32, local_flags);
652 if let Some(_) = &arrnam {
653 array_out.push(val);
654 } else if let Some(_) = &hashnam {
655 hash_out.push((STATELTS[idx].to_string(), val));
656 } else {
657 println!("{}", val); // c:603
658 }
659 }
660 }
661 }
662
663 // c:613-631 — `if (ret) freearray(array); else setaparam(...)`. The result is
664 // accumulated into a LOCAL buffer and only published to the parameter when
665 // every file stat'd cleanly. On any failure the buffer is dropped and the
666 // target parameter keeps whatever it already held — so a failing
667 // `zstat -H h <dangling-link>` must not wipe the assoc a previous successful
668 // zstat left in `h`.
669 if rc == 0 {
670 if let Some(name) = arrnam {
671 // c — `setaparam(name, zarrdup(array_out));` — real indexed array.
672 setaparam(&name, array_out); // c:params.c:3595
673 }
674 if let Some(name) = hashnam {
675 // c — `sethparam(name, ...);` — real assoc array. Flatten
676 // hash_out into alternating [k,v,k,v,...].
677 let mut flat: Vec<String> = Vec::with_capacity(hash_out.len() * 2);
678 for (k, v) in hash_out {
679 flat.push(k);
680 flat.push(v);
681 }
682 sethparam(&name, flat); // c:params.c:3602
683 }
684 }
685 rc
686}
687
688// `bintab` — port of `static struct builtin bintab[]` (stat.c:638).
689
690// `module_features` — port of `static struct features module_features`
691// from stat.c:642.
692
693/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/stat.c:651`.
694#[allow(unused_variables)]
695pub fn setup_(m: *const module) -> i32 {
696 // c:651
697 // C body c:653-654 — `return 0`. Faithful empty-body port.
698 0
699}
700
701/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/stat.c:658`.
702/// C body: `*features = featuresarray(m, &module_features); return 0;`
703pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
704 // c:658
705 *features = featuresarray(m, module_features());
706 0 // c:673
707}
708
709/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/stat.c:666`.
710/// C body: `return handlefeatures(m, &module_features, enables);`
711pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
712 // c:666
713 handlefeatures(m, module_features(), enables) // c:673
714}
715
716/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/stat.c:673`.
717#[allow(unused_variables)]
718pub fn boot_(m: *const module) -> i32 {
719 // c:673
720 // C body c:675-676 — `return 0`. Faithful empty-body port; the
721 // zstat builtin registers via the bn_list dispatch.
722 0
723}
724
725/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/stat.c:680`.
726/// C body: `return setfeatureenables(m, &module_features, NULL);`
727pub fn cleanup_(m: *const module) -> i32 {
728 // c:680
729 setfeatureenables(m, module_features(), None) // c:687
730}
731
732/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/stat.c:687`.
733#[allow(unused_variables)]
734pub fn finish_(m: *const module) -> i32 {
735 // c:687
736 // C body c:689-690 — `return 0`. Faithful empty-body port; the
737 // zstat builtin unregisters via cleanup_'s setfeatureenables.
738 0
739}
740/// `ST_DEV` constant.
741pub const ST_DEV: i32 = 0; // c:33
742/// `ST_INO` constant.
743pub const ST_INO: i32 = 1;
744/// `ST_MODE` constant.
745pub const ST_MODE: i32 = 2;
746/// `ST_NLINK` constant.
747pub const ST_NLINK: i32 = 3;
748/// `ST_UID` constant.
749pub const ST_UID: i32 = 4;
750/// `ST_GID` constant.
751pub const ST_GID: i32 = 5;
752/// `ST_RDEV` constant.
753pub const ST_RDEV: i32 = 6;
754/// `ST_SIZE` constant.
755pub const ST_SIZE: i32 = 7;
756/// `ST_ATIM` constant.
757pub const ST_ATIM: i32 = 8;
758/// `ST_MTIM` constant.
759pub const ST_MTIM: i32 = 9;
760/// `ST_CTIM` constant.
761pub const ST_CTIM: i32 = 10;
762/// `ST_BLKSIZE` constant.
763pub const ST_BLKSIZE: i32 = 11;
764/// `ST_BLOCKS` constant.
765pub const ST_BLOCKS: i32 = 12;
766/// `ST_READLINK` constant.
767pub const ST_READLINK: i32 = 13;
768/// `ST_COUNT` constant.
769pub const ST_COUNT: i32 = 14; // c:34
770
771// ============================================================
772// Port of `enum statflags` from `Src/Modules/stat.c:36-38`.
773// Bitmask flags passed to the print ported + bin_stat dispatch.
774// ============================================================
775/// `STF_NAME` constant.
776pub const STF_NAME: i32 = 1; // c:36
777/// `STF_FILE` constant.
778pub const STF_FILE: i32 = 2;
779/// `STF_STRING` constant.
780pub const STF_STRING: i32 = 4;
781/// `STF_RAW` constant.
782pub const STF_RAW: i32 = 8;
783
784// =====================================================================
785// static struct builtin bintab[] c:638
786// static struct features module_features c:642
787// =====================================================================
788/// `STF_PICK` constant.
789pub const STF_PICK: i32 = 16;
790/// `STF_ARRAY` constant.
791pub const STF_ARRAY: i32 = 32;
792/// `STF_GMT` constant.
793pub const STF_GMT: i32 = 64;
794/// `STF_HASH` constant.
795pub const STF_HASH: i32 = 128;
796/// `STF_OCTAL` constant.
797pub const STF_OCTAL: i32 = 256; // c:38
798
799/// Port of `statelts[]` from `Src/Modules/stat.c:39`. Names of the
800/// 14 stat-elements, indexed by the `ST_*` constants above.
801pub static STATELTS: &[&str] = &[
802 // c:39
803 "device", "inode", "mode", "nlink", "uid", "gid", "rdev", "size", "atime", "mtime", "ctime",
804 "blksize", "blocks", "link",
805];
806
807static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
808
809// Local stubs for the per-module entry points. C uses generic
810// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
811// 3275/3370/3445) but those take `Builtin` + `Features` pointer
812// fields the Rust port doesn't carry. The hardcoded descriptor
813// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
814// WARNING: NOT IN STAT.C — Rust-only module-framework shim.
815// C uses generic featuresarray/handlefeatures/setfeatureenables from
816// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
817// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
818fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
819 vec!["b:stat".to_string(), "b:zstat".to_string()]
820}
821
822// WARNING: NOT IN STAT.C — Rust-only module-framework shim.
823// C uses generic featuresarray/handlefeatures/setfeatureenables from
824// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
825// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
826fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
827 if enables.is_none() {
828 *enables = Some(vec![1; 2]);
829 }
830 0
831}
832
833// WARNING: NOT IN STAT.C — Rust-only module-framework shim.
834// C uses generic featuresarray/handlefeatures/setfeatureenables from
835// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
836// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
837fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
838 0
839}
840
841// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
842// ─── RUST-ONLY ACCESSORS ───
843//
844// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
845// RwLock<T>>` globals declared above. C zsh uses direct global
846// access; Rust needs these wrappers because `OnceLock::get_or_init`
847// is the only way to lazily construct shared state. These ported sit
848// here so the body of this file reads in C source order without
849// the accessor wrappers interleaved between real port ported.
850// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
851
852// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
853// ─── RUST-ONLY ACCESSORS ───
854//
855// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
856// RwLock<T>>` globals declared above. C zsh uses direct global
857// access; Rust needs these wrappers because `OnceLock::get_or_init`
858// is the only way to lazily construct shared state. These ported sit
859// here so the body of this file reads in C source order without
860// the accessor wrappers interleaved between real port ported.
861// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
862
863// WARNING: NOT IN STAT.C — Rust-only module-framework shim.
864// C uses generic featuresarray/handlefeatures/setfeatureenables from
865// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
866// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
867fn module_features() -> &'static Mutex<features> {
868 MODULE_FEATURES.get_or_init(|| {
869 Mutex::new(features {
870 bn_list: None,
871 bn_size: 2,
872 cd_list: None,
873 cd_size: 0,
874 mf_list: None,
875 mf_size: 0,
876 pd_list: None,
877 pd_size: 0,
878 n_abstract: 0,
879 })
880 })
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886 use std::fs::File;
887 use std::io::Write;
888
889 #[test]
890 fn statelts_count_matches_st_count() {
891 let _g = crate::test_util::global_state_lock();
892 assert_eq!(STATELTS.len() as i32, ST_COUNT);
893 }
894
895 #[test]
896 fn statmodeprint_octal_only() {
897 let _g = crate::test_util::global_state_lock();
898 let s = statmodeprint(0o100644, STF_RAW | STF_OCTAL);
899 assert!(s.starts_with('0'));
900 assert!(s.contains("644"));
901 }
902
903 #[test]
904 fn statmodeprint_string_only() {
905 let _g = crate::test_util::global_state_lock();
906 let s = statmodeprint(0o100644, STF_STRING);
907 assert_eq!(s.len(), 10);
908 assert_eq!(&s[..1], "-");
909 }
910
911 #[test]
912 fn statmodeprint_directory() {
913 let _g = crate::test_util::global_state_lock();
914 let s = statmodeprint(0o040755, STF_STRING);
915 assert_eq!(&s[..1], "d");
916 }
917
918 #[test]
919 fn statulprint_decimal() {
920 let _g = crate::test_util::global_state_lock();
921 assert_eq!(statulprint(12345), "12345");
922 }
923
924 #[test]
925 fn statprint_size_via_index() {
926 let _g = crate::test_util::global_state_lock();
927 let dir = tempfile::TempDir::new().unwrap();
928 let path = dir.path().join("x.txt");
929 File::create(&path).unwrap().write_all(b"hello").unwrap();
930 let meta = fs::metadata(&path).unwrap();
931 let s = statprint(&meta, path.to_str().unwrap(), ST_SIZE, 0);
932 assert_eq!(s, "5");
933 }
934
935 /// c:47 — `statmodeprint` for the BLOCK device file type. The
936 /// `pm[0]` lookup at c:84-103 must yield 'b'. Pin all file-type
937 /// chars so a regen that swaps `0o060_000` and `0o020_000`
938 /// silently renders block devices as char devices.
939 #[test]
940 fn statmodeprint_file_type_chars_match_ls_output() {
941 let _g = crate::test_util::global_state_lock();
942 // S_IFREG (regular file)
943 assert_eq!(&statmodeprint(0o100_644, STF_STRING)[..1], "-");
944 // S_IFDIR
945 assert_eq!(&statmodeprint(0o040_755, STF_STRING)[..1], "d");
946 // S_IFLNK (symlink)
947 assert_eq!(&statmodeprint(0o120_777, STF_STRING)[..1], "l");
948 // S_IFCHR (char device)
949 assert_eq!(&statmodeprint(0o020_644, STF_STRING)[..1], "c");
950 // S_IFBLK (block device)
951 assert_eq!(&statmodeprint(0o060_644, STF_STRING)[..1], "b");
952 // S_IFIFO (named pipe)
953 assert_eq!(&statmodeprint(0o010_644, STF_STRING)[..1], "p");
954 // S_IFSOCK
955 assert_eq!(&statmodeprint(0o140_644, STF_STRING)[..1], "s");
956 }
957
958 /// c:111-115 — setuid bit renders as 's' in the user-execute
959 /// slot when execute is set, 'S' when not. Pin both polarities
960 /// for setuid, setgid, and sticky so a regen flipping the
961 /// uppercase/lowercase dispatch gets caught.
962 #[test]
963 fn statmodeprint_setuid_setgid_sticky_render_correctly() {
964 let _g = crate::test_util::global_state_lock();
965 // 4755 = setuid + executable → 's' in user slot
966 let s = statmodeprint(0o104_755, STF_STRING);
967 assert_eq!(
968 s.chars().nth(3),
969 Some('s'),
970 "setuid+x must render as 's' in user-execute slot"
971 );
972
973 // 4644 = setuid + NOT executable → 'S' in user slot
974 let s = statmodeprint(0o104_644, STF_STRING);
975 assert_eq!(
976 s.chars().nth(3),
977 Some('S'),
978 "setuid without x must render as 'S' (uppercase)"
979 );
980
981 // 2755 = setgid + group-x → 's' in group-execute slot
982 let s = statmodeprint(0o102_755, STF_STRING);
983 assert_eq!(
984 s.chars().nth(6),
985 Some('s'),
986 "setgid+gx must render as 's' in group-execute slot"
987 );
988
989 // 2644 = setgid without group-x → 'S'
990 let s = statmodeprint(0o102_644, STF_STRING);
991 assert_eq!(
992 s.chars().nth(6),
993 Some('S'),
994 "setgid without gx must render as 'S'"
995 );
996
997 // 1755 = sticky + world-x → 't' in other-execute slot
998 let s = statmodeprint(0o101_755, STF_STRING);
999 assert_eq!(
1000 s.chars().nth(9),
1001 Some('t'),
1002 "sticky+ox must render as 't' in other-execute slot"
1003 );
1004
1005 // 1644 = sticky without world-x → 'T'
1006 let s = statmodeprint(0o101_644, STF_STRING);
1007 assert_eq!(
1008 s.chars().nth(9),
1009 Some('T'),
1010 "sticky without ox must render as 'T'"
1011 );
1012 }
1013
1014 /// c:47-93 — `STF_RAW | STF_STRING` produces "raw (string)" with
1015 /// the raw form OUTSIDE parens and the string form inside.
1016 /// Pin the format because user scripts grep for "^(0?[0-9]+) \("
1017 /// to split the two halves.
1018 #[test]
1019 fn statmodeprint_raw_and_string_renders_with_parens() {
1020 let _g = crate::test_util::global_state_lock();
1021 let s = statmodeprint(0o100_644, STF_RAW | STF_STRING);
1022 // Decimal raw form, space, paren, 10-char string, close paren
1023 assert!(s.contains(" ("), "missing ' (' separator: {}", s);
1024 assert!(s.ends_with(')'), "missing closing ')': {}", s);
1025 // The closing paren must come right after the 10-char ls form
1026 let open = s.find('(').unwrap();
1027 let close = s.rfind(')').unwrap();
1028 assert_eq!(
1029 close - open - 1,
1030 10,
1031 "expected 10-char ls-mode between parens, got: {:?}",
1032 &s[open + 1..close]
1033 );
1034 }
1035
1036 /// c:47-93 — `statmodeprint(0)` with STF_STRING renders all dashes
1037 /// after the file-type indicator. Edge case: zero permissions on
1038 /// a "no file-type" mode falls through to '?'.
1039 #[test]
1040 fn statmodeprint_zero_mode_renders_unknown_type_no_perms() {
1041 let _g = crate::test_util::global_state_lock();
1042 let s = statmodeprint(0, STF_STRING);
1043 assert_eq!(s.len(), 10);
1044 assert_eq!(&s[..1], "?", "mode with no S_IFMT bits → unknown");
1045 assert_eq!(&s[1..], "---------", "no permission bits → all dashes");
1046 }
1047
1048 /// c:132 — `statuidprint` raw form is just the decimal uid;
1049 /// pin the no-leading-zeros, no-prefix shape so a regen that
1050 /// renders octal silently breaks `${(t)f[uid]}`.
1051 #[test]
1052 fn statuidprint_raw_is_decimal() {
1053 let _g = crate::test_util::global_state_lock();
1054 let s = statuidprint(1000, STF_RAW);
1055 assert_eq!(s, "1000");
1056 }
1057
1058 /// c:132 — `statuidprint` for uid 0 must include "root" in the
1059 /// string form (every Unix has uid 0 = root). Pin the well-known
1060 /// case so a regen that breaks the getpwuid path doesn't silently
1061 /// fall back to numeric.
1062 #[test]
1063 fn statuidprint_uid_zero_resolves_to_root() {
1064 let _g = crate::test_util::global_state_lock();
1065 let s = statuidprint(0, STF_STRING);
1066 // Some hardened systems map uid 0 to a different name, but
1067 // it MUST resolve to non-numeric.
1068 assert!(
1069 !s.parse::<u32>().is_ok(),
1070 "uid 0 fell back to numeric form: {}",
1071 s
1072 );
1073 assert!(!s.is_empty());
1074 }
1075
1076 /// c:161 — `statgidprint` raw form is decimal.
1077 #[test]
1078 fn statgidprint_raw_is_decimal() {
1079 let _g = crate::test_util::global_state_lock();
1080 let s = statgidprint(100, STF_RAW);
1081 assert_eq!(s, "100");
1082 }
1083
1084 /// c:211 — `statulprint` for zero must render "0". A regression
1085 /// that prints "" or "0x0" silently breaks numeric script
1086 /// comparisons.
1087 #[test]
1088 fn statulprint_zero_renders_as_zero_digit() {
1089 let _g = crate::test_util::global_state_lock();
1090 assert_eq!(statulprint(0), "0");
1091 }
1092
1093 /// c:211 — `statulprint` for u64::MAX renders the full decimal
1094 /// digit string. Pin the no-overflow / no-truncation behavior.
1095 #[test]
1096 fn statulprint_u64_max_renders_full_digits() {
1097 let _g = crate::test_util::global_state_lock();
1098 let s = statulprint(u64::MAX);
1099 assert_eq!(s, "18446744073709551615");
1100 }
1101
1102 /// c:36-38 — STF_* flag values must each occupy a unique single
1103 /// bit, AND must not overlap with each other (so they can be
1104 /// OR'd together). Pin the bit-distinctness because the flags
1105 /// are AND-tested individually throughout statprint.
1106 #[test]
1107 fn stf_flag_values_are_distinct_single_bits() {
1108 let _g = crate::test_util::global_state_lock();
1109 for f in [
1110 STF_NAME, STF_FILE, STF_STRING, STF_RAW, STF_PICK, STF_ARRAY, STF_GMT, STF_HASH,
1111 STF_OCTAL,
1112 ] {
1113 assert!(f > 0, "STF_* flag {} must be positive", f);
1114 assert_eq!(
1115 (f as u32).count_ones(),
1116 1,
1117 "STF_* flag {} = 0b{:b} must be a single bit",
1118 f,
1119 f
1120 );
1121 }
1122 // Pairwise: no two flags share a bit
1123 let flags = [
1124 STF_NAME, STF_FILE, STF_STRING, STF_RAW, STF_PICK, STF_ARRAY, STF_GMT, STF_HASH,
1125 STF_OCTAL,
1126 ];
1127 for (i, &a) in flags.iter().enumerate() {
1128 for &b in &flags[i + 1..] {
1129 assert_eq!(a & b, 0, "STF flags {} and {} overlap", a, b);
1130 }
1131 }
1132 }
1133
1134 /// c:651-690 — module-lifecycle stubs all return 0 in C.
1135 #[test]
1136 fn module_lifecycle_shims_all_return_zero() {
1137 let _g = crate::test_util::global_state_lock();
1138 let m: *const module = std::ptr::null();
1139 assert_eq!(setup_(m), 0);
1140 assert_eq!(boot_(m), 0);
1141 assert_eq!(cleanup_(m), 0);
1142 assert_eq!(finish_(m), 0);
1143 }
1144
1145 // ─── zsh-corpus pins for statmodeprint ─────────────────────────
1146
1147 /// Regular file 0644 → "-rw-r--r--" string form.
1148 #[test]
1149 fn stat_corpus_modeprint_regular_644() {
1150 let mode = 0o100_644; // S_IFREG | 0644
1151 let r = statmodeprint(mode, STF_STRING);
1152 assert_eq!(r, "-rw-r--r--", "regular 0644 → -rw-r--r--, got {r:?}");
1153 }
1154
1155 /// Regular file 0755 → "-rwxr-xr-x".
1156 #[test]
1157 fn stat_corpus_modeprint_regular_755() {
1158 let mode = 0o100_755;
1159 let r = statmodeprint(mode, STF_STRING);
1160 assert_eq!(r, "-rwxr-xr-x");
1161 }
1162
1163 /// Directory 0755 → "drwxr-xr-x".
1164 #[test]
1165 fn stat_corpus_modeprint_directory() {
1166 let mode = 0o040_755; // S_IFDIR | 0755
1167 let r = statmodeprint(mode, STF_STRING);
1168 assert_eq!(r, "drwxr-xr-x");
1169 }
1170
1171 /// Symlink → 'l' file-type prefix.
1172 #[test]
1173 fn stat_corpus_modeprint_symlink_prefix() {
1174 let mode = 0o120_777; // S_IFLNK | 0777
1175 let r = statmodeprint(mode, STF_STRING);
1176 assert!(
1177 r.starts_with('l'),
1178 "symlink mode starts with 'l', got {r:?}"
1179 );
1180 }
1181
1182 /// FIFO → 'p' file-type prefix.
1183 #[test]
1184 fn stat_corpus_modeprint_fifo_prefix() {
1185 let mode = 0o010_644;
1186 let r = statmodeprint(mode, STF_STRING);
1187 assert!(r.starts_with('p'), "FIFO mode starts with 'p', got {r:?}");
1188 }
1189
1190 /// Socket → 's' file-type prefix.
1191 #[test]
1192 fn stat_corpus_modeprint_socket_prefix() {
1193 let mode = 0o140_644;
1194 let r = statmodeprint(mode, STF_STRING);
1195 assert!(r.starts_with('s'));
1196 }
1197
1198 /// `STF_RAW | STF_OCTAL` → numeric octal form.
1199 #[test]
1200 fn stat_corpus_modeprint_raw_octal() {
1201 let mode = 0o100_644;
1202 let r = statmodeprint(mode, STF_RAW | STF_OCTAL);
1203 assert!(
1204 r.starts_with('0'),
1205 "octal raw form starts with leading 0, got {r:?}"
1206 );
1207 assert!(
1208 r.contains("100644") || r.contains("644"),
1209 "octal repr contains 100644 or 644, got {r:?}"
1210 );
1211 }
1212
1213 /// `STF_RAW` alone → decimal numeric.
1214 #[test]
1215 fn stat_corpus_modeprint_raw_decimal() {
1216 let mode = 0o100_644;
1217 let r = statmodeprint(mode, STF_RAW);
1218 // Decimal form of 0o100644 = 33188.
1219 assert_eq!(r, "33188", "raw decimal form, got {r:?}");
1220 }
1221
1222 /// Empty flags → empty output.
1223 #[test]
1224 fn stat_corpus_modeprint_no_flags_empty() {
1225 let r = statmodeprint(0o100_644, 0);
1226 assert_eq!(r, "", "no flags → empty output");
1227 }
1228
1229 // ═══════════════════════════════════════════════════════════════════
1230 // Additional C-parity tests for Src/Modules/stat.c statmodeprint
1231 // file-type + rwx bit dispatch.
1232 // ═══════════════════════════════════════════════════════════════════
1233
1234 /// c:84-103 — regular file (S_IFREG=0o100000) → '-' first char.
1235 #[test]
1236 fn statmodeprint_regular_file_starts_with_dash() {
1237 let r = statmodeprint(0o100_644, STF_STRING);
1238 assert!(r.starts_with('-'), "regular file → '-', got {:?}", r);
1239 }
1240
1241 /// c:84-103 — directory (S_IFDIR=0o040000) → 'd' first char.
1242 #[test]
1243 fn statmodeprint_directory_starts_with_d() {
1244 let r = statmodeprint(0o040_755, STF_STRING);
1245 assert!(r.starts_with('d'), "directory → 'd', got {:?}", r);
1246 }
1247
1248 /// c:84-103 — symlink (S_IFLNK=0o120000) → 'l' first char.
1249 #[test]
1250 fn statmodeprint_symlink_starts_with_l() {
1251 let r = statmodeprint(0o120_777, STF_STRING);
1252 assert!(r.starts_with('l'), "symlink → 'l', got {:?}", r);
1253 }
1254
1255 /// c:84-103 — char device (S_IFCHR=0o020000) → 'c' first char.
1256 #[test]
1257 fn statmodeprint_char_device_starts_with_c() {
1258 let r = statmodeprint(0o020_644, STF_STRING);
1259 assert!(r.starts_with('c'), "char device → 'c', got {:?}", r);
1260 }
1261
1262 /// c:84-103 — block device (S_IFBLK=0o060000) → 'b' first char.
1263 #[test]
1264 fn statmodeprint_block_device_starts_with_b() {
1265 let r = statmodeprint(0o060_644, STF_STRING);
1266 assert!(r.starts_with('b'), "block device → 'b', got {:?}", r);
1267 }
1268
1269 /// c:84-103 — socket (S_IFSOCK=0o140000) → 's' first char.
1270 #[test]
1271 fn statmodeprint_socket_starts_with_s() {
1272 let r = statmodeprint(0o140_777, STF_STRING);
1273 assert!(r.starts_with('s'), "socket → 's', got {:?}", r);
1274 }
1275
1276 /// c:84-103 — FIFO (S_IFIFO=0o010000) → 'p' first char.
1277 #[test]
1278 fn statmodeprint_fifo_starts_with_p() {
1279 let r = statmodeprint(0o010_644, STF_STRING);
1280 assert!(r.starts_with('p'), "FIFO → 'p', got {:?}", r);
1281 }
1282
1283 /// c:84-103 — unknown ifmt → '?' fallback.
1284 #[test]
1285 fn statmodeprint_unknown_ifmt_returns_question_mark() {
1286 let r = statmodeprint(0o070_000, STF_STRING);
1287 assert!(r.starts_with('?'), "unknown ifmt → '?', got {:?}", r);
1288 }
1289
1290 /// c:111 — setuid sticky 's' when execute bit set on user.
1291 #[test]
1292 fn statmodeprint_setuid_with_exec_shows_lowercase_s() {
1293 // 0o4755 = setuid + rwxr-xr-x
1294 let r = statmodeprint(0o104_755, STF_STRING);
1295 // Position 3 (user exec) should be 's' (setuid + x).
1296 assert_eq!(r.as_bytes()[3], b's', "setuid+x → 's', got {:?}", r);
1297 }
1298
1299 /// c:111 — setuid uppercase 'S' when execute bit NOT set on user.
1300 #[test]
1301 fn statmodeprint_setuid_without_exec_shows_uppercase_S() {
1302 // 0o4644 = setuid + rw-r--r--
1303 let r = statmodeprint(0o104_644, STF_STRING);
1304 assert_eq!(r.as_bytes()[3], b'S', "setuid-no-x → 'S', got {:?}", r);
1305 }
1306
1307 /// c:115 — sticky bit 't' when other-exec set, 'T' when not.
1308 #[test]
1309 fn statmodeprint_sticky_bit_dispatch() {
1310 // 0o1777 = sticky + rwxrwxrwx
1311 let r1 = statmodeprint(0o101_777, STF_STRING);
1312 assert_eq!(r1.as_bytes()[9], b't', "sticky+other-x → 't'");
1313 // 0o1644 = sticky + rw-r--r--
1314 let r2 = statmodeprint(0o101_644, STF_STRING);
1315 assert_eq!(r2.as_bytes()[9], b'T', "sticky no other-x → 'T'");
1316 }
1317
1318 /// c:228 — `statulprint(N)` formats as decimal.
1319 #[test]
1320 fn statulprint_decimal_format() {
1321 assert_eq!(statulprint(0), "0");
1322 assert_eq!(statulprint(42), "42");
1323 assert_eq!(statulprint(u64::MAX), format!("{}", u64::MAX));
1324 }
1325
1326 // ═══════════════════════════════════════════════════════════════════
1327 // Additional C-parity tests for Src/Modules/stat.c
1328 // c:43 statmodeprint / c:112 statuidprint / c:156 statgidprint /
1329 // c:199 stattimeprint / c:228 statulprint / c:237 statlinkprint / lifecycle
1330 // ═══════════════════════════════════════════════════════════════════
1331
1332 /// c:228 — `statulprint` is pure (deterministic).
1333 #[test]
1334 fn statulprint_is_pure() {
1335 for v in [0u64, 1, 42, 1_000_000, u64::MAX] {
1336 let first = statulprint(v);
1337 for _ in 0..5 {
1338 assert_eq!(statulprint(v), first, "statulprint({}) must be pure", v);
1339 }
1340 }
1341 }
1342
1343 /// c:228 — `statulprint` output is non-empty ASCII digit string.
1344 #[test]
1345 fn statulprint_output_is_ascii_digits() {
1346 for v in [0u64, 1, 42, u32::MAX as u64, u64::MAX] {
1347 let s = statulprint(v);
1348 assert!(!s.is_empty(), "non-empty");
1349 assert!(
1350 s.chars().all(|c| c.is_ascii_digit()),
1351 "all chars must be digits: {:?}",
1352 s
1353 );
1354 }
1355 }
1356
1357 /// c:43 — `statmodeprint` is pure (deterministic for same mode+flags).
1358 #[test]
1359 fn statmodeprint_is_pure() {
1360 let mode = 0o100644u32;
1361 let first = statmodeprint(mode, 0);
1362 for _ in 0..5 {
1363 assert_eq!(statmodeprint(mode, 0), first, "statmodeprint must be pure");
1364 }
1365 }
1366
1367 /// c:43 — `statmodeprint(_, 0)` returns empty (no flags = no output).
1368 #[test]
1369 fn statmodeprint_no_flags_returns_empty() {
1370 assert_eq!(statmodeprint(0o100644, 0), "");
1371 }
1372
1373 /// c:199 — `stattimeprint` is pure.
1374 #[test]
1375 fn stattimeprint_is_pure() {
1376 let t = 1_700_000_000i64;
1377 let first = stattimeprint(t, 0, 0);
1378 for _ in 0..5 {
1379 assert_eq!(stattimeprint(t, 0, 0), first, "stattimeprint must be pure");
1380 }
1381 }
1382
1383 /// c:237 — `statlinkprint` for non-symlink returns empty.
1384 #[test]
1385 fn statlinkprint_non_symlink_returns_empty() {
1386 let r = statlinkprint(0o100644, "/tmp/__not_symlink__");
1387 // Regular file mode bit pattern → empty.
1388 assert!(
1389 r.is_empty() || r == "" || r.starts_with(" "),
1390 "non-symlink should return empty or marker, got {:?}",
1391 r
1392 );
1393 }
1394
1395 /// c:237 — `statlinkprint` empty fname doesn't panic.
1396 #[test]
1397 fn statlinkprint_empty_fname_no_panic() {
1398 let _ = statlinkprint(0o100644, "");
1399 }
1400
1401 /// c:112 — `statuidprint(uid, flags)` is pure (deterministic for same args).
1402 #[test]
1403 fn statuidprint_is_pure() {
1404 for uid in [0u32, 1, 1000, u32::MAX] {
1405 let first = statuidprint(uid, 0);
1406 for _ in 0..5 {
1407 assert_eq!(
1408 statuidprint(uid, 0),
1409 first,
1410 "statuidprint({}, 0) must be pure",
1411 uid
1412 );
1413 }
1414 }
1415 }
1416
1417 /// c:156 — `statgidprint(gid, flags)` is pure.
1418 #[test]
1419 fn statgidprint_is_pure() {
1420 for gid in [0u32, 1, 1000, u32::MAX] {
1421 let first = statgidprint(gid, 0);
1422 for _ in 0..5 {
1423 assert_eq!(
1424 statgidprint(gid, 0),
1425 first,
1426 "statgidprint({}, 0) must be pure",
1427 gid
1428 );
1429 }
1430 }
1431 }
1432
1433 // ═══════════════════════════════════════════════════════════════════
1434 // Additional C-parity tests for Src/Modules/stat.c
1435 // c:43 statmodeprint / c:112 statuidprint / c:156 statgidprint /
1436 // c:199 stattimeprint / c:228 statulprint / c:237 statlinkprint /
1437 // c:300 bin_stat + lifecycle type pins
1438 // ═══════════════════════════════════════════════════════════════════
1439
1440 /// c:43 — `statmodeprint` returns String (compile-time type pin).
1441 #[test]
1442 fn statmodeprint_returns_string_type() {
1443 let _: String = statmodeprint(0o100644, 0);
1444 }
1445
1446 /// c:112 — `statuidprint` returns String.
1447 #[test]
1448 fn statuidprint_returns_string_type() {
1449 let _: String = statuidprint(0, 0);
1450 }
1451
1452 /// c:156 — `statgidprint` returns String.
1453 #[test]
1454 fn statgidprint_returns_string_type() {
1455 let _: String = statgidprint(0, 0);
1456 }
1457
1458 /// c:199 — `stattimeprint` returns String.
1459 #[test]
1460 fn stattimeprint_returns_string_type() {
1461 let _: String = stattimeprint(0, 0, 0);
1462 }
1463
1464 /// c:228 — `statulprint` returns String.
1465 #[test]
1466 fn statulprint_returns_string_type() {
1467 let _: String = statulprint(0);
1468 }
1469
1470 /// c:237 — `statlinkprint` returns String.
1471 #[test]
1472 fn statlinkprint_returns_string_type() {
1473 let _: String = statlinkprint(0o100644, "");
1474 }
1475
1476 /// c:228 — `statulprint(0)` returns "0".
1477 #[test]
1478 fn statulprint_zero_returns_zero_digit() {
1479 assert_eq!(statulprint(0), "0", "0 → \"0\" canonical");
1480 }
1481
1482 /// c:228 — `statulprint(u64::MAX)` returns max decimal repr.
1483 #[test]
1484 fn statulprint_u64_max_returns_canonical_decimal() {
1485 let s = statulprint(u64::MAX);
1486 assert_eq!(s, u64::MAX.to_string(), "u64::MAX matches std decimal");
1487 }
1488
1489 /// c:300 — `bin_stat` returns i32 (compile-time type pin).
1490 #[test]
1491 fn bin_stat_returns_i32_type() {
1492 let _g = crate::test_util::global_state_lock();
1493 let ops = crate::ported::zsh_h::options {
1494 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1495 args: Vec::new(),
1496 argscount: 0,
1497 argsalloc: 0,
1498 };
1499 let _: i32 = bin_stat("stat", &[], &ops, 0);
1500 }
1501
1502 /// c:579 — `setup_` returns i32 (compile-time type pin).
1503 #[test]
1504 fn stat_setup_returns_i32_type() {
1505 let _g = crate::test_util::global_state_lock();
1506 let _: i32 = setup_(std::ptr::null());
1507 }
1508
1509 /// c:587 — features non-empty.
1510 #[test]
1511 fn stat_features_nonempty() {
1512 let _g = crate::test_util::global_state_lock();
1513 let mut feats = Vec::new();
1514 features_(std::ptr::null(), &mut feats);
1515 assert!(!feats.is_empty(), "stat must advertise ≥1 feature");
1516 }
1517
1518 /// c:587 — features use b:/p: prefix per zsh module spec.
1519 #[test]
1520 fn stat_features_use_canonical_prefix() {
1521 let _g = crate::test_util::global_state_lock();
1522 let mut feats = Vec::new();
1523 features_(std::ptr::null(), &mut feats);
1524 for f in &feats {
1525 assert!(
1526 f.starts_with("b:") || f.starts_with("p:"),
1527 "feature {:?} must use b:/p: prefix",
1528 f
1529 );
1530 }
1531 }
1532
1533 /// c:228 — `statulprint` is monotonically non-decreasing-length
1534 /// for monotonically increasing input (10 → 100 → 1000).
1535 #[test]
1536 fn statulprint_length_grows_with_magnitude() {
1537 let a = statulprint(10).len();
1538 let b = statulprint(100).len();
1539 let c = statulprint(1000).len();
1540 assert!(a <= b, "len(10) ≤ len(100), got {} vs {}", a, b);
1541 assert!(b <= c, "len(100) ≤ len(1000), got {} vs {}", b, c);
1542 }
1543
1544 /// c:579-618 — full lifecycle setup→features→enables→boot→cleanup→finish.
1545 #[test]
1546 fn stat_full_lifecycle_returns_zero_for_all() {
1547 let _g = crate::test_util::global_state_lock();
1548 let null = std::ptr::null();
1549 assert_eq!(setup_(null), 0);
1550 let mut feats = Vec::new();
1551 let _ = features_(null, &mut feats);
1552 let mut enables: Option<Vec<i32>> = None;
1553 let _ = enables_(null, &mut enables);
1554 assert_eq!(boot_(null), 0);
1555 assert_eq!(cleanup_(null), 0);
1556 assert_eq!(finish_(null), 0);
1557 }
1558
1559 // ═══════════════════════════════════════════════════════════════════
1560 // Additional C-parity tests for Src/Modules/stat.c
1561 // c:43 statmodeprint / c:112 statuidprint / c:156 statgidprint /
1562 // c:199 stattimeprint / c:228 statulprint / c:300 bin_stat
1563 // ═══════════════════════════════════════════════════════════════════
1564
1565 /// c:43 — `statmodeprint` returns String (compile-time pin, alt).
1566 #[test]
1567 fn statmodeprint_returns_string_pin_alt() {
1568 let _: String = statmodeprint(0o644, 0);
1569 }
1570
1571 /// c:43 — `statmodeprint` is deterministic for any mode.
1572 #[test]
1573 fn statmodeprint_deterministic_for_common_modes() {
1574 for mode in [0o644u32, 0o755, 0o600, 0o777, 0o000] {
1575 let a = statmodeprint(mode, 0);
1576 let b = statmodeprint(mode, 0);
1577 assert_eq!(a, b, "statmodeprint({:o}) must be pure", mode);
1578 }
1579 }
1580
1581 /// c:112 — `statuidprint` returns String (compile-time pin, alt).
1582 #[test]
1583 fn statuidprint_returns_string_pin_alt() {
1584 let _: String = statuidprint(0, 0);
1585 }
1586
1587 /// c:112 — `statuidprint(0, STF_RAW)` returns "0" (raw uid digit).
1588 /// Pin the c:115 STF_RAW arm; flags=0 produces empty by design.
1589 #[test]
1590 fn statuidprint_root_with_raw_flag_returns_zero_digit() {
1591 let s = statuidprint(0, STF_RAW);
1592 assert_eq!(s, "0", "uid 0 + STF_RAW must produce '0'");
1593 }
1594
1595 /// c:156 — `statgidprint` returns String (compile-time pin, alt).
1596 #[test]
1597 fn statgidprint_returns_string_pin_alt() {
1598 let _: String = statgidprint(0, 0);
1599 }
1600
1601 /// c:156 — `statgidprint(0, STF_RAW)` returns "0" (raw gid digit).
1602 /// Pin the c:159 STF_RAW arm; flags=0 produces empty by design.
1603 #[test]
1604 fn statgidprint_zero_with_raw_flag_returns_zero_digit() {
1605 let s = statgidprint(0, STF_RAW);
1606 assert_eq!(s, "0", "gid 0 + STF_RAW must produce '0'");
1607 }
1608
1609 /// c:199 — `stattimeprint` returns String (compile-time pin, alt).
1610 #[test]
1611 fn stattimeprint_returns_string_pin_alt() {
1612 let _: String = stattimeprint(0, 0, 0);
1613 }
1614
1615 /// c:199 — `stattimeprint(0, 0, STF_RAW)` returns "0" (raw epoch).
1616 /// Pin the c:202 STF_RAW arm; flags=0 produces empty by design.
1617 #[test]
1618 fn stattimeprint_epoch_zero_with_raw_flag_returns_zero() {
1619 let s = stattimeprint(0, 0, STF_RAW);
1620 assert_eq!(s, "0", "epoch 0 + STF_RAW must produce '0'");
1621 }
1622
1623 /// c:300 — `bin_stat` exit code is non-negative.
1624 #[test]
1625 fn bin_stat_exit_code_non_negative() {
1626 let _g = crate::test_util::global_state_lock();
1627 let ops = crate::ported::zsh_h::options {
1628 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1629 args: Vec::new(),
1630 argscount: 0,
1631 argsalloc: 0,
1632 };
1633 for argv in [
1634 vec![],
1635 vec!["/tmp".into()],
1636 vec!["/dev/null".into()],
1637 vec!["/__nonexistent_xyz__".into()],
1638 ] {
1639 let r = bin_stat("stat", &argv, &ops, 0);
1640 assert!(
1641 r >= 0,
1642 "exit code must be non-negative, got {} for {:?}",
1643 r,
1644 argv
1645 );
1646 }
1647 }
1648
1649 /// c:300 — `bin_stat` of nonexistent path MUST return nonzero
1650 /// (C uses `stat(2)` which fails ENOENT, then zwarnnam + return 1).
1651 /// In zshrs the port silently returns 0.
1652 #[test]
1653 fn bin_stat_nonexistent_path_returns_nonzero() {
1654 let _g = crate::test_util::global_state_lock();
1655 let ops = crate::ported::zsh_h::options {
1656 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1657 args: Vec::new(),
1658 argscount: 0,
1659 argsalloc: 0,
1660 };
1661 let r = bin_stat("stat", &["/__definitely_no_such_xyz__".into()], &ops, 0);
1662 assert_ne!(r, 0, "nonexistent path → nonzero");
1663 }
1664
1665 /// c:228 — `statulprint` deterministic for powers-of-2.
1666 #[test]
1667 fn statulprint_deterministic_for_powers_of_two() {
1668 for shift in 0..64 {
1669 let v = 1u64 << shift;
1670 let a = statulprint(v);
1671 let b = statulprint(v);
1672 assert_eq!(a, b, "statulprint({}) must be pure", v);
1673 }
1674 }
1675}