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}