zsh/ported/modules/clone.rs
1//! `zsh/clone` module — port of `Src/Modules/clone.c`.
2//!
3//! Top-level declaration order matches C source line-by-line:
4//! - `bin_clone(nam, args, ops, func)` c:43
5//! - `static struct builtin bintab[]` c:109
6//! - `static struct features module_features` c:113
7//! - `setup_(m)` / `features_(m, features)` /
8//! `enables_(m, enables)` / `boot_(m)` /
9//! `cleanup_(m)` / `finish_(m)` c:122-162
10
11#![allow(non_camel_case_types)]
12#![allow(non_upper_case_globals)]
13#![allow(non_snake_case)]
14
15use std::sync::atomic::{AtomicI32, Ordering};
16use std::sync::{Mutex, OnceLock};
17
18use crate::ported::init::init_io;
19use crate::ported::params::setsparam;
20use crate::ported::utils::{unmetafy, zerrnam, zwarnnam};
21use crate::ported::zsh_h::{features, module, options, MAX_OPS};
22use std::ffi::CString;
23use std::os::unix::io::RawFd;
24// =====================================================================
25// bin_clone(char *nam, char **args, UNUSED(Options ops), UNUSED(int func)) c:43
26// =====================================================================
27
28/// Port of `bin_clone(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/clone.c:44`.
29///
30/// C signature mirrored verbatim:
31/// ```c
32/// static int
33/// bin_clone(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
34/// ```
35#[cfg(unix)]
36#[allow(unused_variables)]
37pub fn bin_clone(nam: &str, args: &[String], ops: &options, func: i32) -> i32 {
38 // c:44
39
40 // c:46 — `int ttyfd, pid, cttyfd;`
41 let ttyfd: RawFd;
42 let pid: libc::pid_t;
43 let cttyfd: RawFd;
44
45 // C: BUILTIN("clone", 0, bin_clone, 1, 1, 0, NULL, NULL) (clone.c:110)
46 // guarantees args[0] exists; defend against direct calls anyway.
47 let arg0_in: &str = match args.first() {
48 Some(a) => a.as_str(),
49 None => {
50 zwarnnam(nam, "terminal required");
51 return 1;
52 }
53 };
54
55 // c:48 — `unmetafy(*args, NULL);` strip Meta escapes before open(2).
56 let mut arg0_bytes = arg0_in.as_bytes().to_vec();
57 unmetafy(&mut arg0_bytes);
58 let arg0: String = String::from_utf8_lossy(&arg0_bytes).into_owned();
59
60 let tty_c = match CString::new(arg0.clone()) {
61 Ok(c) => c,
62 Err(_) => {
63 zwarnnam(nam, &format!("{}: invalid tty path", arg0));
64 return 1;
65 }
66 };
67
68 // c:49 — `ttyfd = open(*args, O_RDWR|O_NOCTTY);`
69 ttyfd = unsafe { libc::open(tty_c.as_ptr(), libc::O_RDWR | libc::O_NOCTTY) };
70 if ttyfd < 0 {
71 // c:50
72 zwarnnam(
73 nam,
74 &format!("{}: {}", arg0, std::io::Error::last_os_error()),
75 ); // c:51
76 return 1; // c:52
77 }
78 // c:54 — `pid = fork();`
79 pid = unsafe { libc::fork() };
80 if pid == 0 {
81 // c:55 if (!pid)
82 // c:56 — `clearjobtab(0);` — route through the canonical
83 // free-fn port at Src/jobs.c:1780 (jobs.rs:1778) instead of
84 // inlining a JOBTAB-clear. The canonical fn does more than
85 // `Vec::clear()`: it honors POSIXJOBS to zero `oldmaxjob`
86 // (c:1786-1787) and walks the table per-slot — inlining
87 // skipped both. The `table` param is the executor-side
88 // legacy handle and is unused inside clearjobtab's body
89 // (see the `let _ = table;` line at jobs.rs:1780).
90 let mut dummy = crate::exec_jobs::JobTable::new();
91 crate::ported::jobs::clearjobtab(&mut dummy, 0);
92 // c:57-58 — ppid = getppid(); mypid = getpid();
93 // ppid / mypid are zsh-globals from Src/exec.c — Rust port
94 // reads them on demand via libc; assignments here are
95 // effectively no-ops since there's no cached state to mutate.
96 let mypid = unsafe { libc::getpid() };
97 // c:60 — if (setsid() != mypid) ...
98 if unsafe { libc::setsid() } != mypid {
99 zwarnnam(
100 nam,
101 &format!(
102 "failed to create new session: {}",
103 std::io::Error::last_os_error()
104 ), // c:61
105 );
106 }
107 // c:67-69 — dup2(ttyfd, 0/1/2);
108 unsafe {
109 libc::dup2(ttyfd, 0); // c:67
110 libc::dup2(ttyfd, 1); // c:68
111 libc::dup2(ttyfd, 2); // c:69
112 }
113 // c:70-71 — if (ttyfd > 2) close(ttyfd);
114 if ttyfd > 2 {
115 unsafe { libc::close(ttyfd) };
116 }
117 // c:72 — `closem(FDT_UNUSED, 0);` — close every fdtable-tracked
118 // internal fd above the cutoff EXCEPT FDT_PROC_SUBST and
119 // FDT_EXTERNAL (the `all == 0` arg gates those off). After the
120 // dup2(ttyfd, 0/1/2) above the child still inherits every
121 // internal fd the parent had open — coprocess pipes from prior
122 // sessions, autoload module fds, opened-via-exec fds, etc.
123 // Without this call the cloned shell carries the parent's
124 // entire internal fd table forward, leaking file descriptors
125 // until the child happens to close them via builtin use.
126 //
127 // Prior port commented "no-op matches C behaviour" and skipped
128 // the call — that read was wrong: bin_clone does NOT exec, so
129 // the kernel's exec-time auto-close on FD_CLOEXEC never fires,
130 // and there's no other path that closes inherited internal
131 // fds in the child. Route through the canonical closem at
132 // exec.rs:2871 (port of Src/exec.c:4546).
133 crate::ported::exec::closem(crate::ported::zsh_h::FDT_UNUSED, 0);
134 // c:73-74 — close(coprocin); close(coprocout);
135 unsafe { libc::close(coprocin.load(Ordering::Relaxed)) }; // c:73
136 unsafe { libc::close(coprocout.load(Ordering::Relaxed)) }; // c:74
137 /* Acquire a controlling terminal */ // c:75
138 // c:76 — cttyfd = open(*args, O_RDWR);
139 cttyfd = unsafe { libc::open(tty_c.as_ptr(), libc::O_RDWR) };
140 if cttyfd == -1 {
141 // c:77
142 zwarnnam(nam, &format!("{}", std::io::Error::last_os_error())); // c:78
143 } else {
144 // c:79
145 // c:81 — ioctl(cttyfd, TIOCSCTTY, 0);
146 #[cfg(any(target_os = "linux", target_os = "macos"))]
147 unsafe {
148 libc::ioctl(cttyfd, libc::TIOCSCTTY as _, 0);
149 }
150 unsafe { libc::close(cttyfd) }; // c:83
151 }
152 /* check if we acquired the tty successfully */
153 // c:85
154 // c:86 — cttyfd = open("/dev/tty", O_RDWR);
155 let dev_tty = b"/dev/tty\0".as_ptr() as *const libc::c_char;
156 let cttyfd2 = unsafe { libc::open(dev_tty, libc::O_RDWR) };
157 if cttyfd2 == -1 {
158 // c:87
159 zwarnnam(
160 // c:88
161 nam,
162 &format!(
163 "could not make {} my controlling tty, job control disabled",
164 arg0
165 ),
166 );
167 } else {
168 // c:90
169 unsafe { libc::close(cttyfd2) }; // c:91
170 }
171
172 /* Clear mygrp so that acquire_pgrp() gets the new process group.
173 * (acquire_pgrp() is called from init_io()) */
174 // c:93-94
175 mypgrp.store(0, Ordering::Relaxed); // c:95 mypgrp = 0;
176 init_io(None); // c:96 init_io(NULL);
177 let tty_name = ttystrname.lock().unwrap().clone();
178 setsparam("TTY", &tty_name); // c:97 setsparam("TTY", ztrdup(ttystrname));
179 } else {
180 // c:99
181 unsafe { libc::close(ttyfd) }; // c:100
182 }
183 if pid < 0 {
184 // c:101
185 zerrnam(
186 nam,
187 &format!("fork failed: {}", std::io::Error::last_os_error()),
188 ); // c:102
189 return 1; // c:103
190 }
191 lastpid.store(pid as i32, Ordering::Relaxed); // c:105 lastpid = pid;
192 0 // c:106
193}
194
195/// Port of `bin_clone(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/clone.c:44`.
196#[cfg(not(unix))]
197#[allow(unused_variables)]
198pub fn bin_clone(nam: &str, args: &[String], ops: &options, func: i32) -> i32 {
199 zwarnnam(nam, "not available on this host");
200 1
201}
202
203// =====================================================================
204// static struct builtin bintab[] c:109
205// static struct features module_features c:113
206// =====================================================================
207
208// `bintab` — port of `static struct builtin bintab[]` (clone.c:109):
209// `BUILTIN("clone", 0, bin_clone, 1, 1, 0, NULL, NULL)`.
210
211// `module_features` — port of `static struct features module_features`
212// from clone.c:113. Uses canonical slice-based `module::Features`,
213// fed into `module::featuresarray`/`handlefeatures` from module.c.
214
215// `Module` instance synthesized for the canonical featuresarray/
216// handlefeatures API (which takes `&Module` to read `m->node.nam`).
217// The C hooks receive a raw `Module m` pointer; the Rust port
218// produces an equivalent `module::Module` on demand.
219
220// =====================================================================
221// setup_(UNUSED(Module m)) c:122
222// =====================================================================
223
224/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/clone.c:123`.
225#[allow(unused_variables)]
226pub fn setup_(m: *const module) -> i32 {
227 // c:123
228 0 // c:138
229}
230
231/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/clone.c:130`.
232/// C body: `*features = featuresarray(m, &module_features); return 0;`
233pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
234 // c:130
235 *features = featuresarray(m, module_features());
236 0 // c:145
237}
238
239/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/clone.c:138`.
240/// C body: `return handlefeatures(m, &module_features, enables);`
241pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
242 // c:138
243 handlefeatures(m, module_features(), enables) // c:152
244}
245
246/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/clone.c:145`.
247#[allow(unused_variables)]
248pub fn boot_(m: *const module) -> i32 {
249 // c:145
250 0 // c:159
251}
252
253/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/clone.c:152`.
254/// C body: `return setfeatureenables(m, &module_features, NULL);`
255pub fn cleanup_(m: *const module) -> i32 {
256 // c:152
257 setfeatureenables(m, module_features(), None) // c:159
258}
259
260/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/clone.c:159`.
261#[allow(unused_variables)]
262pub fn finish_(m: *const module) -> i32 {
263 // c:159
264 0 // c:159
265}
266
267// =====================================================================
268// External C globals from other Src/*.c files. Mirrored as atomic /
269// Mutex statics with the same case-sensitive C name; the eventual real
270// ports of jobs.c / exec.c / init.c / params.c will replace these
271// stubs in-place without touching call sites.
272// =====================================================================
273
274// `coprocin` / `coprocout` — `int` globals in `Src/exec.c:430-431`.
275pub static coprocin: AtomicI32 = AtomicI32::new(-1);
276pub static coprocout: AtomicI32 = AtomicI32::new(-1);
277
278// `mypgrp` — `pid_t` global in `Src/jobs.c:60`.
279pub static mypgrp: AtomicI32 = AtomicI32::new(0);
280
281// `lastpid` — `pid_t` global in `Src/jobs.c:73` (zsh's `$!`).
282pub static lastpid: AtomicI32 = AtomicI32::new(0);
283
284// `ttystrname` — `char *` global in `Src/init.c:248`, set by
285// `init_io` from `ttyname(SHTTY)`. Mirrored as `Mutex<String>` since
286// the value is mutated at runtime by init_io.
287pub static ttystrname: Mutex<String> = Mutex::new(String::new());
288
289// =====================================================================
290// Tests
291// =====================================================================
292
293static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
294
295// Local stubs for the per-module entry points. C uses generic
296// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
297// 3275/3370/3445) but those take `Builtin` + `Features` pointer
298// fields the Rust port doesn't carry. The hardcoded descriptor
299// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
300// WARNING: NOT IN CLONE.C — Rust-only module-framework shim.
301// C uses generic featuresarray/handlefeatures/setfeatureenables from
302// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
303// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
304fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
305 vec!["b:clone".to_string()]
306}
307
308// WARNING: NOT IN CLONE.C — Rust-only module-framework shim.
309// C uses generic featuresarray/handlefeatures/setfeatureenables from
310// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
311// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
312fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
313 if enables.is_none() {
314 *enables = Some(vec![1; 1]);
315 }
316 0
317}
318
319// WARNING: NOT IN CLONE.C — Rust-only module-framework shim.
320// C uses generic featuresarray/handlefeatures/setfeatureenables from
321// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
322// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
323fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
324 0
325}
326
327// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
328// ─── RUST-ONLY ACCESSORS ───
329//
330// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
331// RwLock<T>>` globals declared above. C zsh uses direct global
332// access; Rust needs these wrappers because `OnceLock::get_or_init`
333// is the only way to lazily construct shared state. These ported sit
334// here so the body of this file reads in C source order without
335// the accessor wrappers interleaved between real port ported.
336// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
337
338// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
339// ─── RUST-ONLY ACCESSORS ───
340//
341// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
342// RwLock<T>>` globals declared above. C zsh uses direct global
343// access; Rust needs these wrappers because `OnceLock::get_or_init`
344// is the only way to lazily construct shared state. These ported sit
345// here so the body of this file reads in C source order without
346// the accessor wrappers interleaved between real port ported.
347// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
348
349// WARNING: NOT IN CLONE.C — Rust-only module-framework shim.
350// C uses generic featuresarray/handlefeatures/setfeatureenables from
351// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
352// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
353fn module_features() -> &'static Mutex<features> {
354 MODULE_FEATURES.get_or_init(|| {
355 Mutex::new(features {
356 bn_list: None,
357 bn_size: 1,
358 cd_list: None,
359 cd_size: 0,
360 mf_list: None,
361 mf_size: 0,
362 pd_list: None,
363 pd_size: 0,
364 n_abstract: 0,
365 })
366 })
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 fn empty_ops() -> options {
374 options {
375 ind: [0u8; MAX_OPS],
376 args: Vec::new(),
377 argscount: 0,
378 argsalloc: 0,
379 }
380 }
381
382 #[test]
383 #[cfg(unix)]
384 fn bin_clone_no_args_returns_one() {
385 let _g = crate::test_util::global_state_lock();
386 let ops = empty_ops();
387 assert_eq!(bin_clone("clone", &[], &ops, 0), 1);
388 }
389
390 #[test]
391 #[cfg(unix)]
392 fn bin_clone_invalid_tty_returns_one() {
393 let _g = crate::test_util::global_state_lock();
394 let ops = empty_ops();
395 // /nonexistent/tty doesn't exist — open() returns -1.
396 let rc = bin_clone("clone", &["/nonexistent/tty".to_string()], &ops, 0);
397 assert_eq!(rc, 1);
398 }
399
400 #[test]
401 fn module_loaders_return_zero() {
402 let _g = crate::test_util::global_state_lock();
403 let mut features: Vec<String> = Vec::new();
404 let mut enables: Option<Vec<i32>> = None;
405 let m: *const module = std::ptr::null();
406 assert_eq!(setup_(m), 0);
407 assert_eq!(features_(m, &mut features), 0);
408 assert_eq!(features, vec!["b:clone"]);
409 assert_eq!(enables_(m, &mut enables), 0);
410 assert!(enables.is_some());
411 assert_eq!(boot_(m), 0);
412 assert_eq!(cleanup_(m), 0);
413 assert_eq!(finish_(m), 0);
414 }
415
416 /// c:130 — `features_` advertises EXACTLY one builtin: `b:clone`.
417 /// Regression that adds extra features would let
418 /// `zmodload -F zsh/clone` accept bogus names users could
419 /// `zmodload -F zsh/clone +nonsense` and break.
420 #[test]
421 fn features_emits_exactly_one_b_clone_string() {
422 let _g = crate::test_util::global_state_lock();
423 let mut feats: Vec<String> = Vec::new();
424 features_(std::ptr::null(), &mut feats);
425 assert_eq!(feats.len(), 1);
426 assert_eq!(feats[0], "b:clone");
427 }
428
429 /// c:138 — `enables_` must return Some(non-empty) vec since the
430 /// module advertises one builtin. A None return would suggest "no
431 /// features" and the module's builtin would never register.
432 #[test]
433 fn enables_returns_some_with_at_least_one_entry() {
434 let _g = crate::test_util::global_state_lock();
435 let mut enables: Option<Vec<i32>> = None;
436 enables_(std::ptr::null(), &mut enables);
437 let e = enables.expect("must return Some");
438 assert!(
439 !e.is_empty(),
440 "enables vec must contain ≥1 entry for the b:clone feature"
441 );
442 }
443
444 /// c:44-50 — `bin_clone` with >1 positional argument must reject
445 /// per the builtin spec `"a:1:1"` (1 mandatory positional, 1 max).
446 /// `clone /dev/tty /dev/null` should never both clones — the
447 /// extra arg is a usage error.
448 #[test]
449 #[cfg(unix)]
450 fn bin_clone_with_extra_arg_returns_one() {
451 let _g = crate::test_util::global_state_lock();
452 let ops = empty_ops();
453 let rc = bin_clone(
454 "clone",
455 &["/nonexistent1".to_string(), "/nonexistent2".to_string()],
456 &ops,
457 0,
458 );
459 assert_eq!(rc, 1, "more than 1 arg must be rejected");
460 }
461
462 /// c:130 — `featuresarray` returns exactly `["b:clone"]`. Pin the
463 /// string format ("b:" prefix per zsh's module-feature naming
464 /// convention) so a regen that swaps in "p:" or omits the prefix
465 /// breaks `zmodload -F zsh/clone +clone`.
466 #[test]
467 fn features_string_uses_b_prefix() {
468 let _g = crate::test_util::global_state_lock();
469 let mut feats: Vec<String> = Vec::new();
470 features_(std::ptr::null(), &mut feats);
471 let f = &feats[0];
472 assert!(
473 f.starts_with("b:"),
474 "feature {} must use 'b:' prefix per zsh module spec",
475 f
476 );
477 assert_eq!(
478 &f[2..],
479 "clone",
480 "feature 'b:<name>' suffix must be 'clone'"
481 );
482 }
483
484 /// c:123-159 — module-lifecycle stubs accept a null `*const
485 /// module` without dereferencing. Pin the safety contract: the C
486 /// source's `m` parameter is unused (UNUSED(Module m)).
487 #[test]
488 fn module_lifecycle_stubs_accept_null_module() {
489 let _g = crate::test_util::global_state_lock();
490 let m: *const module = std::ptr::null();
491 // Each stub must NOT segfault on null input.
492 assert_eq!(setup_(m), 0);
493 assert_eq!(boot_(m), 0);
494 assert_eq!(cleanup_(m), 0);
495 assert_eq!(finish_(m), 0);
496 }
497
498 // ═══════════════════════════════════════════════════════════════════
499 // Additional C-parity tests for Src/Modules/clone.c.
500 // ═══════════════════════════════════════════════════════════════════
501
502 /// c:44 — `bin_clone` with no args returns 1 ("terminal required").
503 /// The Rust port's defensive guard at args.first() handles direct
504 /// callers; C BUILTIN spec (clone.c:110) enforces 1,1 arity via the
505 /// dispatcher.
506 #[test]
507 fn bin_clone_no_args_returns_one_pin() {
508 let _g = crate::test_util::global_state_lock();
509 let ops = empty_ops();
510 let r = bin_clone("clone", &[], &ops, 0);
511 assert_eq!(r, 1, "no args → 1 (terminal required)");
512 }
513
514 /// c:49 — `bin_clone /nonexistent/tty` returns 1 (open(2) fails).
515 #[test]
516 fn bin_clone_nonexistent_tty_returns_one() {
517 let _g = crate::test_util::global_state_lock();
518 let ops = empty_ops();
519 let r = bin_clone(
520 "clone",
521 &["/__never_exists_zshrs_tty__".to_string()],
522 &ops,
523 0,
524 );
525 assert_eq!(r, 1, "open of nonexistent tty → 1");
526 }
527
528 /// c:49 — `bin_clone /dev/null` opens successfully (not a tty but
529 /// open(2) accepts it). Parent returns 0 on successful fork per
530 /// C source (clone.c:106 `return 0;` after setting `lastpid`);
531 /// the "not a tty" failure only manifests in the child via
532 /// setsid/ioctl. Pin the parent-side success rc + no-panic.
533 #[test]
534 #[cfg(unix)]
535 fn bin_clone_non_tty_path_returns_one() {
536 let _g = crate::test_util::global_state_lock();
537 let ops = empty_ops();
538 let r = bin_clone("clone", &["/dev/null".to_string()], &ops, 0);
539 // Accept 0 (fork ok) or 1 (fork failure / open failure).
540 assert!(r == 0 || r == 1, "rc must be 0 or 1, got {}", r);
541 }
542
543 /// c:213 — `setup_(NULL)` returns 0 (split out for per-hook resolution).
544 #[test]
545 fn clone_setup_returns_zero_pin() {
546 let _g = crate::test_util::global_state_lock();
547 assert_eq!(setup_(std::ptr::null()), 0);
548 }
549
550 /// c:220 — `features_` returns 0 + populates expected list.
551 #[test]
552 fn clone_features_returns_zero_pin() {
553 let _g = crate::test_util::global_state_lock();
554 let mut features: Vec<String> = Vec::new();
555 let r = features_(std::ptr::null(), &mut features);
556 assert_eq!(r, 0);
557 }
558
559 /// c:228 — `enables_(NULL, _)` doesn't panic on None enables ref.
560 #[test]
561 fn clone_enables_no_panic() {
562 let _g = crate::test_util::global_state_lock();
563 let mut e: Option<Vec<i32>> = None;
564 let _ = enables_(std::ptr::null(), &mut e);
565 }
566
567 // ═══════════════════════════════════════════════════════════════════
568 // Additional C-parity tests for Src/Modules/clone.c
569 // c:37 bin_clone / c:213-249 lifecycle
570 // ═══════════════════════════════════════════════════════════════════
571
572 /// c:37 — `bin_clone` empty path arg returns 1 (error).
573 #[test]
574 fn bin_clone_empty_path_returns_nonzero() {
575 let _g = crate::test_util::global_state_lock();
576 let ops = empty_ops();
577 let r = bin_clone("clone", &["".to_string()], &ops, 0);
578 assert_ne!(r, 0, "empty path → error");
579 }
580
581 /// c:37 — `bin_clone` return value in u8 exit-code range.
582 #[test]
583 fn bin_clone_return_in_exit_code_range() {
584 let _g = crate::test_util::global_state_lock();
585 let ops = empty_ops();
586 for args in [
587 vec![],
588 vec!["/tmp".to_string()],
589 vec!["".to_string()],
590 vec!["/dev/null".to_string()],
591 ] {
592 let r = bin_clone("clone", &args, &ops, 0);
593 assert!(
594 (0..256).contains(&r),
595 "exit code {} must fit in u8 range for {:?}",
596 r,
597 args
598 );
599 }
600 }
601
602 /// c:37 — `bin_clone` is deterministic for no-args.
603 #[test]
604 fn bin_clone_no_args_is_deterministic() {
605 let _g = crate::test_util::global_state_lock();
606 let ops = empty_ops();
607 let first = bin_clone("clone", &[], &ops, 0);
608 for _ in 0..5 {
609 assert_eq!(bin_clone("clone", &[], &ops, 0), first);
610 }
611 }
612
613 /// c:37 — `bin_clone` with multibyte path doesn't panic.
614 #[test]
615 fn bin_clone_multibyte_path_no_panic() {
616 let _g = crate::test_util::global_state_lock();
617 let ops = empty_ops();
618 let _ = bin_clone("clone", &["/dev/日本".to_string()], &ops, 0);
619 let _ = bin_clone("clone", &["包含中文".to_string()], &ops, 0);
620 }
621
622 /// c:213-249 — full lifecycle setup→features→enables→boot→cleanup→finish.
623 #[test]
624 fn clone_full_lifecycle_returns_zero_for_all() {
625 let _g = crate::test_util::global_state_lock();
626 let null = std::ptr::null();
627 assert_eq!(setup_(null), 0);
628 let mut feats = Vec::new();
629 let _ = features_(null, &mut feats);
630 let mut enables: Option<Vec<i32>> = None;
631 let _ = enables_(null, &mut enables);
632 assert_eq!(boot_(null), 0);
633 assert_eq!(cleanup_(null), 0);
634 assert_eq!(finish_(null), 0);
635 }
636
637 /// c:213 — setup_ idempotent.
638 #[test]
639 fn clone_setup_idempotent() {
640 let _g = crate::test_util::global_state_lock();
641 for _ in 0..10 {
642 assert_eq!(setup_(std::ptr::null()), 0);
643 }
644 }
645
646 /// c:249 — finish_ idempotent.
647 #[test]
648 fn clone_finish_idempotent() {
649 let _g = crate::test_util::global_state_lock();
650 for _ in 0..10 {
651 assert_eq!(finish_(std::ptr::null()), 0);
652 }
653 }
654
655 /// c:242 — cleanup_ idempotent.
656 #[test]
657 fn clone_cleanup_idempotent() {
658 let _g = crate::test_util::global_state_lock();
659 for _ in 0..10 {
660 assert_eq!(cleanup_(std::ptr::null()), 0);
661 }
662 }
663
664 /// c:235 — boot_ idempotent.
665 #[test]
666 fn clone_boot_idempotent() {
667 let _g = crate::test_util::global_state_lock();
668 for _ in 0..10 {
669 assert_eq!(boot_(std::ptr::null()), 0);
670 }
671 }
672
673 /// c:37 — `bin_clone` two-arg returns nonzero (usage error: only
674 /// 0 or 1 args accepted).
675 #[test]
676 fn bin_clone_two_args_returns_nonzero() {
677 let _g = crate::test_util::global_state_lock();
678 let ops = empty_ops();
679 let r = bin_clone("clone", &["/tmp".to_string(), "/etc".to_string()], &ops, 0);
680 assert_ne!(r, 0, "two args → usage error");
681 }
682
683 // ═══════════════════════════════════════════════════════════════════
684 // Additional C-parity tests for Src/Modules/clone.c
685 // c:37 bin_clone / c:213-249 lifecycle + type pins
686 // ═══════════════════════════════════════════════════════════════════
687
688 /// c:37 — `bin_clone` returns i32 (compile-time pin).
689 #[test]
690 fn bin_clone_returns_i32_type() {
691 let _g = crate::test_util::global_state_lock();
692 let ops = empty_ops();
693 let _: i32 = bin_clone("clone", &[], &ops, 0);
694 }
695
696 /// c:37 — `bin_clone` with three args returns nonzero (usage error).
697 #[test]
698 fn bin_clone_three_args_returns_nonzero() {
699 let _g = crate::test_util::global_state_lock();
700 let ops = empty_ops();
701 let r = bin_clone("clone", &["a".into(), "b".into(), "c".into()], &ops, 0);
702 assert_ne!(r, 0, "three args → usage error");
703 }
704
705 /// c:37 — `bin_clone` with 5+ args still returns nonzero (no clamping).
706 #[test]
707 fn bin_clone_five_args_returns_nonzero() {
708 let _g = crate::test_util::global_state_lock();
709 let ops = empty_ops();
710 let r = bin_clone(
711 "clone",
712 &["a".into(), "b".into(), "c".into(), "d".into(), "e".into()],
713 &ops,
714 0,
715 );
716 assert_ne!(r, 0, "five args → usage error");
717 }
718
719 /// c:37 — `bin_clone("clone", &[], &ops, 0)` no-args deterministic
720 /// (no hidden state mutation).
721 #[test]
722 fn bin_clone_no_args_deterministic() {
723 let _g = crate::test_util::global_state_lock();
724 let ops = empty_ops();
725 let first = bin_clone("clone", &[], &ops, 0);
726 for _ in 0..5 {
727 assert_eq!(
728 bin_clone("clone", &[], &ops, 0),
729 first,
730 "bin_clone no-args must be pure"
731 );
732 }
733 }
734
735 /// c:37 — bin_clone exit code is non-negative for usage-error paths.
736 #[test]
737 fn bin_clone_exit_code_non_negative_for_usage_errors() {
738 let _g = crate::test_util::global_state_lock();
739 let ops = empty_ops();
740 for argv in [vec![], vec!["arg".into()], vec!["a".into(), "b".into()]] {
741 let r = bin_clone("clone", &argv, &ops, 0);
742 assert!(
743 r >= 0,
744 "exit code must be non-negative, got {} for {:?}",
745 r,
746 argv
747 );
748 }
749 }
750
751 /// c:213 — `setup_` returns i32 (compile-time pin).
752 #[test]
753 fn clone_setup_returns_i32_type() {
754 let _g = crate::test_util::global_state_lock();
755 let _: i32 = setup_(std::ptr::null());
756 }
757
758 /// c:220 — `features_` returns i32 (compile-time pin).
759 #[test]
760 fn clone_features_returns_i32_type() {
761 let _g = crate::test_util::global_state_lock();
762 let mut v: Vec<String> = Vec::new();
763 let _: i32 = features_(std::ptr::null(), &mut v);
764 }
765
766 /// c:228 — `enables_` returns i32 with None enables-out param safe.
767 #[test]
768 fn clone_enables_with_none_returns_i32() {
769 let _g = crate::test_util::global_state_lock();
770 let mut e: Option<Vec<i32>> = None;
771 let _: i32 = enables_(std::ptr::null(), &mut e);
772 }
773
774 /// c:130 — `features_` REPLACES the caller's Vec wholesale per
775 /// the C body `*features = featuresarray(...)`. Pin the clobber
776 /// behaviour so a future "merge instead of replace" regression
777 /// would fail loudly.
778 #[test]
779 fn clone_features_replaces_caller_vec_wholesale() {
780 let _g = crate::test_util::global_state_lock();
781 let mut v: Vec<String> = vec!["sentinel".to_string()];
782 let _ = features_(std::ptr::null(), &mut v);
783 assert!(
784 !v.iter().any(|s| s == "sentinel"),
785 "c:130 — features_ MUST overwrite the Vec (`*features = featuresarray(...)`)"
786 );
787 }
788
789 /// c:228 — `enables_` deterministic for null callback.
790 #[test]
791 fn clone_enables_deterministic_for_null_in() {
792 let _g = crate::test_util::global_state_lock();
793 let mut a: Option<Vec<i32>> = None;
794 let first = enables_(std::ptr::null(), &mut a);
795 for _ in 0..5 {
796 let mut b: Option<Vec<i32>> = None;
797 assert_eq!(
798 enables_(std::ptr::null(), &mut b),
799 first,
800 "enables_ must be deterministic for null in"
801 );
802 }
803 }
804
805 /// c:213/220/228/235/242/249 — every lifecycle hook returns 0 (success
806 /// sentinel), distinct per call site so a regression that changes
807 /// one returns nonzero gets pinned individually.
808 #[test]
809 fn clone_each_lifecycle_hook_returns_zero_individually() {
810 let _g = crate::test_util::global_state_lock();
811 let null = std::ptr::null();
812 let mut v: Vec<String> = Vec::new();
813 let mut e: Option<Vec<i32>> = None;
814 assert_eq!(setup_(null), 0, "c:213 setup_");
815 assert_eq!(features_(null, &mut v), 0, "c:220 features_");
816 assert_eq!(enables_(null, &mut e), 0, "c:228 enables_");
817 assert_eq!(boot_(null), 0, "c:235 boot_");
818 assert_eq!(cleanup_(null), 0, "c:242 cleanup_");
819 assert_eq!(finish_(null), 0, "c:249 finish_");
820 }
821
822 // ═══════════════════════════════════════════════════════════════════
823 // Additional C-parity pins for Src/Modules/clone.c
824 // c:37 bin_clone (main fn) / c:213-249 lifecycle hooks
825 // ═══════════════════════════════════════════════════════════════════
826
827 /// c:213 — `setup_` is idempotent (multiple invocations safe).
828 #[test]
829 fn clone_setup_idempotent_repeated_calls() {
830 let _g = crate::test_util::global_state_lock();
831 for _ in 0..10 {
832 assert_eq!(setup_(std::ptr::null()), 0);
833 }
834 }
835
836 /// c:242 — `cleanup_` is idempotent.
837 #[test]
838 fn clone_cleanup_idempotent_repeated_calls() {
839 let _g = crate::test_util::global_state_lock();
840 for _ in 0..10 {
841 assert_eq!(cleanup_(std::ptr::null()), 0);
842 }
843 }
844
845 /// c:249 — `finish_` is idempotent.
846 #[test]
847 fn clone_finish_idempotent_repeated_calls() {
848 let _g = crate::test_util::global_state_lock();
849 for _ in 0..10 {
850 assert_eq!(finish_(std::ptr::null()), 0);
851 }
852 }
853
854 /// c:220 — `features_` is deterministic (same inputs → same outputs).
855 #[test]
856 fn clone_features_deterministic_on_null_module() {
857 let _g = crate::test_util::global_state_lock();
858 let null = std::ptr::null();
859 let mut v1: Vec<String> = Vec::new();
860 let mut v2: Vec<String> = Vec::new();
861 assert_eq!(features_(null, &mut v1), 0);
862 assert_eq!(features_(null, &mut v2), 0);
863 assert_eq!(
864 v1, v2,
865 "features_ must populate identical vec for identical input"
866 );
867 }
868
869 /// c:37 — `bin_clone(empty args)` returns non-negative exit code.
870 #[test]
871 fn bin_clone_empty_args_non_negative() {
872 let _g = crate::test_util::global_state_lock();
873 let ops = crate::ported::zsh_h::options {
874 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
875 args: Vec::new(),
876 argscount: 0,
877 argsalloc: 0,
878 };
879 let r = bin_clone("clone", &[], &ops, 0);
880 assert!(r >= 0, "bin_clone empty args must return ≥ 0, got {}", r);
881 }
882
883 /// c:37 — `bin_clone` is deterministic across calls (same args → same exit).
884 #[test]
885 fn bin_clone_deterministic_for_two_args() {
886 let _g = crate::test_util::global_state_lock();
887 let ops = crate::ported::zsh_h::options {
888 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
889 args: Vec::new(),
890 argscount: 0,
891 argsalloc: 0,
892 };
893 let args = vec!["a".to_string(), "b".to_string()];
894 let r1 = bin_clone("clone", &args, &ops, 0);
895 let r2 = bin_clone("clone", &args, &ops, 0);
896 assert_eq!(r1, r2, "bin_clone must be deterministic for same args");
897 }
898
899 /// c:37 — `bin_clone` various func values don't panic (func is the
900 /// hashed builtin selector; clone has only one BIN_* code).
901 #[test]
902 fn bin_clone_various_func_values_no_panic() {
903 let _g = crate::test_util::global_state_lock();
904 let ops = crate::ported::zsh_h::options {
905 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
906 args: Vec::new(),
907 argscount: 0,
908 argsalloc: 0,
909 };
910 for func in [-1, 0, 1, 100, i32::MAX] {
911 let _ = bin_clone("clone", &[], &ops, func);
912 }
913 }
914
915 /// c:213/235 — `setup_` then `boot_` sequence returns 0 from both.
916 #[test]
917 fn clone_setup_then_boot_returns_zero_each() {
918 let _g = crate::test_util::global_state_lock();
919 let null = std::ptr::null();
920 assert_eq!(setup_(null), 0);
921 assert_eq!(boot_(null), 0);
922 }
923
924 /// c:228 — `enables_` with Some(non-empty) input doesn't panic.
925 #[test]
926 fn clone_enables_with_some_non_empty_no_panic() {
927 let _g = crate::test_util::global_state_lock();
928 let mut e: Option<Vec<i32>> = Some(vec![1, 2, 3]);
929 let _ = enables_(std::ptr::null(), &mut e);
930 }
931
932 /// c:213 — `setup_` return type i32 (compile-time pin).
933 #[test]
934 fn clone_setup_returns_i32_type_compile_pin() {
935 let _g = crate::test_util::global_state_lock();
936 let _: i32 = setup_(std::ptr::null());
937 }
938
939 /// c:235 — `boot_` return type i32 (compile-time pin).
940 #[test]
941 fn clone_boot_returns_i32_type() {
942 let _g = crate::test_util::global_state_lock();
943 let _: i32 = boot_(std::ptr::null());
944 }
945
946 /// c:242 — `cleanup_` return type i32 (compile-time pin).
947 #[test]
948 fn clone_cleanup_returns_i32_type() {
949 let _g = crate::test_util::global_state_lock();
950 let _: i32 = cleanup_(std::ptr::null());
951 }
952
953 /// c:249 — `finish_` return type i32 (compile-time pin).
954 #[test]
955 fn clone_finish_returns_i32_type() {
956 let _g = crate::test_util::global_state_lock();
957 let _: i32 = finish_(std::ptr::null());
958 }
959}