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}