Skip to main content

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}