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