Skip to main content

zsh/ported/modules/
files.rs

1//! Direct port of `Src/Modules/files.c` — the `zsh/files` module.
2//!
3//! Provides built-in implementations of: chgrp, chmod, chown, ln,
4//! mkdir, mv, rm, rmdir, sync (plus the `zf_*` safe-named aliases).
5//! Every function below maps 1:1 to its C counterpart with a `// c:NNN`
6//! citation against the upstream source.
7
8#![allow(non_camel_case_types, non_snake_case)]
9
10use crate::ported::utils::zwarnnam;
11use crate::ported::zsh_h::{module, options, OPT_ARG, OPT_ISSET};
12use std::io::Read;
13use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
14use std::sync::{Mutex, OnceLock};
15
16/// Direct port of `recursivecmd(char *nam, int opt_noerr, int opt_recurse, int opt_safe, char **args, RecurseFunc dirpre_func, RecurseFunc dirpost_func, RecurseFunc leaf_func, void *magic)` from `Src/Modules/files.c:378`.
17/// C body (c:381-446): walk argv, dispatch each via recursivecmd_doone.
18/// The dirsav-based chdir-back stack (c:396-399, c:438-446) is omitted
19/// in the Rust port — std::fs operations take absolute paths so the
20/// chdir dance C uses to safely descend isn't needed.
21/// WARNING: param names don't match C — Rust=(opt_noerr, opt_recurse, opt_safe, args, dirpre_func, dirpost_func, leaf_func) vs C=(nam, opt_noerr, opt_recurse, opt_safe, args, dirpre_func, dirpost_func, leaf_func, magic)
22pub fn recursivecmd<P, R, L>(
23    // c:378
24    nam: &str,
25    opt_noerr: i32,
26    opt_recurse: i32,
27    opt_safe: i32,
28    args: &[String],
29    dirpre_func: P,
30    dirpost_func: R,
31    leaf_func: L,
32) -> i32
33// c:378
34where
35    P: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
36    R: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
37    L: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
38{
39    let _ = opt_noerr;
40    let reccmd = recursivecmd {
41        nam,
42        opt_noerr,
43        opt_recurse,
44        opt_safe,
45        dirpre_func,
46        dirpost_func,
47        leaf_func,
48    };
49    let mut err = 0i32;
50    for arg in args {
51        // c:401
52        if (err & 2) != 0 {
53            break;
54        }
55        // c:402-403 — `rp = ztrdup(*args); unmetafy(rp, &len);`
56        // Build rp = unmetafied path; the original metafied `arg` is
57        // used for user-facing diagnostics, `rp` for syscalls.
58        let rp = crate::ported::utils::unmeta(arg);
59        let first = if opt_safe != 0 { 0 } else { 1 }; // c:421/c:434
60        err |= recursivecmd_doone(&reccmd, arg, &rp, first); // c:431/c:433
61    }
62    if err != 0 {
63        1
64    } else {
65        0
66    } // c:445 !!err
67}
68
69// =====================================================================
70// recursivecmd family — `Src/Modules/files.c:365-526`.
71// =====================================================================
72
73/// Port of `struct recursivecmd` from `Src/Modules/files.c:365`.
74/// Holds the per-call recursion options + callback function pointers.
75/// Rust uses generic closures for the callbacks; the struct is the
76/// owned context object passed through the recursion.
77pub struct recursivecmd<'a, P, R, L>
78where
79    P: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
80    R: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
81    L: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
82{
83    pub nam: &'a str,     // c:366
84    pub opt_noerr: i32,   // c:367
85    pub opt_recurse: i32, // c:368
86    pub opt_safe: i32,    // c:378
87    pub dirpre_func: P,   // c:378
88    pub dirpost_func: R,  // c:378
89    pub leaf_func: L,     // c:378
90}
91
92// =====================================================================
93// ask() — `Src/Modules/files.c:41`.
94// =====================================================================
95
96/// Direct port of `ask()` from `Src/Modules/files.c:41`.
97/// C body (c:43-46): read one char from stdin; consume the rest of
98/// the line; return 1 for `y`/`Y`, 0 otherwise.
99pub fn ask() -> i32 {
100    // c:41
101    let mut bytes = std::io::stdin().lock().bytes();
102    let a = bytes.next().and_then(|r| r.ok()).unwrap_or(0); // c:43 getchar
103    for c in bytes.by_ref() {
104        // c:44-45
105        if matches!(c, Ok(b'\n') | Err(_)) {
106            break;
107        }
108    }
109    (a == b'y' || a == b'Y') as i32 // c:46
110}
111
112// =====================================================================
113// bin_sync — `Src/Modules/files.c:53`.
114// =====================================================================
115
116/// Direct port of `bin_sync(UNUSED(char *nam), UNUSED(char **args), UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/files.c:53`.
117/// C body (c:55-57): `sync(); return 0;`.
118// sync builtin                                                             // c:53
119/// WARNING: param names don't match C — Rust=(_nam, _args, _func) vs C=(nam, args, ops, func)
120pub fn bin_sync(
121    _nam: &str,
122    _args: &[String], // c:53
123    _ops: &options,
124    _func: i32,
125) -> i32 {
126    unsafe {
127        libc::sync();
128    } // c:55
129    0 // c:63
130}
131
132// =====================================================================
133// bin_mkdir + domkdir — `Src/Modules/files.c:63`, `:115`.
134// =====================================================================
135
136/// Direct port of `bin_mkdir(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/files.c:63`.
137/// C body (c:65-110): default mode = 0777 & ~umask; parse -m; for
138/// each arg, strip trailing slashes; with -p walk each `/` segment.
139// mkdir builtin                                                            // c:63
140/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
141pub fn bin_mkdir(
142    nam: &str,
143    args: &[String], // c:63
144    ops: &options,
145    _func: i32,
146) -> i32 {
147    // C's `BUILTIN("mkdir", 0, bin_mkdir, 1, -1, ...)` table entry
148    // (Src/Modules/files.c:810) declares `minargs=1`; the builtin
149    // dispatcher (Src/builtin.c:430) enforces it BEFORE calling
150    // bin_mkdir. zshrs may call bin_mkdir directly (test paths /
151    // fusevm bridge), so self-validate to keep the C-observable
152    // "not enough arguments → exit 1" contract intact.
153    if args.is_empty() {
154        zwarnnam(nam, "not enough arguments"); // c:builtin.c:434
155        return 1;
156    }
157    let oumask = unsafe { libc::umask(0) }; // c:65
158    let mut mode: u32 = 0o777 & !(oumask as u32); // c:66
159    let mut err = 0i32;
160    unsafe {
161        libc::umask(oumask);
162    } // c:69
163    if OPT_ISSET(ops, b'm') {
164        // c:70
165        let str_arg = OPT_ARG(ops, b'm').unwrap_or(""); // c:71
166        match i64::from_str_radix(str_arg, 8) {
167            // c:73 zstrtol base 8
168            Ok(m) => mode = m as u32,
169            Err(_) => {
170                zwarnnam(
171                    nam, // c:75
172                    &format!("invalid mode `{}'", str_arg),
173                );
174                return 1; // c:76
175            }
176        }
177    }
178    let p_flag = if OPT_ISSET(ops, b'p') { 1 } else { 0 }; // c:84
179    for arg in args {
180        // c:80
181        let trimmed: String = if arg.starts_with('/') {
182            // c:81-83
183            let body = arg.trim_end_matches('/');
184            if body.is_empty() {
185                "/".to_string()
186            } else {
187                body.to_string()
188            }
189        } else {
190            arg.trim_end_matches('/').to_string()
191        };
192        if p_flag != 0 {
193            // c:84
194            let bytes = trimmed.as_bytes();
195            let mut i = 0usize;
196            loop {
197                while i < bytes.len() && bytes[i] == b'/' {
198                    i += 1;
199                } // c:88-89
200                while i < bytes.len() && bytes[i] != b'/' {
201                    i += 1;
202                } // c:90-91
203                if i >= bytes.len() {
204                    // c:92
205                    err |= domkdir(nam, &trimmed, mode, 1); // c:93
206                    break;
207                }
208                let prefix = &trimmed[..i]; // c:97
209                let e = domkdir(nam, prefix, mode | 0o300, 1); // c:98
210                if e != 0 {
211                    // c:99
212                    err = 1; // c:100
213                    break; // c:101
214                }
215            }
216        } else {
217            err |= domkdir(nam, &trimmed, mode, 0); // c:115
218        }
219    }
220    err // c:115
221}
222
223/// Direct port of `domkdir(char *nam, char *path, mode_t mode, int p)` from `Src/Modules/files.c:115`.
224/// C body (c:120-141): retry up to 8 times if EEXIST + p && stat
225/// shows existing entry is itself a directory.
226pub fn domkdir(nam: &str, path: &str, mode: u32, p: i32) -> i32 {
227    // c:115
228    let mut n = 8; // c:120
229    let mut last_err: i32 = 0;
230    // c:121 — `char const *rpath = unmeta(path);` — strip Meta escapes
231    // BEFORE the mkdir(2)/stat(2) calls so the kernel sees the raw
232    // POSIX path, not zsh's metafied encoding. Diagnostic at c:142
233    // uses the ORIGINAL metafied `path` for the user-visible message.
234    let rpath = crate::ported::utils::unmeta(path);
235    while n > 0 {
236        // c:122
237        n -= 1;
238        let oumask = unsafe { libc::umask(0) }; // c:123
239        let mut builder = std::fs::DirBuilder::new();
240        builder.mode(mode);
241        // c:Src/Modules/files.c:domkdir — when `-p` is set, the C body
242        // walks each intermediate path component, creating each as
243        // needed (the loop at c:131 increments past ENOENT and retries).
244        // The Rust port previously called `DirBuilder::create` which
245        // creates ONLY the leaf and fails with ENOENT for missing
246        // parents. Use `recursive(true)` so `mkdir -p a/b/c` works with
247        // missing `a` and `b`. The non-`-p` path (`p == 0`) keeps the
248        // strict single-level create so `mkdir x/y` with no `x` still
249        // errors the same way C does.
250        if p != 0 {
251            builder.recursive(true);
252        }
253        let result = builder.create(&rpath); // c:124 mkdir(rpath, mode)
254        unsafe {
255            libc::umask(oumask);
256        } // c:125
257        match result {
258            Ok(()) => return 0, // c:127
259            Err(e) => last_err = e.raw_os_error().unwrap_or(0),
260        }
261        if p == 0 || last_err != libc::EEXIST {
262            break;
263        } // c:129
264        match std::fs::metadata(&rpath) {
265            // c:130 stat
266            Ok(meta) if meta.is_dir() => return 0, // c:138
267            Ok(_) => break,                        // c:139
268            Err(e) => {
269                last_err = e.raw_os_error().unwrap_or(0);
270                if last_err == libc::ENOENT {
271                    continue;
272                } // c:131
273                break; // c:135
274            }
275        }
276    }
277    zwarnnam(
278        nam, // c:142
279        &format!(
280            "cannot make directory `{}': {}",
281            path,
282            std::io::Error::from_raw_os_error(last_err)
283        ),
284    );
285    1 // c:150
286}
287
288// =====================================================================
289// bin_rmdir — `Src/Modules/files.c:150`.
290// =====================================================================
291
292/// Direct port of `bin_rmdir(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/files.c:150`.
293/// C body (c:154-164): for each arg, call rmdir(2); accumulate err.
294// rmdir builtin                                                            // c:150
295/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
296pub fn bin_rmdir(
297    nam: &str,
298    args: &[String], // c:150
299    _ops: &options,
300    _func: i32,
301) -> i32 {
302    // C `BUILTIN("rmdir", 0, bin_rmdir, 1, -1, ...)` (files.c:813).
303    // See bin_mkdir for why the minargs self-check sits here.
304    if args.is_empty() {
305        zwarnnam(nam, "not enough arguments"); // c:builtin.c:434
306        return 1;
307    }
308    let mut err = 0i32;
309    for arg in args {
310        // c:154
311        // c:155 — `char *rpath = unmeta(*args);`. The path must be the
312        // raw POSIX form (Meta-escapes decoded back to their literal
313        // bytes) before reaching rmdir(2). Prior port passed the
314        // metafied bytes straight through CString::new, so any arg
315        // containing a Meta-escaped byte (e.g. `foo\x83\x12` for the
316        // literal "foo2") tried to rmdir the metafied filename
317        // instead of the real one.
318        let rpath = crate::ported::utils::unmeta(arg);
319        let cpath = match std::ffi::CString::new(rpath.as_str()) {
320            Ok(c) => c,
321            Err(_) => {
322                // c:157-158 — `if (!rpath) zwarnnam(nam,"%s: %e",*args,ENAMETOOLONG)`.
323                // Rust unmeta doesn't NULL-return; the only failure
324                // path here is interior NUL after Meta-decode, which
325                // we surface with the same ENAMETOOLONG diagnostic
326                // shape since that's the only "path too long / bad"
327                // class C surfaces from this site.
328                zwarnnam(nam, &format!("{}: {}", arg, "name too long"));
329                err = 1;
330                continue;
331            }
332        };
333        if unsafe { libc::rmdir(cpath.as_ptr()) } != 0 {
334            // c:160
335            zwarnnam(
336                nam, // c:161
337                &format!(
338                    "cannot remove directory `{}': {}",
339                    arg,
340                    crate::ported::compat::last_errstr()
341                ),
342            );
343            err = 1; // c:162
344        }
345    }
346    err // c:165
347}
348
349// =====================================================================
350// BIN_* / MV_* constants — `Src/Modules/files.c:170-178`.
351// =====================================================================
352/// `BIN_LN` constant.
353pub const BIN_LN: i32 = 0; // c:170
354/// `BIN_MV` constant.
355pub const BIN_MV: i32 = 1; // c:171
356/// `MV_NODIRS` constant.
357pub const MV_NODIRS: i32 = 1 << 0; // c:173
358/// `MV_FORCE` constant.
359pub const MV_FORCE: i32 = 1 << 1; // c:174
360/// `MV_INTERACTIVE` constant.
361pub const MV_INTERACTIVE: i32 = 1 << 2; // c:175
362/// `MV_ASKNW` constant.
363pub const MV_ASKNW: i32 = 1 << 3; // c:176
364/// `MV_ATOMIC` constant.
365pub const MV_ATOMIC: i32 = 1 << 4; // c:177
366/// `MV_NOCHASETARGET` constant.
367pub const MV_NOCHASETARGET: i32 = 1 << 5; // c:178
368
369/// Direct port of `bin_ln(char *nam, char **args, Options ops, int func)` from `Src/Modules/files.c:200`.
370/// C body (c:209-296):
371///   - func == BIN_MV → movefn = rename, MV_ASKNW unless -f, MV_ATOMIC
372///   - else → MV_FORCE if -f; -h/-n adds MV_NOCHASETARGET; -s →
373///     symlink; otherwise link with MV_NODIRS unless -d
374///   - -i without -f → MV_INTERACTIVE
375///   - last-arg-is-dir handling: chase into the dir for each src
376/// WARNING: param names don't match C — Rust=(nam, args, func) vs C=(nam, args, ops, func)
377pub fn bin_ln(
378    nam: &str,
379    args: &[String], // c:200
380    ops: &options,
381    func: i32,
382) -> i32 {
383    let movefn: MoveFunc;
384    let mut flags: i32;
385    let mut err = 0i32;
386    if func == BIN_MV {
387        // c:209
388        movefn = mv_rename; // c:210
389        flags = if OPT_ISSET(ops, b'f') { 0 } else { MV_ASKNW }; // c:211
390        flags |= MV_ATOMIC; // c:212
391    } else {
392        flags = if OPT_ISSET(ops, b'f') { MV_FORCE } else { 0 }; // c:215
393        if OPT_ISSET(ops, b'h') || OPT_ISSET(ops, b'n') {
394            // c:217
395            flags |= MV_NOCHASETARGET;
396        }
397        if OPT_ISSET(ops, b's') {
398            // c:219
399            movefn = mv_symlink; // c:220
400        } else {
401            movefn = mv_link; // c:226
402            if !OPT_ISSET(ops, b'd') {
403                // c:227
404                flags |= MV_NODIRS;
405            }
406        }
407    }
408    if OPT_ISSET(ops, b'i') && !OPT_ISSET(ops, b'f') {
409        // c:230
410        flags |= MV_INTERACTIVE;
411    }
412    if args.is_empty() {
413        zwarnnam(nam, "missing file argument");
414        return 1;
415    }
416    let last_idx = args.len() - 1; // c:232 a = args; for(; a[1]; a++)
417    let mut have_dir = false;
418    if last_idx > 0 {
419        // c:233
420        let target = &args[last_idx];
421        // c:232 — `rp = unmeta(*a);` — strip Meta escapes before stat/
422        // lstat/unlink so the kernel sees the raw POSIX target path.
423        // Diagnostics (zwarnnam) keep the original metafied form.
424        let rp = crate::ported::utils::unmeta(target);
425        if let Ok(meta) = std::fs::metadata(&rp) {
426            // c:233 stat(rp, &st)
427            if meta.is_dir() {
428                // c:233 S_ISDIR
429                have_dir = true;
430                if (flags & MV_NOCHASETARGET) != 0 {
431                    // c:235
432                    if let Ok(lmeta) = std::fs::symlink_metadata(&rp) {
433                        // c:236 lstat(rp, &st)
434                        if lmeta.file_type().is_symlink() {
435                            // c:236 S_ISLNK
436                            // c:244-256 — multi-source symlink-to-dir
437                            // resolution: error unless -f and exactly
438                            // one source.
439                            if last_idx > 1 {
440                                // c:244
441                                zwarnnam(
442                                    nam, // c:246
443                                    &format!("{}: not a directory", target),
444                                );
445                                return 1; // c:247
446                            }
447                            if (flags & MV_FORCE) != 0 {
448                                // c:249
449                                let _ = std::fs::remove_file(&rp); // c:250 unlink(rp)
450                                have_dir = false; // c:251
451                            } else {
452                                zwarnnam(
453                                    nam, // c:254
454                                    &format!("{}: file exists", target),
455                                );
456                                return 1; // c:255
457                            }
458                        }
459                    }
460                }
461            }
462        }
463    }
464    if have_dir {
465        // c:havedir branch
466        // c:276-294 — target is dir, chase into it for each source.
467        let dir = args[last_idx].trim_end_matches('/').to_string();
468        for src in &args[..last_idx] {
469            // c:281
470            let basename = match src.rsplit_once('/') {
471                // c:283-285 strrchr
472                Some((_, n)) => n,
473                None => src.as_str(),
474            };
475            let dest = format!("{}/{}", dir, basename); // c:289 strcat
476            err |= domove(nam, movefn, src, &dest, flags); // c:290
477        }
478        return err; // c:295
479    }
480    if last_idx > 1 {
481        // c:265
482        zwarnnam(nam, "last of many arguments must be a directory"); // c:266
483        return 1; // c:267
484    }
485    let (src, dest) = if args.len() < 2 {
486        // c:269 !args[1]
487        let basename = match args[0].rsplit_once('/') {
488            // c:270 strrchr
489            Some((_, n)) => n,
490            None => args[0].as_str(),
491        };
492        (args[0].clone(), basename.to_string()) // c:272 args[1] = ptr+1
493    } else {
494        (args[0].clone(), args[1].clone())
495    };
496    domove(nam, movefn, &src, &dest, flags) // c:275
497}
498
499/// Direct port of `domove(char *nam, MoveFunc movefn, char *p, char *q, int flags)` from `Src/Modules/files.c:298`.
500/// C body (c:300-360): if MV_NODIRS, refuse src that is dir; if dest
501/// exists, force/interactive/asknw checks; unlink dest if not atomic;
502/// then call movefn(src, dest) and report errno on failure.
503pub fn domove(nam: &str, movefn: MoveFunc, p: &str, q: &str, flags: i32) -> i32 {
504    // c:298
505    // c:303-304 — `pbuf = ztrdup(unmeta(p)); qbuf = unmeta(q);`
506    // All syscalls below use pbuf/qbuf (unmetafied raw POSIX bytes);
507    // diagnostics use the original metafied `p` / `q`. Prior port
508    // routed every lstat / access / movefn / remove_file through the
509    // metafied form, so paths with high bytes operated on phantom
510    // filenames containing the literal `Meta + (byte^32)` two-byte
511    // encoding.
512    let pbuf = crate::ported::utils::unmeta(p);
513    let qbuf = crate::ported::utils::unmeta(q);
514    if (flags & MV_NODIRS) != 0 {
515        // c:305
516        match std::fs::symlink_metadata(&pbuf) {
517            // c:307 lstat(pbuf, &st)
518            Ok(meta) if meta.is_dir() => {
519                // c:307 S_ISDIR
520                zwarnnam(nam, &format!("{}: is a directory", p)); // c:308
521                return 1; // c:309
522            }
523            Err(e) => {
524                zwarnnam(nam, &format!("{}: {}", p, e)); // c:308
525                return 1;
526            }
527            _ => {}
528        }
529    }
530    if let Ok(qmeta) = std::fs::symlink_metadata(&qbuf) {
531        // c:315 lstat
532        let mut doit = (flags & MV_FORCE) != 0; // c:316
533        if qmeta.is_dir() {
534            // c:317 S_ISDIR
535            zwarnnam(nam, &format!("{}: cannot overwrite directory", q)); // c:319
536            return 1; // c:320
537        } else if (flags & MV_INTERACTIVE) != 0 {
538            // c:322
539            eprint!("{}: replace `{}'? ", nam, q); // c:324-326
540            if ask() == 0 {
541                // c:328
542                return 0; // c:329
543            }
544            doit = true; // c:331
545        } else if (flags & MV_ASKNW) != 0                                    // c:333
546                && !qmeta.file_type().is_symlink()                           // c:334 !S_ISLNK
547                && unsafe {                                                  // c:335 access(qbuf, W_OK)
548                    let cq = std::ffi::CString::new(qbuf.as_str()).ok();
549                    cq.map(|c| libc::access(c.as_ptr(), libc::W_OK))
550                        .unwrap_or(-1) != 0
551                }
552        {
553            let mode = qmeta.permissions().mode() & 0o7777;
554            eprint!("{}: replace `{}', overriding mode {:04o}? ", nam, q, mode); // c:337-340
555            if ask() == 0 {
556                // c:342
557                return 0; // c:343
558            }
559            doit = true; // c:345
560        }
561        if doit && (flags & MV_ATOMIC) == 0 {
562            // c:347
563            let _ = std::fs::remove_file(&qbuf); // c:348 unlink(qbuf)
564        }
565    }
566    let r = {
567        // c:350 movefn(pbuf, qbuf)
568        let cp = std::ffi::CString::new(pbuf.as_str()).unwrap_or_default();
569        let cq = std::ffi::CString::new(qbuf.as_str()).unwrap_or_default();
570        movefn(&cp, &cq)
571    };
572    if r != 0 {
573        // c:350
574        let osek = std::io::Error::last_os_error();
575        let errfile = if osek.raw_os_error() == Some(libc::ENOENT)           // c:352-355
576            && std::fs::symlink_metadata(&pbuf).is_ok()
577        {
578            q
579        } else {
580            p
581        };
582        // Bug #112 — use C strerror via last_errstr to avoid the
583        // " (os error N)" Rust suffix that Display appends.
584        zwarnnam(
585            nam,
586            &format!("`{}': {}", errfile, crate::ported::compat::last_errstr()),
587        ); // c:357
588        return 1; // c:358
589    }
590    0 // c:362
591}
592
593/// Direct port of `recursivecmd_doone(struct recursivecmd const *reccmd, char *arg, char *rp, struct dirsav *ds, int first)` from `Src/Modules/files.c:450`.
594/// C body (c:455-462): lstat the path; if recurse + S_ISDIR → dive
595/// via recursivecmd_dorec; else call leaf_func.
596/// WARNING: param names don't match C — Rust=(arg, rp, first) vs C=(reccmd, arg, rp, ds, first)
597pub fn recursivecmd_doone<P, R, L>(
598    // c:450
599    reccmd: &recursivecmd<P, R, L>,
600    arg: &str,
601    rp: &str,
602    first: i32,
603) -> i32
604// c:450
605where
606    P: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
607    R: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
608    L: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
609{
610    let st = std::fs::symlink_metadata(rp); // c:455 lstat
611    if reccmd.opt_recurse != 0 {
612        // c:457
613        if let Ok(ref meta) = st {
614            if meta.is_dir() {
615                // c:458 S_ISDIR
616                return recursivecmd_dorec(reccmd, arg, rp, meta, first); // c:465
617            }
618        }
619    }
620    let sp = st.as_ref().ok(); // c:465 sp
621    (reccmd.leaf_func)(arg, rp, sp) // c:465
622}
623
624/// Direct port of `recursivecmd_dorec(struct recursivecmd const *reccmd, char *arg, char *rp, struct stat const *sp, struct dirsav *ds, int first)` from `Src/Modules/files.c:465`.
625/// C body (c:475-525): dirpre callback, opendir + readdir each entry,
626/// recurse via recursivecmd_doone, then dirpost callback.
627/// WARNING: param names don't match C — Rust=(arg, rp, sp, _first) vs C=(reccmd, arg, rp, sp, ds, first)
628pub fn recursivecmd_dorec<P, R, L>(
629    // c:465
630    reccmd: &recursivecmd<P, R, L>,
631    arg: &str,
632    rp: &str,
633    sp: &std::fs::Metadata,
634    _first: i32,
635) -> i32
636// c:465
637where
638    P: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
639    R: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
640    L: Fn(&str, &str, Option<&std::fs::Metadata>) -> i32,
641{
642    let err1 = (reccmd.dirpre_func)(arg, rp, Some(sp)); // c:475 dirpre_func
643    if (err1 & 2) != 0 {
644        return 2;
645    } // c:476
646    let dir = match std::fs::read_dir(rp) {
647        // c:489 opendir
648        Ok(d) => d,
649        Err(e) => {
650            if reccmd.opt_noerr == 0 {
651                // c:491
652                zwarnnam(reccmd.nam, &format!("{}: {}", arg, e)); // c:492
653            }
654            return err1 | 1; // c:493
655        }
656    };
657    let mut err = err1;
658    for entry in dir.flatten() {
659        // c:497 readdir
660        let name = entry.file_name();
661        let name_str = name.to_string_lossy();
662        if name_str == "." || name_str == ".." {
663            continue;
664        } // c:498
665        let narg = format!("{}/{}", arg.trim_end_matches('/'), name_str); // c:507-510
666        let nrp = entry.path();
667        let nrp_str = nrp.to_string_lossy();
668        if (err & 2) != 0 {
669            break;
670        } // c:503
671        err |= recursivecmd_doone(reccmd, &narg, &nrp_str, 0); // c:530
672    }
673    if (err & 2) != 0 {
674        return 2;
675    } // c:530
676    err | (reccmd.dirpost_func)(arg, rp, Some(sp)) // c:530
677}
678
679/// Direct port of `recurse_donothing(UNUSED(char *arg), UNUSED(char *rp), UNUSED(struct stat const *sp), UNUSED(void *magic))` from `Src/Modules/files.c:530`.
680/// C body: `return 0;`.
681/// WARNING: param names don't match C — Rust=(_arg, _rp) vs C=(arg, rp, sp, magic)
682pub fn recurse_donothing(
683    _arg: &str,
684    _rp: &str, // c:530
685    _sp: Option<&std::fs::Metadata>,
686) -> i32 {
687    0 // c:533
688}
689
690// =====================================================================
691// bin_rm — `Src/Modules/files.c:537-630`.
692// =====================================================================
693
694/// Port of `struct rmmagic` from `Src/Modules/files.c:537`.
695pub struct rmmagic<'a> {
696    pub nam: &'a str,       // c:546
697    pub opt_force: i32,     // c:546
698    pub opt_interact: i32,  // c:546
699    pub opt_unlinkdir: i32, // c:546
700}
701
702/// Direct port of `rm_leaf(char *arg, char *rp, struct stat const *sp, void *magic)` from `Src/Modules/files.c:546`.
703/// C body (c:551-589):
704///   - if !opt_unlinkdir || !opt_force: lstat (if not provided);
705///     refuse directories; ask if interactive; warn if read-only
706///   - unlink(rp); error path returns 1 unless -f
707/// WARNING: param names don't match C — Rust=(arg, rp, sp) vs C=(arg, rp, sp, magic)
708pub fn rm_leaf(
709    arg: &str,
710    rp: &str,
711    sp: Option<&std::fs::Metadata>, // c:546
712    rmm: &rmmagic,
713) -> i32 {
714    if rmm.opt_unlinkdir == 0 || rmm.opt_force == 0 {
715        // c:551
716        let owned;
717        let sp_use = if let Some(s) = sp {
718            Some(s)
719        } else {
720            // c:552-554
721            owned = std::fs::symlink_metadata(rp).ok(); // c:553 lstat
722            owned.as_ref()
723        };
724        if let Some(meta) = sp_use {
725            // c:556
726            if rmm.opt_unlinkdir == 0 && meta.is_dir() {
727                // c:557 S_ISDIR
728                if rmm.opt_force != 0 {
729                    return 0;
730                } // c:558-559
731                zwarnnam(
732                    rmm.nam, // c:560
733                    &format!("{}: is a directory", arg),
734                );
735                return 1; // c:561
736            }
737            if rmm.opt_interact != 0 {
738                // c:563
739                eprint!("{}: remove `{}'? ", rmm.nam, arg); // c:564-568
740                if ask() == 0 {
741                    return 0;
742                } // c:570
743            } else if rmm.opt_force == 0                                     // c:571
744                    && !meta.file_type().is_symlink()                        // c:572 !S_ISLNK
745                    && unsafe {                                              // c:573 access W_OK
746                        let crp = std::ffi::CString::new(rp).ok();
747                        crp.map(|c| libc::access(c.as_ptr(), libc::W_OK))
748                            .unwrap_or(-1) != 0
749                    }
750            {
751                let mode = meta.permissions().mode() & 0o7777;
752                eprint!(
753                    "{}: remove `{}', overriding mode {:04o}? ",
754                    rmm.nam, arg, mode
755                ); // c:574-579
756                if ask() == 0 {
757                    return 0;
758                } // c:581
759            }
760        }
761    }
762    let crp = match std::ffi::CString::new(rp) {
763        Ok(c) => c,
764        Err(_) => return 1,
765    };
766    if unsafe { libc::unlink(crp.as_ptr()) } != 0 && rmm.opt_force == 0 {
767        // c:594
768        zwarnnam(
769            rmm.nam, // c:594
770            &format!("{}: {}", arg, crate::ported::compat::last_errstr()),
771        );
772        return 1; // c:594
773    }
774    0 // c:594
775}
776
777/// Direct port of `rm_dirpost(char *arg, char *rp, UNUSED(struct stat const *sp), void *magic)` from `Src/Modules/files.c:594`.
778/// C body (c:599-613): rmdir(rp); error path returns 1 unless -f.
779/// WARNING: param names don't match C — Rust=(arg, rp, _sp) vs C=(arg, rp, sp, magic)
780pub fn rm_dirpost(
781    arg: &str,
782    rp: &str,
783    _sp: Option<&std::fs::Metadata>, // c:594
784    rmm: &rmmagic,
785) -> i32 {
786    let crp = match std::ffi::CString::new(rp) {
787        Ok(c) => c,
788        Err(_) => return 1,
789    };
790    if unsafe { libc::rmdir(crp.as_ptr()) } != 0 && rmm.opt_force == 0 {
791        // c:608
792        zwarnnam(
793            rmm.nam, // c:616
794            &format!("{}: {}", arg, crate::ported::compat::last_errstr()),
795        );
796        return 1; // c:616
797    }
798    0 // c:616
799}
800
801/// Direct port of `bin_rm(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/files.c:616`.
802/// C body (c:621-633): build rmmagic; recursivecmd with rm_dirpost
803/// + rm_leaf; -f swallows the err code.
804// rm builtin                                                               // c:535
805/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
806pub fn bin_rm(
807    nam: &str,
808    args: &[String], // c:616
809    ops: &options,
810    _func: i32,
811) -> i32 {
812    // C `BUILTIN("rm", 0, bin_rm, 1, -1, ...)` (files.c:814).
813    // See bin_mkdir for why the minargs self-check sits here.
814    if args.is_empty() {
815        zwarnnam(nam, "not enough arguments"); // c:builtin.c:434
816        return 1;
817    }
818    let rmm = rmmagic {
819        nam,                                                 // c:621
820        opt_force: if OPT_ISSET(ops, b'f') { 1 } else { 0 }, // c:622
821        opt_interact: if OPT_ISSET(ops, b'i') && !OPT_ISSET(ops, b'f')
822        // c:623
823        {
824            1
825        } else {
826            0
827        },
828        opt_unlinkdir: if OPT_ISSET(ops, b'd') { 1 } else { 0 }, // c:624
829    };
830    let recurse = if !OPT_ISSET(ops, b'd')                                   // c:626
831        && (OPT_ISSET(ops, b'R') || OPT_ISSET(ops, b'r'))
832    {
833        1
834    } else {
835        0
836    };
837    let safe = if OPT_ISSET(ops, b's') { 1 } else { 0 }; // c:627
838    let err = recursivecmd(
839        nam,
840        rmm.opt_force,
841        recurse,
842        safe,
843        args,                                // c:625
844        |_a, _r, _s| 0,                      // dirpre = recurse_donothing
845        |a, r, s| rm_dirpost(a, r, s, &rmm), // dirpost
846        |a, r, s| rm_leaf(a, r, s, &rmm),
847    ); // leaf
848    if rmm.opt_force != 0 {
849        0
850    } else {
851        err
852    } // c:631
853}
854
855// =====================================================================
856// bin_chmod — `Src/Modules/files.c:642`.
857// =====================================================================
858
859/// Port of `struct chmodmagic` from `Src/Modules/files.c:642`.
860pub struct chmodmagic<'a> {
861    pub nam: &'a str, // c:642
862    pub mode: u32,    // c:642
863}
864
865/// Direct port of `chmod_dochmod(char *arg, char *rp, UNUSED(struct stat const *sp), void *magic)` from `Src/Modules/files.c:642`.
866/// C body (c:646-652): `chmod(rp, mode)`; warn + return 1 on failure.
867/// WARNING: param names don't match C — Rust=(arg, rp, _sp) vs C=(arg, rp, sp, magic)
868pub fn chmod_dochmod(
869    arg: &str,
870    rp: &str,
871    _sp: Option<&std::fs::Metadata>, // c:642
872    chm: &chmodmagic,
873) -> i32 {
874    let crp = match std::ffi::CString::new(rp) {
875        Ok(c) => c,
876        Err(_) => return 1,
877    };
878    if unsafe { libc::chmod(crp.as_ptr(), chm.mode as libc::mode_t) } != 0 {
879        // c:646
880        zwarnnam(
881            chm.nam, // c:655
882            &format!("{}: {}", arg, crate::ported::compat::last_errstr()),
883        );
884        return 1; // c:655
885    }
886    0 // c:655
887}
888
889/// Direct port of `bin_chmod(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/files.c:655`.
890/// C body (c:659-672): parse `args[0]` as octal mode; recursivecmd
891/// over `args[1..]` applying chmod_dochmod.
892// chmod builtin                                                            // c:633
893/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
894pub fn bin_chmod(
895    nam: &str,
896    args: &[String], // c:655
897    ops: &options,
898    _func: i32,
899) -> i32 {
900    if args.is_empty() {
901        zwarnnam(nam, "missing mode");
902        return 1;
903    }
904    let mode = match i64::from_str_radix(&args[0], 8) {
905        // c:663 zstrtol base 8
906        Ok(m) => m as u32,
907        Err(_) => {
908            zwarnnam(nam, &format!("invalid mode `{}'", args[0])); // c:665
909            return 1; // c:666
910        }
911    };
912    let chm = chmodmagic { nam, mode };
913    let recurse = if OPT_ISSET(ops, b'R') { 1 } else { 0 }; // c:670
914    let safe = if OPT_ISSET(ops, b's') { 1 } else { 0 }; // c:670
915    recursivecmd(
916        nam,
917        0,
918        recurse,
919        safe,
920        &args[1..],                             // c:669
921        |a, r, s| chmod_dochmod(a, r, s, &chm), // dirpre
922        |_a, _r, _s| 0,                         // dirpost = recurse_donothing
923        |a, r, s| chmod_dochmod(a, r, s, &chm),
924    ) // leaf
925}
926
927// =====================================================================
928// bin_chown — `Src/Modules/files.c:674-801`.
929// =====================================================================
930
931/// Port of `struct chownmagic` from `Src/Modules/files.c:682`.
932pub struct chownmagic<'a> {
933    pub nam: &'a str, // c:682
934    pub uid: i64,     // c:682 (uid_t but -1 sentinel)
935    pub gid: i64,     // c:682
936}
937
938/// Direct port of `chown_dochown(char *arg, char *rp, UNUSED(struct stat const *sp), void *magic)` from `Src/Modules/files.c:682`.
939/// C body (c:686-692): `chown(rp, uid, gid)`; warn + return 1 on failure.
940/// WARNING: param names don't match C — Rust=(arg, rp, _sp) vs C=(arg, rp, sp, magic)
941pub fn chown_dochown(
942    arg: &str,
943    rp: &str,
944    _sp: Option<&std::fs::Metadata>, // c:682
945    chm: &chownmagic,
946) -> i32 {
947    let crp = match std::ffi::CString::new(rp) {
948        Ok(c) => c,
949        Err(_) => return 1,
950    };
951    let uid = if chm.uid < 0 {
952        libc::uid_t::MAX
953    } else {
954        chm.uid as libc::uid_t
955    };
956    let gid = if chm.gid < 0 {
957        libc::gid_t::MAX
958    } else {
959        chm.gid as libc::gid_t
960    };
961    if unsafe { libc::chown(crp.as_ptr(), uid, gid) } != 0 {
962        // c:695
963        zwarnnam(
964            chm.nam, // c:695
965            &format!("{}: {}", arg, crate::ported::compat::last_errstr()),
966        );
967        return 1; // c:695
968    }
969    0 // c:695
970}
971
972/// Direct port of `chown_dolchown(char *arg, char *rp, UNUSED(struct stat const *sp), void *magic)` from `Src/Modules/files.c:695`.
973/// C body (c:699-705): `lchown(rp, uid, gid)`.
974/// WARNING: param names don't match C — Rust=(arg, rp, _sp) vs C=(arg, rp, sp, magic)
975pub fn chown_dolchown(
976    arg: &str,
977    rp: &str,
978    _sp: Option<&std::fs::Metadata>, // c:695
979    chm: &chownmagic,
980) -> i32 {
981    let crp = match std::ffi::CString::new(rp) {
982        Ok(c) => c,
983        Err(_) => return 1,
984    };
985    let uid = if chm.uid < 0 {
986        libc::uid_t::MAX
987    } else {
988        chm.uid as libc::uid_t
989    };
990    let gid = if chm.gid < 0 {
991        libc::gid_t::MAX
992    } else {
993        chm.gid as libc::gid_t
994    };
995    if unsafe { libc::lchown(crp.as_ptr(), uid, gid) } != 0 {
996        // c:708
997        zwarnnam(
998            chm.nam, // c:708
999            &format!("{}: {}", arg, crate::ported::compat::last_errstr()),
1000        );
1001        return 1; // c:708
1002    }
1003    0 // c:708
1004}
1005
1006/// Direct port of `bin_chown(char *nam, char **args, Options ops, int func)` from `Src/Modules/files.c:725`.
1007/// C body (c:729-797): parse `user[:group]` spec; for chgrp, skip the
1008/// user half; getpwnam / getnumeric / getgrnam fallbacks; recursivecmd
1009/// with chown_dochown or chown_dolchown for `-h`.
1010// chown builtin                                                            // c:672
1011/// WARNING: param names don't match C — Rust=(nam, args, func) vs C=(nam, args, ops, func)
1012pub fn bin_chown(
1013    nam: &str,
1014    args: &[String], // c:725
1015    ops: &options,
1016    func: i32,
1017) -> i32 {
1018    if args.is_empty() {
1019        zwarnnam(nam, "missing argument");
1020        return 1;
1021    }
1022    let uspec = args[0].clone(); // c:728
1023    let mut chm = chownmagic {
1024        nam,
1025        uid: -1,
1026        gid: -1,
1027    };
1028    let mut p_idx = 0usize;
1029    let mut do_group_only = false;
1030    if func == BIN_CHGRP {
1031        // c:733
1032        chm.uid = -1; // c:734
1033        do_group_only = true; // c:735 goto dogroup
1034    } else {
1035        // c:737-741 — locate `:` or `.` separator.
1036        let end = uspec.find(':').or_else(|| uspec.find('.')); // c:737-738
1037        if end == Some(0) {
1038            // c:739
1039            chm.uid = -1; // c:740
1040            p_idx = 1; // c:741
1041            do_group_only = true; // c:742 goto dogroup
1042        } else {
1043            let user_part = if let Some(e) = end {
1044                &uspec[..e]
1045            } else {
1046                &uspec[..]
1047            };
1048            // c:746 — getpwnam(p)
1049            let cuser = std::ffi::CString::new(user_part).unwrap_or_default();
1050            let pwd = unsafe { libc::getpwnam(cuser.as_ptr()) }; // c:746
1051            let uid = if !pwd.is_null() {
1052                unsafe { (*pwd).pw_uid as i64 } // c:748
1053            } else {
1054                let mut errp = 0i32;
1055                let n = getnumeric(user_part, &mut errp); // c:751
1056                if errp != 0 {
1057                    // c:752
1058                    zwarnnam(
1059                        nam, // c:753
1060                        &format!("{}: no such user", user_part),
1061                    );
1062                    return 1; // c:755
1063                }
1064                n as i64
1065            };
1066            chm.uid = uid;
1067            if let Some(e) = end {
1068                // c:759
1069                let group_part = &uspec[e + 1..];
1070                if group_part.is_empty() {
1071                    // c:761
1072                    let p2 = if !pwd.is_null() {
1073                        pwd
1074                    }
1075                    // c:762
1076                    else {
1077                        unsafe { libc::getpwuid(uid as libc::uid_t) }
1078                    };
1079                    if p2.is_null() {
1080                        // c:763
1081                        zwarnnam(
1082                            nam, // c:764
1083                            &format!("{}: no such user", uspec),
1084                        );
1085                        return 1; // c:766
1086                    }
1087                    chm.gid = unsafe { (*p2).pw_gid as i64 }; // c:768
1088                } else if group_part == ":" {
1089                    // c:769
1090                    chm.gid = -1; // c:770
1091                } else {
1092                    p_idx = 0; // not used past this point
1093                    let cgrp = std::ffi::CString::new(group_part).unwrap_or_default();
1094                    let grp = unsafe { libc::getgrnam(cgrp.as_ptr()) }; // c:773 dogroup
1095                    if !grp.is_null() {
1096                        chm.gid = unsafe { (*grp).gr_gid as i64 }; // c:775
1097                    } else {
1098                        let mut errp = 0i32;
1099                        let n = getnumeric(group_part, &mut errp); // c:778
1100                        if errp != 0 {
1101                            // c:779
1102                            zwarnnam(
1103                                nam, // c:780
1104                                &format!("{}: no such group", group_part),
1105                            );
1106                            return 1; // c:782
1107                        }
1108                        chm.gid = n as i64;
1109                    }
1110                }
1111            }
1112        }
1113    }
1114    if do_group_only {
1115        // c:773 dogroup label
1116        let group_part = &uspec[p_idx..];
1117        let cgrp = std::ffi::CString::new(group_part).unwrap_or_default();
1118        let grp = unsafe { libc::getgrnam(cgrp.as_ptr()) }; // c:773
1119        if !grp.is_null() {
1120            chm.gid = unsafe { (*grp).gr_gid as i64 }; // c:775
1121        } else {
1122            let mut errp = 0i32;
1123            let n = getnumeric(group_part, &mut errp); // c:778
1124            if errp != 0 {
1125                // c:779
1126                zwarnnam(
1127                    nam, // c:780
1128                    &format!("{}: no such group", group_part),
1129                );
1130                return 1; // c:782
1131            }
1132            chm.gid = n as i64;
1133        }
1134    }
1135    let recurse = if OPT_ISSET(ops, b'R') { 1 } else { 0 }; // c:792
1136    let safe = if OPT_ISSET(ops, b's') { 1 } else { 0 }; // c:792
1137    let h_flag = OPT_ISSET(ops, b'h'); // c:793
1138    recursivecmd(
1139        nam,
1140        0,
1141        recurse,
1142        safe,
1143        &args[1..], // c:791
1144        |a, r, s| {
1145            if h_flag {
1146                chown_dolchown(a, r, s, &chm)
1147            } else {
1148                chown_dochown(a, r, s, &chm)
1149            }
1150        }, // dirpre
1151        |_a, _r, _s| 0, // dirpost = recurse_donothing
1152        |a, r, s| {
1153            if h_flag {
1154                chown_dolchown(a, r, s, &chm)
1155            } else {
1156                chown_dochown(a, r, s, &chm)
1157            }
1158        },
1159    ) // leaf
1160}
1161
1162// =====================================================================
1163// `LN_OPTS` — `Src/Modules/files.c:799`.
1164// =====================================================================
1165
1166/// `LN_OPTS` — `Src/Modules/files.c:799` (`"dfhins"` when HAVE_LSTAT,
1167/// `"dfi"` otherwise). zshrs targets POSIX hosts where lstat is
1168/// always present, so the long form is canonical.
1169pub const LN_OPTS: &str = "dfhins"; // c:799
1170
1171// `module_bintab` — port of `static struct builtin bintab[]` (files.c:803)
1172// in the canonical `module::Builtin` shape (the local `FilesBuiltin`
1173// table above is kept for the internal dispatcher in
1174// `src/extensions/` which needs the `funcid` int).
1175
1176// `module_features` — port of `static struct features module_features`
1177// from files.c:828.
1178
1179/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/files.c:838`.
1180#[allow(unused_variables)]
1181pub fn setup_(m: *const module) -> i32 {
1182    // c:838
1183    // C body c:840-841 — `return 0`. Faithful empty-body port.
1184    0
1185}
1186
1187/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/files.c:845`.
1188pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
1189    // c:845
1190    *features = featuresarray(m, module_features());
1191    0
1192}
1193
1194/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/files.c:853`.
1195pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
1196    // c:853
1197    handlefeatures(m, module_features(), enables)
1198}
1199
1200/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/files.c:860`.
1201#[allow(unused_variables)]
1202pub fn boot_(m: *const module) -> i32 {
1203    // c:860
1204    // C body c:862-863 — `return 0`. Faithful empty-body port; the
1205    //                    chmod/chown/chgrp/sync/etc. builtins register
1206    //                    via the bn_list feature dispatch.
1207    0
1208}
1209
1210/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/files.c:867`.
1211pub fn cleanup_(m: *const module) -> i32 {
1212    // c:867
1213    setfeatureenables(m, module_features(), None)
1214}
1215
1216/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/files.c:874`.
1217#[allow(unused_variables)]
1218pub fn finish_(m: *const module) -> i32 {
1219    // c:874
1220    // C body c:876-877 — `return 0`. Faithful empty-body port; the
1221    //                    builtins unregister via cleanup_'s setfeatureenables.
1222    0
1223}
1224
1225/// `bin_chown` func discriminant — `Src/Modules/files.c:41`
1226/// (`enum { BIN_CHOWN, BIN_CHGRP };`).
1227pub const BIN_CHOWN: i32 = 0; // c:719
1228
1229// bintab[] (`static struct builtin bintab[]` at files.c:803) lives in
1230// the `MODULE_BINTAB` slice below in canonical `module::Builtin` shape;
1231// the dispatcher in src/extensions/ reads it through the singleton
1232// rather than a private aggregate type.
1233
1234// =====================================================================
1235// module entries — `Src/Modules/files.c:828-876`.
1236// =====================================================================
1237/// `BIN_CHGRP` constant.
1238pub const BIN_CHGRP: i32 = 1; // c:719
1239
1240// =====================================================================
1241// bin_ln + domove — `Src/Modules/files.c:200`, `:298`.
1242// =====================================================================
1243
1244// `enum MoveFunc` deleted — C uses a bare function-pointer typedef
1245// `typedef int (*MoveFunc)(char const *, char const *);` at
1246// `Src/Modules/files.c:32`. Rust ports it directly as the same
1247// function-pointer type alias below, so call sites can do
1248// `movefn = mv_rename;` matching C's `movefn = rename;`.
1249/// `MoveFunc` type alias.
1250#[allow(non_camel_case_types)]
1251pub type MoveFunc = fn(p: &std::ffi::CStr, q: &std::ffi::CStr) -> i32;
1252
1253/// Adapter for `rename(2)` — used by `bin_mv`.
1254pub fn mv_rename(p: &std::ffi::CStr, q: &std::ffi::CStr) -> i32 {
1255    unsafe { libc::rename(p.as_ptr(), q.as_ptr()) }
1256}
1257
1258/// Adapter for `symlink(2)` — used by `bin_ln -s`.
1259pub fn mv_symlink(p: &std::ffi::CStr, q: &std::ffi::CStr) -> i32 {
1260    unsafe { libc::symlink(p.as_ptr(), q.as_ptr()) }
1261}
1262
1263/// Adapter for `link(2)` — used by `bin_ln` default.
1264pub fn mv_link(p: &std::ffi::CStr, q: &std::ffi::CStr) -> i32 {
1265    unsafe { libc::link(p.as_ptr(), q.as_ptr()) }
1266}
1267
1268/// Direct port of `getnumeric(char *p, int *errp)` from `Src/Modules/files.c:708`.
1269/// C body (c:712-719): parse leading digits as base-10 unsigned long;
1270/// `*errp = !!*p` after parse — set when there are trailing non-digits.
1271pub fn getnumeric(p: &str, errp: &mut i32) -> u64 {
1272    // c:708
1273    if !p.chars().next().is_some_and(|c| c.is_ascii_digit()) {
1274        // c:708
1275        *errp = 1; // c:713
1276        return 0; // c:714
1277    }
1278    let end = p.find(|c: char| !c.is_ascii_digit()).unwrap_or(p.len());
1279    let ret = p[..end].parse::<u64>().unwrap_or(0); // c:725 strtoul
1280    *errp = if end < p.len() { 1 } else { 0 }; // c:725
1281    ret // c:725
1282}
1283
1284static MODULE_FEATURES: OnceLock<Mutex<crate::ported::zsh_h::features>> = OnceLock::new();
1285
1286// Local stubs for the per-module entry points. C uses generic
1287// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
1288// 3275/3370/3445) but those take `Builtin` + `Features` pointer
1289// fields the Rust port doesn't carry. The hardcoded descriptor
1290// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
1291// WARNING: NOT IN FILES.C — Rust-only module-framework shim.
1292// C uses generic featuresarray/handlefeatures/setfeatureenables from
1293// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1294// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1295fn featuresarray(_m: *const module, _f: &Mutex<crate::ported::zsh_h::features>) -> Vec<String> {
1296    vec![
1297        "b:chgrp".to_string(),
1298        "b:chmod".to_string(),
1299        "b:chown".to_string(),
1300        "b:ln".to_string(),
1301        "b:mkdir".to_string(),
1302        "b:mv".to_string(),
1303        "b:rm".to_string(),
1304        "b:rmdir".to_string(),
1305        "b:sync".to_string(),
1306        "b:zf_chgrp".to_string(),
1307        "b:zf_chmod".to_string(),
1308        "b:zf_chown".to_string(),
1309        "b:zf_ln".to_string(),
1310        "b:zf_mkdir".to_string(),
1311        "b:zf_mv".to_string(),
1312        "b:zf_rm".to_string(),
1313        "b:zf_rmdir".to_string(),
1314        "b:zf_sync".to_string(),
1315    ]
1316}
1317
1318// WARNING: NOT IN FILES.C — Rust-only module-framework shim.
1319// C uses generic featuresarray/handlefeatures/setfeatureenables from
1320// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1321// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1322fn handlefeatures(
1323    _m: *const module,
1324    _f: &Mutex<crate::ported::zsh_h::features>,
1325    enables: &mut Option<Vec<i32>>,
1326) -> i32 {
1327    if enables.is_none() {
1328        *enables = Some(vec![1; 18]);
1329    }
1330    0
1331}
1332
1333// WARNING: NOT IN FILES.C — Rust-only module-framework shim.
1334// C uses generic featuresarray/handlefeatures/setfeatureenables from
1335// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1336// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1337fn setfeatureenables(
1338    _m: *const module,
1339    _f: &Mutex<crate::ported::zsh_h::features>,
1340    _e: Option<&[i32]>,
1341) -> i32 {
1342    0
1343}
1344
1345// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1346// ─── RUST-ONLY ACCESSORS ───
1347//
1348// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1349// RwLock<T>>` globals declared above. C zsh uses direct global
1350// access; Rust needs these wrappers because `OnceLock::get_or_init`
1351// is the only way to lazily construct shared state. These ported sit
1352// here so the body of this file reads in C source order without
1353// the accessor wrappers interleaved between real port ported.
1354// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1355
1356// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1357// ─── RUST-ONLY ACCESSORS ───
1358//
1359// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1360// RwLock<T>>` globals declared above. C zsh uses direct global
1361// access; Rust needs these wrappers because `OnceLock::get_or_init`
1362// is the only way to lazily construct shared state. These ported sit
1363// here so the body of this file reads in C source order without
1364// the accessor wrappers interleaved between real port ported.
1365// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1366
1367// WARNING: NOT IN FILES.C — Rust-only module-framework shim.
1368// C uses generic featuresarray/handlefeatures/setfeatureenables from
1369// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1370// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1371fn module_features() -> &'static Mutex<crate::ported::zsh_h::features> {
1372    MODULE_FEATURES.get_or_init(|| {
1373        Mutex::new(crate::ported::zsh_h::features {
1374            bn_list: None,
1375            bn_size: 18,
1376            cd_list: None,
1377            cd_size: 0,
1378            mf_list: None,
1379            mf_size: 0,
1380            pd_list: None,
1381            pd_size: 0,
1382            n_abstract: 0,
1383        })
1384    })
1385}
1386
1387#[cfg(test)]
1388mod tests {
1389    use super::*;
1390
1391    fn empty_ops() -> options {
1392        options {
1393            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1394            args: Vec::new(),
1395            argscount: 0,
1396            argsalloc: 0,
1397        }
1398    }
1399
1400    /// c:115 — `domkdir` against a path that already exists must
1401    /// surface mkdir(2)'s EEXIST. Silent success would mask
1402    /// existing-directory clobbering — a real safety regression.
1403    #[test]
1404    fn domkdir_fails_when_path_already_exists() {
1405        let _g = crate::test_util::global_state_lock();
1406        let tmp = std::env::temp_dir();
1407        assert_ne!(domkdir("mkdir", tmp.to_str().unwrap(), 0o755, 0), 0);
1408    }
1409
1410    /// c:75-76 — `mkdir -m <invalid-octal>` early-returns 1 BEFORE
1411    /// the per-arg mkdir loop. Catches a regression where the bad
1412    /// mode silently falls through to the umask-derived default.
1413    #[test]
1414    fn mkdir_with_invalid_mode_short_circuits_before_filesystem_op() {
1415        let _g = crate::test_util::global_state_lock();
1416        let mut ops = empty_ops();
1417        ops.ind[b'm' as usize] = (1 << 2) | 1;
1418        ops.args.push("not-octal".to_string());
1419        // Path is bogus on purpose: if the mode-parse early-return
1420        // failed, mkdir(2) would still try this path and fail
1421        // differently. The assertion catches the right error origin.
1422        let r = bin_mkdir(
1423            "mkdir",
1424            &["/tmp/zshrs_test_invalid_mode".to_string()],
1425            &ops,
1426            0,
1427        );
1428        assert_eq!(r, 1);
1429    }
1430
1431    /// c:115-150 — `domkdir` with `p=1` against a path that already
1432    /// exists AS A DIRECTORY must return 0 (success). This is the
1433    /// `mkdir -p` idempotence rule: re-running `mkdir -p /existing`
1434    /// must NOT error.
1435    #[test]
1436    fn domkdir_with_p_flag_on_existing_dir_returns_zero() {
1437        let _g = crate::test_util::global_state_lock();
1438        let tmp = std::env::temp_dir();
1439        let r = domkdir("mkdir", tmp.to_str().unwrap(), 0o755, /*p=*/ 1);
1440        assert_eq!(r, 0, "mkdir -p /tmp must succeed when /tmp already exists");
1441    }
1442
1443    /// c:115-150 — same path with `p=0` (`mkdir` without -p) must
1444    /// FAIL when the path already exists. Symmetrical to the above:
1445    /// behavior pivots entirely on the `p` flag.
1446    #[test]
1447    fn domkdir_without_p_on_existing_dir_fails() {
1448        let _g = crate::test_util::global_state_lock();
1449        let tmp = std::env::temp_dir();
1450        let r = domkdir("mkdir", tmp.to_str().unwrap(), 0o755, /*p=*/ 0);
1451        assert_ne!(r, 0, "mkdir (no -p) on existing dir must fail per POSIX");
1452    }
1453
1454    /// c:80-115 + c:builtin.c:430-435 — `bin_mkdir` with no args.
1455    /// The C body itself returns 0 (per-arg loop has no iterations),
1456    /// but the BUILTIN dispatch table declares `minargs=1`
1457    /// (Src/Modules/files.c:810), so the framework rejects empty
1458    /// argv with "not enough arguments" → exit 1 before the body
1459    /// runs. zshrs's bin_mkdir self-enforces the same minargs check
1460    /// for direct-call paths (test / fusevm bridge) so the
1461    /// user-visible `mkdir; echo $?` parity holds.
1462    #[test]
1463    fn bin_mkdir_with_no_args_returns_nonzero() {
1464        let _g = crate::test_util::global_state_lock();
1465        let ops = empty_ops();
1466        let r = bin_mkdir("mkdir", &[], &ops, 0);
1467        assert_ne!(r, 0, "bin_mkdir on empty argv → minargs error");
1468    }
1469
1470    /// c:80-115 — `bin_mkdir -p` should successfully create a deeply
1471    /// nested path. Catches a regression where the nested-walk logic
1472    /// (c:88-101) leaves a parent half-created and reports failure.
1473    #[test]
1474    fn bin_mkdir_p_creates_nested_path() {
1475        let _g = crate::test_util::global_state_lock();
1476        let mut ops = empty_ops();
1477        ops.ind[b'p' as usize] = (1 << 2) | 1;
1478        // Unique under /tmp to keep tests independent of one another
1479        let pid = std::process::id();
1480        let base = format!("/tmp/zshrs_test_mkdir_p_{}", pid);
1481        let nested = format!("{}/a/b/c", base);
1482        let _ = std::fs::remove_dir_all(&base);
1483        let r = bin_mkdir("mkdir", &[nested.clone()], &ops, 0);
1484        assert_eq!(r, 0, "mkdir -p should create the whole chain");
1485        assert!(
1486            std::path::Path::new(&nested).is_dir(),
1487            "leaf dir {} should exist after mkdir -p",
1488            nested
1489        );
1490        let _ = std::fs::remove_dir_all(&base);
1491    }
1492
1493    /// c:81-83 — bin_mkdir's trailing-slash trim. `bin_mkdir /tmp/foo/`
1494    /// must call domkdir with the trailing slash stripped. Verified
1495    /// through the happy-path: the directory ends up at the trimmed
1496    /// path, even though the user supplied the trailing slash.
1497    #[test]
1498    fn bin_mkdir_strips_trailing_slashes() {
1499        let _g = crate::test_util::global_state_lock();
1500        let pid = std::process::id();
1501        let target = format!("/tmp/zshrs_test_mkdir_trailing_{}/", pid);
1502        let _ = std::fs::remove_dir_all(target.trim_end_matches('/'));
1503        let ops = empty_ops();
1504        let r = bin_mkdir("mkdir", &[target.clone()], &ops, 0);
1505        assert_eq!(r, 0, "trailing slash should not break mkdir");
1506        assert!(std::path::Path::new(target.trim_end_matches('/')).is_dir());
1507        let _ = std::fs::remove_dir_all(target.trim_end_matches('/'));
1508    }
1509
1510    /// c:150-200 — `bin_rmdir` on a path that does not exist returns
1511    /// non-zero, matching rmdir(2)'s ENOENT contract. Regression
1512    /// suppressing the per-arg error accumulator would mask this.
1513    #[test]
1514    fn bin_rmdir_on_nonexistent_path_returns_nonzero() {
1515        let _g = crate::test_util::global_state_lock();
1516        let ops = empty_ops();
1517        let r = bin_rmdir(
1518            "rmdir",
1519            &["/__definitely_not_a_dir_xyzzy_zshrs_test__".to_string()],
1520            &ops,
1521            0,
1522        );
1523        assert_ne!(r, 0, "rmdir of nonexistent path must report failure");
1524    }
1525
1526    /// c:150-200 + c:builtin.c:430-435 — same framework minargs
1527    /// story as bin_mkdir above: BUILTIN table sets `minargs=1`,
1528    /// dispatch rejects empty argv → exit 1. zshrs self-enforces.
1529    #[test]
1530    fn bin_rmdir_with_no_args_returns_nonzero() {
1531        let _g = crate::test_util::global_state_lock();
1532        let ops = empty_ops();
1533        let r = bin_rmdir("rmdir", &[], &ops, 0);
1534        assert_ne!(r, 0, "rmdir empty argv → minargs error");
1535    }
1536
1537    /// c:150-200 — `bin_rmdir` happy path: create then remove. Proves
1538    /// the rmdir(2) call is actually wired through.
1539    #[test]
1540    fn bin_rmdir_removes_an_empty_directory() {
1541        let _g = crate::test_util::global_state_lock();
1542        let pid = std::process::id();
1543        let target = format!("/tmp/zshrs_test_rmdir_{}", pid);
1544        let _ = std::fs::remove_dir_all(&target);
1545        std::fs::create_dir(&target).expect("setup mkdir");
1546        let ops = empty_ops();
1547        let r = bin_rmdir("rmdir", &[target.clone()], &ops, 0);
1548        assert_eq!(r, 0, "rmdir on existing empty dir should succeed");
1549        assert!(
1550            !std::path::Path::new(&target).exists(),
1551            "directory should be gone after rmdir"
1552        );
1553    }
1554
1555    /// c:838-879 — `setup_` / `boot_` / `cleanup_` / `finish_` module
1556    /// lifecycle stubs. C versions are empty (`return 0`). The Rust
1557    /// port must match.
1558    #[test]
1559    fn module_lifecycle_shims_all_return_zero() {
1560        let _g = crate::test_util::global_state_lock();
1561        let null_module = std::ptr::null();
1562        assert_eq!(setup_(null_module), 0);
1563        assert_eq!(boot_(null_module), 0);
1564        assert_eq!(cleanup_(null_module), 0);
1565        assert_eq!(finish_(null_module), 0);
1566    }
1567
1568    /// c:616 + c:builtin.c:430-435 — same framework minargs story
1569    /// as bin_mkdir above. zshrs self-enforces.
1570    #[test]
1571    fn bin_rm_no_args_returns_nonzero() {
1572        let _g = crate::test_util::global_state_lock();
1573        let ops = empty_ops();
1574        let r = bin_rm("rm", &[], &ops, 0);
1575        assert_ne!(r, 0, "rm empty argv → minargs error");
1576    }
1577
1578    /// c:616 — `bin_rm` on a nonexistent path returns nonzero.
1579    #[test]
1580    fn bin_rm_nonexistent_path_returns_one() {
1581        let _g = crate::test_util::global_state_lock();
1582        let ops = empty_ops();
1583        let r = bin_rm("rm", &["/__zshrs_test_no_such_path__".to_string()], &ops, 0);
1584        assert_ne!(r, 0, "rm on nonexistent path must error");
1585    }
1586
1587    /// c:546 — `rm_leaf` on a real file removes it. Uses rmmagic
1588    /// struct (c:475-480) matching the port signature.
1589    #[test]
1590    fn rm_leaf_removes_existing_file() {
1591        let _g = crate::test_util::global_state_lock();
1592        let pid = std::process::id();
1593        let f = format!("/tmp/zshrs_test_rm_leaf_{}.txt", pid);
1594        let _ = std::fs::write(&f, "x");
1595        assert!(std::path::Path::new(&f).exists());
1596        let rmm = rmmagic {
1597            nam: "rm",
1598            opt_force: 1,
1599            opt_interact: 0,
1600            opt_unlinkdir: 0,
1601        };
1602        let r = rm_leaf(&f, &f, None, &rmm);
1603        assert_eq!(r, 0, "rm_leaf on existing file must succeed");
1604        assert!(
1605            !std::path::Path::new(&f).exists(),
1606            "file must be gone after rm_leaf"
1607        );
1608    }
1609
1610    /// c:546 — `rm_leaf` on a nonexistent path with opt_force=1
1611    /// returns 0 silently (matches POSIX `rm -f`). Pin so a regen
1612    /// that ignores force flag silently errors on `rm -f /missing`.
1613    #[test]
1614    fn rm_leaf_force_silently_succeeds_on_missing() {
1615        let _g = crate::test_util::global_state_lock();
1616        let rmm = rmmagic {
1617            nam: "rm",
1618            opt_force: 1,
1619            opt_interact: 0,
1620            opt_unlinkdir: 0,
1621        };
1622        let r = rm_leaf("/__never__", "/__never__", None, &rmm);
1623        assert_eq!(r, 0, "rm -f on missing path must succeed silently");
1624    }
1625
1626    /// c:655 — `bin_chmod` with no args returns 1 (the port has a
1627    /// "missing mode" arity guard at line 607). Pin the explicit
1628    /// error so a regen removing the guard silently mis-routes.
1629    #[test]
1630    fn bin_chmod_no_args_returns_one() {
1631        let _g = crate::test_util::global_state_lock();
1632        let ops = empty_ops();
1633        let r = bin_chmod("chmod", &[], &ops, 0);
1634        assert_eq!(r, 1, "missing mode must surface as error");
1635    }
1636
1637    /// c:725 — `bin_chown` with no args returns 1 (port arity guard).
1638    #[test]
1639    fn bin_chown_no_args_returns_one() {
1640        let _g = crate::test_util::global_state_lock();
1641        let ops = empty_ops();
1642        let r = bin_chown("chown", &[], &ops, 0);
1643        assert_eq!(r, 1, "missing owner must surface as error");
1644    }
1645
1646    /// c:200 — `bin_ln` with FEWER than 2 args is a usage error.
1647    #[test]
1648    fn bin_ln_one_arg_returns_nonzero() {
1649        let _g = crate::test_util::global_state_lock();
1650        let ops = empty_ops();
1651        let r = bin_ln("ln", &["/tmp".to_string()], &ops, 0);
1652        assert_ne!(r, 0, "ln with <2 args must error");
1653    }
1654
1655    /// c:53 — `bin_sync` ignores all args, returns 0 (fire-and-
1656    /// forget sync(2)).
1657    #[test]
1658    fn bin_sync_returns_zero_regardless_of_args() {
1659        let _g = crate::test_util::global_state_lock();
1660        let ops = empty_ops();
1661        assert_eq!(bin_sync("sync", &[], &ops, 0), 0);
1662        assert_eq!(bin_sync("sync", &["ignored".to_string()], &ops, 0), 0);
1663        assert_eq!(
1664            bin_sync("sync", &["a".to_string(), "b".to_string()], &ops, 0),
1665            0
1666        );
1667    }
1668
1669    // ─── zsh-corpus pins for bin_mkdir / bin_rmdir / bin_rm ─────────
1670
1671    /// `bin_mkdir` with no args returns non-zero (usage error).
1672    #[test]
1673    fn files_corpus_bin_mkdir_no_args_returns_nonzero() {
1674        let _g = crate::test_util::global_state_lock();
1675        let ops = empty_ops();
1676        let r = bin_mkdir("mkdir", &[], &ops, 0);
1677        assert_ne!(r, 0, "mkdir with no args = usage error");
1678    }
1679
1680    /// `bin_rmdir` with no args returns non-zero.
1681    #[test]
1682    fn files_corpus_bin_rmdir_no_args_returns_nonzero() {
1683        let _g = crate::test_util::global_state_lock();
1684        let ops = empty_ops();
1685        let r = bin_rmdir("rmdir", &[], &ops, 0);
1686        assert_ne!(r, 0, "rmdir with no args = usage error");
1687    }
1688
1689    /// `bin_rm` with no args returns non-zero (usage error).
1690    #[test]
1691    fn files_corpus_bin_rm_no_args_returns_nonzero() {
1692        let _g = crate::test_util::global_state_lock();
1693        let ops = empty_ops();
1694        let r = bin_rm("rm", &[], &ops, 0);
1695        assert_ne!(r, 0, "rm with no args = usage error");
1696    }
1697
1698    /// `bin_mkdir` creates a new directory successfully.
1699    #[test]
1700    fn files_corpus_bin_mkdir_creates_directory() {
1701        let _g = crate::test_util::global_state_lock();
1702        let ops = empty_ops();
1703        let dir = tempfile::tempdir().unwrap();
1704        let target = dir.path().join("newdir");
1705        let target_str = target.to_str().unwrap().to_string();
1706        let r = bin_mkdir("mkdir", &[target_str], &ops, 0);
1707        assert_eq!(r, 0, "mkdir on new path succeeds");
1708        assert!(target.exists(), "new dir actually created");
1709        assert!(target.is_dir(), "new path is a directory");
1710    }
1711
1712    /// `bin_rmdir` removes an empty dir.
1713    #[test]
1714    fn files_corpus_bin_rmdir_removes_empty_dir() {
1715        let _g = crate::test_util::global_state_lock();
1716        let ops = empty_ops();
1717        let dir = tempfile::tempdir().unwrap();
1718        let target = dir.path().join("toremove");
1719        std::fs::create_dir(&target).unwrap();
1720        assert!(target.exists());
1721        let r = bin_rmdir("rmdir", &[target.to_str().unwrap().to_string()], &ops, 0);
1722        assert_eq!(r, 0, "rmdir on empty dir succeeds");
1723        assert!(!target.exists(), "dir removed");
1724    }
1725
1726    /// `domkdir` creates a dir with the given mode.
1727    #[test]
1728    fn files_corpus_domkdir_creates_with_mode() {
1729        let _g = crate::test_util::global_state_lock();
1730        let dir = tempfile::tempdir().unwrap();
1731        let target = dir.path().join("modedir");
1732        let r = domkdir("mkdir", target.to_str().unwrap(), 0o755, 0);
1733        assert_eq!(r, 0);
1734        assert!(target.is_dir());
1735    }
1736
1737    /// `domkdir` fails if dir already exists (without -p flag).
1738    #[test]
1739    fn files_corpus_domkdir_existing_path_fails_without_p() {
1740        let _g = crate::test_util::global_state_lock();
1741        let dir = tempfile::tempdir().unwrap();
1742        let r = domkdir("mkdir", dir.path().to_str().unwrap(), 0o755, 0);
1743        assert_ne!(r, 0, "mkdir on existing dir without -p = error");
1744    }
1745
1746    // ═══════════════════════════════════════════════════════════════════
1747    // C-parity tests pinning Src/Modules/files.c.
1748    // ═══════════════════════════════════════════════════════════════════
1749
1750    /// `domkdir` creates a fresh dir and returns 0.
1751    /// C `Src/Modules/files.c:domkdir` — mkdir() syscall path.
1752    #[test]
1753    fn domkdir_fresh_path_returns_zero() {
1754        let _g = crate::test_util::global_state_lock();
1755        let parent = tempfile::tempdir().unwrap();
1756        let path = parent.path().join("newdir");
1757        let r = domkdir("mkdir", path.to_str().unwrap(), 0o755, 0);
1758        assert_eq!(r, 0, "fresh mkdir returns 0 (success)");
1759        assert!(path.exists(), "directory should exist after mkdir");
1760    }
1761
1762    /// `domkdir -p` creates parents as needed.
1763    /// C `Src/Modules/files.c:domkdir` with `p=1` flag:
1764    ///   recursively creates each intermediate directory if missing.
1765    /// ZSHRS BUG: Rust port at modules/files.rs:222 does NOT walk
1766    /// intermediate dirs — `mkdir -p a/b/c` fails when `a` and `b`
1767    /// don't exist. C-compatible `mkdir -p` is mandatory shell
1768    /// behavior; this breaks any script that relies on it.
1769    #[test]
1770    fn domkdir_with_p_creates_parents() {
1771        let _g = crate::test_util::global_state_lock();
1772        let parent = tempfile::tempdir().unwrap();
1773        let nested = parent.path().join("a/b/c");
1774        let r = domkdir("mkdir", nested.to_str().unwrap(), 0o755, 1);
1775        assert_eq!(r, 0, "mkdir -p with nested path returns 0");
1776        assert!(nested.exists(), "nested dir should exist after mkdir -p");
1777    }
1778
1779    // ═══════════════════════════════════════════════════════════════════
1780    // Additional C-parity tests for Src/Modules/files.c.
1781    // ═══════════════════════════════════════════════════════════════════
1782
1783    /// c:708 — `getnumeric("0")` returns 0 with err=0.
1784    #[test]
1785    fn getnumeric_zero_returns_zero_no_err() {
1786        let mut err = 0;
1787        let r = getnumeric("0", &mut err);
1788        assert_eq!(r, 0);
1789        assert_eq!(err, 0);
1790    }
1791
1792    /// c:708 — `getnumeric("12345")` returns 12345 with err=0.
1793    #[test]
1794    fn getnumeric_canonical_decimal() {
1795        let mut err = 0;
1796        let r = getnumeric("12345", &mut err);
1797        assert_eq!(r, 12345);
1798        assert_eq!(err, 0);
1799    }
1800
1801    /// c:725 — `getnumeric("100abc")` returns 100 but sets err=1
1802    /// (trailing non-digit).
1803    #[test]
1804    fn getnumeric_trailing_garbage_sets_err() {
1805        let mut err = 0;
1806        let r = getnumeric("100abc", &mut err);
1807        assert_eq!(r, 100, "digits consumed");
1808        assert_eq!(err, 1, "trailing non-digit sets err");
1809    }
1810
1811    /// c:713 — `getnumeric("abc")` returns 0 and sets err=1 (no leading digit).
1812    #[test]
1813    fn getnumeric_no_leading_digit_sets_err() {
1814        let mut err = 0;
1815        let r = getnumeric("abc", &mut err);
1816        assert_eq!(r, 0);
1817        assert_eq!(err, 1);
1818    }
1819
1820    /// c:708 — `getnumeric("")` empty input: no leading digit → err=1, ret=0.
1821    #[test]
1822    fn getnumeric_empty_sets_err() {
1823        let mut err = 0;
1824        let r = getnumeric("", &mut err);
1825        assert_eq!(r, 0);
1826        assert_eq!(err, 1, "empty input is invalid");
1827    }
1828
1829    /// c:713 — getnumeric with negative-sign prefix sets err (no leading digit).
1830    #[test]
1831    fn getnumeric_negative_sign_sets_err() {
1832        let mut err = 0;
1833        let r = getnumeric("-5", &mut err);
1834        assert_eq!(r, 0);
1835        assert_eq!(err, 1, "minus sign isn't a digit");
1836    }
1837
1838    /// c:53 — `bin_sync` always returns 0 (no-op success).
1839    #[test]
1840    fn bin_sync_returns_zero() {
1841        let _g = crate::test_util::global_state_lock();
1842        let ops = empty_ops();
1843        let r = bin_sync("sync", &[], &ops, 0);
1844        assert_eq!(r, 0, "sync builtin must return 0");
1845    }
1846
1847    /// c:222 — `domkdir` on existing dir returns nonzero (already exists).
1848    #[test]
1849    fn domkdir_existing_returns_nonzero() {
1850        let _g = crate::test_util::global_state_lock();
1851        let dir = tempfile::tempdir().unwrap();
1852        // Attempt mkdir of the already-existing temp dir.
1853        let r = domkdir("mkdir", dir.path().to_str().unwrap(), 0o755, 0);
1854        assert_ne!(r, 0, "existing dir → nonzero error");
1855    }
1856
1857    /// c:1205 — `mv_rename` on missing source path returns nonzero.
1858    #[test]
1859    fn mv_rename_missing_source_returns_nonzero() {
1860        let _g = crate::test_util::global_state_lock();
1861        let src = std::ffi::CString::new("/__nonexistent_zshrs_xyz__").unwrap();
1862        let dst = std::ffi::CString::new("/tmp/zshrs_mv_dst").unwrap();
1863        let r = mv_rename(&src, &dst);
1864        assert_ne!(r, 0, "missing source → error");
1865    }
1866
1867    /// c:1210 — `mv_symlink` to nonexistent target returns nonzero or
1868    /// succeeds creating dangling link. Test no panic.
1869    #[test]
1870    fn mv_symlink_no_panic_on_dangling() {
1871        let _g = crate::test_util::global_state_lock();
1872        let dir = tempfile::tempdir().unwrap();
1873        let dst_path = dir.path().join("zshrs_symlink_test");
1874        let src = std::ffi::CString::new("/__nonexistent_zshrs_xyz__").unwrap();
1875        let dst = std::ffi::CString::new(dst_path.to_str().unwrap()).unwrap();
1876        let _ = mv_symlink(&src, &dst);
1877    }
1878
1879    /// Lifecycle stubs return 0.
1880    #[test]
1881    fn files_lifecycle_stubs_return_zero() {
1882        let _g = crate::test_util::global_state_lock();
1883        assert_eq!(setup_(std::ptr::null()), 0);
1884        assert_eq!(boot_(std::ptr::null()), 0);
1885        assert_eq!(cleanup_(std::ptr::null()), 0);
1886        assert_eq!(finish_(std::ptr::null()), 0);
1887    }
1888
1889    // ═══════════════════════════════════════════════════════════════════
1890    // Additional C-parity tests for Src/Modules/files.c
1891    // c:116 bin_sync / c:137 bin_mkdir / c:275 bin_rmdir / c:347 bin_ln /
1892    // c:757 bin_rm / c:845 bin_chmod / c:963 bin_chown / c:1132+ lifecycle
1893    // ═══════════════════════════════════════════════════════════════════
1894
1895    /// c:116 — `bin_sync` returns u8 exit-code range.
1896    #[test]
1897    fn bin_sync_returns_in_exit_code_range() {
1898        let _g = crate::test_util::global_state_lock();
1899        let ops = empty_ops();
1900        let r = bin_sync("sync", &[], &ops, 0);
1901        assert!((0..256).contains(&r), "exit code {} must fit in u8", r);
1902    }
1903
1904    /// c:116 — `bin_sync` is idempotent (safe to call many times).
1905    #[test]
1906    fn bin_sync_idempotent() {
1907        let _g = crate::test_util::global_state_lock();
1908        let ops = empty_ops();
1909        for _ in 0..5 {
1910            let r = bin_sync("sync", &[], &ops, 0);
1911            assert_eq!(r, 0, "sync must return 0");
1912        }
1913    }
1914
1915    /// c:137 — `bin_mkdir` empty path returns nonzero.
1916    #[test]
1917    fn bin_mkdir_empty_path_returns_nonzero() {
1918        let _g = crate::test_util::global_state_lock();
1919        let ops = empty_ops();
1920        let r = bin_mkdir("mkdir", &["".to_string()], &ops, 0);
1921        assert_ne!(r, 0, "empty path → error");
1922    }
1923
1924    /// c:275 — `bin_rmdir` empty path returns nonzero.
1925    #[test]
1926    fn bin_rmdir_empty_path_returns_nonzero() {
1927        let _g = crate::test_util::global_state_lock();
1928        let ops = empty_ops();
1929        let r = bin_rmdir("rmdir", &["".to_string()], &ops, 0);
1930        assert_ne!(r, 0, "empty path → error");
1931    }
1932
1933    /// c:757 — `bin_rm` empty path returns nonzero.
1934    #[test]
1935    fn bin_rm_empty_path_returns_nonzero() {
1936        let _g = crate::test_util::global_state_lock();
1937        let ops = empty_ops();
1938        let r = bin_rm("rm", &["".to_string()], &ops, 0);
1939        assert_ne!(r, 0, "empty path → error");
1940    }
1941
1942    /// c:347 — `bin_ln` no args returns nonzero (usage error).
1943    #[test]
1944    fn bin_ln_no_args_returns_nonzero() {
1945        let _g = crate::test_util::global_state_lock();
1946        let ops = empty_ops();
1947        let r = bin_ln("ln", &[], &ops, 0);
1948        assert_ne!(r, 0, "no args → error");
1949    }
1950
1951    /// c:845 — `bin_chmod` no args returns nonzero.
1952    #[test]
1953    fn bin_chmod_no_args_returns_nonzero() {
1954        let _g = crate::test_util::global_state_lock();
1955        let ops = empty_ops();
1956        let r = bin_chmod("chmod", &[], &ops, 0);
1957        assert_ne!(r, 0, "no args → error");
1958    }
1959
1960    /// c:963 — `bin_chown` no args returns nonzero.
1961    #[test]
1962    fn bin_chown_no_args_returns_nonzero() {
1963        let _g = crate::test_util::global_state_lock();
1964        let ops = empty_ops();
1965        let r = bin_chown("chown", &[], &ops, 0);
1966        assert_ne!(r, 0, "no args → error");
1967    }
1968
1969    /// c:137-963 — all file-op builtin returns fit in u8 exit-code range.
1970    #[test]
1971    fn files_builtins_return_in_exit_code_range() {
1972        let _g = crate::test_util::global_state_lock();
1973        let ops = empty_ops();
1974        for r in [
1975            bin_mkdir("mkdir", &[], &ops, 0),
1976            bin_rmdir("rmdir", &[], &ops, 0),
1977            bin_rm("rm", &[], &ops, 0),
1978            bin_ln("ln", &[], &ops, 0),
1979            bin_chmod("chmod", &[], &ops, 0),
1980            bin_chown("chown", &[], &ops, 0),
1981        ] {
1982            assert!(
1983                (0..256).contains(&r),
1984                "exit code {} must fit in u8 range",
1985                r
1986            );
1987        }
1988    }
1989
1990    /// c:1132+ — full lifecycle setup→features→enables→boot→cleanup→finish.
1991    #[test]
1992    fn files_full_lifecycle_returns_zero_for_all() {
1993        let _g = crate::test_util::global_state_lock();
1994        let null = std::ptr::null();
1995        assert_eq!(setup_(null), 0);
1996        let mut feats = Vec::new();
1997        let _ = features_(null, &mut feats);
1998        let mut enables: Option<Vec<i32>> = None;
1999        let _ = enables_(null, &mut enables);
2000        assert_eq!(boot_(null), 0);
2001        assert_eq!(cleanup_(null), 0);
2002        assert_eq!(finish_(null), 0);
2003    }
2004
2005    // ═══════════════════════════════════════════════════════════════════
2006    // Additional C-parity tests for Src/Modules/files.c
2007    // c:116 bin_sync / c:137 bin_mkdir / c:222 domkdir / c:1222 getnumeric +
2008    // c:1132-1169 lifecycle type pins
2009    // ═══════════════════════════════════════════════════════════════════
2010
2011    /// c:116 — `bin_sync` returns i32 (compile-time type pin).
2012    #[test]
2013    fn bin_sync_returns_i32_type() {
2014        let _g = crate::test_util::global_state_lock();
2015        let ops = empty_ops();
2016        let _: i32 = bin_sync("sync", &[], &ops, 0);
2017    }
2018
2019    /// c:137 — `bin_mkdir` returns i32 (compile-time type pin).
2020    #[test]
2021    fn bin_mkdir_returns_i32_type() {
2022        let _g = crate::test_util::global_state_lock();
2023        let ops = empty_ops();
2024        let _: i32 = bin_mkdir("mkdir", &[], &ops, 0);
2025    }
2026
2027    /// c:222 — `domkdir` returns i32 (compile-time type pin).
2028    #[test]
2029    fn domkdir_returns_i32_type() {
2030        let _g = crate::test_util::global_state_lock();
2031        let _: i32 = domkdir("mkdir", "/__never_zshrs_xyz__", 0o755, 0);
2032    }
2033
2034    /// c:222 — `domkdir` for nonexistent parent fails.
2035    #[test]
2036    fn domkdir_nonexistent_parent_returns_nonzero() {
2037        let _g = crate::test_util::global_state_lock();
2038        let r = domkdir("mkdir", "/__never_real_parent__/foo", 0o755, 0);
2039        assert_ne!(r, 0, "nonexistent parent → error");
2040    }
2041
2042    /// c:1222 — `getnumeric("0")` returns (0, no-error).
2043    #[test]
2044    fn getnumeric_zero_no_error_pin() {
2045        let mut err = 0i32;
2046        let v = getnumeric("0", &mut err);
2047        assert_eq!(v, 0, "0 → 0");
2048        assert_eq!(err, 0, "no error flag on 0");
2049    }
2050
2051    /// c:1222 — `getnumeric` returns u64 (compile-time type pin).
2052    #[test]
2053    fn getnumeric_returns_u64_type() {
2054        let mut err = 0i32;
2055        let _: u64 = getnumeric("0", &mut err);
2056    }
2057
2058    /// c:1222 — `getnumeric` is deterministic for stable input.
2059    #[test]
2060    fn getnumeric_is_deterministic() {
2061        for s in ["0", "42", "100", "garbage", ""] {
2062            let mut err = 0i32;
2063            let first = getnumeric(s, &mut err);
2064            for _ in 0..3 {
2065                let mut err2 = 0i32;
2066                let v = getnumeric(s, &mut err2);
2067                assert_eq!(v, first, "getnumeric({:?}) must be deterministic", s);
2068            }
2069        }
2070    }
2071
2072    /// c:1132 — `setup_` returns i32 (compile-time type pin).
2073    #[test]
2074    fn files_setup_returns_i32_type() {
2075        let _g = crate::test_util::global_state_lock();
2076        let _: i32 = setup_(std::ptr::null());
2077    }
2078
2079    /// c:1139 — features list non-empty.
2080    #[test]
2081    fn files_features_nonempty() {
2082        let _g = crate::test_util::global_state_lock();
2083        let mut feats = Vec::new();
2084        features_(std::ptr::null(), &mut feats);
2085        assert!(!feats.is_empty(), "files module advertises ≥1 feature");
2086    }
2087
2088    /// c:1139 — features use b:/p: prefix per zsh module spec.
2089    #[test]
2090    fn files_features_use_canonical_prefix() {
2091        let _g = crate::test_util::global_state_lock();
2092        let mut feats = Vec::new();
2093        features_(std::ptr::null(), &mut feats);
2094        for f in &feats {
2095            assert!(
2096                f.starts_with("b:") || f.starts_with("p:"),
2097                "feature {:?} must use b:/p: prefix",
2098                f
2099            );
2100        }
2101    }
2102
2103    /// c:116 — `bin_sync` is idempotent (safe to call repeatedly).
2104    #[test]
2105    fn bin_sync_idempotent_full_sweep() {
2106        let _g = crate::test_util::global_state_lock();
2107        let ops = empty_ops();
2108        for _ in 0..5 {
2109            let r = bin_sync("sync", &[], &ops, 0);
2110            assert_eq!(r, 0, "sync always returns 0");
2111        }
2112    }
2113
2114    /// c:1162 + c:1169 — cleanup/finish idempotent.
2115    #[test]
2116    fn files_cleanup_finish_idempotent() {
2117        let _g = crate::test_util::global_state_lock();
2118        for _ in 0..5 {
2119            assert_eq!(cleanup_(std::ptr::null()), 0);
2120            assert_eq!(finish_(std::ptr::null()), 0);
2121        }
2122    }
2123
2124    /// c:222 — `domkdir` creates real directory then succeeds.
2125    #[test]
2126    fn domkdir_real_path_succeeds() {
2127        let _g = crate::test_util::global_state_lock();
2128        let dir = tempfile::tempdir().unwrap();
2129        let target = dir.path().join("new_dir");
2130        let r = domkdir("mkdir", target.to_str().unwrap(), 0o755, 0);
2131        assert_eq!(r, 0, "creating new dir under tempdir → 0");
2132        assert!(target.exists(), "directory was created");
2133        assert!(target.is_dir(), "result is a directory");
2134    }
2135
2136    // ═══════════════════════════════════════════════════════════════════
2137    // Additional C-parity tests for Src/Modules/files.c
2138    // c:116 bin_sync / c:137 bin_mkdir / c:222 domkdir / c:275 bin_rmdir /
2139    // c:347 bin_ln / c:757 bin_rm / c:845 bin_chmod / c:963 bin_chown
2140    // ═══════════════════════════════════════════════════════════════════
2141
2142    /// c:116 — `bin_sync` returns i32 (compile-time pin, alt).
2143    #[test]
2144    fn bin_sync_returns_i32_pin_alt() {
2145        let _g = crate::test_util::global_state_lock();
2146        let ops = empty_ops();
2147        let _: i32 = bin_sync("sync", &[], &ops, 0);
2148    }
2149
2150    /// c:116 — `bin_sync` is deterministic (always returns 0).
2151    #[test]
2152    fn bin_sync_always_returns_zero() {
2153        let _g = crate::test_util::global_state_lock();
2154        let ops = empty_ops();
2155        for _ in 0..10 {
2156            assert_eq!(bin_sync("sync", &[], &ops, 0), 0, "sync always returns 0");
2157        }
2158    }
2159
2160    /// c:137 — `bin_mkdir` no-args returns nonzero (usage error).
2161    #[test]
2162    fn bin_mkdir_no_args_returns_nonzero() {
2163        let _g = crate::test_util::global_state_lock();
2164        let ops = empty_ops();
2165        let r = bin_mkdir("mkdir", &[], &ops, 0);
2166        assert_ne!(r, 0, "mkdir no args → usage error");
2167    }
2168
2169    /// c:275 — `bin_rmdir` no-args returns nonzero (usage error).
2170    #[test]
2171    fn bin_rmdir_no_args_returns_nonzero() {
2172        let _g = crate::test_util::global_state_lock();
2173        let ops = empty_ops();
2174        let r = bin_rmdir("rmdir", &[], &ops, 0);
2175        assert_ne!(r, 0, "rmdir no args → usage error");
2176    }
2177
2178    /// c:347 — `bin_ln` no-args returns nonzero (usage error, alt).
2179    #[test]
2180    fn bin_ln_no_args_usage_error_alt() {
2181        let _g = crate::test_util::global_state_lock();
2182        let ops = empty_ops();
2183        let r = bin_ln("ln", &[], &ops, 0);
2184        assert_ne!(r, 0, "ln no args → usage error");
2185    }
2186
2187    /// c:757 — `bin_rm` no-args returns nonzero (usage error, alt).
2188    #[test]
2189    fn bin_rm_no_args_usage_error_alt() {
2190        let _g = crate::test_util::global_state_lock();
2191        let ops = empty_ops();
2192        let r = bin_rm("rm", &[], &ops, 0);
2193        assert_ne!(r, 0, "rm no args → usage error");
2194    }
2195
2196    /// c:845 — `bin_chmod` no-args returns nonzero (usage error, alt).
2197    #[test]
2198    fn bin_chmod_no_args_usage_error_alt() {
2199        let _g = crate::test_util::global_state_lock();
2200        let ops = empty_ops();
2201        let r = bin_chmod("chmod", &[], &ops, 0);
2202        assert_ne!(r, 0, "chmod no args → usage error");
2203    }
2204
2205    /// c:963 — `bin_chown` no-args returns nonzero (usage error, alt).
2206    #[test]
2207    fn bin_chown_no_args_usage_error_alt() {
2208        let _g = crate::test_util::global_state_lock();
2209        let ops = empty_ops();
2210        let r = bin_chown("chown", &[], &ops, 0);
2211        assert_ne!(r, 0, "chown no args → usage error");
2212    }
2213
2214    /// c:222 — `domkdir` on already-existing dir returns nonzero (EEXIST).
2215    #[test]
2216    fn domkdir_existing_dir_returns_nonzero() {
2217        let _g = crate::test_util::global_state_lock();
2218        let r = domkdir("mkdir", "/tmp", 0o755, 0);
2219        assert_ne!(r, 0, "mkdir /tmp (already exists) must error");
2220    }
2221
2222    /// c:222 — `domkdir` empty path returns nonzero (no path).
2223    #[test]
2224    fn domkdir_empty_path_returns_nonzero() {
2225        let _g = crate::test_util::global_state_lock();
2226        let r = domkdir("mkdir", "", 0o755, 0);
2227        assert_ne!(r, 0, "mkdir empty path must error");
2228    }
2229
2230    /// c:222 — `domkdir` returns i32 (compile-time pin, alt).
2231    #[test]
2232    fn domkdir_returns_i32_pin_alt() {
2233        let _g = crate::test_util::global_state_lock();
2234        let _: i32 = domkdir("mkdir", "/__nonexistent_xyz__", 0o755, 0);
2235    }
2236}