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