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            crate::ported::utils::zsh_errno_msg(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::utils::zsh_errno_msg(std::io::Error::last_os_error().raw_os_error().unwrap_or(0))
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, crate::ported::utils::zsh_errno_msg(e.raw_os_error().unwrap_or(0)))); // c:308 `%e`
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 — `%e` (zsh_errno_msg: strerror, first letter lowered)
583        // without the " (os error N)" suffix io::Error Display appends.
584        zwarnnam(
585            nam,
586            &format!("`{}': {}", errfile, crate::ported::utils::zsh_errno_msg(osek.raw_os_error().unwrap_or(0))),
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, crate::ported::utils::zsh_errno_msg(e.raw_os_error().unwrap_or(0)))); // c:492 `%e`
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::utils::zsh_errno_msg(std::io::Error::last_os_error().raw_os_error().unwrap_or(0))),
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::utils::zsh_errno_msg(std::io::Error::last_os_error().raw_os_error().unwrap_or(0))),
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::utils::zsh_errno_msg(std::io::Error::last_os_error().raw_os_error().unwrap_or(0))),
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::utils::zsh_errno_msg(std::io::Error::last_os_error().raw_os_error().unwrap_or(0))),
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::utils::zsh_errno_msg(std::io::Error::last_os_error().raw_os_error().unwrap_or(0))),
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(m: *const module, f: &Mutex<crate::ported::zsh_h::features>, enables: &mut Option<Vec<i32>>) -> i32 {
1323    // c:3392 — the name-keyed variant in src/ported/module.rs; this
1324    // module ships no `Features` descriptor tables for the per-feature
1325    // ADDED bit to live on (see MODULE_FEATURE_ENABLES there).
1326    crate::ported::module::handlefeatures("zsh/files", &featuresarray(m, f), enables)
1327}
1328
1329// WARNING: NOT IN FILES.C — Rust-only module-framework shim.
1330// C uses generic featuresarray/handlefeatures/setfeatureenables from
1331// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1332// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1333fn setfeatureenables(
1334    _m: *const module,
1335    _f: &Mutex<crate::ported::zsh_h::features>,
1336    _e: Option<&[i32]>,
1337) -> i32 {
1338    0
1339}
1340
1341// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1342// ─── RUST-ONLY ACCESSORS ───
1343//
1344// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1345// RwLock<T>>` globals declared above. C zsh uses direct global
1346// access; Rust needs these wrappers because `OnceLock::get_or_init`
1347// is the only way to lazily construct shared state. These ported sit
1348// here so the body of this file reads in C source order without
1349// the accessor wrappers interleaved between real port ported.
1350// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1351
1352// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1353// ─── RUST-ONLY ACCESSORS ───
1354//
1355// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1356// RwLock<T>>` globals declared above. C zsh uses direct global
1357// access; Rust needs these wrappers because `OnceLock::get_or_init`
1358// is the only way to lazily construct shared state. These ported sit
1359// here so the body of this file reads in C source order without
1360// the accessor wrappers interleaved between real port ported.
1361// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1362
1363// WARNING: NOT IN FILES.C — Rust-only module-framework shim.
1364// C uses generic featuresarray/handlefeatures/setfeatureenables from
1365// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1366// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1367fn module_features() -> &'static Mutex<crate::ported::zsh_h::features> {
1368    MODULE_FEATURES.get_or_init(|| {
1369        Mutex::new(crate::ported::zsh_h::features {
1370            bn_list: None,
1371            bn_size: 18,
1372            cd_list: None,
1373            cd_size: 0,
1374            mf_list: None,
1375            mf_size: 0,
1376            pd_list: None,
1377            pd_size: 0,
1378            n_abstract: 0,
1379        })
1380    })
1381}
1382
1383#[cfg(test)]
1384mod tests {
1385    use super::*;
1386
1387    fn empty_ops() -> options {
1388        options {
1389            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1390            args: Vec::new(),
1391            argscount: 0,
1392            argsalloc: 0,
1393        }
1394    }
1395
1396    /// c:115 — `domkdir` against a path that already exists must
1397    /// surface mkdir(2)'s EEXIST. Silent success would mask
1398    /// existing-directory clobbering — a real safety regression.
1399    #[test]
1400    fn domkdir_fails_when_path_already_exists() {
1401        let _g = crate::test_util::global_state_lock();
1402        let tmp = std::env::temp_dir();
1403        assert_ne!(domkdir("mkdir", tmp.to_str().unwrap(), 0o755, 0), 0);
1404    }
1405
1406    /// c:75-76 — `mkdir -m <invalid-octal>` early-returns 1 BEFORE
1407    /// the per-arg mkdir loop. Catches a regression where the bad
1408    /// mode silently falls through to the umask-derived default.
1409    #[test]
1410    fn mkdir_with_invalid_mode_short_circuits_before_filesystem_op() {
1411        let _g = crate::test_util::global_state_lock();
1412        let mut ops = empty_ops();
1413        ops.ind[b'm' as usize] = (1 << 2) | 1;
1414        ops.args.push("not-octal".to_string());
1415        // Path is bogus on purpose: if the mode-parse early-return
1416        // failed, mkdir(2) would still try this path and fail
1417        // differently. The assertion catches the right error origin.
1418        let r = bin_mkdir(
1419            "mkdir",
1420            &["/tmp/zshrs_test_invalid_mode".to_string()],
1421            &ops,
1422            0,
1423        );
1424        assert_eq!(r, 1);
1425    }
1426
1427    /// c:115-150 — `domkdir` with `p=1` against a path that already
1428    /// exists AS A DIRECTORY must return 0 (success). This is the
1429    /// `mkdir -p` idempotence rule: re-running `mkdir -p /existing`
1430    /// must NOT error.
1431    #[test]
1432    fn domkdir_with_p_flag_on_existing_dir_returns_zero() {
1433        let _g = crate::test_util::global_state_lock();
1434        let tmp = std::env::temp_dir();
1435        let r = domkdir("mkdir", tmp.to_str().unwrap(), 0o755, /*p=*/ 1);
1436        assert_eq!(r, 0, "mkdir -p /tmp must succeed when /tmp already exists");
1437    }
1438
1439    /// c:115-150 — same path with `p=0` (`mkdir` without -p) must
1440    /// FAIL when the path already exists. Symmetrical to the above:
1441    /// behavior pivots entirely on the `p` flag.
1442    #[test]
1443    fn domkdir_without_p_on_existing_dir_fails() {
1444        let _g = crate::test_util::global_state_lock();
1445        let tmp = std::env::temp_dir();
1446        let r = domkdir("mkdir", tmp.to_str().unwrap(), 0o755, /*p=*/ 0);
1447        assert_ne!(r, 0, "mkdir (no -p) on existing dir must fail per POSIX");
1448    }
1449
1450    /// c:80-115 + c:builtin.c:430-435 — `bin_mkdir` with no args.
1451    /// The C body itself returns 0 (per-arg loop has no iterations),
1452    /// but the BUILTIN dispatch table declares `minargs=1`
1453    /// (Src/Modules/files.c:810), so the framework rejects empty
1454    /// argv with "not enough arguments" → exit 1 before the body
1455    /// runs. zshrs's bin_mkdir self-enforces the same minargs check
1456    /// for direct-call paths (test / fusevm bridge) so the
1457    /// user-visible `mkdir; echo $?` parity holds.
1458    #[test]
1459    fn bin_mkdir_with_no_args_returns_nonzero() {
1460        let _g = crate::test_util::global_state_lock();
1461        let ops = empty_ops();
1462        let r = bin_mkdir("mkdir", &[], &ops, 0);
1463        assert_ne!(r, 0, "bin_mkdir on empty argv → minargs error");
1464    }
1465
1466    /// c:80-115 — `bin_mkdir -p` should successfully create a deeply
1467    /// nested path. Catches a regression where the nested-walk logic
1468    /// (c:88-101) leaves a parent half-created and reports failure.
1469    #[test]
1470    fn bin_mkdir_p_creates_nested_path() {
1471        let _g = crate::test_util::global_state_lock();
1472        let mut ops = empty_ops();
1473        ops.ind[b'p' as usize] = (1 << 2) | 1;
1474        // Unique under /tmp to keep tests independent of one another
1475        let pid = std::process::id();
1476        let base = format!("/tmp/zshrs_test_mkdir_p_{}", pid);
1477        let nested = format!("{}/a/b/c", base);
1478        let _ = std::fs::remove_dir_all(&base);
1479        let r = bin_mkdir("mkdir", &[nested.clone()], &ops, 0);
1480        assert_eq!(r, 0, "mkdir -p should create the whole chain");
1481        assert!(
1482            std::path::Path::new(&nested).is_dir(),
1483            "leaf dir {} should exist after mkdir -p",
1484            nested
1485        );
1486        let _ = std::fs::remove_dir_all(&base);
1487    }
1488
1489    /// c:81-83 — bin_mkdir's trailing-slash trim. `bin_mkdir /tmp/foo/`
1490    /// must call domkdir with the trailing slash stripped. Verified
1491    /// through the happy-path: the directory ends up at the trimmed
1492    /// path, even though the user supplied the trailing slash.
1493    #[test]
1494    fn bin_mkdir_strips_trailing_slashes() {
1495        let _g = crate::test_util::global_state_lock();
1496        let pid = std::process::id();
1497        let target = format!("/tmp/zshrs_test_mkdir_trailing_{}/", pid);
1498        let _ = std::fs::remove_dir_all(target.trim_end_matches('/'));
1499        let ops = empty_ops();
1500        let r = bin_mkdir("mkdir", &[target.clone()], &ops, 0);
1501        assert_eq!(r, 0, "trailing slash should not break mkdir");
1502        assert!(std::path::Path::new(target.trim_end_matches('/')).is_dir());
1503        let _ = std::fs::remove_dir_all(target.trim_end_matches('/'));
1504    }
1505
1506    /// c:150-200 — `bin_rmdir` on a path that does not exist returns
1507    /// non-zero, matching rmdir(2)'s ENOENT contract. Regression
1508    /// suppressing the per-arg error accumulator would mask this.
1509    #[test]
1510    fn bin_rmdir_on_nonexistent_path_returns_nonzero() {
1511        let _g = crate::test_util::global_state_lock();
1512        let ops = empty_ops();
1513        let r = bin_rmdir(
1514            "rmdir",
1515            &["/__definitely_not_a_dir_xyzzy_zshrs_test__".to_string()],
1516            &ops,
1517            0,
1518        );
1519        assert_ne!(r, 0, "rmdir of nonexistent path must report failure");
1520    }
1521
1522    /// c:150-200 + c:builtin.c:430-435 — same framework minargs
1523    /// story as bin_mkdir above: BUILTIN table sets `minargs=1`,
1524    /// dispatch rejects empty argv → exit 1. zshrs self-enforces.
1525    #[test]
1526    fn bin_rmdir_with_no_args_returns_nonzero() {
1527        let _g = crate::test_util::global_state_lock();
1528        let ops = empty_ops();
1529        let r = bin_rmdir("rmdir", &[], &ops, 0);
1530        assert_ne!(r, 0, "rmdir empty argv → minargs error");
1531    }
1532
1533    /// c:150-200 — `bin_rmdir` happy path: create then remove. Proves
1534    /// the rmdir(2) call is actually wired through.
1535    #[test]
1536    fn bin_rmdir_removes_an_empty_directory() {
1537        let _g = crate::test_util::global_state_lock();
1538        let pid = std::process::id();
1539        let target = format!("/tmp/zshrs_test_rmdir_{}", pid);
1540        let _ = std::fs::remove_dir_all(&target);
1541        std::fs::create_dir(&target).expect("setup mkdir");
1542        let ops = empty_ops();
1543        let r = bin_rmdir("rmdir", &[target.clone()], &ops, 0);
1544        assert_eq!(r, 0, "rmdir on existing empty dir should succeed");
1545        assert!(
1546            !std::path::Path::new(&target).exists(),
1547            "directory should be gone after rmdir"
1548        );
1549    }
1550
1551    /// c:838-879 — `setup_` / `boot_` / `cleanup_` / `finish_` module
1552    /// lifecycle stubs. C versions are empty (`return 0`). The Rust
1553    /// port must match.
1554    #[test]
1555    fn module_lifecycle_shims_all_return_zero() {
1556        let _g = crate::test_util::global_state_lock();
1557        let null_module = std::ptr::null();
1558        assert_eq!(setup_(null_module), 0);
1559        assert_eq!(boot_(null_module), 0);
1560        assert_eq!(cleanup_(null_module), 0);
1561        assert_eq!(finish_(null_module), 0);
1562    }
1563
1564    /// c:616 + c:builtin.c:430-435 — same framework minargs story
1565    /// as bin_mkdir above. zshrs self-enforces.
1566    #[test]
1567    fn bin_rm_no_args_returns_nonzero() {
1568        let _g = crate::test_util::global_state_lock();
1569        let ops = empty_ops();
1570        let r = bin_rm("rm", &[], &ops, 0);
1571        assert_ne!(r, 0, "rm empty argv → minargs error");
1572    }
1573
1574    /// c:616 — `bin_rm` on a nonexistent path returns nonzero.
1575    #[test]
1576    fn bin_rm_nonexistent_path_returns_one() {
1577        let _g = crate::test_util::global_state_lock();
1578        let ops = empty_ops();
1579        let r = bin_rm("rm", &["/__zshrs_test_no_such_path__".to_string()], &ops, 0);
1580        assert_ne!(r, 0, "rm on nonexistent path must error");
1581    }
1582
1583    /// c:546 — `rm_leaf` on a real file removes it. Uses rmmagic
1584    /// struct (c:475-480) matching the port signature.
1585    #[test]
1586    fn rm_leaf_removes_existing_file() {
1587        let _g = crate::test_util::global_state_lock();
1588        let pid = std::process::id();
1589        let f = format!("/tmp/zshrs_test_rm_leaf_{}.txt", pid);
1590        let _ = std::fs::write(&f, "x");
1591        assert!(std::path::Path::new(&f).exists());
1592        let rmm = rmmagic {
1593            nam: "rm",
1594            opt_force: 1,
1595            opt_interact: 0,
1596            opt_unlinkdir: 0,
1597        };
1598        let r = rm_leaf(&f, &f, None, &rmm);
1599        assert_eq!(r, 0, "rm_leaf on existing file must succeed");
1600        assert!(
1601            !std::path::Path::new(&f).exists(),
1602            "file must be gone after rm_leaf"
1603        );
1604    }
1605
1606    /// c:546 — `rm_leaf` on a nonexistent path with opt_force=1
1607    /// returns 0 silently (matches POSIX `rm -f`). Pin so a regen
1608    /// that ignores force flag silently errors on `rm -f /missing`.
1609    #[test]
1610    fn rm_leaf_force_silently_succeeds_on_missing() {
1611        let _g = crate::test_util::global_state_lock();
1612        let rmm = rmmagic {
1613            nam: "rm",
1614            opt_force: 1,
1615            opt_interact: 0,
1616            opt_unlinkdir: 0,
1617        };
1618        let r = rm_leaf("/__never__", "/__never__", None, &rmm);
1619        assert_eq!(r, 0, "rm -f on missing path must succeed silently");
1620    }
1621
1622    /// c:655 — `bin_chmod` with no args returns 1 (the port has a
1623    /// "missing mode" arity guard at line 607). Pin the explicit
1624    /// error so a regen removing the guard silently mis-routes.
1625    #[test]
1626    fn bin_chmod_no_args_returns_one() {
1627        let _g = crate::test_util::global_state_lock();
1628        let ops = empty_ops();
1629        let r = bin_chmod("chmod", &[], &ops, 0);
1630        assert_eq!(r, 1, "missing mode must surface as error");
1631    }
1632
1633    /// c:725 — `bin_chown` with no args returns 1 (port arity guard).
1634    #[test]
1635    fn bin_chown_no_args_returns_one() {
1636        let _g = crate::test_util::global_state_lock();
1637        let ops = empty_ops();
1638        let r = bin_chown("chown", &[], &ops, 0);
1639        assert_eq!(r, 1, "missing owner must surface as error");
1640    }
1641
1642    /// c:200 — `bin_ln` with FEWER than 2 args is a usage error.
1643    #[test]
1644    fn bin_ln_one_arg_returns_nonzero() {
1645        let _g = crate::test_util::global_state_lock();
1646        let ops = empty_ops();
1647        let r = bin_ln("ln", &["/tmp".to_string()], &ops, 0);
1648        assert_ne!(r, 0, "ln with <2 args must error");
1649    }
1650
1651    /// c:53 — `bin_sync` ignores all args, returns 0 (fire-and-
1652    /// forget sync(2)).
1653    #[test]
1654    fn bin_sync_returns_zero_regardless_of_args() {
1655        let _g = crate::test_util::global_state_lock();
1656        let ops = empty_ops();
1657        assert_eq!(bin_sync("sync", &[], &ops, 0), 0);
1658        assert_eq!(bin_sync("sync", &["ignored".to_string()], &ops, 0), 0);
1659        assert_eq!(
1660            bin_sync("sync", &["a".to_string(), "b".to_string()], &ops, 0),
1661            0
1662        );
1663    }
1664
1665    // ─── zsh-corpus pins for bin_mkdir / bin_rmdir / bin_rm ─────────
1666
1667    /// `bin_mkdir` with no args returns non-zero (usage error).
1668    #[test]
1669    fn files_corpus_bin_mkdir_no_args_returns_nonzero() {
1670        let _g = crate::test_util::global_state_lock();
1671        let ops = empty_ops();
1672        let r = bin_mkdir("mkdir", &[], &ops, 0);
1673        assert_ne!(r, 0, "mkdir with no args = usage error");
1674    }
1675
1676    /// `bin_rmdir` with no args returns non-zero.
1677    #[test]
1678    fn files_corpus_bin_rmdir_no_args_returns_nonzero() {
1679        let _g = crate::test_util::global_state_lock();
1680        let ops = empty_ops();
1681        let r = bin_rmdir("rmdir", &[], &ops, 0);
1682        assert_ne!(r, 0, "rmdir with no args = usage error");
1683    }
1684
1685    /// `bin_rm` with no args returns non-zero (usage error).
1686    #[test]
1687    fn files_corpus_bin_rm_no_args_returns_nonzero() {
1688        let _g = crate::test_util::global_state_lock();
1689        let ops = empty_ops();
1690        let r = bin_rm("rm", &[], &ops, 0);
1691        assert_ne!(r, 0, "rm with no args = usage error");
1692    }
1693
1694    /// `bin_mkdir` creates a new directory successfully.
1695    #[test]
1696    fn files_corpus_bin_mkdir_creates_directory() {
1697        let _g = crate::test_util::global_state_lock();
1698        let ops = empty_ops();
1699        let dir = tempfile::tempdir().unwrap();
1700        let target = dir.path().join("newdir");
1701        let target_str = target.to_str().unwrap().to_string();
1702        let r = bin_mkdir("mkdir", &[target_str], &ops, 0);
1703        assert_eq!(r, 0, "mkdir on new path succeeds");
1704        assert!(target.exists(), "new dir actually created");
1705        assert!(target.is_dir(), "new path is a directory");
1706    }
1707
1708    /// `bin_rmdir` removes an empty dir.
1709    #[test]
1710    fn files_corpus_bin_rmdir_removes_empty_dir() {
1711        let _g = crate::test_util::global_state_lock();
1712        let ops = empty_ops();
1713        let dir = tempfile::tempdir().unwrap();
1714        let target = dir.path().join("toremove");
1715        std::fs::create_dir(&target).unwrap();
1716        assert!(target.exists());
1717        let r = bin_rmdir("rmdir", &[target.to_str().unwrap().to_string()], &ops, 0);
1718        assert_eq!(r, 0, "rmdir on empty dir succeeds");
1719        assert!(!target.exists(), "dir removed");
1720    }
1721
1722    /// `domkdir` creates a dir with the given mode.
1723    #[test]
1724    fn files_corpus_domkdir_creates_with_mode() {
1725        let _g = crate::test_util::global_state_lock();
1726        let dir = tempfile::tempdir().unwrap();
1727        let target = dir.path().join("modedir");
1728        let r = domkdir("mkdir", target.to_str().unwrap(), 0o755, 0);
1729        assert_eq!(r, 0);
1730        assert!(target.is_dir());
1731    }
1732
1733    /// `domkdir` fails if dir already exists (without -p flag).
1734    #[test]
1735    fn files_corpus_domkdir_existing_path_fails_without_p() {
1736        let _g = crate::test_util::global_state_lock();
1737        let dir = tempfile::tempdir().unwrap();
1738        let r = domkdir("mkdir", dir.path().to_str().unwrap(), 0o755, 0);
1739        assert_ne!(r, 0, "mkdir on existing dir without -p = error");
1740    }
1741
1742    // ═══════════════════════════════════════════════════════════════════
1743    // C-parity tests pinning Src/Modules/files.c.
1744    // ═══════════════════════════════════════════════════════════════════
1745
1746    /// `domkdir` creates a fresh dir and returns 0.
1747    /// C `Src/Modules/files.c:domkdir` — mkdir() syscall path.
1748    #[test]
1749    fn domkdir_fresh_path_returns_zero() {
1750        let _g = crate::test_util::global_state_lock();
1751        let parent = tempfile::tempdir().unwrap();
1752        let path = parent.path().join("newdir");
1753        let r = domkdir("mkdir", path.to_str().unwrap(), 0o755, 0);
1754        assert_eq!(r, 0, "fresh mkdir returns 0 (success)");
1755        assert!(path.exists(), "directory should exist after mkdir");
1756    }
1757
1758    /// `domkdir -p` creates parents as needed.
1759    /// C `Src/Modules/files.c:domkdir` with `p=1` flag:
1760    ///   recursively creates each intermediate directory if missing.
1761    /// ZSHRS BUG: Rust port at modules/files.rs:222 does NOT walk
1762    /// intermediate dirs — `mkdir -p a/b/c` fails when `a` and `b`
1763    /// don't exist. C-compatible `mkdir -p` is mandatory shell
1764    /// behavior; this breaks any script that relies on it.
1765    #[test]
1766    fn domkdir_with_p_creates_parents() {
1767        let _g = crate::test_util::global_state_lock();
1768        let parent = tempfile::tempdir().unwrap();
1769        let nested = parent.path().join("a/b/c");
1770        let r = domkdir("mkdir", nested.to_str().unwrap(), 0o755, 1);
1771        assert_eq!(r, 0, "mkdir -p with nested path returns 0");
1772        assert!(nested.exists(), "nested dir should exist after mkdir -p");
1773    }
1774
1775    // ═══════════════════════════════════════════════════════════════════
1776    // Additional C-parity tests for Src/Modules/files.c.
1777    // ═══════════════════════════════════════════════════════════════════
1778
1779    /// c:708 — `getnumeric("0")` returns 0 with err=0.
1780    #[test]
1781    fn getnumeric_zero_returns_zero_no_err() {
1782        let mut err = 0;
1783        let r = getnumeric("0", &mut err);
1784        assert_eq!(r, 0);
1785        assert_eq!(err, 0);
1786    }
1787
1788    /// c:708 — `getnumeric("12345")` returns 12345 with err=0.
1789    #[test]
1790    fn getnumeric_canonical_decimal() {
1791        let mut err = 0;
1792        let r = getnumeric("12345", &mut err);
1793        assert_eq!(r, 12345);
1794        assert_eq!(err, 0);
1795    }
1796
1797    /// c:725 — `getnumeric("100abc")` returns 100 but sets err=1
1798    /// (trailing non-digit).
1799    #[test]
1800    fn getnumeric_trailing_garbage_sets_err() {
1801        let mut err = 0;
1802        let r = getnumeric("100abc", &mut err);
1803        assert_eq!(r, 100, "digits consumed");
1804        assert_eq!(err, 1, "trailing non-digit sets err");
1805    }
1806
1807    /// c:713 — `getnumeric("abc")` returns 0 and sets err=1 (no leading digit).
1808    #[test]
1809    fn getnumeric_no_leading_digit_sets_err() {
1810        let mut err = 0;
1811        let r = getnumeric("abc", &mut err);
1812        assert_eq!(r, 0);
1813        assert_eq!(err, 1);
1814    }
1815
1816    /// c:708 — `getnumeric("")` empty input: no leading digit → err=1, ret=0.
1817    #[test]
1818    fn getnumeric_empty_sets_err() {
1819        let mut err = 0;
1820        let r = getnumeric("", &mut err);
1821        assert_eq!(r, 0);
1822        assert_eq!(err, 1, "empty input is invalid");
1823    }
1824
1825    /// c:713 — getnumeric with negative-sign prefix sets err (no leading digit).
1826    #[test]
1827    fn getnumeric_negative_sign_sets_err() {
1828        let mut err = 0;
1829        let r = getnumeric("-5", &mut err);
1830        assert_eq!(r, 0);
1831        assert_eq!(err, 1, "minus sign isn't a digit");
1832    }
1833
1834    /// c:53 — `bin_sync` always returns 0 (no-op success).
1835    #[test]
1836    fn bin_sync_returns_zero() {
1837        let _g = crate::test_util::global_state_lock();
1838        let ops = empty_ops();
1839        let r = bin_sync("sync", &[], &ops, 0);
1840        assert_eq!(r, 0, "sync builtin must return 0");
1841    }
1842
1843    /// c:222 — `domkdir` on existing dir returns nonzero (already exists).
1844    #[test]
1845    fn domkdir_existing_returns_nonzero() {
1846        let _g = crate::test_util::global_state_lock();
1847        let dir = tempfile::tempdir().unwrap();
1848        // Attempt mkdir of the already-existing temp dir.
1849        let r = domkdir("mkdir", dir.path().to_str().unwrap(), 0o755, 0);
1850        assert_ne!(r, 0, "existing dir → nonzero error");
1851    }
1852
1853    /// c:1205 — `mv_rename` on missing source path returns nonzero.
1854    #[test]
1855    fn mv_rename_missing_source_returns_nonzero() {
1856        let _g = crate::test_util::global_state_lock();
1857        let src = std::ffi::CString::new("/__nonexistent_zshrs_xyz__").unwrap();
1858        let dst = std::ffi::CString::new("/tmp/zshrs_mv_dst").unwrap();
1859        let r = mv_rename(&src, &dst);
1860        assert_ne!(r, 0, "missing source → error");
1861    }
1862
1863    /// c:1210 — `mv_symlink` to nonexistent target returns nonzero or
1864    /// succeeds creating dangling link. Test no panic.
1865    #[test]
1866    fn mv_symlink_no_panic_on_dangling() {
1867        let _g = crate::test_util::global_state_lock();
1868        let dir = tempfile::tempdir().unwrap();
1869        let dst_path = dir.path().join("zshrs_symlink_test");
1870        let src = std::ffi::CString::new("/__nonexistent_zshrs_xyz__").unwrap();
1871        let dst = std::ffi::CString::new(dst_path.to_str().unwrap()).unwrap();
1872        let _ = mv_symlink(&src, &dst);
1873    }
1874
1875    /// Lifecycle stubs return 0.
1876    #[test]
1877    fn files_lifecycle_stubs_return_zero() {
1878        let _g = crate::test_util::global_state_lock();
1879        assert_eq!(setup_(std::ptr::null()), 0);
1880        assert_eq!(boot_(std::ptr::null()), 0);
1881        assert_eq!(cleanup_(std::ptr::null()), 0);
1882        assert_eq!(finish_(std::ptr::null()), 0);
1883    }
1884
1885    // ═══════════════════════════════════════════════════════════════════
1886    // Additional C-parity tests for Src/Modules/files.c
1887    // c:116 bin_sync / c:137 bin_mkdir / c:275 bin_rmdir / c:347 bin_ln /
1888    // c:757 bin_rm / c:845 bin_chmod / c:963 bin_chown / c:1132+ lifecycle
1889    // ═══════════════════════════════════════════════════════════════════
1890
1891    /// c:116 — `bin_sync` returns u8 exit-code range.
1892    #[test]
1893    fn bin_sync_returns_in_exit_code_range() {
1894        let _g = crate::test_util::global_state_lock();
1895        let ops = empty_ops();
1896        let r = bin_sync("sync", &[], &ops, 0);
1897        assert!((0..256).contains(&r), "exit code {} must fit in u8", r);
1898    }
1899
1900    /// c:116 — `bin_sync` is idempotent (safe to call many times).
1901    #[test]
1902    fn bin_sync_idempotent() {
1903        let _g = crate::test_util::global_state_lock();
1904        let ops = empty_ops();
1905        for _ in 0..5 {
1906            let r = bin_sync("sync", &[], &ops, 0);
1907            assert_eq!(r, 0, "sync must return 0");
1908        }
1909    }
1910
1911    /// c:137 — `bin_mkdir` empty path returns nonzero.
1912    #[test]
1913    fn bin_mkdir_empty_path_returns_nonzero() {
1914        let _g = crate::test_util::global_state_lock();
1915        let ops = empty_ops();
1916        let r = bin_mkdir("mkdir", &["".to_string()], &ops, 0);
1917        assert_ne!(r, 0, "empty path → error");
1918    }
1919
1920    /// c:275 — `bin_rmdir` empty path returns nonzero.
1921    #[test]
1922    fn bin_rmdir_empty_path_returns_nonzero() {
1923        let _g = crate::test_util::global_state_lock();
1924        let ops = empty_ops();
1925        let r = bin_rmdir("rmdir", &["".to_string()], &ops, 0);
1926        assert_ne!(r, 0, "empty path → error");
1927    }
1928
1929    /// c:757 — `bin_rm` empty path returns nonzero.
1930    #[test]
1931    fn bin_rm_empty_path_returns_nonzero() {
1932        let _g = crate::test_util::global_state_lock();
1933        let ops = empty_ops();
1934        let r = bin_rm("rm", &["".to_string()], &ops, 0);
1935        assert_ne!(r, 0, "empty path → error");
1936    }
1937
1938    /// c:347 — `bin_ln` no args returns nonzero (usage error).
1939    #[test]
1940    fn bin_ln_no_args_returns_nonzero() {
1941        let _g = crate::test_util::global_state_lock();
1942        let ops = empty_ops();
1943        let r = bin_ln("ln", &[], &ops, 0);
1944        assert_ne!(r, 0, "no args → error");
1945    }
1946
1947    /// c:845 — `bin_chmod` no args returns nonzero.
1948    #[test]
1949    fn bin_chmod_no_args_returns_nonzero() {
1950        let _g = crate::test_util::global_state_lock();
1951        let ops = empty_ops();
1952        let r = bin_chmod("chmod", &[], &ops, 0);
1953        assert_ne!(r, 0, "no args → error");
1954    }
1955
1956    /// c:963 — `bin_chown` no args returns nonzero.
1957    #[test]
1958    fn bin_chown_no_args_returns_nonzero() {
1959        let _g = crate::test_util::global_state_lock();
1960        let ops = empty_ops();
1961        let r = bin_chown("chown", &[], &ops, 0);
1962        assert_ne!(r, 0, "no args → error");
1963    }
1964
1965    /// c:137-963 — all file-op builtin returns fit in u8 exit-code range.
1966    #[test]
1967    fn files_builtins_return_in_exit_code_range() {
1968        let _g = crate::test_util::global_state_lock();
1969        let ops = empty_ops();
1970        for r in [
1971            bin_mkdir("mkdir", &[], &ops, 0),
1972            bin_rmdir("rmdir", &[], &ops, 0),
1973            bin_rm("rm", &[], &ops, 0),
1974            bin_ln("ln", &[], &ops, 0),
1975            bin_chmod("chmod", &[], &ops, 0),
1976            bin_chown("chown", &[], &ops, 0),
1977        ] {
1978            assert!(
1979                (0..256).contains(&r),
1980                "exit code {} must fit in u8 range",
1981                r
1982            );
1983        }
1984    }
1985
1986    /// c:1132+ — full lifecycle setup→features→enables→boot→cleanup→finish.
1987    #[test]
1988    fn files_full_lifecycle_returns_zero_for_all() {
1989        let _g = crate::test_util::global_state_lock();
1990        let null = std::ptr::null();
1991        assert_eq!(setup_(null), 0);
1992        let mut feats = Vec::new();
1993        let _ = features_(null, &mut feats);
1994        let mut enables: Option<Vec<i32>> = None;
1995        let _ = enables_(null, &mut enables);
1996        assert_eq!(boot_(null), 0);
1997        assert_eq!(cleanup_(null), 0);
1998        assert_eq!(finish_(null), 0);
1999    }
2000
2001    // ═══════════════════════════════════════════════════════════════════
2002    // Additional C-parity tests for Src/Modules/files.c
2003    // c:116 bin_sync / c:137 bin_mkdir / c:222 domkdir / c:1222 getnumeric +
2004    // c:1132-1169 lifecycle type pins
2005    // ═══════════════════════════════════════════════════════════════════
2006
2007    /// c:116 — `bin_sync` returns i32 (compile-time type pin).
2008    #[test]
2009    fn bin_sync_returns_i32_type() {
2010        let _g = crate::test_util::global_state_lock();
2011        let ops = empty_ops();
2012        let _: i32 = bin_sync("sync", &[], &ops, 0);
2013    }
2014
2015    /// c:137 — `bin_mkdir` returns i32 (compile-time type pin).
2016    #[test]
2017    fn bin_mkdir_returns_i32_type() {
2018        let _g = crate::test_util::global_state_lock();
2019        let ops = empty_ops();
2020        let _: i32 = bin_mkdir("mkdir", &[], &ops, 0);
2021    }
2022
2023    /// c:222 — `domkdir` returns i32 (compile-time type pin).
2024    #[test]
2025    fn domkdir_returns_i32_type() {
2026        let _g = crate::test_util::global_state_lock();
2027        let _: i32 = domkdir("mkdir", "/__never_zshrs_xyz__", 0o755, 0);
2028    }
2029
2030    /// c:222 — `domkdir` for nonexistent parent fails.
2031    #[test]
2032    fn domkdir_nonexistent_parent_returns_nonzero() {
2033        let _g = crate::test_util::global_state_lock();
2034        let r = domkdir("mkdir", "/__never_real_parent__/foo", 0o755, 0);
2035        assert_ne!(r, 0, "nonexistent parent → error");
2036    }
2037
2038    /// c:1222 — `getnumeric("0")` returns (0, no-error).
2039    #[test]
2040    fn getnumeric_zero_no_error_pin() {
2041        let mut err = 0i32;
2042        let v = getnumeric("0", &mut err);
2043        assert_eq!(v, 0, "0 → 0");
2044        assert_eq!(err, 0, "no error flag on 0");
2045    }
2046
2047    /// c:1222 — `getnumeric` returns u64 (compile-time type pin).
2048    #[test]
2049    fn getnumeric_returns_u64_type() {
2050        let mut err = 0i32;
2051        let _: u64 = getnumeric("0", &mut err);
2052    }
2053
2054    /// c:1222 — `getnumeric` is deterministic for stable input.
2055    #[test]
2056    fn getnumeric_is_deterministic() {
2057        for s in ["0", "42", "100", "garbage", ""] {
2058            let mut err = 0i32;
2059            let first = getnumeric(s, &mut err);
2060            for _ in 0..3 {
2061                let mut err2 = 0i32;
2062                let v = getnumeric(s, &mut err2);
2063                assert_eq!(v, first, "getnumeric({:?}) must be deterministic", s);
2064            }
2065        }
2066    }
2067
2068    /// c:1132 — `setup_` returns i32 (compile-time type pin).
2069    #[test]
2070    fn files_setup_returns_i32_type() {
2071        let _g = crate::test_util::global_state_lock();
2072        let _: i32 = setup_(std::ptr::null());
2073    }
2074
2075    /// c:1139 — features list non-empty.
2076    #[test]
2077    fn files_features_nonempty() {
2078        let _g = crate::test_util::global_state_lock();
2079        let mut feats = Vec::new();
2080        features_(std::ptr::null(), &mut feats);
2081        assert!(!feats.is_empty(), "files module advertises ≥1 feature");
2082    }
2083
2084    /// c:1139 — features use b:/p: prefix per zsh module spec.
2085    #[test]
2086    fn files_features_use_canonical_prefix() {
2087        let _g = crate::test_util::global_state_lock();
2088        let mut feats = Vec::new();
2089        features_(std::ptr::null(), &mut feats);
2090        for f in &feats {
2091            assert!(
2092                f.starts_with("b:") || f.starts_with("p:"),
2093                "feature {:?} must use b:/p: prefix",
2094                f
2095            );
2096        }
2097    }
2098
2099    /// c:116 — `bin_sync` is idempotent (safe to call repeatedly).
2100    #[test]
2101    fn bin_sync_idempotent_full_sweep() {
2102        let _g = crate::test_util::global_state_lock();
2103        let ops = empty_ops();
2104        for _ in 0..5 {
2105            let r = bin_sync("sync", &[], &ops, 0);
2106            assert_eq!(r, 0, "sync always returns 0");
2107        }
2108    }
2109
2110    /// c:1162 + c:1169 — cleanup/finish idempotent.
2111    #[test]
2112    fn files_cleanup_finish_idempotent() {
2113        let _g = crate::test_util::global_state_lock();
2114        for _ in 0..5 {
2115            assert_eq!(cleanup_(std::ptr::null()), 0);
2116            assert_eq!(finish_(std::ptr::null()), 0);
2117        }
2118    }
2119
2120    /// c:222 — `domkdir` creates real directory then succeeds.
2121    #[test]
2122    fn domkdir_real_path_succeeds() {
2123        let _g = crate::test_util::global_state_lock();
2124        let dir = tempfile::tempdir().unwrap();
2125        let target = dir.path().join("new_dir");
2126        let r = domkdir("mkdir", target.to_str().unwrap(), 0o755, 0);
2127        assert_eq!(r, 0, "creating new dir under tempdir → 0");
2128        assert!(target.exists(), "directory was created");
2129        assert!(target.is_dir(), "result is a directory");
2130    }
2131
2132    // ═══════════════════════════════════════════════════════════════════
2133    // Additional C-parity tests for Src/Modules/files.c
2134    // c:116 bin_sync / c:137 bin_mkdir / c:222 domkdir / c:275 bin_rmdir /
2135    // c:347 bin_ln / c:757 bin_rm / c:845 bin_chmod / c:963 bin_chown
2136    // ═══════════════════════════════════════════════════════════════════
2137
2138    /// c:116 — `bin_sync` returns i32 (compile-time pin, alt).
2139    #[test]
2140    fn bin_sync_returns_i32_pin_alt() {
2141        let _g = crate::test_util::global_state_lock();
2142        let ops = empty_ops();
2143        let _: i32 = bin_sync("sync", &[], &ops, 0);
2144    }
2145
2146    /// c:116 — `bin_sync` is deterministic (always returns 0).
2147    #[test]
2148    fn bin_sync_always_returns_zero() {
2149        let _g = crate::test_util::global_state_lock();
2150        let ops = empty_ops();
2151        for _ in 0..10 {
2152            assert_eq!(bin_sync("sync", &[], &ops, 0), 0, "sync always returns 0");
2153        }
2154    }
2155
2156    /// c:137 — `bin_mkdir` no-args returns nonzero (usage error).
2157    #[test]
2158    fn bin_mkdir_no_args_returns_nonzero() {
2159        let _g = crate::test_util::global_state_lock();
2160        let ops = empty_ops();
2161        let r = bin_mkdir("mkdir", &[], &ops, 0);
2162        assert_ne!(r, 0, "mkdir no args → usage error");
2163    }
2164
2165    /// c:275 — `bin_rmdir` no-args returns nonzero (usage error).
2166    #[test]
2167    fn bin_rmdir_no_args_returns_nonzero() {
2168        let _g = crate::test_util::global_state_lock();
2169        let ops = empty_ops();
2170        let r = bin_rmdir("rmdir", &[], &ops, 0);
2171        assert_ne!(r, 0, "rmdir no args → usage error");
2172    }
2173
2174    /// c:347 — `bin_ln` no-args returns nonzero (usage error, alt).
2175    #[test]
2176    fn bin_ln_no_args_usage_error_alt() {
2177        let _g = crate::test_util::global_state_lock();
2178        let ops = empty_ops();
2179        let r = bin_ln("ln", &[], &ops, 0);
2180        assert_ne!(r, 0, "ln no args → usage error");
2181    }
2182
2183    /// c:757 — `bin_rm` no-args returns nonzero (usage error, alt).
2184    #[test]
2185    fn bin_rm_no_args_usage_error_alt() {
2186        let _g = crate::test_util::global_state_lock();
2187        let ops = empty_ops();
2188        let r = bin_rm("rm", &[], &ops, 0);
2189        assert_ne!(r, 0, "rm no args → usage error");
2190    }
2191
2192    /// c:845 — `bin_chmod` no-args returns nonzero (usage error, alt).
2193    #[test]
2194    fn bin_chmod_no_args_usage_error_alt() {
2195        let _g = crate::test_util::global_state_lock();
2196        let ops = empty_ops();
2197        let r = bin_chmod("chmod", &[], &ops, 0);
2198        assert_ne!(r, 0, "chmod no args → usage error");
2199    }
2200
2201    /// c:963 — `bin_chown` no-args returns nonzero (usage error, alt).
2202    #[test]
2203    fn bin_chown_no_args_usage_error_alt() {
2204        let _g = crate::test_util::global_state_lock();
2205        let ops = empty_ops();
2206        let r = bin_chown("chown", &[], &ops, 0);
2207        assert_ne!(r, 0, "chown no args → usage error");
2208    }
2209
2210    /// c:222 — `domkdir` on already-existing dir returns nonzero (EEXIST).
2211    #[test]
2212    fn domkdir_existing_dir_returns_nonzero() {
2213        let _g = crate::test_util::global_state_lock();
2214        let r = domkdir("mkdir", "/tmp", 0o755, 0);
2215        assert_ne!(r, 0, "mkdir /tmp (already exists) must error");
2216    }
2217
2218    /// c:222 — `domkdir` empty path returns nonzero (no path).
2219    #[test]
2220    fn domkdir_empty_path_returns_nonzero() {
2221        let _g = crate::test_util::global_state_lock();
2222        let r = domkdir("mkdir", "", 0o755, 0);
2223        assert_ne!(r, 0, "mkdir empty path must error");
2224    }
2225
2226    /// c:222 — `domkdir` returns i32 (compile-time pin, alt).
2227    #[test]
2228    fn domkdir_returns_i32_pin_alt() {
2229        let _g = crate::test_util::global_state_lock();
2230        let _: i32 = domkdir("mkdir", "/__nonexistent_xyz__", 0o755, 0);
2231    }
2232}