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