Skip to main content

zsh/ported/modules/
system.rs

1//! `zsh/system` module — port of `Src/Modules/system.c`.
2//!
3//! Functions for the errnos special parameter.                              // c:828
4//! Functions for the sysparams special parameter.                           // c:842
5//! The load/unload routines required by the zsh library interface          // c:916
6//!
7//! Provides the system-call builtins: `sysread`, `syswrite`, `sysopen`,
8//! `sysseek`, `syserror`, `zsystem` (with subcommands `flock` and
9//! `supports`); the `systell` math function; the `errnos` and
10//! `sysparams` special parameters.
11//!
12//! C source: 21 ported total — `getposint`, `bin_sysread`, `bin_syswrite`,
13//! `bin_sysopen`, `bin_sysseek`, `math_systell`, `bin_syserror`,
14//! `bin_zsystem_flock`, `bin_zsystem_supports`, `bin_zsystem`,
15//! `errnosgetfn`, `fillpmsysparams`, `getpmsysparams`,
16//! `scanpmsysparams`, plus 6 module loaders (`setup_`, `features_`,
17//! `enables_`, `boot_`, `cleanup_`, `finish_`).
18//!
19//! Zero `struct` / `enum` definitions in system.c (only the
20//! `static struct { const char *name; int oflag; } openopts[]` ad-hoc
21//! anonymous-struct array at c:283 — mirrored as a Rust
22//! `&[(&str, i32)]` slice; not a public type).
23//!
24//! Order in this file mirrors C source order verbatim.
25
26use crate::ported::math::{matheval, mnumber, MN_FLOAT, MN_INTEGER};
27use crate::ported::options::{opt_state_get, opt_state_set};
28use crate::ported::params::{isident, paramtab, setiparam, setsparam};
29use crate::ported::utils::{metafy, movefd, unmeta, zclose, zerr, zstrtol, zwarnnam};
30use crate::ported::zsh_h::{module, options, OPT_ARG, OPT_ISSET};
31use std::sync::{Mutex, OnceLock};
32
33const SYSREAD_BUFSIZE: usize = 8192; // c:45
34
35/// Port of `getposint(char *instr, char *nam)` from `Src/Modules/system.c:45`. Parses
36/// `instr` as a non-negative integer (zstrtol with base 10); emits
37/// `zwarnnam` and returns -1 on parse error or negative.
38///
39/// C signature: `static int getposint(char *instr, char *nam)`.
40pub fn getposint(instr: &str, nam: &str) -> i32 {
41    // c:45
42    // c:45 — `ret = (int)zstrtol(instr, &eptr, 10);`
43    let (ret, eptr) = zstrtol(instr, 10);
44    let ret = ret as i32;
45    // c:51 — `if (*eptr || ret < 0)`
46    if !eptr.is_empty() || ret < 0 {
47        zwarnnam(nam, &format!("integer expected: {}", instr)); // c:52
48        return -1; // c:53
49    }
50    ret // c:56
51}
52
53/// Port of `bin_sysread(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/system.c:72`.
54/// C: `int bin_sysread(char *nam, char **args, Options ops, int func)`.
55/// Builtin spec: `"c:i:o:s:t:"` (system.c:820).
56///
57/// Return values per c:60-67:
58///   0 — Successfully read (and written if `-o`)
59///   1 — Error in parameters
60///   2 — Read error (errno set)
61///   3 — Write error (errno set; partial residue stashed)
62///   4 — Timeout on read
63///   5 — Zero bytes read (EOF)
64#[allow(unused_variables)]
65pub fn bin_sysread(
66    nam: &str,
67    args: &[String], // c:72
68    ops: &options,
69    func: i32,
70) -> i32 {
71    // c:74 — `int infd = 0, outfd = -1, bufsize = SYSREAD_BUFSIZE, count;`
72    let mut infd: i32 = 0; // c:74
73    let mut outfd: i32 = -1; // c:74
74    let mut bufsize: usize = SYSREAD_BUFSIZE; // c:74
75                                              // c:75 — `char *outvar = NULL, *countvar = NULL, *inbuf;`
76    let mut outvar: Option<String> = None; // c:75
77    let mut countvar: Option<String> = None; // c:75
78
79    // c:80 — `if (OPT_ISSET(ops, 'i')) { infd = getposint(OPT_ARG(ops,'i'),nam); ...}`
80    if OPT_ISSET(ops, b'i') {
81        // c:80
82        infd = getposint(OPT_ARG(ops, b'i').unwrap_or(""), nam); // c:81
83        if infd < 0 {
84            return 1;
85        } // c:82-83
86    }
87    // c:87 — `if (OPT_ISSET(ops, 'o')) { outfd = getposint(OPT_ARG(ops,'o'),nam); ...}`
88    if OPT_ISSET(ops, b'o') {
89        // c:87
90        outfd = getposint(OPT_ARG(ops, b'o').unwrap_or(""), nam); // c:88
91        if outfd < 0 {
92            return 1;
93        } // c:89-90
94    }
95    // c:94 — `if (OPT_ISSET(ops, 's')) bufsize = getposint(OPT_ARG(ops,'s'),nam);`
96    if OPT_ISSET(ops, b's') {
97        // c:94
98        let v = getposint(OPT_ARG(ops, b's').unwrap_or(""), nam); // c:95
99        if v < 0 {
100            return 1;
101        } // c:96-97
102        bufsize = v as usize;
103    }
104    // c:101 — `if (OPT_ISSET(ops, 'c')) { countvar = OPT_ARG(ops,'c'); isident...}`
105    if OPT_ISSET(ops, b'c') {
106        // c:101
107        let cv = OPT_ARG(ops, b'c').unwrap_or("").to_string(); // c:102
108        if !isident(&cv) {
109            // c:103
110            zwarnnam(nam, &format!("not an identifier: {}", cv)); // c:104
111            return 1; // c:105
112        }
113        countvar = Some(cv);
114    }
115    // c:109 — `if (*args) { outvar = *args; isident... }`
116    if !args.is_empty() {
117        // c:109
118        let ov = args[0].clone(); // c:116
119        if !isident(&ov) {
120            // c:117
121            zwarnnam(nam, &format!("not an identifier: {}", ov)); // c:118
122            return 1; // c:119
123        }
124        outvar = Some(ov);
125    }
126    let timeout_arg: Option<&str> = if OPT_ISSET(ops, b't') {
127        // c:127
128        OPT_ARG(ops, b't')
129    } else {
130        None
131    };
132
133    // c:123 — `inbuf = zhalloc(bufsize);`
134    let mut inbuf = vec![0u8; bufsize]; // c:123
135
136    // c:127-185 — `-t` poll(2) wait. C uses HAVE_POLL → poll(); else
137    // select(). Rust has poll(2) on every supported unix; pick the
138    // poll branch (c:129-152).
139    if let Some(t_str) = timeout_arg {
140        // c:137 — `to_mn = matheval(OPT_ARG(ops,'t'));`
141        // c:138-139 `if (errflag) return 1;` — mathevali's zerr already
142        // wrote the diagnostic in C; Rust captures it in Err — surface
143        // it via zerr() so callers see the parse error on stderr.
144        let to_mn = match matheval(t_str) {
145            Ok(m) => m,
146            Err(msg) => {
147                zerr(&msg);
148                return 1; // c:138-139 errflag
149            }
150        };
151        // c:140-143 — float→int conversion of seconds × 1000.
152        let to_int: i32 = if to_mn.type_ == MN_FLOAT {
153            (1000.0 * to_mn.d) as i32 // c:141
154        } else {
155            (1000 * to_mn.l) as i32 // c:143
156        };
157        // c:145-148 — `while ((ret = poll(...)) < 0) { if (errno !=
158        //              EINTR || errflag || retflag || breaks ||
159        //              contflag) break; }`. Same shell-control-flow
160        //              flag bail as bin_syswrite (a4fd96ac0e).
161        use std::sync::atomic::Ordering::Relaxed;
162        let mut ret;
163        loop {
164            let mut pfd = libc::pollfd {
165                // c:130-135
166                fd: infd,
167                events: libc::POLLIN,
168                revents: 0,
169            };
170            ret = unsafe { libc::poll(&mut pfd, 1, to_int) };
171            if ret >= 0 {
172                break;
173            }
174            let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
175            let interrupted = crate::ported::utils::errflag.load(Relaxed) != 0
176                || crate::ported::exec::retflag.load(Relaxed) != 0
177                || crate::ported::builtin::BREAKS.load(Relaxed) != 0
178                || crate::ported::builtin::CONTFLAG.load(Relaxed) != 0;
179            if eno != libc::EINTR || interrupted {
180                break; // c:177
181            }
182        }
183        // c:149-151 — `if (ret <= 0) return ret ? 2 : 4;`
184        if ret <= 0 {
185            return if ret != 0 { 2 } else { 4 };
186        }
187    }
188
189    // c:188-191 — `while ((count = read(infd, inbuf, bufsize)) < 0)
190    //                  { if (errno != EINTR || errflag || retflag
191    //                       || breaks || contflag) break; }`.
192    // Same control-flow flag bail as bin_syswrite (a4fd96ac0e).
193    use std::sync::atomic::Ordering::Relaxed;
194    let mut count: isize;
195    loop {
196        count = unsafe { libc::read(infd, inbuf.as_mut_ptr() as *mut libc::c_void, bufsize) };
197        if count >= 0 {
198            break;
199        }
200        let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
201        let interrupted = crate::ported::utils::errflag.load(Relaxed) != 0
202            || crate::ported::exec::retflag.load(Relaxed) != 0
203            || crate::ported::builtin::BREAKS.load(Relaxed) != 0
204            || crate::ported::builtin::CONTFLAG.load(Relaxed) != 0;
205        if eno != libc::EINTR || interrupted {
206            break; // c:189
207        }
208    }
209    // c:192-193 — `if (countvar) setiparam(countvar, count);`
210    if let Some(ref cv) = countvar {
211        setiparam(cv, count as i64); // c:192
212    }
213    // c:194-195 — `if (count < 0) return 2;`
214    if count < 0 {
215        return 2;
216    }
217    let count = count as usize;
218
219    // c:197-218 — outfd write path with EINTR retry + partial residue.
220    if outfd >= 0 {
221        // c:197
222        if count == 0 {
223            return 5;
224        } // c:198-199
225        let mut p = 0usize;
226        let mut remaining = count;
227        while remaining > 0 {
228            // c:200
229            let ret = unsafe {
230                libc::write(outfd, inbuf[p..].as_ptr() as *const libc::c_void, remaining)
231            };
232            if ret < 0 {
233                // c:204
234                // c:205-206 — `if (errno == EINTR && !errflag &&
235                //               !retflag && !breaks && !contflag) continue;`
236                // C only retries when ALL FOUR control-flow flags are
237                // clear AND errno is EINTR. Prior port retried on any
238                // EINTR regardless of shell state.
239                use std::sync::atomic::Ordering::Relaxed;
240                let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
241                let interrupted = crate::ported::utils::errflag.load(Relaxed) != 0
242                    || crate::ported::exec::retflag.load(Relaxed) != 0
243                    || crate::ported::builtin::BREAKS.load(Relaxed) != 0
244                    || crate::ported::builtin::CONTFLAG.load(Relaxed) != 0;
245                if eno == libc::EINTR && !interrupted {
246                    // c:205-207 — clean EINTR, retry.
247                    continue;
248                }
249                // c:208-212 — stash residue + remaining count.
250                if let Some(ref ov) = outvar {
251                    let buf_remaining = String::from_utf8_lossy(&inbuf[p..p + remaining]);
252                    let m = metafy(&buf_remaining);
253                    setsparam(ov, &m); // c:209
254                }
255                if let Some(ref cv) = countvar {
256                    setiparam(cv, remaining as i64); // c:210
257                }
258                return 3; // c:212
259            }
260            p += ret as usize; // c:214
261            remaining -= ret as usize; // c:215
262        }
263        return 0; // c:217
264    }
265
266    // c:220-225 — no outfd: stash buffer in `outvar` (default REPLY).
267    let target = outvar.unwrap_or_else(|| "REPLY".to_string()); // c:220-221
268    let buf_str = String::from_utf8_lossy(&inbuf[..count]);
269    let m = metafy(&buf_str);
270    setsparam(&target, &m); // c:223
271    if count != 0 {
272        0
273    } else {
274        5
275    } // c:225
276}
277
278/// Port of `bin_syswrite(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/system.c:238`.
279///
280/// C signature: `static int bin_syswrite(char *nam, char **args,
281///                                        Options ops, int func)`.
282/// Builtin spec: `"c:o:"` (system.c:821), 1 mandatory positional
283/// arg.
284///
285/// Return values per c:230-233:
286///   0 — Successfully written
287///   1 — Error in parameters
288///   2 — Write error (errno set)
289/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
290pub fn bin_syswrite(
291    nam: &str,
292    args: &[String], // c:238
293    ops: &options,
294    _func: i32,
295) -> i32 {
296    // c:240-241 — `int outfd = 1, len, count, totcount;
297    //              char *countvar = NULL;`
298    let mut outfd: i32 = 1; // c:240
299    let mut countvar: Option<String> = None; // c:241
300
301    // c:246 — `if (OPT_ISSET(ops, 'o')) { outfd = getposint(OPT_ARG(ops,'o'),nam); ...}`
302    if OPT_ISSET(ops, b'o') {
303        // c:246
304        outfd = getposint(OPT_ARG(ops, b'o').unwrap_or(""), nam); // c:247
305        if outfd < 0 {
306            return 1;
307        } // c:248-249
308    }
309    // c:253 — `if (OPT_ISSET(ops, 'c')) { countvar = OPT_ARG(ops,'c'); isident...}`
310    if OPT_ISSET(ops, b'c') {
311        // c:253
312        let cv = OPT_ARG(ops, b'c').unwrap_or("").to_string(); // c:254
313        if !isident(&cv) {
314            // c:255
315            zwarnnam(nam, &format!("not an identifier: {}", cv)); // c:256
316            return 1; // c:257
317        }
318        countvar = Some(cv);
319    }
320    // c:262 — `unmetafy(*args, &len);` — first positional arg = data.
321    let data = match args.first() {
322        // c:262
323        Some(d) => d.clone(),
324        None => return 1,
325    };
326
327    // c:262 — `unmetafy(*args, &len);`
328    let unmeta = unmeta(&data);
329    let bytes = unmeta.as_bytes();
330    let mut totcount: usize = 0; // c:261
331    let mut len = bytes.len();
332    let mut p = 0usize;
333
334    // c:263-275 — write loop with EINTR retry and partial residue.
335    // C body:
336    //   while ((count = write(outfd, *args, len)) < 0) {
337    //       if (errno != EINTR || errflag || retflag || breaks || contflag)
338    //       {
339    //           if (countvar) setiparam(countvar, totcount);
340    //           return 2;
341    //       }
342    //   }
343    //
344    // Prior Rust port only checked `errno != EINTR` — ignored
345    // errflag / retflag / breaks / contflag. That meant the EINTR
346    // retry loop would keep spinning even after Ctrl-C (which
347    // sets errflag via the SIGINT trap) or a `return` from an
348    // enclosing function body. Now matches C: any of the four
349    // shell control-flow flags bails out the retry the same as
350    // a non-EINTR errno.
351    use std::sync::atomic::Ordering::Relaxed;
352    while len > 0 {
353        // c:263
354        let count = unsafe { libc::write(outfd, bytes[p..].as_ptr() as *const libc::c_void, len) };
355        if count < 0 {
356            // c:264
357            let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
358            let interrupted = crate::ported::utils::errflag.load(Relaxed) != 0
359                || crate::ported::exec::retflag.load(Relaxed) != 0
360                || crate::ported::builtin::BREAKS.load(Relaxed) != 0
361                || crate::ported::builtin::CONTFLAG.load(Relaxed) != 0;
362            if eno != libc::EINTR || interrupted {
363                // c:265
364                if let Some(ref cv) = countvar {
365                    // c:267-268
366                    setiparam(cv, totcount as i64); // c:268
367                }
368                return 2; // c:269
369            }
370            continue;
371        }
372        p += count as usize; // c:272 *args += count
373        totcount += count as usize; // c:273
374        len -= count as usize; // c:274
375    }
376    // c:276-277 — `if (countvar) setiparam(countvar, totcount);`
377    if let Some(ref cv) = countvar {
378        setiparam(cv, totcount as i64); // c:277
379    }
380    0 // c:279
381}
382
383/// Port of `bin_sysopen(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/system.c:319`.
384///
385/// C signature: `static int bin_sysopen(char *nam, char **args,
386///                                       Options ops, int func)`.
387/// Builtin spec: `"rwau:o:m:"` (system.c:822), 1 mandatory
388/// positional arg (the file path).
389///
390/// Return values per c:312-314: 0 success / 1 bad params / 2 open error.
391/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
392pub fn bin_sysopen(
393    nam: &str,
394    args: &[String], // c:319
395    ops: &options,
396    _func: i32,
397) -> i32 {
398    // c:321-323 — `int read = OPT_ISSET(ops, 'r');` etc.
399    let read_flag = OPT_ISSET(ops, b'r'); // c:321
400    let write_flag = OPT_ISSET(ops, b'w'); // c:322
401    let append_flag = OPT_ISSET(ops, b'a'); // c:323
402
403    // c:323-325 — flags = O_NOCTTY | append | (RDWR/WRONLY/RDONLY).
404    let append_flag_bit = if append_flag { libc::O_APPEND } else { 0 };
405    let mut flags = libc::O_NOCTTY
406        | append_flag_bit
407        | if append_flag || write_flag {
408            if read_flag {
409                libc::O_RDWR
410            } else {
411                libc::O_WRONLY
412            }
413        } else {
414            libc::O_RDONLY
415        };
416
417    // c:328 — `mode_t perms = 0666;`
418    let mut perms: u32 = 0o666;
419    let mut explicit: i32 = -1; // c:327
420
421    // c:335 — `if (!OPT_ISSET(ops, 'u')) { ... return 1; }`
422    if !OPT_ISSET(ops, b'u') {
423        zwarnnam(nam, "file descriptor not specified"); // c:336
424        return 1; // c:337
425    }
426    let fdvar = OPT_ARG(ops, b'u').unwrap_or("").to_string(); // c:340
427    let path = match args.first() {
428        Some(p) => p.clone(),
429        None => return 1,
430    };
431    let o_arg: Option<&str> = if OPT_ISSET(ops, b'o') {
432        OPT_ARG(ops, b'o')
433    } else {
434        None
435    };
436    let m_arg: Option<&str> = if OPT_ISSET(ops, b'm') {
437        OPT_ARG(ops, b'm')
438    } else {
439        None
440    };
441
442    // c:341-347 — fdvar is either single digit (explicit fd) or identifier.
443    if fdvar.len() == 1 && fdvar.chars().next().unwrap().is_ascii_digit() {
444        explicit = fdvar.parse().unwrap(); // c:343
445    } else if !isident(&fdvar) {
446        // c:344
447        zwarnnam(nam, &format!("not an identifier: {}", fdvar)); // c:345
448        return 1; // c:346
449    }
450
451    // c:350-369 — comma-list of O_* names from -o, case-insensitive,
452    // optional `O_` prefix.
453    if let Some(opts) = o_arg {
454        for tok in opts.split(',') {
455            // c:355 strchr ','
456            let mut name: &str = tok;
457            // c:353 — `if (!strncasecmp(opt, "O_", 2)) opt += 2;`
458            if name.len() >= 2 && name[..2].eq_ignore_ascii_case("O_") {
459                name = &name[2..];
460            }
461            // c:357-358 — case-insensitive lookup in openopts[].
462            // openopts[] is the c:283-308 anonymous-struct table:
463            // `static struct { const char *name; int oflag; } openopts[]`.
464            // Inlined here as a const slice so the lookup is bit-for-bit
465            // identical to C (same name/oflag rows, same order, walked
466            // backwards via `for (o = N-1; o >= 0; o--)` at c:357).
467            #[cfg(unix)]
468            {
469                const OPENOPTS: &[(&str, i32)] = &[
470                    ("cloexec", libc::O_CLOEXEC),   // c:285
471                    ("nofollow", libc::O_NOFOLLOW), // c:292
472                    ("sync", libc::O_SYNC),         // c:295
473                    #[cfg(target_os = "linux")]
474                    ("noatime", libc::O_NOATIME), // c:298
475                    ("nonblock", libc::O_NONBLOCK), // c:301
476                    ("excl", libc::O_EXCL | libc::O_CREAT), // c:303
477                    ("creat", libc::O_CREAT),       // c:304
478                    ("create", libc::O_CREAT),      // c:305
479                    ("truncate", libc::O_TRUNC),    // c:306
480                    ("trunc", libc::O_TRUNC),       // c:307
481                ];
482                let mut found: Option<i32> = None;
483                for (n, oflag) in OPENOPTS.iter().rev() {
484                    // c:357 walks backwards
485                    if n.eq_ignore_ascii_case(name) {
486                        found = Some(*oflag);
487                        break;
488                    }
489                }
490                let oflag = match found {
491                    Some(f) => f,
492                    None => {
493                        zwarnnam(nam, &format!("unsupported option: {}\n", tok)); // c:360
494                        return 1; // c:361
495                    }
496                };
497                flags |= oflag; // c:367
498            }
499        }
500    }
501
502    // c:372-381 — -m: octal permissions string.
503    if let Some(m) = m_arg {
504        let mode_str: &str = m;
505        // c:374-375 — `while (*ptr >= '0' && *ptr <= '7') ptr++;`
506        let mut ptr = 0;
507        let bytes = mode_str.as_bytes();
508        while ptr < bytes.len() && (b'0'..=b'7').contains(&bytes[ptr]) {
509            ptr += 1;
510        }
511        // c:376 — `if (*ptr || ptr - opt < 3)`
512        if ptr < bytes.len() || ptr < 3 {
513            zwarnnam(nam, &format!("invalid mode {}", mode_str)); // c:377
514            return 1; // c:378
515        }
516        // c:380 — `perms = zstrtol(opt, 0, 8);`
517        let (v, _) = zstrtol(mode_str, 8);
518        perms = v as u32;
519    }
520
521    // c:383-391 — `open(*args, flags[, perms])`; `*args` is path.
522    let path_c = match std::ffi::CString::new(path.as_bytes()) {
523        Ok(s) => s,
524        Err(_) => return 1,
525    };
526    let fd = unsafe {
527        if (flags & libc::O_CREAT) != 0 {
528            // c:383
529            libc::open(path_c.as_ptr(), flags, perms as libc::c_uint) // c:384
530        } else {
531            libc::open(path_c.as_ptr(), flags) // c:386
532        }
533    };
534    if fd == -1 {
535        // c:388
536        let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
537        // c:389 — `zwarnnam(nam, "can't open file %s: %e", *args, errno);`
538        // %e per Src/utils.c:352-368 (lowercased strerror).
539        zwarnnam(
540            nam,
541            &format!(
542                "can't open file {}: {}",
543                path,
544                crate::ported::utils::zsh_errno_msg(eno)
545            ),
546        ); // c:389
547        return 2; // c:390
548    }
549
550    // c:392 — `moved_fd = (explicit > -1) ? redup(fd, explicit) : movefd(fd);`
551    let moved_fd: i32 = if explicit > -1 {
552        crate::ported::utils::redup(fd, explicit) // c:392 redup branch
553    } else {
554        movefd(fd) // c:392 movefd branch
555    };
556    if moved_fd == -1 {
557        // c:393
558        zwarnnam(nam, &format!("can't open file {}", path)); // c:394
559        return 2; // c:395
560    }
561
562    // c:398-411 — reapply FD_CLOEXEC after dup2 if requested.
563    if (flags & libc::O_CLOEXEC) != 0 && fd != moved_fd {
564        // c:406
565        unsafe {
566            libc::fcntl(moved_fd, libc::F_SETFD, libc::FD_CLOEXEC);
567        } // c:410
568    }
569
570    // c:412 — `fdtable[moved_fd] = FDT_EXTERNAL;`. movefd/redup marks
571    // the lifted fd FDT_INTERNAL (shell-owned); sysopen hands the fd to
572    // the USER (via `sysopen -u fd`), so re-mark it FDT_EXTERNAL. Without
573    // this a later `exec {fd}<&-` (p10k's `_p9k_worker_stop` /
574    // gitstatus `gitstatus_stop_p9k_`) was rejected with "file
575    // descriptor N used by shell, not closed", killing gitstatus init.
576    // Bug #648.
577    crate::ported::utils::fdtable_set(moved_fd, crate::ported::zsh_h::FDT_EXTERNAL);
578
579    // c:413-418 — `if (explicit == -1) { setiparam(fdvar, moved_fd); ... }`
580    if explicit == -1 {
581        setiparam(&fdvar, moved_fd as i64); // c:414
582    }
583
584    0 // c:433
585}
586
587/// Port of `bin_sysseek(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/system.c:433`.
588///
589/// C signature: `static int bin_sysseek(char *nam, char **args,
590///                                       Options ops, int func)`.
591/// Builtin spec: `"u:w:"` (system.c:823), 1 mandatory positional
592/// arg (the offset).
593///
594/// Return values per c:425-428: 0 success / 1 bad params / 2 lseek error.
595/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
596pub fn bin_sysseek(
597    nam: &str,
598    args: &[String], // c:433
599    ops: &options,
600    _func: i32,
601) -> i32 {
602    // c:435 — `int w = SEEK_SET, fd = 0;`
603    let mut w: i32 = libc::SEEK_SET; // c:435
604    let mut fd: i32 = 0; // c:435
605
606    // c:441-446 — `if (OPT_ISSET(ops, 'u')) { fd = getposint(OPT_ARG(ops,'u'),nam); ...}`
607    if OPT_ISSET(ops, b'u') {
608        // c:441
609        fd = getposint(OPT_ARG(ops, b'u').unwrap_or(""), nam); // c:442
610        if fd < 0 {
611            return 1;
612        } // c:443-444
613    }
614    // c:449-460 — `-w` whence parse (case-insensitive).
615    if OPT_ISSET(ops, b'w') {
616        // c:449
617        let whence = OPT_ARG(ops, b'w').unwrap_or(""); // c:450
618        if whence.eq_ignore_ascii_case("current") || whence == "1" {
619            // c:451
620            w = libc::SEEK_CUR; // c:452
621        } else if whence.eq_ignore_ascii_case("end") || whence == "2" {
622            // c:453
623            w = libc::SEEK_END; // c:454
624        } else if !whence.eq_ignore_ascii_case("start") && whence != "0" {
625            // c:455
626            zwarnnam(nam, &format!("unknown argument to -w: {}", whence)); // c:456
627            return 1; // c:457
628        }
629    }
630
631    // c:461 — `pos = (off_t)mathevali(*args);`
632    let pos_str = match args.first() {
633        Some(s) => s.clone(),
634        None => return 1,
635    };
636    let pos = match crate::ported::math::mathevali(&pos_str) {
637        // c:461 — mathevali errflag → zerr already in C; surface Err msg.
638        Ok(v) => v,
639        Err(msg) => {
640            zerr(&msg);
641            return 1;
642        }
643    };
644    // c:462 — `return (lseek(fd, pos, w) == -1) ? 2 : 0;`
645    if unsafe { libc::lseek(fd, pos as libc::off_t, w) } == -1 {
646        // c:462
647        2
648    } else {
649        0
650    }
651}
652
653/// Port of `math_systell(UNUSED(char *name), UNUSED(int argc), mnumber *argv, UNUSED(int id))` from `Src/Modules/system.c:467`.
654///
655/// C signature: `static mnumber math_systell(char *name, int argc,
656///                                            mnumber *argv, int id)`.
657/// Returns the current `lseek(fd, 0, SEEK_CUR)` position of `argv[0]`
658/// as an `mnumber`. Negative fds error via `zerr` and return 0.
659#[allow(unused_variables)]
660pub fn math_systell(name: &str, argc: i32, argv: &[mnumber], id: i32) -> mnumber {
661    // c:467
662    // C's mathfunc dispatch (mathfunc.c::stdmathfn) enforces min/max
663    // arg counts BEFORE calling the per-fn implementation — so the C
664    // body can `argv->u.l` safely. The Rust port calls through a
665    // generic adapter without the upstream guard; check argc/len here
666    // so a direct test call (or future dispatch divergence) doesn't
667    // index `argv[0]` on an empty slice. Mirror C's "missing arg →
668    // return 0 mnumber" failure shape.
669    if argc < 1 || argv.is_empty() {
670        return mnumber {
671            type_: MN_INTEGER,
672            l: 0,
673            d: 0.0,
674        };
675    }
676    // c:467 — `int fd = (argv->type == MN_INTEGER) ? argv->u.l : (int)argv->u.d;`
677    let fd: i32 = if argv[0].type_ == MN_INTEGER {
678        argv[0].l as i32
679    } else {
680        argv[0].d as i32
681    };
682    // c:470-472 — `mnumber ret; ret.type = MN_INTEGER; ret.u.l = 0;`
683    let mut ret = mnumber {
684        type_: MN_INTEGER, // c:471
685        l: 0,              // c:472
686        d: 0.0,
687    };
688    // c:474-477 — `if (fd < 0) { zerr("file descriptor out of range"); return ret; }`
689    //
690    // C uses zerr (not zwarn) — the difference matters: zerr sets the
691    // shell's errflag so the failure propagates through `set -e`
692    // (errexit) and lastval, while zwarn just prints to stderr without
693    // touching error state. Prior Rust port called zwarn so
694    // `set -e; : $((systell(-1)))` silently continued instead of
695    // aborting the script as C does.
696    if fd < 0 {
697        crate::ported::utils::zerr("file descriptor out of range"); // c:475
698        return ret;
699    }
700    // c:478 — `ret.u.l = lseek(fd, 0, SEEK_CUR);`
701    ret.l = unsafe { libc::lseek(fd, 0, libc::SEEK_CUR) } as i64;
702    ret // c:494
703}
704
705/// Port of `bin_syserror(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/system.c:494`.
706///
707/// C signature: `static int bin_syserror(char *nam, char **args,
708///                                        Options ops, int func)`.
709/// Builtin spec: `"e:p:"` (system.c:819), 0-1 positional args
710/// (the errno number or symbolic name).
711///
712/// Return values per c:485-489: 0 success / 1 bad params / 2 unknown errno name.
713/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
714pub fn bin_syserror(
715    nam: &str,
716    args: &[String], // c:494
717    ops: &options,
718    _func: i32,
719) -> i32 {
720    // c:496-497 — `int num = 0; char *errvar = NULL, *msg, *pfx = "", *str;`
721    let mut num: i32 = 0;
722    let mut errvar: Option<String> = None;
723    let mut pfx: String = String::new();
724
725    // c:500-505 — `if (OPT_ISSET(ops, 'e')) { errvar = OPT_ARG(...); isident...}`
726    if OPT_ISSET(ops, b'e') {
727        // c:500
728        let ev = OPT_ARG(ops, b'e').unwrap_or("").to_string(); // c:501
729        if !isident(&ev) {
730            // c:502
731            zwarnnam(nam, &format!("not an identifier: {}", ev)); // c:503
732            return 1; // c:504
733        }
734        errvar = Some(ev);
735    }
736    // c:508 — `if (OPT_ISSET(ops, 'p')) pfx = OPT_ARG(ops, 'p');`
737    if OPT_ISSET(ops, b'p') {
738        // c:508
739        pfx = OPT_ARG(ops, b'p').unwrap_or("").to_string(); // c:509
740    }
741
742    // c:511-530 — name parse: empty → use current errno; all-digit →
743    // atoi; symbolic → lookup in sys_errnames, return 2 on miss.
744    if args.is_empty() {
745        // c:511
746        // c:512 — `num = errno;`
747        num = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
748    } else {
749        let arg = &args[0];
750        let bytes = arg.as_bytes();
751        let mut ptr = 0usize;
752        // c:514-516 — `while (*ptr && idigit(*ptr)) ptr++;`
753        while ptr < bytes.len() && bytes[ptr].is_ascii_digit() {
754            ptr += 1;
755        }
756        if ptr == bytes.len() && ptr > 0 {
757            // c:517
758            num = arg.parse::<i32>().unwrap_or(0); // c:518
759        } else {
760            // c:519
761            // c:521-526 — walk SYS_ERRNAMES looking for *args.
762            let mut found = false;
763            for (idx, (ename, _)) in SYS_ERRNAMES.iter().enumerate() {
764                if *ename == arg {
765                    // c:522
766                    num = (idx as i32) + 1; // c:523
767                    found = true;
768                    break; // c:524
769                }
770            }
771            if !found {
772                // c:527
773                return 2; // c:528
774            }
775        }
776    }
777
778    // c:532 — `msg = strerror(num);`. Use libc::strerror so the
779    // output matches C zsh byte-for-byte (e.g. "No such file or
780    // directory"). std::io::Error::from_raw_os_error(n).to_string()
781    // would append " (os error N)" — wrong format for the
782    // `${(t)errvar}` consumer. Bug #316 in docs/BUGS.md.
783    let msg = unsafe {
784        let p = libc::strerror(num);
785        if p.is_null() {
786            String::new()
787        } else {
788            std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
789        }
790    };
791    // c:533-539 — write back to errvar or stderr.
792    if let Some(ev) = errvar {
793        let str_out = format!("{}{}", pfx, msg); // c:534-535
794        setsparam(&ev, &str_out); // c:536
795    } else {
796        eprintln!("{}{}", pfx, msg); // c:538
797    }
798    0 // c:541
799}
800
801/// Port of `bin_zsystem_flock(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/system.c:546`.
802///
803/// C signature: `static int bin_zsystem_flock(char *nam, char **args,
804///                                              Options ops, int func)`.
805/// Subcommand of `zsystem flock`. Parses its own option chain (no
806/// builtin opt-spec since the parent `zsystem` BUILTIN at c:824 has
807/// `optstr=NULL`).
808///
809/// Return values per inline comments: 0 success / 1 param/lock error
810/// / 2 timeout exhausted / 255 not supported on this platform.
811/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
812pub fn bin_zsystem_flock(
813    nam: &str,
814    args: &[String], // c:546
815    _ops: &options,
816    _func: i32,
817) -> i32 {
818    // c:548-551 — option-state locals.
819    let mut cloexec: bool = true; // c:548
820    let mut unlock: bool = false;
821    let mut readlock: bool = false;
822    let mut timeout: f64 = -1.0; // c:549
823                                 // c:550 — `long timeout_interval = 1e6;` (microseconds).
824    let mut timeout_interval: i64 = 1_000_000;
825    let mut fdvar: Option<String> = None; // c:552
826
827    // c:558-661 — option-chain parser. `while (*args && **args == '-')`.
828    let mut i = 0usize;
829    while i < args.len() && args[i].starts_with('-') {
830        let arg = &args[i];
831        i += 1;
832        let optptr = &arg[1..];
833        if optptr.is_empty() || optptr == "-" {
834            // c:562
835            break;
836        }
837        let chars: Vec<char> = optptr.chars().collect();
838        let mut idx = 0usize;
839        while idx < chars.len() {
840            let opt = chars[idx];
841            match opt {
842                'e' => {
843                    // c:566 keep lock on exec
844                    cloexec = false; // c:568
845                }
846                'f' => {
847                    // c:571 fd variable
848                    let rest: String = chars[idx + 1..].iter().collect();
849                    let fdvar_str = if !rest.is_empty() {
850                        idx = chars.len(); // c:574-575 consume rest
851                        rest
852                    } else if i < args.len() {
853                        let v = args[i].clone(); // c:577
854                        i += 1;
855                        v
856                    } else {
857                        zwarnnam(
858                            nam,
859                            &format!("flock: option {} requires a variable name", opt),
860                        );
861                        return 1;
862                    };
863                    if !isident(&fdvar_str) {
864                        // c:579
865                        zwarnnam(
866                            nam,
867                            &format!("flock: option {} requires a variable name", opt),
868                        );
869                        return 1; // c:582
870                    }
871                    fdvar = Some(fdvar_str);
872                    break;
873                }
874                'r' => readlock = true, // c:586-588
875                't' => {
876                    // c:591 timeout in seconds
877                    let rest: String = chars[idx + 1..].iter().collect();
878                    let optarg = if !rest.is_empty() {
879                        idx = chars.len();
880                        rest
881                    } else if i < args.len() {
882                        let v = args[i].clone();
883                        i += 1;
884                        v
885                    } else {
886                        zwarnnam(
887                            nam,
888                            &format!("flock: option {} requires a numeric timeout", opt),
889                        );
890                        return 1;
891                    };
892                    let tp = match matheval(&optarg) {
893                        Ok(m) => m,
894                        Err(msg) => {
895                            zerr(&msg);
896                            return 1;
897                        }
898                    };
899                    timeout = if (tp.type_ & MN_FLOAT) != 0 {
900                        // c:604
901                        tp.d
902                    } else {
903                        tp.l as f64
904                    };
905                    // c:614-618 — overflow guard at 2^30-1.
906                    if timeout > 1073741823.0 {
907                        zwarnnam(nam, &format!("flock: invalid timeout value: '{}'", optarg));
908                        return 1;
909                    }
910                    break;
911                }
912                'i' => {
913                    // c:621 retry interval
914                    let rest: String = chars[idx + 1..].iter().collect();
915                    let optarg = if !rest.is_empty() {
916                        idx = chars.len();
917                        rest
918                    } else if i < args.len() {
919                        let v = args[i].clone();
920                        i += 1;
921                        v
922                    } else {
923                        zwarnnam(
924                            nam,
925                            &format!("flock: option {} requires a numeric retry interval", opt),
926                        );
927                        return 1;
928                    };
929                    let mut tp = match matheval(&optarg) {
930                        Ok(m) => m,
931                        Err(msg) => {
932                            zerr(&msg);
933                            return 1;
934                        }
935                    };
936                    if (tp.type_ & MN_FLOAT) == 0 {
937                        // c:636
938                        tp.type_ = MN_FLOAT;
939                        tp.d = tp.l as f64;
940                    }
941                    tp.d = (tp.d * 1e6).ceil(); // c:640
942                    if tp.d < 1.0 || tp.d > 0.999 * (i64::MAX as f64) {
943                        // c:641
944                        zwarnnam(nam, &format!("flock: invalid interval value: '{}'", optarg));
945                        return 1; // c:645
946                    }
947                    timeout_interval = tp.d as i64; // c:647
948                    break;
949                }
950                'u' => unlock = true, // c:650-652
951                _ => {
952                    zwarnnam(nam, &format!("flock: unknown option: {}", opt)); // c:656
953                    return 1; // c:657
954                }
955            }
956            idx += 1;
957        }
958    }
959
960    // c:664-667 — `if (!args[0]) { zwarnnam("flock: not enough arguments"); return 1; }`
961    if i >= args.len() {
962        zwarnnam(nam, "flock: not enough arguments");
963        return 1;
964    }
965    if i + 1 < args.len() {
966        // c:668-671
967        zwarnnam(nam, "flock: too many arguments");
968        return 1;
969    }
970    let path = &args[i];
971
972    // c:674-682 — -u: unlock. argument is fd; close releases POSIX lock.
973    if unlock {
974        let flock_fd: i32 = match crate::ported::math::mathevali(path) {
975            Ok(v) => v as i32,
976            Err(msg) => {
977                zerr(&msg);
978                return 1;
979            }
980        };
981        // c:676 — zcloselockfd(flock_fd) returns -1 if not in our lockfd table.
982        if crate::ported::utils::zcloselockfd(flock_fd) < 0 {
983            // c:676
984            zwarnnam(
985                nam,
986                &format!("flock: file descriptor {} not in use for locking", flock_fd),
987            );
988            return 1;
989        }
990        return 0; // c:681
991    }
992
993    // c:684-687 — flags = readlock ? O_RDONLY|O_NOCTTY : O_RDWR|O_NOCTTY.
994    let flags = if readlock {
995        libc::O_RDONLY | libc::O_NOCTTY
996    } else {
997        libc::O_RDWR | libc::O_NOCTTY
998    };
999    // c:688 — open(unmeta(args[0]), flags).
1000    let path_unmeta = unmeta(path);
1001    let path_c = match std::ffi::CString::new(path_unmeta) {
1002        Ok(s) => s,
1003        Err(_) => return 1,
1004    };
1005    let mut flock_fd = unsafe { libc::open(path_c.as_ptr(), flags) }; // c:688
1006    if flock_fd < 0 {
1007        // c:689 — `zwarnnam(nam, "failed to open %s for writing: %e", args[0], errno);`
1008        let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1009        zwarnnam(
1010            nam,
1011            &format!(
1012                "failed to open {} for writing: {}",
1013                path,
1014                crate::ported::utils::zsh_errno_msg(eno)
1015            ),
1016        );
1017        return 1;
1018    }
1019    // c:692 — `flock_fd = movefd(flock_fd);`
1020    flock_fd = movefd(flock_fd); // c:692
1021    if flock_fd == -1 {
1022        return 1;
1023    } // c:693-694
1024
1025    // c:695-702 — set FD_CLOEXEC if cloexec.
1026    if cloexec {
1027        let fdflags = unsafe { libc::fcntl(flock_fd, libc::F_GETFD, 0) };
1028        if fdflags != -1 {
1029            unsafe {
1030                libc::fcntl(flock_fd, libc::F_SETFD, fdflags | libc::FD_CLOEXEC);
1031            }
1032        }
1033    }
1034    // c:703 — `addlockfd(flock_fd, cloexec);`
1035    crate::ported::utils::addlockfd(flock_fd, cloexec); // c:703
1036
1037    // c:705-708 — assemble struct flock.
1038    let lock_type: libc::c_short = if readlock {
1039        libc::F_RDLCK as libc::c_short
1040    } else {
1041        libc::F_WRLCK as libc::c_short
1042    };
1043    #[allow(clippy::unnecessary_cast)]
1044    let lck = libc::flock {
1045        l_type: lock_type,                         // c:705
1046        l_whence: libc::SEEK_SET as libc::c_short, // c:706
1047        l_start: 0,                                // c:707
1048        l_len: 0,                                  // c:708
1049        l_pid: 0,
1050    };
1051
1052    use std::sync::atomic::Ordering::Relaxed;
1053    if timeout > 0.0 {
1054        // c:710
1055        // c:711-749 — timed retry loop.
1056        // C body c:729-749:
1057        //   while (fcntl(flock_fd, F_SETLK, &lck) < 0) {
1058        //       if (errflag) { zclose(flock_fd); return 1; }    ← c:730
1059        //       if (errno != EINTR && errno != EACCES && errno != EAGAIN) {
1060        //           zclose(flock_fd);
1061        //           zwarnnam(nam, 'failed to lock file ...');
1062        //           return 1;
1063        //       }
1064        //       ... deadline check + sleep ...
1065        //   }
1066        //
1067        // Prior port skipped the errflag check at c:730 — Ctrl-C
1068        // during a flock-acquire wait wouldn't bail out the retry.
1069        let deadline = std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout);
1070        loop {
1071            let r = unsafe { libc::fcntl(flock_fd, libc::F_SETLK, &lck) };
1072            if r >= 0 {
1073                break;
1074            }
1075            // c:730 — `if (errflag) { zclose; return 1; }`.
1076            if crate::ported::utils::errflag.load(Relaxed) != 0 {
1077                zclose(flock_fd); // c:731
1078                return 1; // c:732
1079            }
1080            let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1081            if eno != libc::EINTR && eno != libc::EACCES && eno != libc::EAGAIN {
1082                zclose(flock_fd); // c:735
1083                                  // c:736 — `zwarnnam(nam, "failed to lock file %s: %e", args[0], errno);`
1084                                  // Format from the errno captured BEFORE zclose — close(2)
1085                                  // may clobber errno.
1086                zwarnnam(
1087                    nam,
1088                    &format!(
1089                        "failed to lock file {}: {}",
1090                        path,
1091                        crate::ported::utils::zsh_errno_msg(eno)
1092                    ),
1093                );
1094                return 1;
1095            }
1096            let now = std::time::Instant::now();
1097            if now >= deadline {
1098                zclose(flock_fd); // c:742
1099                return 2; // c:743
1100            }
1101            let remaining = deadline - now;
1102            let remaining_us = remaining.as_micros() as i64;
1103            let interval = remaining_us.min(timeout_interval);
1104            std::thread::sleep(std::time::Duration::from_micros(interval as u64));
1105        }
1106    } else {
1107        // c:751-762 — no timeout: F_SETLK if timeout==0 (non-blocking),
1108        // else F_SETLKW (blocking).
1109        // C body:
1110        //   while (fcntl(flock_fd, ...) < 0) {
1111        //       if (errflag) { zclose; return 1; }   ← c:752
1112        //       if (errno == EINTR) continue;
1113        //       zclose(flock_fd);
1114        //       zwarnnam(nam, 'failed to lock file ...');
1115        //       return 1;
1116        //   }
1117        let cmd = if timeout == 0.0 {
1118            libc::F_SETLK
1119        } else {
1120            libc::F_SETLKW
1121        };
1122        loop {
1123            let r = unsafe { libc::fcntl(flock_fd, cmd, &lck) };
1124            if r >= 0 {
1125                break;
1126            }
1127            // c:752 — errflag bail-out.
1128            if crate::ported::utils::errflag.load(Relaxed) != 0 {
1129                zclose(flock_fd); // c:753
1130                return 1; // c:754
1131            }
1132            let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1133            if eno == libc::EINTR {
1134                continue;
1135            } // c:756-757
1136            zclose(flock_fd); // c:758
1137                              // c:759 — `zwarnnam(nam, "failed to lock file %s: %e", args[0], errno);`
1138                              // Format from the errno captured BEFORE zclose.
1139            zwarnnam(
1140                nam,
1141                &format!(
1142                    "failed to lock file {}: {}",
1143                    path,
1144                    crate::ported::utils::zsh_errno_msg(eno)
1145                ),
1146            );
1147            return 1;
1148        }
1149    }
1150
1151    // c:764-765 — `if (fdvar) setiparam(fdvar, flock_fd);`
1152    if let Some(ref var) = fdvar {
1153        setiparam(var, flock_fd as i64); // c:765
1154    }
1155    0 // c:781
1156}
1157
1158/// Port of `bin_zsystem_supports(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/system.c:781`.
1159///
1160/// C signature: `static int bin_zsystem_supports(char *nam, char **args,
1161///                                                 Options ops, int func)`.
1162///
1163/// Returns 0 if the named feature is supported, 1 if not, 255 on
1164/// argument-count error.
1165/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
1166pub fn bin_zsystem_supports(
1167    nam: &str,
1168    args: &[String], // c:781
1169    _ops: &options,
1170    _func: i32,
1171) -> i32 {
1172    // c:784-787 — `if (!args[0]) ... return 255;`
1173    if args.is_empty() {
1174        zwarnnam(nam, "supports: not enough arguments");
1175        return 255;
1176    }
1177    // c:788-791 — `if (args[1]) ... return 255;`
1178    if args.len() > 1 {
1179        zwarnnam(nam, "supports: too many arguments");
1180        return 255;
1181    }
1182    // c:794 — `if (!strcmp(*args, "supports")) return 0;`
1183    if args[0] == "supports" {
1184        return 0;
1185    } // c:794-795
1186      // c:796-799 — HAVE_FCNTL_H gate; flock is universal on supported unix.
1187    #[cfg(unix)]
1188    if args[0] == "flock" {
1189        return 0;
1190    } // c:806-798
1191    1 // c:806
1192}
1193
1194/// Port of `bin_zsystem(char *nam, char **args, Options ops, int func)` from `Src/Modules/system.c:806`.
1195///
1196/// C signature: `static int bin_zsystem(char *nam, char **args,
1197///                                       Options ops, int func)`.
1198/// The `zsystem` builtin dispatcher — peels the first arg and routes
1199/// to `bin_zsystem_flock` or `bin_zsystem_supports`.
1200/// WARNING: param names don't match C — Rust=(nam, args, func) vs C=(nam, args, ops, func)
1201pub fn bin_zsystem(
1202    nam: &str,
1203    args: &[String], // c:806
1204    ops: &options,
1205    func: i32,
1206) -> i32 {
1207    if args.is_empty() {
1208        zwarnnam(nam, "subcommand expected");
1209        return 1;
1210    }
1211    // c:809 — `if (!strcmp(*args, "flock"))`
1212    if args[0] == "flock" {
1213        return bin_zsystem_flock(nam, &args[1..], ops, func); // c:810
1214    }
1215    // c:811 — `else if (!strcmp(*args, "supports"))`
1216    if args[0] == "supports" {
1217        return bin_zsystem_supports(nam, &args[1..], ops, func); // c:812
1218    }
1219    zwarnnam(nam, &format!("unknown subcommand: {}", args[0])); // c:814
1220    1 // c:815
1221}
1222
1223// ---------------------------------------------------------------------------
1224// Special-parameter callbacks (errnos + sysparams).
1225// ---------------------------------------------------------------------------
1226
1227/// Port of `errnosgetfn(UNUSED(Param pm))` from `Src/Modules/system.c:832`. The
1228/// getter for the `${errnos}` special array. C body returns
1229/// `arrdup((char **)sys_errnames)` — a fresh duplicate of the
1230/// errno-name table. Rust port returns the names as `Vec<String>`.
1231///
1232/// Port of `static char **errnosgetfn(Param pm)` from `Src/Modules/system.c:832`.
1233pub fn errnosgetfn(_pm: *mut crate::ported::zsh_h::param) -> Vec<String> {
1234    // c:832
1235    SYS_ERRNAMES.iter().map(|(n, _)| n.to_string()).collect() // c:846 arrdup
1236}
1237
1238/// Port of `fillpmsysparams(Param pm, const char *name)` from `Src/Modules/system.c:846`.
1239/// Populates a synthesised Param node for one of the three
1240/// `${sysparams[NAME]}` keys: `pid` / `ppid` / `procsubstpid`.
1241///
1242/// C signature: `static void fillpmsysparams(Param pm, const char *name)`.
1243/// Rust port returns the rendered string (or None for PM_UNSET) since
1244/// zshrs's magic-assoc dispatcher reads the value directly.
1245/// WARNING: param names don't match C — Rust=(name) vs C=(pm, name)
1246pub fn fillpmsysparams(name: &str) -> Option<String> {
1247    // c:846
1248    // Faithful port of c:854-867:
1249    //   if (!strcmp(name, 'pid')) num = (int)getpid();
1250    //   else if (!strcmp(name, 'ppid')) num = (int)getppid();
1251    //   else if (!strcmp(name, 'procsubstpid')) num = (int)procsubstpid;
1252    //   else { pm->u.str = ''; pm->node.flags |= PM_UNSET; return; }
1253    //   sprintf(buf, '%d', num); pm->u.str = dupstring(buf);
1254    //
1255    // Prior port hardcoded procsubstpid=0 — \$sysparams[procsubstpid]
1256    // always read 0 even after a process substitution fired. The
1257    // canonical procsubstpid lives in exec.rs as an AtomicI32 (c:220
1258    // port) and gets stamped at every <(...) / >(...) invocation
1259    // (c:5092 / c:5143). Read it directly.
1260    let num: i32 = match name {
1261        "pid" => unsafe { libc::getpid() },   // c:854-855
1262        "ppid" => unsafe { libc::getppid() }, // c:856-857
1263        // c:858-859 — `procsubstpid` from the live atomic so the
1264        // value matches what was last assigned by the exec-side
1265        // process-substitution code.
1266        "procsubstpid" => {
1267            crate::ported::exec::procsubstpid.load(std::sync::atomic::Ordering::Relaxed)
1268        }
1269        _ => return None, // c:861-863 PM_UNSET
1270    };
1271    Some(format!("{}", num)) // c:866-867 sprintf %d
1272}
1273
1274/// Port of `static HashNode getpmsysparams(UNUSED(HashTable ht), const char *name)`
1275/// from `Src/Modules/system.c:873-883`. Returns a synthesised Param
1276/// with u_str set via fillpmsysparams, or PM_UNSET when name isn't
1277/// pid/ppid/procsubstpid.
1278pub fn getpmsysparams(
1279    _ht: *mut crate::ported::zsh_h::HashTable,
1280    name: &str,
1281) -> Option<crate::ported::zsh_h::Param> {
1282    // c:873
1283    use crate::ported::zsh_h::{hashnode, param, Param, PM_READONLY, PM_SCALAR, PM_UNSET};
1284    let mk = |s: String, extra: i32| -> Param {
1285        Box::new(param {
1286            node: hashnode {
1287                next: None,
1288                nam: name.to_string(),
1289                flags: PM_SCALAR as i32 | PM_READONLY as i32 | extra,
1290            },
1291            u_data: 0,
1292            u_tied: None,
1293            u_arr: None,
1294            u_str: Some(s),
1295            u_val: 0,
1296            u_dval: 0.0,
1297            u_hash: None,
1298            gsu_s: None,
1299            gsu_i: None,
1300            gsu_f: None,
1301            gsu_a: None,
1302            gsu_h: None,
1303            base: 0,
1304            width: 0,
1305            env: None,
1306            ename: None,
1307            old: None,
1308            level: 0,
1309        })
1310    };
1311    // c:879 — `fillpmsysparams(pm, name)`. Wrap the rendered value
1312    // in a Param; PM_UNSET when name didn't match a known key.
1313    match fillpmsysparams(name) {
1314        Some(v) => Some(mk(v, 0)),
1315        None => Some(mk(String::new(), PM_UNSET as i32)),
1316    }
1317}
1318
1319/// Port of `static void scanpmsysparams(UNUSED(HashTable ht), ScanFunc func, int flags)`
1320/// from `Src/Modules/system.c:885-895`. Walks the three fixed keys
1321/// (pid/ppid/procsubstpid) and invokes the callback with a transient
1322/// Param per entry.
1323pub fn scanpmsysparams(
1324    _ht: *mut crate::ported::zsh_h::HashTable,
1325    func: Option<crate::ported::zsh_h::ScanFunc>,
1326    flags: i32,
1327) {
1328    // c:885
1329    use crate::ported::zsh_h::{hashnode, param, PM_READONLY, PM_SCALAR};
1330    let f = match func {
1331        Some(f) => f,
1332        None => return,
1333    };
1334    for n in ["pid", "ppid", "procsubstpid"] {
1335        if let Some(v) = fillpmsysparams(n) {
1336            let pm = param {
1337                node: hashnode {
1338                    next: None,
1339                    nam: n.to_string(),
1340                    flags: PM_SCALAR as i32 | PM_READONLY as i32,
1341                },
1342                u_data: 0,
1343                u_tied: None,
1344                u_arr: None,
1345                u_str: Some(v),
1346                u_val: 0,
1347                u_dval: 0.0,
1348                u_hash: None,
1349                gsu_s: None,
1350                gsu_i: None,
1351                gsu_f: None,
1352                gsu_a: None,
1353                gsu_h: None,
1354                base: 0,
1355                width: 0,
1356                env: None,
1357                ename: None,
1358                old: None,
1359                level: 0,
1360            };
1361            let node_box = Box::new(pm.node.clone());
1362            f(&node_box, flags); // c:891 / c:893 func call
1363        }
1364    }
1365}
1366
1367// ---------------------------------------------------------------------------
1368// Module loaders.
1369// ---------------------------------------------------------------------------
1370
1371// =====================================================================
1372// static struct features module_features                            c:910 (system.c)
1373// =====================================================================
1374
1375// `bintab` — port of `static struct builtin bintab[]` (system.c).
1376
1377// `mftab` — port of `static struct mathfunc mftab[]` (system.c).
1378
1379// `partab` — port of `static struct paramdef partab[]` (system.c).
1380
1381// `module_features` — port of `static struct features module_features`
1382// from system.c:910.
1383
1384/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/system.c:920`.
1385#[allow(unused_variables)]
1386pub fn setup_(m: *const module) -> i32 {
1387    // c:920
1388    // C body c:922-923 — `return 0`. Faithful empty-body port.
1389    0
1390}
1391
1392/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/system.c:927`.
1393/// C body: `*features = featuresarray(m, &module_features); return 0;`
1394pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
1395    *features = featuresarray(m, module_features());
1396    0 // c:942
1397}
1398
1399/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/system.c:935`.
1400/// C body: `return handlefeatures(m, &module_features, enables);`
1401pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
1402    handlefeatures(m, module_features(), enables) // c:942
1403}
1404
1405/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/system.c:942`.
1406#[allow(unused_variables)]
1407pub fn boot_(m: *const module) -> i32 {
1408    // c:942
1409    // C body c:944-945 — `return 0`. Faithful empty-body port; the
1410    //                    syserror/sysread/syswrite/zsystem builtins
1411    //                    register via the bn_list feature dispatch.
1412    0
1413}
1414
1415/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/system.c:950`.
1416/// C body: `return setfeatureenables(m, &module_features, NULL);`
1417pub fn cleanup_(m: *const module) -> i32 {
1418    setfeatureenables(m, module_features(), None) // c:957
1419}
1420
1421/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/system.c:957`.
1422#[allow(unused_variables)]
1423pub fn finish_(m: *const module) -> i32 {
1424    // c:957
1425    // C body c:959-960 — `return 0`. Faithful empty-body port; the
1426    //                    builtins unregister via cleanup_'s setfeatureenables.
1427    0
1428}
1429
1430// ---------------------------------------------------------------------------
1431// `sys_errnames[]` table — port of `Src/Modules/errnames.c:9` (which
1432// is auto-generated at build time by `Src/Modules/errnames2.awk`
1433// from the platform's `<errno.h>`).
1434//
1435// PORT.md ABSOLUTE FREEZE forbids creating `src/ported/modules/
1436// errnames.rs`, so the table lives in this file. Other modules
1437// (`fusevm_bridge`, `params`, `parameter`) read it via
1438// `crate::modules::system::SYS_ERRNAMES`. The previous table name
1439// `ERRNO_NAMES` is kept as an alias for those existing call sites.
1440// ---------------------------------------------------------------------------
1441
1442/// Linux errno table — kernel order, sourced from
1443/// `<asm-generic/errno.h>` + `<asm-generic/errno-base.h>`.
1444#[cfg(target_os = "linux")]
1445pub static SYS_ERRNAMES: &[(&str, i32)] = &[
1446    ("EPERM", 1),
1447    ("ENOENT", 2),
1448    ("ESRCH", 3),
1449    ("EINTR", 4),
1450    ("EIO", 5),
1451    ("ENXIO", 6),
1452    ("E2BIG", 7),
1453    ("ENOEXEC", 8),
1454    ("EBADF", 9),
1455    ("ECHILD", 10),
1456    ("EAGAIN", 11),
1457    ("ENOMEM", 12),
1458    ("EACCES", 13),
1459    ("EFAULT", 14),
1460    ("ENOTBLK", 15),
1461    ("EBUSY", 16),
1462    ("EEXIST", 17),
1463    ("EXDEV", 18),
1464    ("ENODEV", 19),
1465    ("ENOTDIR", 20),
1466    ("EISDIR", 21),
1467    ("EINVAL", 22),
1468    ("ENFILE", 23),
1469    ("EMFILE", 24),
1470    ("ENOTTY", 25),
1471    ("ETXTBSY", 26),
1472    ("EFBIG", 27),
1473    ("ENOSPC", 28),
1474    ("ESPIPE", 29),
1475    ("EROFS", 30),
1476    ("EMLINK", 31),
1477    ("EPIPE", 32),
1478    ("EDOM", 33),
1479    ("ERANGE", 34),
1480    ("EDEADLK", 35),
1481    ("ENAMETOOLONG", 36),
1482    ("ENOLCK", 37),
1483    ("ENOSYS", 38),
1484    ("ENOTEMPTY", 39),
1485    ("ELOOP", 40),
1486];
1487
1488/// macOS errno table — Apple's `<sys/errno.h>` (Homebrew/older-SDK shape).
1489#[cfg(target_os = "macos")]
1490pub static SYS_ERRNAMES: &[(&str, i32)] = &[
1491    ("EPERM", 1),
1492    ("ENOENT", 2),
1493    ("ESRCH", 3),
1494    ("EINTR", 4),
1495    ("EIO", 5),
1496    ("ENXIO", 6),
1497    ("E2BIG", 7),
1498    ("ENOEXEC", 8),
1499    ("EBADF", 9),
1500    ("ECHILD", 10),
1501    ("EDEADLK", 11),
1502    ("ENOMEM", 12),
1503    ("EACCES", 13),
1504    ("EFAULT", 14),
1505    ("ENOTBLK", 15),
1506    ("EBUSY", 16),
1507    ("EEXIST", 17),
1508    ("EXDEV", 18),
1509    ("ENODEV", 19),
1510    ("ENOTDIR", 20),
1511    ("EISDIR", 21),
1512    ("EINVAL", 22),
1513    ("ENFILE", 23),
1514    ("EMFILE", 24),
1515    ("ENOTTY", 25),
1516    ("ETXTBSY", 26),
1517    ("EFBIG", 27),
1518    ("ENOSPC", 28),
1519    ("ESPIPE", 29),
1520    ("EROFS", 30),
1521    ("EMLINK", 31),
1522    ("EPIPE", 32),
1523    ("EDOM", 33),
1524    ("ERANGE", 34),
1525    ("EAGAIN", 35),
1526    ("EINPROGRESS", 36),
1527    ("EALREADY", 37),
1528    ("ENOTSOCK", 38),
1529    ("EDESTADDRREQ", 39),
1530    ("EMSGSIZE", 40),
1531    ("EPROTOTYPE", 41),
1532    ("ENOPROTOOPT", 42),
1533    ("EPROTONOSUPPORT", 43),
1534    ("ESOCKTNOSUPPORT", 44),
1535    ("ENOTSUP", 45),
1536    ("EPFNOSUPPORT", 46),
1537    ("EAFNOSUPPORT", 47),
1538    ("EADDRINUSE", 48),
1539    ("EADDRNOTAVAIL", 49),
1540    ("ENETDOWN", 50),
1541    ("ENETUNREACH", 51),
1542    ("ENETRESET", 52),
1543    ("ECONNABORTED", 53),
1544    ("ECONNRESET", 54),
1545    ("ENOBUFS", 55),
1546    ("EISCONN", 56),
1547    ("ENOTCONN", 57),
1548    ("ESHUTDOWN", 58),
1549    ("ETOOMANYREFS", 59),
1550    ("ETIMEDOUT", 60),
1551    ("ECONNREFUSED", 61),
1552    ("ELOOP", 62),
1553    ("ENAMETOOLONG", 63),
1554    ("EHOSTDOWN", 64),
1555    ("EHOSTUNREACH", 65),
1556    ("ENOTEMPTY", 66),
1557    ("EPROCLIM", 67),
1558    ("EUSERS", 68),
1559    ("EDQUOT", 69),
1560    ("ESTALE", 70),
1561    ("EREMOTE", 71),
1562    ("EBADRPC", 72),
1563    ("ERPCMISMATCH", 73),
1564    ("EPROGUNAVAIL", 74),
1565    ("EPROGMISMATCH", 75),
1566    ("EPROCUNAVAIL", 76),
1567    ("ENOLCK", 77),
1568    ("ENOSYS", 78),
1569    ("EFTYPE", 79),
1570    ("EAUTH", 80),
1571    ("ENEEDAUTH", 81),
1572    ("EPWROFF", 82),
1573    ("EDEVERR", 83),
1574    ("EOVERFLOW", 84),
1575    ("EBADEXEC", 85),
1576    ("EBADARCH", 86),
1577    ("ESHLIBVERS", 87),
1578    ("EBADMACHO", 88),
1579    ("ECANCELED", 89),
1580    ("EIDRM", 90),
1581    ("ENOMSG", 91),
1582    ("EILSEQ", 92),
1583    ("ENOATTR", 93),
1584    ("EBADMSG", 94),
1585    ("EMULTIHOP", 95),
1586    ("ENODATA", 96),
1587    ("ENOLINK", 97),
1588    ("ENOSR", 98),
1589    ("ENOSTR", 99),
1590    ("EPROTO", 100),
1591    ("ETIME", 101),
1592    ("EOPNOTSUPP", 102),
1593    ("ENOPOLICY", 103),
1594    ("ENOTRECOVERABLE", 104),
1595    ("EOWNERDEAD", 105),
1596    ("EQFULL", 106),
1597    ("ENOTCAPABLE", 107), // sys/errno.h:265 — ELAST on current macOS SDKs
1598];
1599
1600/// Fallback for platforms zshrs doesn't have a verified table for —
1601/// the POSIX-portable subset (errnos 1-34).
1602#[cfg(not(any(target_os = "macos", target_os = "linux")))]
1603pub static SYS_ERRNAMES: &[(&str, i32)] = &[
1604    ("EPERM", 1),
1605    ("ENOENT", 2),
1606    ("ESRCH", 3),
1607    ("EINTR", 4),
1608    ("EIO", 5),
1609    ("ENXIO", 6),
1610    ("E2BIG", 7),
1611    ("ENOEXEC", 8),
1612    ("EBADF", 9),
1613    ("ECHILD", 10),
1614    ("ENOMEM", 12),
1615    ("EACCES", 13),
1616    ("EFAULT", 14),
1617    ("EBUSY", 16),
1618    ("EEXIST", 17),
1619    ("EXDEV", 18),
1620    ("ENODEV", 19),
1621    ("ENOTDIR", 20),
1622    ("EISDIR", 21),
1623    ("EINVAL", 22),
1624    ("ENFILE", 23),
1625    ("EMFILE", 24),
1626    ("ENOTTY", 25),
1627    ("EFBIG", 27),
1628    ("ENOSPC", 28),
1629    ("ESPIPE", 29),
1630    ("EROFS", 30),
1631    ("EMLINK", 31),
1632    ("EPIPE", 32),
1633    ("EDOM", 33),
1634    ("ERANGE", 34),
1635];
1636
1637/// Back-compat alias: pre-rewrite call sites in `fusevm_bridge`,
1638/// `params`, and `parameter` reference the table as `ERRNO_NAMES`.
1639/// New code should use `SYS_ERRNAMES` (matches the C identifier).
1640pub static ERRNO_NAMES: &[(&str, i32)] = SYS_ERRNAMES;
1641
1642static MODULE_FEATURES: OnceLock<Mutex<crate::ported::zsh_h::features>> = OnceLock::new();
1643
1644// Local stubs for the per-module entry points. C uses generic
1645// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
1646// 3275/3370/3445) but those take `Builtin` + `Features` pointer
1647// fields the Rust port doesn't carry. The hardcoded descriptor
1648// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
1649// WARNING: NOT IN SYSTEM.C — Rust-only module-framework shim.
1650// C uses generic featuresarray/handlefeatures/setfeatureenables from
1651// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1652// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1653fn featuresarray(_m: *const module, _f: &Mutex<crate::ported::zsh_h::features>) -> Vec<String> {
1654    vec![
1655        "b:syserror".to_string(),
1656        "b:sysread".to_string(),
1657        "b:syswrite".to_string(),
1658        "b:sysopen".to_string(),
1659        "b:sysseek".to_string(),
1660        "b:zsystem".to_string(),
1661        "f:systell".to_string(),
1662        "p:errnos".to_string(),
1663        "p:sysparams".to_string(),
1664    ]
1665}
1666
1667// WARNING: NOT IN SYSTEM.C — Rust-only module-framework shim.
1668// C uses generic featuresarray/handlefeatures/setfeatureenables from
1669// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1670// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1671fn handlefeatures(
1672    _m: *const module,
1673    _f: &Mutex<crate::ported::zsh_h::features>,
1674    enables: &mut Option<Vec<i32>>,
1675) -> i32 {
1676    if enables.is_none() {
1677        *enables = Some(vec![1; 9]);
1678    }
1679    0
1680}
1681
1682// WARNING: NOT IN SYSTEM.C — Rust-only module-framework shim.
1683// C uses generic featuresarray/handlefeatures/setfeatureenables from
1684// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1685// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1686fn setfeatureenables(
1687    _m: *const module,
1688    _f: &Mutex<crate::ported::zsh_h::features>,
1689    _e: Option<&[i32]>,
1690) -> i32 {
1691    0
1692}
1693
1694// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1695// ─── RUST-ONLY ACCESSORS ───
1696//
1697// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1698// RwLock<T>>` globals declared above. C zsh uses direct global
1699// access; Rust needs these wrappers because `OnceLock::get_or_init`
1700// is the only way to lazily construct shared state. These ported sit
1701// here so the body of this file reads in C source order without
1702// the accessor wrappers interleaved between real port ported.
1703// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1704
1705// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1706// ─── RUST-ONLY ACCESSORS ───
1707//
1708// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
1709// RwLock<T>>` globals declared above. C zsh uses direct global
1710// access; Rust needs these wrappers because `OnceLock::get_or_init`
1711// is the only way to lazily construct shared state. These ported sit
1712// here so the body of this file reads in C source order without
1713// the accessor wrappers interleaved between real port ported.
1714// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1715
1716// WARNING: NOT IN SYSTEM.C — Rust-only module-framework shim.
1717// C uses generic featuresarray/handlefeatures/setfeatureenables from
1718// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
1719// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
1720fn module_features() -> &'static Mutex<crate::ported::zsh_h::features> {
1721    MODULE_FEATURES.get_or_init(|| {
1722        Mutex::new(crate::ported::zsh_h::features {
1723            bn_list: None,
1724            bn_size: 6,
1725            cd_list: None,
1726            cd_size: 0,
1727            mf_list: None,
1728            mf_size: 1,
1729            pd_list: None,
1730            pd_size: 2,
1731            n_abstract: 0,
1732        })
1733    })
1734}
1735
1736#[cfg(test)]
1737mod tests {
1738    use super::*;
1739    use crate::ported::math::{mnumber, MN_INTEGER};
1740    use crate::zsh_h::{options, MAX_OPS};
1741    use std::fs::File;
1742    use std::io::Write;
1743    use tempfile::TempDir;
1744
1745    /// Verifies `getposint` parses non-negative ints and rejects
1746    /// negatives + trailing garbage per c:51.
1747    #[test]
1748    fn getposint_basic() {
1749        let _g = crate::test_util::global_state_lock();
1750        assert_eq!(getposint("42", "test"), 42);
1751        assert_eq!(getposint("0", "test"), 0);
1752        assert_eq!(getposint("-1", "test"), -1); // negative → -1
1753        assert_eq!(getposint("abc", "test"), -1); // garbage → -1
1754    }
1755
1756    /// Port of `bin_zsystem(char *nam, char **args, Options ops, int func)` from `Src/Modules/system.c:806`.
1757    /// Verifies `bin_zsystem_supports` per c:794-800.
1758    #[test]
1759    fn bin_zsystem_supports_self() {
1760        let _g = crate::test_util::global_state_lock();
1761        let ops = empty_ops();
1762        assert_eq!(
1763            bin_zsystem_supports("zsystem", &["supports".to_string()], &ops, 0),
1764            0
1765        );
1766        #[cfg(unix)]
1767        assert_eq!(
1768            bin_zsystem_supports("zsystem", &["flock".to_string()], &ops, 0),
1769            0
1770        );
1771        assert_eq!(
1772            bin_zsystem_supports("zsystem", &["nosuchfeature".to_string()], &ops, 0),
1773            1
1774        );
1775    }
1776
1777    /// Verifies `bin_zsystem_supports` arg-count guards (c:784-791).
1778    #[test]
1779    fn bin_zsystem_supports_arg_count() {
1780        let _g = crate::test_util::global_state_lock();
1781        let ops = empty_ops();
1782        assert_eq!(bin_zsystem_supports("zsystem", &[], &ops, 0), 255);
1783        assert_eq!(
1784            bin_zsystem_supports("zsystem", &["a".to_string(), "b".to_string()], &ops, 0),
1785            255
1786        );
1787    }
1788
1789    /// Verifies `bin_zsystem` dispatches to the right subcommand
1790    /// (c:809/811/814).
1791    #[test]
1792    fn bin_zsystem_dispatch() {
1793        let _g = crate::test_util::global_state_lock();
1794        let ops = empty_ops();
1795        assert_eq!(
1796            bin_zsystem(
1797                "zsystem",
1798                &["supports".to_string(), "supports".to_string()],
1799                &ops,
1800                0
1801            ),
1802            0
1803        );
1804        assert_eq!(bin_zsystem("zsystem", &["unknown".to_string()], &ops, 0), 1);
1805        assert_eq!(bin_zsystem("zsystem", &[], &ops, 0), 1);
1806    }
1807
1808    /// Verifies `errnosgetfn` returns the dup'd table (c:835).
1809    #[test]
1810    fn errnosgetfn_returns_table() {
1811        let _g = crate::test_util::global_state_lock();
1812        let names = errnosgetfn(std::ptr::null_mut());
1813        assert!(names.contains(&"EPERM".to_string()));
1814        assert!(names.contains(&"ENOENT".to_string()));
1815        assert!(names.contains(&"EINVAL".to_string()));
1816    }
1817
1818    /// Port of `scanpmsysparams(UNUSED(HashTable ht), ScanFunc func, int flags)` from `Src/Modules/system.c:885`.
1819    /// Verifies `fillpmsysparams` for the three known keys
1820    /// (c:854-862) and PM_UNSET fallback (c:861-863).
1821    #[test]
1822    fn fillpmsysparams_keys() {
1823        let _g = crate::test_util::global_state_lock();
1824        assert!(fillpmsysparams("pid").is_some());
1825        assert!(fillpmsysparams("ppid").is_some());
1826        assert!(fillpmsysparams("procsubstpid").is_some());
1827        assert!(fillpmsysparams("nonsense").is_none());
1828    }
1829
1830    /// Verifies `getpmsysparams` proxies through fillpmsysparams
1831    /// (c:878).
1832    #[test]
1833    fn getpmsysparams_pid_set() {
1834        let _g = crate::test_util::global_state_lock();
1835        use crate::ported::zsh_h::PM_UNSET;
1836        let pm_pid = getpmsysparams(std::ptr::null_mut(), "pid").expect("pid Param");
1837        assert!(pm_pid.node.flags & PM_UNSET as i32 == 0, "pid must be set");
1838        let pm_bad = getpmsysparams(std::ptr::null_mut(), "nonsense").expect("Param");
1839        assert!(
1840            pm_bad.node.flags & PM_UNSET as i32 != 0,
1841            "unknown key PM_UNSET"
1842        );
1843    }
1844
1845    /// Verifies `scanpmsysparams` yields all three known keys
1846    /// (c:889-894).
1847    #[test]
1848    fn scanpmsysparams_three_entries() {
1849        let _g = crate::test_util::global_state_lock();
1850        use std::sync::Mutex;
1851        static KEYS: Mutex<Vec<String>> = Mutex::new(Vec::new());
1852        KEYS.lock().unwrap().clear();
1853        fn cb(node: &crate::ported::zsh_h::HashNode, _flags: i32) {
1854            KEYS.lock().unwrap().push(node.nam.clone());
1855        }
1856        scanpmsysparams(std::ptr::null_mut(), Some(cb), 0);
1857        let collected = KEYS.lock().unwrap().clone();
1858        assert!(collected.iter().any(|k| k == "pid"));
1859        assert!(collected.iter().any(|k| k == "ppid"));
1860        assert!(collected.iter().any(|k| k == "procsubstpid"));
1861    }
1862
1863    fn empty_ops() -> options {
1864        options {
1865            ind: [0u8; MAX_OPS],
1866            args: Vec::new(),
1867            argscount: 0,
1868            argsalloc: 0,
1869        }
1870    }
1871    /// Port of `bin_sysopen(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/system.c:319`.
1872    fn ops_with(args: &[(u8, &str)]) -> options {
1873        // ind[c] encodes "set" in low 2 bits (1 = -X, 2 = +X) plus the
1874        // 1-based args[] slot shifted up by 2 (per zsh.h:1412 OPT_ARG
1875        // macro `args[(ind[c]>>2) - 1]`). idx=0 → ind=4 (slot 1, set
1876        // via -), idx=1 → ind=8 (slot 2), etc.
1877        let mut ops = empty_ops();
1878        for (idx, (opt, val)) in args.iter().enumerate() {
1879            ops.ind[*opt as usize] = (((idx + 1) << 2) | 1) as u8;
1880            ops.args.push(val.to_string());
1881            ops.argscount = (idx + 1) as i32;
1882            ops.argsalloc = (idx + 1) as i32;
1883        }
1884        ops
1885    }
1886
1887    /// Verifies `bin_syserror` writes message to errvar with prefix
1888    /// (c:533-536).
1889    #[test]
1890    fn bin_syserror_to_errvar_with_prefix() {
1891        let _g = crate::test_util::global_state_lock();
1892        let ops = ops_with(&[(b'e', "myerr"), (b'p', "PFX:")]);
1893        let r = bin_syserror("syserror", &["ENOENT".to_string()], &ops, 0);
1894        assert_eq!(r, 0);
1895        // Side-effect param flows through params::setsparam → paramtab().
1896        let val = paramtab()
1897            .read()
1898            .ok()
1899            .and_then(|t| t.get("myerr").and_then(|p| p.u_str.clone()))
1900            .unwrap_or_default();
1901        assert!(
1902            val.starts_with("PFX:"),
1903            "expected PFX: prefix, got {:?}",
1904            val
1905        );
1906    }
1907
1908    /// Verifies `bin_syserror` returns 2 for unknown errno name
1909    /// (c:527-528).
1910    #[test]
1911    fn bin_syserror_unknown_name_returns_2() {
1912        let _g = crate::test_util::global_state_lock();
1913        let ops = ops_with(&[(b'e', "myerr2")]);
1914        assert_eq!(
1915            bin_syserror("syserror", &["ENOTAREALERROR".to_string()], &ops, 0),
1916            2
1917        );
1918    }
1919
1920    /// Port of `bin_sysopen(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/system.c:319`.
1921    /// Verifies `bin_sysopen` opens a file and stores fd in the
1922    /// named variable (c:413-414) when -u is a non-digit identifier.
1923    #[test]
1924    #[cfg(unix)]
1925    fn bin_sysopen_writes_fd_to_var() {
1926        let _g = crate::test_util::global_state_lock();
1927        // `assignnparam` (params.rs:4464) short-circuits with
1928        // `if unset(EXECOPT) { return None; }`. The Rust options table
1929        // doesn't pre-populate EXECOPT=true the way C's
1930        // createoptiontable does at shell start, so the test must do
1931        // it manually — same pattern as the existing tests in
1932        // params.rs (8212/8547/9392/9442).
1933        let saved_exec = opt_state_get("exec").unwrap_or(false);
1934        opt_state_set("exec", true);
1935
1936        let dir = TempDir::new().unwrap();
1937        let p = dir.path().join("a.txt");
1938        let ops = ops_with(&[(b'u', "MYFD"), (b'o', "creat")]);
1939        // Set the -w flag manually (no arg).
1940        let mut ops = ops;
1941        ops.ind[b'w' as usize] = 1;
1942        let r = bin_sysopen("sysopen", &[p.to_str().unwrap().to_string()], &ops, 0);
1943        assert_eq!(r, 0);
1944        // Side-effect param flows through params::setiparam → paramtab().
1945        // `setiparam` (params.rs:4649) builds an `mnumber{ MN_INTEGER, .l = fd }`
1946        // and routes via `assignnparam` — the value lands in `u_val` (i64),
1947        // NOT `u_str`. The original test read `u_str`, got `""`, and
1948        // `parse::<i32>()` returned ParseIntError::Empty. Read u_val.
1949        let fd: i32 = paramtab()
1950            .read()
1951            .ok()
1952            .and_then(|t| t.get("MYFD").map(|p| p.u_val as i32))
1953            .expect("MYFD not set by sysopen");
1954        assert!(fd >= 10, "movefd should lift fd to 10+, got {}", fd);
1955        unsafe {
1956            libc::close(fd);
1957        }
1958        opt_state_set("exec", saved_exec);
1959    }
1960
1961    /// Port of `bin_sysopen(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/system.c:319`.
1962    /// Verifies `bin_sysseek` lseek + return-code shape (c:461-462).
1963    #[test]
1964    #[cfg(unix)]
1965    fn bin_sysseek_basic() {
1966        let _g = crate::test_util::global_state_lock();
1967        let dir = TempDir::new().unwrap();
1968        let p = dir.path().join("b.txt");
1969        {
1970            let mut f = File::create(&p).unwrap();
1971            f.write_all(b"hello world").unwrap();
1972        }
1973        let path_c = std::ffi::CString::new(p.to_str().unwrap()).unwrap();
1974        let fd = unsafe { libc::open(path_c.as_ptr(), libc::O_RDONLY) };
1975        assert!(fd >= 0);
1976        let ops = ops_with(&[(b'u', &fd.to_string()), (b'w', "start")]);
1977        let r = bin_sysseek("sysseek", &["5".to_string()], &ops, 0);
1978        assert_eq!(r, 0);
1979        unsafe {
1980            libc::close(fd);
1981        }
1982    }
1983
1984    /// Direct test of `setiparam` writeback into `paramtab()` — pins
1985    /// the exact contract `bin_sysopen` relies on at c:413-414. The
1986    /// `assignnparam` short-circuits with `unset(EXECOPT) → return
1987    /// None`, so the test must set "exec" true first (same pattern as
1988    /// the params.rs internal tests at 8212/8547/9392).
1989    #[test]
1990    fn setiparam_writes_integer_to_paramtab() {
1991        let _g = crate::test_util::global_state_lock();
1992        let saved_exec = opt_state_get("exec").unwrap_or(false);
1993        opt_state_set("exec", true);
1994        let name = "ZSHRS_TEST_SETIPARAM_FD_INT";
1995        let _ = setiparam(name, 12345);
1996        let val = paramtab()
1997            .read()
1998            .ok()
1999            .and_then(|t| t.get(name).map(|p| p.u_val));
2000        opt_state_set("exec", saved_exec);
2001        assert_eq!(
2002            val,
2003            Some(12345),
2004            "setiparam should put the integer in paramtab().get(name).u_val"
2005        );
2006    }
2007
2008    /// Verifies `math_systell` returns lseek(SEEK_CUR) (c:478).
2009    #[test]
2010    #[cfg(unix)]
2011    fn math_systell_returns_lseek_cur() {
2012        let _g = crate::test_util::global_state_lock();
2013        let dir = TempDir::new().unwrap();
2014        let p = dir.path().join("c.txt");
2015        {
2016            let mut f = File::create(&p).unwrap();
2017            f.write_all(b"hello world").unwrap();
2018        }
2019        let path_c = std::ffi::CString::new(p.to_str().unwrap()).unwrap();
2020        let fd = unsafe { libc::open(path_c.as_ptr(), libc::O_RDONLY) };
2021        unsafe {
2022            libc::lseek(fd, 7, libc::SEEK_SET);
2023        }
2024        let argv = vec![mnumber {
2025            l: fd as i64,
2026            d: 0.0,
2027            type_: MN_INTEGER,
2028        }];
2029        let r = math_systell("systell", 1, &argv, 0);
2030        assert_eq!(r.type_, MN_INTEGER);
2031        assert_eq!(r.l, 7);
2032        unsafe {
2033            libc::close(fd);
2034        }
2035    }
2036
2037    // ─── zsh-corpus pins for getposint ──────────────────────────────
2038
2039    /// `getposint("42", "name")` returns 42.
2040    #[test]
2041    fn system_corpus_getposint_decimal() {
2042        let _g = crate::test_util::global_state_lock();
2043        assert_eq!(getposint("42", "test"), 42);
2044    }
2045
2046    /// `getposint("0", "name")` returns 0.
2047    #[test]
2048    fn system_corpus_getposint_zero() {
2049        let _g = crate::test_util::global_state_lock();
2050        assert_eq!(getposint("0", "test"), 0);
2051    }
2052
2053    /// `getposint("-5", "name")` returns -1 (error per c:51).
2054    #[test]
2055    fn system_corpus_getposint_negative_returns_error() {
2056        let _g = crate::test_util::global_state_lock();
2057        assert_eq!(
2058            getposint("-5", "test"),
2059            -1,
2060            "negative input rejected per c:51"
2061        );
2062    }
2063
2064    /// `getposint("abc", "name")` returns -1 (non-integer).
2065    #[test]
2066    fn system_corpus_getposint_non_numeric_returns_error() {
2067        let _g = crate::test_util::global_state_lock();
2068        assert_eq!(getposint("abc", "test"), -1);
2069    }
2070
2071    /// `getposint("42abc", "name")` returns -1 (trailing garbage).
2072    #[test]
2073    fn system_corpus_getposint_trailing_garbage_returns_error() {
2074        let _g = crate::test_util::global_state_lock();
2075        assert_eq!(
2076            getposint("42abc", "test"),
2077            -1,
2078            "trailing non-digits rejected per c:51 eptr check"
2079        );
2080    }
2081
2082    /// `getposint("")` returns 0 (zstrtol parses empty as 0,
2083    /// eptr is empty too, ret is 0, so neither error branch fires —
2084    /// matches C behavior at system.c:51).
2085    #[test]
2086    fn system_corpus_getposint_empty_returns_zero() {
2087        let _g = crate::test_util::global_state_lock();
2088        assert_eq!(
2089            getposint("", "test"),
2090            0,
2091            "empty input: zstrtol returns 0, neither error branch hits"
2092        );
2093    }
2094
2095    /// `getposint("1000000", "name")` returns 1000000 (large positive).
2096    #[test]
2097    fn system_corpus_getposint_large_positive() {
2098        let _g = crate::test_util::global_state_lock();
2099        assert_eq!(getposint("1000000", "test"), 1_000_000);
2100    }
2101
2102    // ═══════════════════════════════════════════════════════════════════
2103    // Additional C-parity tests for Src/Modules/system.c.
2104    // ═══════════════════════════════════════════════════════════════════
2105
2106    /// c:45 — `getposint("0")` returns 0 (zero is non-negative, valid).
2107    #[test]
2108    fn getposint_zero_returns_zero() {
2109        let _g = crate::test_util::global_state_lock();
2110        assert_eq!(getposint("0", "test"), 0, "zero is valid positive int");
2111    }
2112
2113    /// c:51 — `getposint("-5")` returns -1 (negative rejected).
2114    #[test]
2115    fn getposint_negative_returns_minus_one() {
2116        let _g = crate::test_util::global_state_lock();
2117        assert_eq!(
2118            getposint("-5", "test"),
2119            -1,
2120            "negative rejected per ret < 0 branch"
2121        );
2122    }
2123
2124    /// c:51 — `getposint("12abc")` returns -1 (trailing garbage).
2125    #[test]
2126    fn getposint_trailing_garbage_returns_minus_one() {
2127        let _g = crate::test_util::global_state_lock();
2128        assert_eq!(
2129            getposint("12abc", "test"),
2130            -1,
2131            "trailing non-digit rejected per *eptr != \\0"
2132        );
2133    }
2134
2135    /// c:45 — `getposint("42")` returns 42 (canonical positive int).
2136    #[test]
2137    fn getposint_canonical_positive() {
2138        let _g = crate::test_util::global_state_lock();
2139        assert_eq!(getposint("42", "test"), 42);
2140        assert_eq!(getposint("1", "test"), 1);
2141    }
2142
2143    /// c:45 — `getposint` is deterministic.
2144    #[test]
2145    fn getposint_is_deterministic() {
2146        let _g = crate::test_util::global_state_lock();
2147        for s in &["0", "42", "1000", "-1", "abc"] {
2148            let first = getposint(s, "test");
2149            for _ in 0..5 {
2150                assert_eq!(getposint(s, "test"), first, "{:?} must be pure", s);
2151            }
2152        }
2153    }
2154
2155    /// c:1085 — `errnosgetfn` returns a vec of error name strings
2156    /// (errno names like "EACCES", "ENOENT").
2157    #[test]
2158    fn errnosgetfn_returns_nonempty_vec() {
2159        let _g = crate::test_util::global_state_lock();
2160        let names = errnosgetfn(std::ptr::null_mut());
2161        // Must contain at least the common POSIX errnos.
2162        assert!(!names.is_empty(), "errno table must not be empty");
2163    }
2164
2165    /// c:1085 — errnosgetfn output should contain "EACCES" (a POSIX
2166    /// errno guaranteed on every Unix system).
2167    #[test]
2168    #[cfg(unix)]
2169    fn errnosgetfn_includes_eacces() {
2170        let _g = crate::test_util::global_state_lock();
2171        let names = errnosgetfn(std::ptr::null_mut());
2172        assert!(
2173            names.iter().any(|n| n == "EACCES"),
2174            "errno table must include EACCES, got {:?}",
2175            names
2176        );
2177    }
2178
2179    /// c:1098 — `fillpmsysparams("anything")` returns Option<String>
2180    /// (no panic on lookup of arbitrary key).
2181    #[test]
2182    fn fillpmsysparams_does_not_panic_on_arbitrary_key() {
2183        let _g = crate::test_util::global_state_lock();
2184        let _ = fillpmsysparams("zshrs_never_real_sysparam_key");
2185    }
2186
2187    /// c:1224 — `setup_(NULL)` returns 0 (no-op).
2188    #[test]
2189    fn system_setup_returns_zero() {
2190        let _g = crate::test_util::global_state_lock();
2191        assert_eq!(setup_(std::ptr::null()), 0);
2192    }
2193
2194    /// c:1245-1261 — lifecycle stubs all return 0.
2195    #[test]
2196    fn system_lifecycle_stubs_return_zero() {
2197        let _g = crate::test_util::global_state_lock();
2198        assert_eq!(boot_(std::ptr::null()), 0);
2199        assert_eq!(cleanup_(std::ptr::null()), 0);
2200        assert_eq!(finish_(std::ptr::null()), 0);
2201    }
2202
2203    // ═══════════════════════════════════════════════════════════════════
2204    // Additional C-parity tests for Src/Modules/system.c
2205    // c:40 getposint / c:1085 errnosgetfn / c:1098 fillpmsysparams
2206    // c:1118 getpmsysparams / c:1162 scanpmsysparams / lifecycle
2207    // ═══════════════════════════════════════════════════════════════════
2208
2209    /// c:40 — `getposint` accepts leading whitespace per strtol(3) convention.
2210    /// C body uses zstrtol which skips leading whitespace.
2211    #[test]
2212    fn getposint_leading_whitespace_accepted_per_strtol() {
2213        let _g = crate::test_util::global_state_lock();
2214        let r = getposint(" 5", "test");
2215        assert_eq!(r, 5, "strtol skips leading whitespace, parses 5");
2216    }
2217
2218    /// c:40 — `getposint` rejects hex-prefix (only decimal allowed).
2219    #[test]
2220    fn getposint_hex_prefix_returns_minus_one() {
2221        let _g = crate::test_util::global_state_lock();
2222        let r = getposint("0x10", "test");
2223        assert_eq!(r, -1, "hex prefix must reject");
2224    }
2225
2226    /// c:40 — `getposint` is deterministic for arbitrary input.
2227    #[test]
2228    fn getposint_deterministic_for_any_input() {
2229        let _g = crate::test_util::global_state_lock();
2230        for input in ["42", "-1", "abc", "", "0", "999999999"] {
2231            let first = getposint(input, "test");
2232            for _ in 0..3 {
2233                assert_eq!(
2234                    getposint(input, "test"),
2235                    first,
2236                    "must be deterministic for {:?}",
2237                    input
2238                );
2239            }
2240        }
2241    }
2242
2243    /// c:1085 — `errnosgetfn(null)` is safe.
2244    #[test]
2245    fn errnosgetfn_null_pm_safe() {
2246        let _g = crate::test_util::global_state_lock();
2247        let _ = errnosgetfn(std::ptr::null_mut());
2248    }
2249
2250    /// c:1085 — `errnosgetfn` entries all start with 'E' (errno names).
2251    #[test]
2252    fn errnosgetfn_all_entries_start_with_e() {
2253        let _g = crate::test_util::global_state_lock();
2254        let v = errnosgetfn(std::ptr::null_mut());
2255        for entry in &v {
2256            assert!(
2257                entry.starts_with('E'),
2258                "errno name {:?} must start with 'E'",
2259                entry
2260            );
2261        }
2262    }
2263
2264    /// c:1085 — `errnosgetfn` is deterministic.
2265    #[test]
2266    fn errnosgetfn_is_deterministic() {
2267        let _g = crate::test_util::global_state_lock();
2268        let first = errnosgetfn(std::ptr::null_mut());
2269        for _ in 0..5 {
2270            assert_eq!(errnosgetfn(std::ptr::null_mut()), first);
2271        }
2272    }
2273
2274    /// c:1098 — `fillpmsysparams("nothing_real")` returns None.
2275    #[test]
2276    fn fillpmsysparams_unknown_key_returns_none() {
2277        let _g = crate::test_util::global_state_lock();
2278        let r = fillpmsysparams("definitely_not_a_sysparam_xyz");
2279        assert!(r.is_none(), "unknown key → None");
2280    }
2281
2282    /// c:1098 — `fillpmsysparams("pid")` returns Some.
2283    #[test]
2284    fn fillpmsysparams_pid_returns_some() {
2285        let _g = crate::test_util::global_state_lock();
2286        let r = fillpmsysparams("pid");
2287        assert!(r.is_some(), "pid key must resolve");
2288    }
2289
2290    /// c:1162 — `scanpmsysparams` with None callback safe.
2291    #[test]
2292    fn scanpmsysparams_none_callback_no_panic() {
2293        let _g = crate::test_util::global_state_lock();
2294        scanpmsysparams(std::ptr::null_mut(), None, 0);
2295    }
2296
2297    /// c:1224-1261 — full lifecycle setup→features→enables→boot→cleanup→finish.
2298    #[test]
2299    fn system_full_lifecycle_returns_zero_for_all() {
2300        let _g = crate::test_util::global_state_lock();
2301        let null = std::ptr::null();
2302        assert_eq!(setup_(null), 0);
2303        let mut feats = Vec::new();
2304        let _ = features_(null, &mut feats);
2305        let mut enables: Option<Vec<i32>> = None;
2306        let _ = enables_(null, &mut enables);
2307        assert_eq!(boot_(null), 0);
2308        assert_eq!(cleanup_(null), 0);
2309        assert_eq!(finish_(null), 0);
2310    }
2311
2312    // ═══════════════════════════════════════════════════════════════════
2313    // Additional C-parity tests for Src/Modules/system.c
2314    // c:40 getposint / c:1085 errnosgetfn / c:1098 fillpmsysparams /
2315    // c:1118 getpmsysparams / c:1162 scanpmsysparams
2316    // ═══════════════════════════════════════════════════════════════════
2317
2318    /// c:40 — `getposint` returns i32 (compile-time type pin).
2319    #[test]
2320    fn getposint_returns_i32_type() {
2321        let _g = crate::test_util::global_state_lock();
2322        let _: i32 = getposint("0", "test");
2323    }
2324
2325    /// c:40 — `getposint("999999999999")` overflow returns -1 (per C).
2326    #[test]
2327    fn getposint_overflow_returns_minus_one() {
2328        let _g = crate::test_util::global_state_lock();
2329        let r = getposint("999999999999999999999", "test");
2330        // C body uses strtol which clamps + sets errno; expected -1 sentinel.
2331        assert_eq!(r, -1, "overflow → -1");
2332    }
2333
2334    /// c:1085 — `errnosgetfn` returns Vec<String> (compile-time pin).
2335    #[test]
2336    fn errnosgetfn_returns_vec_string_type() {
2337        let _g = crate::test_util::global_state_lock();
2338        let _: Vec<String> = errnosgetfn(std::ptr::null_mut());
2339    }
2340
2341    /// c:1085 — `errnosgetfn` returns non-empty vec on Unix.
2342    #[cfg(unix)]
2343    #[test]
2344    fn errnosgetfn_unix_non_empty() {
2345        let _g = crate::test_util::global_state_lock();
2346        let v = errnosgetfn(std::ptr::null_mut());
2347        assert!(!v.is_empty(), "Unix must have errnos");
2348    }
2349
2350    /// c:1098 — `fillpmsysparams` returns Option<String> (compile-time pin).
2351    #[test]
2352    fn fillpmsysparams_returns_option_string_type() {
2353        let _g = crate::test_util::global_state_lock();
2354        let _: Option<String> = fillpmsysparams("anything");
2355    }
2356
2357    /// c:1098 — `fillpmsysparams("ppid")` returns Some.
2358    #[test]
2359    fn fillpmsysparams_ppid_returns_some() {
2360        let _g = crate::test_util::global_state_lock();
2361        let r = fillpmsysparams("ppid");
2362        assert!(r.is_some(), "ppid is known sysparam");
2363    }
2364
2365    /// c:1098 — `fillpmsysparams("")` empty name returns None.
2366    #[test]
2367    fn fillpmsysparams_empty_returns_none() {
2368        let _g = crate::test_util::global_state_lock();
2369        assert!(fillpmsysparams("").is_none(), "empty → None");
2370    }
2371
2372    /// c:40 — `getposint` is deterministic for negative input.
2373    #[test]
2374    fn getposint_negative_is_deterministic() {
2375        let _g = crate::test_util::global_state_lock();
2376        let first = getposint("-42", "test");
2377        for _ in 0..3 {
2378            assert_eq!(
2379                getposint("-42", "test"),
2380                first,
2381                "getposint(-42) must be deterministic"
2382            );
2383        }
2384    }
2385
2386    /// c:1162 — `scanpmsysparams(null, None, 0)` is safe (returns void).
2387    #[test]
2388    fn scanpmsysparams_returns_void_signature() {
2389        let _g = crate::test_util::global_state_lock();
2390        let _: () = scanpmsysparams(std::ptr::null_mut(), None, 0);
2391    }
2392
2393    /// c:40 — `getposint("0", _)` returns 0 (zero is valid pos int).
2394    #[test]
2395    fn getposint_zero_returns_zero_pin() {
2396        let _g = crate::test_util::global_state_lock();
2397        assert_eq!(getposint("0", "test"), 0, "0 is valid pos int");
2398    }
2399
2400    // ═══════════════════════════════════════════════════════════════════
2401    // Additional C-parity tests for Src/Modules/system.c
2402    // c:65 bin_sysread / c:264 bin_syswrite / c:345 bin_sysopen /
2403    // c:534 bin_sysseek / c:631 bin_syserror / c:718 bin_zsystem_flock /
2404    // c:1018 bin_zsystem_supports / c:1053 bin_zsystem +
2405    // c:1224-1261 lifecycle type pins
2406    // ═══════════════════════════════════════════════════════════════════
2407
2408    fn empty_ops_sys() -> crate::ported::zsh_h::options {
2409        crate::ported::zsh_h::options {
2410            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2411            args: Vec::new(),
2412            argscount: 0,
2413            argsalloc: 0,
2414        }
2415    }
2416
2417    /// c:65 — `bin_sysread` returns i32 (compile-time type pin).
2418    #[test]
2419    fn bin_sysread_returns_i32_type() {
2420        let _g = crate::test_util::global_state_lock();
2421        let ops = empty_ops_sys();
2422        let _: i32 = bin_sysread("sysread", &[], &ops, 0);
2423    }
2424
2425    /// c:264 — `bin_syswrite` returns i32.
2426    #[test]
2427    fn bin_syswrite_returns_i32_type() {
2428        let _g = crate::test_util::global_state_lock();
2429        let ops = empty_ops_sys();
2430        let _: i32 = bin_syswrite("syswrite", &[], &ops, 0);
2431    }
2432
2433    /// c:345 — `bin_sysopen` returns i32.
2434    #[test]
2435    fn bin_sysopen_returns_i32_type() {
2436        let _g = crate::test_util::global_state_lock();
2437        let ops = empty_ops_sys();
2438        let _: i32 = bin_sysopen("sysopen", &[], &ops, 0);
2439    }
2440
2441    /// c:534 — `bin_sysseek` returns i32.
2442    #[test]
2443    fn bin_sysseek_returns_i32_type() {
2444        let _g = crate::test_util::global_state_lock();
2445        let ops = empty_ops_sys();
2446        let _: i32 = bin_sysseek("sysseek", &[], &ops, 0);
2447    }
2448
2449    /// c:631 — `bin_syserror` returns i32.
2450    #[test]
2451    fn bin_syserror_returns_i32_type() {
2452        let _g = crate::test_util::global_state_lock();
2453        let ops = empty_ops_sys();
2454        let _: i32 = bin_syserror("syserror", &[], &ops, 0);
2455    }
2456
2457    /// c:718 — `bin_zsystem_flock` returns i32.
2458    #[test]
2459    fn bin_zsystem_flock_returns_i32_type() {
2460        let _g = crate::test_util::global_state_lock();
2461        let ops = empty_ops_sys();
2462        let _: i32 = bin_zsystem_flock("zsystem", &[], &ops, 0);
2463    }
2464
2465    /// c:1018 — `bin_zsystem_supports` returns i32.
2466    #[test]
2467    fn bin_zsystem_supports_returns_i32_type() {
2468        let _g = crate::test_util::global_state_lock();
2469        let ops = empty_ops_sys();
2470        let _: i32 = bin_zsystem_supports("zsystem", &[], &ops, 0);
2471    }
2472
2473    /// c:1053 — `bin_zsystem` returns i32.
2474    #[test]
2475    fn bin_zsystem_returns_i32_type() {
2476        let _g = crate::test_util::global_state_lock();
2477        let ops = empty_ops_sys();
2478        let _: i32 = bin_zsystem("zsystem", &[], &ops, 0);
2479    }
2480
2481    /// c:1053 — `bin_zsystem` with no args returns nonzero (usage error).
2482    #[test]
2483    fn bin_zsystem_no_args_returns_nonzero() {
2484        let _g = crate::test_util::global_state_lock();
2485        let ops = empty_ops_sys();
2486        let r = bin_zsystem("zsystem", &[], &ops, 0);
2487        assert_ne!(r, 0, "no args → usage error");
2488    }
2489
2490    /// c:1018 — `bin_zsystem_supports` is deterministic for same input.
2491    #[test]
2492    fn bin_zsystem_supports_is_deterministic() {
2493        let _g = crate::test_util::global_state_lock();
2494        let ops = empty_ops_sys();
2495        let first = bin_zsystem_supports("zsystem", &["flock".into()], &ops, 0);
2496        for _ in 0..3 {
2497            assert_eq!(
2498                bin_zsystem_supports("zsystem", &["flock".into()], &ops, 0),
2499                first,
2500                "bin_zsystem_supports must be deterministic",
2501            );
2502        }
2503    }
2504
2505    /// c:1224 — `setup_` returns i32 (compile-time type pin).
2506    #[test]
2507    fn system_setup_returns_i32_type() {
2508        let _g = crate::test_util::global_state_lock();
2509        let _: i32 = setup_(std::ptr::null());
2510    }
2511
2512    /// c:1255 + c:1261 — cleanup/finish idempotent.
2513    #[test]
2514    fn system_cleanup_finish_idempotent() {
2515        let _g = crate::test_util::global_state_lock();
2516        for _ in 0..5 {
2517            assert_eq!(cleanup_(std::ptr::null()), 0);
2518            assert_eq!(finish_(std::ptr::null()), 0);
2519        }
2520    }
2521
2522    /// c:1232 — features non-empty + use canonical b:/p:/f:/c: prefix
2523    /// per zsh's module-feature naming spec (b=builtin, p=param, f=mathfunc,
2524    /// c=condition).
2525    #[test]
2526    fn system_features_nonempty_canonical_prefix() {
2527        let _g = crate::test_util::global_state_lock();
2528        let mut feats = Vec::new();
2529        features_(std::ptr::null(), &mut feats);
2530        assert!(!feats.is_empty(), "system advertises ≥1 feature");
2531        for f in &feats {
2532            let ok = f.starts_with("b:")
2533                || f.starts_with("p:")
2534                || f.starts_with("f:")
2535                || f.starts_with("c:");
2536            assert!(ok, "feature {:?} must use b:/p:/f:/c: prefix", f);
2537        }
2538    }
2539
2540    // ═══════════════════════════════════════════════════════════════════
2541    // Additional C-parity tests for Src/Modules/system.c
2542    // c:40 getposint / c:65 bin_sysread / c:264 bin_syswrite /
2543    // c:345 bin_sysopen / c:534 bin_sysseek / c:598 math_systell /
2544    // c:631 bin_syserror / c:1085 errnosgetfn
2545    // ═══════════════════════════════════════════════════════════════════
2546
2547    /// c:40 — `getposint` returns i32 (compile-time pin).
2548    #[test]
2549    fn system_getposint_returns_i32_type() {
2550        let _g = crate::test_util::global_state_lock();
2551        let _: i32 = getposint("42", "test");
2552    }
2553
2554    /// c:40 — `getposint("42", _)` returns 42.
2555    #[test]
2556    fn system_getposint_42_returns_42() {
2557        let _g = crate::test_util::global_state_lock();
2558        assert_eq!(getposint("42", "test"), 42, "'42' parses to 42");
2559    }
2560
2561    /// c:40 — `getposint("garbage", _)` is non-positive.
2562    #[test]
2563    fn system_getposint_garbage_non_positive() {
2564        let _g = crate::test_util::global_state_lock();
2565        let r = getposint("garbage", "test");
2566        assert!(
2567            r <= 0,
2568            "garbage must return non-positive sentinel; got {}",
2569            r
2570        );
2571    }
2572
2573    /// c:65 — `bin_sysread` no-args returns nonzero. The C source reads
2574    /// stdin into `REPLY` when no outvar is given, so this test asserts
2575    /// non-success — which is only true when stdin yields no data
2576    /// (EOF → rc=5). Under `cargo test` in a terminal, stdin is the
2577    /// inherited TTY and a read might succeed with TTY chatter → rc=0,
2578    /// flaking the test. Pin stdin to a closed pipe so the read
2579    /// deterministically returns 0 bytes → bin_sysread returns 5 (EOF).
2580    #[test]
2581    fn bin_sysread_no_args_returns_nonzero() {
2582        let _g = crate::test_util::global_state_lock();
2583        // Create a pipe and close the write end so reading the read
2584        // end returns 0 bytes (EOF). dup2 the read end over fd 0 for
2585        // the duration of the call.
2586        let mut pipefds: [libc::c_int; 2] = [0, 0];
2587        let pipe_rc = unsafe { libc::pipe(pipefds.as_mut_ptr()) };
2588        if pipe_rc != 0 {
2589            // pipe(2) failed — fall back to original behavior; the
2590            // test will still pass if stdin happens to be empty.
2591            let ops = empty_ops_sys();
2592            let r = bin_sysread("sysread", &[], &ops, 0);
2593            assert_ne!(r, 0, "sysread no args → usage error");
2594            return;
2595        }
2596        let read_fd = pipefds[0];
2597        let write_fd = pipefds[1];
2598        unsafe { libc::close(write_fd) }; // close write → EOF on read
2599        let saved_stdin = unsafe { libc::dup(0) };
2600        unsafe { libc::dup2(read_fd, 0) };
2601        let ops = empty_ops_sys();
2602        let r = bin_sysread("sysread", &[], &ops, 0);
2603        // Restore stdin and clean up.
2604        unsafe { libc::dup2(saved_stdin, 0) };
2605        unsafe { libc::close(saved_stdin) };
2606        unsafe { libc::close(read_fd) };
2607        assert_ne!(r, 0, "sysread no args + closed stdin → EOF (rc=5)");
2608    }
2609
2610    /// c:264 — `bin_syswrite` no-args returns nonzero (usage error).
2611    #[test]
2612    fn bin_syswrite_no_args_returns_nonzero() {
2613        let _g = crate::test_util::global_state_lock();
2614        let ops = empty_ops_sys();
2615        let r = bin_syswrite("syswrite", &[], &ops, 0);
2616        assert_ne!(r, 0, "syswrite no args → usage error");
2617    }
2618
2619    /// c:345 — `bin_sysopen` no-args returns nonzero (usage error).
2620    #[test]
2621    fn bin_sysopen_no_args_returns_nonzero() {
2622        let _g = crate::test_util::global_state_lock();
2623        let ops = empty_ops_sys();
2624        let r = bin_sysopen("sysopen", &[], &ops, 0);
2625        assert_ne!(r, 0, "sysopen no args → usage error");
2626    }
2627
2628    /// c:534 — `bin_sysseek` no-args returns nonzero (usage error).
2629    #[test]
2630    fn bin_sysseek_no_args_returns_nonzero() {
2631        let _g = crate::test_util::global_state_lock();
2632        let ops = empty_ops_sys();
2633        let r = bin_sysseek("sysseek", &[], &ops, 0);
2634        assert_ne!(r, 0, "sysseek no args → usage error");
2635    }
2636
2637    /// c:598 — `math_systell` MUST safely return mnumber without
2638    /// panicking; C source validates argc before argv[0] access.
2639    /// In zshrs the port indexes `argv[0]` without bounds check at c:601.
2640    #[test]
2641    fn math_systell_returns_mnumber_type() {
2642        let _g = crate::test_util::global_state_lock();
2643        let _: crate::ported::zsh_h::mnumber = math_systell("systell", 0, &[], 0);
2644    }
2645
2646    /// c:1085 — `errnosgetfn(null)` returns Vec<String> (compile-time pin).
2647    #[test]
2648    fn errnosgetfn_null_returns_vec_string_type() {
2649        let _g = crate::test_util::global_state_lock();
2650        let _: Vec<String> = errnosgetfn(std::ptr::null_mut());
2651    }
2652
2653    /// c:1085 — `errnosgetfn(null)` is non-empty (POSIX errno table
2654    /// has dozens of entries).
2655    #[test]
2656    fn errnosgetfn_null_non_empty() {
2657        let _g = crate::test_util::global_state_lock();
2658        let v = errnosgetfn(std::ptr::null_mut());
2659        assert!(!v.is_empty(), "errnos table must have ≥1 entry on POSIX");
2660    }
2661
2662    /// c:631 — `bin_syserror` returns i32 (compile-time pin, alt).
2663    #[test]
2664    fn bin_syserror_returns_i32_pin_alt() {
2665        let _g = crate::test_util::global_state_lock();
2666        let ops = empty_ops_sys();
2667        let _: i32 = bin_syserror("syserror", &[], &ops, 0);
2668    }
2669}