zsh/ported/init.rs
1//! init.c - main loop and initialization routines
2//!
3//! Port of Src/init.c
4
5use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
6use std::sync::Mutex;
7
8use crate::ported::builtin::{realexit, LASTVAL, RETFLAG, STOPMSG};
9use crate::ported::context::zcontext_restore;
10use crate::ported::hist::{curhist, curline, hbegin, hend, hist_ring, histlinect, stophist};
11use crate::ported::lex::{set_tok, tok, ENDINPUT};
12use crate::ported::mem::popheap;
13use crate::ported::options::{dosetopt, emulation};
14use crate::ported::params::{getsparam, TERMFLAGS};
15use crate::ported::signals::{
16 dotrap, install_handler, intr, queue_signals, signal_ignore, sigtrapped, unqueue_signals,
17};
18use crate::ported::signals_h::dont_queue_signals;
19use crate::ported::text::getpermtext;
20use crate::ported::utils::{callhookfunc, errflag, movefd, unmeta, ERRFLAG_ERROR};
21use crate::ported::zsh_h::{
22 eprog, hookdef, interact, islogin, isset, jobbing, Eprog, CONTINUEONERROR, EMULATE_KSH,
23 EMULATE_SH, GLOBALRCS, HISTBEEP, HISTIGNOREDUPS, HIST_DUP, HIST_TMPSTORE, HOOKF_ALL,
24 HOOK_SUFFIX, HUP, IGNOREEOF, INTERACTIVE, LEXERR, PRIVILEGED, RCS, SHINSTDIN, SINGLECOMMAND,
25 TERM_BAD, TERM_NOUP, TERM_UNKNOWN, ZEXIT_NORMAL, ZLE_CMD_POSTEXEC, ZLE_CMD_PREEXEC,
26};
27// =========================================================================
28// File-scope globals from init.c
29// =========================================================================
30
31/// Port of `int noexitct` from Src/init.c:44.
32pub static noexitct: AtomicI32 = AtomicI32::new(0); // c:44
33
34// buffer for $_ and its length // c:46
35
36/// Port of `char *zunderscore` from Src/init.c:49.
37pub static zunderscore: Mutex<String> = Mutex::new(String::new()); // c:49
38
39/// Port of `size_t underscorelen` from Src/init.c:52.
40pub static underscorelen: AtomicUsize = AtomicUsize::new(0); // c:52
41
42/// Port of `int underscoreused` from Src/init.c:55.
43pub static underscoreused: AtomicI32 = AtomicI32::new(0); // c:55
44
45// what level of sourcing we are at // c:57
46
47/// Port of `int sourcelevel` from Src/init.c:60.
48pub static sourcelevel: AtomicI32 = AtomicI32::new(0); // c:60
49
50// the shell tty fd // c:62
51
52/// Port of `mod_export int SHTTY` from Src/init.c:65.
53pub static SHTTY: AtomicI32 = AtomicI32::new(-1); // c:65
54
55// the FILE attached to the shell tty // c:67
56// `mod_export FILE *shout;` — represented as a libc::FILE pointer. // c:70
57pub static shout: Mutex<usize> = Mutex::new(0); // c:70
58
59// termcap strings // c:72
60
61/// Port of `mod_export char *tcstr[TC_COUNT]` from Src/init.c:75.
62pub static tcstr: Mutex<[String; 39]> = Mutex::new([
63 // c:75
64 String::new(),
65 String::new(),
66 String::new(),
67 String::new(),
68 String::new(),
69 String::new(),
70 String::new(),
71 String::new(),
72 String::new(),
73 String::new(),
74 String::new(),
75 String::new(),
76 String::new(),
77 String::new(),
78 String::new(),
79 String::new(),
80 String::new(),
81 String::new(),
82 String::new(),
83 String::new(),
84 String::new(),
85 String::new(),
86 String::new(),
87 String::new(),
88 String::new(),
89 String::new(),
90 String::new(),
91 String::new(),
92 String::new(),
93 String::new(),
94 String::new(),
95 String::new(),
96 String::new(),
97 String::new(),
98 String::new(),
99 String::new(),
100 String::new(),
101 String::new(),
102 String::new(),
103]);
104
105// lengths of each termcap string // c:78
106
107/// Port of `mod_export int tclen[TC_COUNT]` from Src/init.c:81.
108pub static tclen: Mutex<[i32; 39]> = Mutex::new([0; 39]); // c:81
109
110// Values of the li, co and am entries // c:82
111
112/// Port of `int tclines` from Src/init.c:85.
113pub static tclines: AtomicI32 = AtomicI32::new(0); // c:85
114
115/// Port of `int tccolumns` from Src/init.c:85.
116pub static tccolumns: AtomicI32 = AtomicI32::new(0); // c:85
117
118/// Port of `mod_export int hasam` from Src/init.c:87.
119pub static hasam: AtomicI32 = AtomicI32::new(0); // c:87
120
121/// Port of `int hasxn` from Src/init.c:89.
122pub static hasxn: AtomicI32 = AtomicI32::new(0); // c:89
123
124// Value of the Co (max_colors) entry: may not be set // c:91
125
126/// Port of `mod_export int tccolours` from Src/init.c:94.
127pub static tccolours: AtomicI32 = AtomicI32::new(0); // c:94
128
129// SIGCHLD mask // c:96
130// `mod_export sigset_t sigchld_mask;` — owned by signals layer. // c:99
131
132/// Port of `struct hookdef zshhooks[]` from `Src/init.c:101-106`:
133/// ```c
134/// struct hookdef zshhooks[] = {
135/// HOOKDEF("exit", NULL, HOOKF_ALL),
136/// HOOKDEF("before_trap", NULL, HOOKF_ALL),
137/// HOOKDEF("after_trap", NULL, HOOKF_ALL),
138/// HOOKDEF("get_color_attr", NULL, HOOKF_ALL),
139/// };
140/// ```
141/// Stored as `AtomicPtr<hookdef>` holding the base pointer of a
142/// heap-leaked `[hookdef; 4]` so that `addhookdefs(NULL, zshhooks,
143/// 4)` at `setupvals()` (c:1085) can walk the array via `h++` exactly
144/// as the C call does. The leak is intentional — C's static-storage
145/// `zshhooks[]` has program lifetime, and the registered hookdef
146/// pointers must stay valid for any later `runhookdef` dispatch.
147pub static zshhooks: once_cell::sync::Lazy<
148 // c:101
149 std::sync::atomic::AtomicPtr<hookdef>,
150> = once_cell::sync::Lazy::new(|| {
151 let arr: Box<[hookdef; 4]> = Box::new([
152 hookdef {
153 // c:102
154 next: std::ptr::null_mut(),
155 name: "exit".to_string(),
156 def: None,
157 flags: HOOKF_ALL,
158 funcs: std::ptr::null_mut(),
159 },
160 hookdef {
161 // c:103
162 next: std::ptr::null_mut(),
163 name: "before_trap".to_string(),
164 def: None,
165 flags: HOOKF_ALL,
166 funcs: std::ptr::null_mut(),
167 },
168 hookdef {
169 // c:104
170 next: std::ptr::null_mut(),
171 name: "after_trap".to_string(),
172 def: None,
173 flags: HOOKF_ALL,
174 funcs: std::ptr::null_mut(),
175 },
176 hookdef {
177 // c:105
178 next: std::ptr::null_mut(),
179 name: "get_color_attr".to_string(),
180 def: None,
181 flags: HOOKF_ALL,
182 funcs: std::ptr::null_mut(),
183 },
184 ]);
185 let base = Box::into_raw(arr) as *mut hookdef;
186 std::sync::atomic::AtomicPtr::new(base)
187});
188
189// original argv[0]. This is already metafied // c:258
190
191/// Port of `static char *argv0` from Src/init.c:259.
192static argv0: Mutex<String> = Mutex::new(String::new()); // c:259
193
194/// Port of `mod_export ZleEntryPoint zle_entry_ptr` from Src/init.c:1730.
195/// Stored as a usize representing a fn pointer (0 == NULL).
196pub static zle_entry_ptr: AtomicUsize = AtomicUsize::new(0); // c:1730
197
198/// Port of `mod_export int zle_load_state` from Src/init.c:1739.
199pub static zle_load_state: AtomicI32 = AtomicI32::new(0); // c:1739
200
201/// Port of `mod_export CompctlReadFn compctlreadptr` from Src/init.c:1831.
202pub static compctlreadptr: AtomicUsize = AtomicUsize::new(0); // c:1831
203
204/// Port of `mod_export int use_exit_printed` from Src/init.c:1846.
205pub static use_exit_printed: AtomicI32 = AtomicI32::new(0); // c:1846
206
207// =========================================================================
208// Static arrays from init.c
209// =========================================================================
210
211/// Port of `static char *tccapnams[TC_COUNT]` from Src/init.c:747.
212const tccapnams: [&str; 39] = [
213 // c:747
214 "cl", "le", "LE", "nd", "RI", "up", "UP", "do", "DO", "dc", "DC", "ic", "IC", "cd", "ce", "al",
215 "dl", "ta", "md", "mh", "so", "us", "ZH", "me", "se", "ue", "ZR", "ch", "ku", "kd", "kl", "kr",
216 "sc", "rc", "bc", "AF", "AB", "vi", "ve",
217];
218
219/// Port of `static void parseargs(...)` from Src/init.c:263.
220fn parseargs(
221 zsh_name: &str,
222 argv: &mut Vec<String>, // c:263
223 runscript: &mut Option<String>,
224 cmdptr: &mut Option<String>,
225) {
226 let mut idx: usize = 0; // c:265
227 let flags: i32 = 1; /* PARSEARGS_TOPLEVEL */
228 // c:267
229 let flags = if argv.first().map(|s| s.starts_with('-')).unwrap_or(false)
230 // c:268-269
231 {
232 flags | 2 /* PARSEARGS_LOGIN */
233 } else {
234 flags
235 };
236
237 *argv0.lock().unwrap() = argv[idx].clone(); // c:271
238 // argzero = posixzero = *argv++ // c:271
239 idx += 1;
240 // SHIN = 0; // c:272
241
242 // parseopts(zsh_name, &argv, opts, cmdptr, NULL, flags) // c:280
243 let _ = parseopts(zsh_name, argv, &mut idx, cmdptr, flags);
244
245 // c:291-292 — `if (opts[SHINSTDIN]) opts[USEZLE] = opts[USEZLE] &&
246 // isatty(0);`. SHINSTDIN starts unset here (set below), so this is
247 // normally a no-op; honor it for an explicitly-set SHINSTDIN.
248 if isset(SHINSTDIN) {
249 let usezle = isset(crate::ported::zsh_h::USEZLE) && unsafe { libc::isatty(0) != 0 };
250 // USEZLE's canonical option name is `zle` (zsh_h.rs:4145
251 // `opt_name(USEZLE) == "zle"`); `isset(USEZLE)` reads the `zle`
252 // key, so the write MUST use `zle` too. The prior `"usezle"` key
253 // was never read → this `opts[USEZLE] = opts[USEZLE] && isatty(0)`
254 // downgrade silently did nothing.
255 crate::ported::options::opt_state_set("zle", usezle); // c:292
256 }
257
258 // c:294 — `paramlist = znewlinklist();`
259 let mut paramlist: Vec<String> = Vec::new();
260 if idx < argv.len() {
261 // c:295 — there's a non-option argument.
262 // c:296 — `if (unset(SHINSTDIN))` — a positional arg is the
263 // script to run (or $0 under -c) ONLY when not already forced to
264 // read stdin.
265 if !isset(SHINSTDIN) {
266 // c:297 — `posixzero = *argv;`
267 crate::ported::utils::set_posixzero(Some(argv[idx].clone()));
268 if cmdptr.is_some() {
269 // c:299 — `argzero = *argv;` ($0 under -c)
270 crate::ported::utils::set_argzero(Some(argv[idx].clone()));
271 } else {
272 // c:301 — `*runscript = *argv;`
273 *runscript = Some(argv[idx].clone());
274 }
275 // c:302 — `opts[INTERACTIVE] &= 1;`. A script source makes
276 // the shell non-interactive unless `-i` forced it on. zshrs
277 // collapsed INTERACTIVE to a bool (no 2-sentinel), so a
278 // non-tty default-on can't be distinguished from explicit
279 // -i; reading from a file is non-interactive, so clear it.
280 crate::ported::options::opt_state_set("interactive", false);
281 idx += 1;
282 }
283 // c:305-306 — remaining args become positional parameters.
284 while idx < argv.len() {
285 paramlist.push(argv[idx].clone());
286 idx += 1;
287 }
288 } else if cmdptr.is_none() {
289 // c:307-308 — `else if (!*cmdptr) opts[SHINSTDIN] = 1;` — no
290 // script and no `-c`: read commands from stdin.
291 crate::ported::options::opt_state_set("shinstdin", true);
292 }
293 // c:309-310 — `if (isset(SINGLECOMMAND)) opts[INTERACTIVE] &= 1;`
294 if isset(SINGLECOMMAND) {
295 crate::ported::options::opt_state_set("interactive", false);
296 }
297 // c:311 — `opts[INTERACTIVE] = !!opts[INTERACTIVE];` is a no-op for
298 // the bool port.
299 // c:312-315 — `MONITOR`/`HASHDIRS` default (2) → INTERACTIVE.
300 let interactive = isset(INTERACTIVE);
301 crate::ported::options::opt_state_set("monitor", interactive);
302 crate::ported::options::opt_state_set("hashdirs", interactive);
303 // c:316 — `pparams = paramlist;`
304 if !paramlist.is_empty() {
305 if let Ok(mut p) = crate::ported::builtin::PPARAMS.lock() {
306 *p = paramlist;
307 }
308 }
309}
310
311/// Port of `static void parseopts_insert(...)` from Src/init.c:328.
312///
313/// Insert into list in order of pointer value.
314fn parseopts_insert(optlist: &mut Vec<usize>, base: usize, optno: i32) {
315 // c:328
316 let ptr = base + (if optno < 0 { -optno } else { optno }) as usize; // c:348
317 for (i, &node) in optlist.iter().enumerate() {
318 // c:348
319 if ptr < node {
320 // c:348
321 optlist.insert(i, ptr); // c:348
322 return; // c:348
323 }
324 }
325 optlist.push(ptr); // c:348
326}
327
328/// Port of `mod_export int parseopts(...)` from Src/init.c:390.
329/// Rust idiom replacement: index-walk over `argv` Vec covers the C
330/// argv pointer-advance; the `emulate_required` / `toplevel` state
331/// tracking mirrors the C source's local flags. The long-option
332/// table lookups happen against the shared options.rs registry.
333pub fn parseopts(
334 _nam: &str,
335 argv: &mut Vec<String>,
336 idx: &mut usize, // c:390
337 cmdp: &mut Option<String>,
338 flags: i32,
339) -> i32 {
340 let toplevel = (flags & 1) != 0; // c:396
341 let mut emulate_required = toplevel; // c:397
342 *cmdp = None; // c:400
343
344 while *idx < argv.len() {
345 // c:418
346 let arg = argv[*idx].clone();
347 if !(arg.starts_with('-') || arg.starts_with('+')) {
348 break;
349 }
350 if arg == "--version" {
351 // c:434
352 println!("zshrs (C-port)"); // c:435-436
353 if toplevel {
354 std::process::exit(0);
355 } // c:437
356 }
357 if arg == "--help" {
358 // c:439
359 printhelp(); // c:440
360 if toplevel {
361 std::process::exit(0);
362 } // c:441
363 }
364 if arg == "-c" {
365 // c:470
366 if emulate_required {
367 // c:471
368 parseopts_setemulate(_nam, flags); // c:472
369 emulate_required = false; // c:473
370 }
371 *idx += 1;
372 *cmdp = argv.get(*idx).cloned(); // c:476
373 *idx += 1;
374 continue;
375 }
376 // c:Src/init.c:478-490 — `-o NAME` / `+o NAME`: a long-name
377 // option whose name follows (`-o nullglob`). `optlookup` resolves
378 // the name; `dosetopt` applies it with the sign as the on/off
379 // action (`-` sets, `+` unsets).
380 if arg == "-o" || arg == "+o" {
381 if emulate_required {
382 parseopts_setemulate(_nam, flags); // c:481-483
383 emulate_required = false;
384 }
385 let action = arg.starts_with('-'); // c:420
386 *idx += 1;
387 if let Some(name) = argv.get(*idx).cloned() {
388 let optno = crate::ported::options::optlookup(&name); // c:493
389 if optno != crate::ported::zsh_h::OPT_INVALID {
390 // c:501 — dosetopt(optno, action, toplevel, new_opts)
391 crate::ported::options::dosetopt(optno, action as i32, toplevel as i32);
392 }
393 }
394 *idx += 1;
395 continue;
396 }
397 // zshrs-specific long flags (`--zsh`, `--bash`, `--dap`, …) are
398 // NOT zsh options and the faithful c:511 `optlookup` would reject
399 // them with "no such option". They are consumed by the bin's
400 // front-end before zsh_main, so any remaining `--long` here is
401 // skipped rather than ported through optlookup. (This is the one
402 // deliberate divergence — it's why the prior port stubbed the
403 // whole loop; the single-letter / `-o` forms below are safe to
404 // apply faithfully.)
405 if arg.starts_with("--") {
406 *idx += 1;
407 continue;
408 }
409 // c:Src/init.c:516-534 — a cluster of single option letters
410 // (`-x`, `-v`, `-xv`, `+x`). Each char maps via `optlookupc` to a
411 // (possibly negated, for inverted-sense letters like `f`) option
412 // number; `dosetopt` applies it with the sign as the action.
413 // OPT_INVALID letters are skipped (deferred to the front-end)
414 // rather than erroring as C does at c:517 — zshrs accepts some
415 // non-zsh single-dash flags the C table doesn't know.
416 if emulate_required {
417 parseopts_setemulate(_nam, flags); // c:516-519
418 emulate_required = false;
419 }
420 let action = arg.starts_with('-'); // c:420
421 for c in arg[1..].chars() {
422 let optno = crate::ported::options::optlookupc(c); // c:520
423 if optno != crate::ported::zsh_h::OPT_INVALID {
424 // c:526 — dosetopt(optno, action, toplevel, new_opts)
425 crate::ported::options::dosetopt(optno, action as i32, toplevel as i32);
426 }
427 }
428 *idx += 1;
429 }
430 if emulate_required {
431 // c:557
432 parseopts_setemulate(_nam, flags); // c:557
433 }
434 0 // c:557
435}
436
437/// Port of `static void printhelp(void)` from Src/init.c:557.
438fn printhelp() {
439 // c:557
440 let argz = argv0.lock().unwrap().clone(); // c:557
441 println!("Usage: {} [<options>] [<argument> ...]", argz); // c:559
442 println!(); // c:560
443 println!("Special options:"); // c:560
444 println!(" --help show this message, then exit"); // c:561
445 println!(" --version show zsh version number, then exit"); // c:562
446 println!(" -b end option processing, like --"); // c:564
447 println!(" -c take first argument as a command to execute"); // c:577
448 println!(" -o OPTION set an option by name (see below)"); // c:577
449 println!(); // c:577
450 println!("Normal options are named. An option may be turned on by"); // c:577
451 println!("`-o OPTION', `--OPTION', `+o no_OPTION' or `+-no-OPTION'. An"); // c:577
452 println!("option may be turned off by `-o no_OPTION', `--no-OPTION',"); // c:577
453 println!("`+o OPTION' or `+-OPTION'. Options are listed below only in"); // c:577
454 println!("`--OPTION' or `--no-OPTION' form."); // c:577
455 // printoptionlist(); // c:577
456}
457
458/// Port of `mod_export void init_io(char *cmd)` from Src/init.c:577.
459pub fn init_io(_cmd: Option<&str>) {
460 // c:577
461 // stdout, stderr fully buffered // c:577
462 // setvbuf(stdout, outbuf, _IOFBF, BUFSIZ); setvbuf(stderr, ...) // c:587-591
463 // (Rust's stdout/stderr are line/block buffered by default)
464
465 // Close any existing shout // c:605-614
466 *shout.lock().unwrap() = 0;
467 if SHTTY.load(Ordering::SeqCst) != -1 {
468 // c:615
469 // c:616 — `zclose(SHTTY);` — fdtable-aware close. SHTTY was
470 // registered as FDT_INTERNAL by movefd at one of the open
471 // sites below (c:627 ttyname/O_RDWR open, c:658 dup(0), c:662
472 // dup(1), c:668 /dev/tty open — all routed through movefd
473 // which sets fdtable[fd] = FDT_INTERNAL at utils.rs:2243).
474 // Prior port used raw libc::close which skipped the
475 // fdtable_set(fd, FDT_UNUSED) clear that zclose does at
476 // utils.rs:2402. Same leak shape as random.rs finish_
477 // (b3107b5a46), tcp.rs tcp_close (9b4dae375a), and
478 // zpty.rs deleteptycmd (c37083d09f) — stale FDT_INTERNAL
479 // marker survives the close → kernel-reused fd inherits the
480 // module-owned classification → closem(FDT_UNUSED, 0) calls
481 // from sibling builtins skip closing it.
482 let _ = crate::ported::utils::zclose(SHTTY.load(Ordering::SeqCst));
483 SHTTY.store(-1, Ordering::SeqCst); // c:617
484 }
485
486 // xtrerr = stderr; // c:621
487
488 // Make sure the tty is opened read/write. // c:623
489 //
490 // C uses `if (isatty(0))` — the truthy test, NOT a strict
491 // `== 1`. POSIX guarantees isatty returns 0 on false but
492 // "non-zero" on true with the exact value implementation-
493 // defined. macOS/glibc/musl all return 1 today but the
494 // strict `== 1` check is brittle — match C's truthy test
495 // so a future libc that returns any non-zero value still
496 // hits the SHTTY-open path.
497 // ttystrname is the canonical "name of the controlling tty" global
498 // (init.c declares it; clone.c reads it for $TTY after fork). The
499 // C `init_io` keeps it in lockstep with `SHTTY`: every open-site
500 // and every dup-fallback either replaces it with `ttyname(fd)` of
501 // the new SHTTY or resets to "" / "/dev/tty" on failure. Mirror
502 // every store here so $TTY isn't empty after init.
503 let set_ttystrname = |s: String| {
504 // c:625 zsfree(ttystrname); ttystrname = ztrdup(...)
505 *crate::ported::modules::clone::ttystrname.lock().unwrap() = s;
506 };
507 #[cfg(unix)]
508 let ttyname_of = |fd: i32| -> Option<String> {
509 unsafe {
510 let p = libc::ttyname(fd);
511 if p.is_null() {
512 None
513 } else {
514 std::ffi::CStr::from_ptr(p)
515 .to_str()
516 .ok()
517 .map(|s| s.to_string())
518 }
519 }
520 };
521 #[cfg(unix)]
522 unsafe {
523 if libc::isatty(0) != 0 {
524 // c:624
525 let name_ptr = libc::ttyname(0); // c:626
526 if !name_ptr.is_null() {
527 let name = std::ffi::CStr::from_ptr(name_ptr);
528 let cstr = std::ffi::CString::new(name.to_bytes()).unwrap();
529 // c:626 ttystrname = ztrdup(ttyname(0))
530 set_ttystrname(name.to_string_lossy().into_owned());
531 let fd = libc::open(
532 cstr.as_ptr(), // c:627
533 libc::O_RDWR | libc::O_NOCTTY,
534 );
535 SHTTY.store(movefd(fd), Ordering::SeqCst);
536 }
537 if SHTTY.load(Ordering::SeqCst) == -1 {
538 // c:658
539 SHTTY.store(movefd(libc::dup(0)), Ordering::SeqCst);
540 // c:659
541 }
542 }
543 if SHTTY.load(Ordering::SeqCst) == -1 && libc::isatty(1) != 0 {
544 // c:662
545 SHTTY.store(movefd(libc::dup(1)), Ordering::SeqCst);
546 // c:663
547 // c:664-665 — zsfree(ttystrname); ttystrname = ztrdup(ttyname(1));
548 if let Some(n) = ttyname_of(1) {
549 set_ttystrname(n);
550 }
551 }
552 if SHTTY.load(Ordering::SeqCst) == -1 {
553 // c:667
554 let dev_tty = std::ffi::CString::new("/dev/tty").unwrap();
555 let fd = libc::open(dev_tty.as_ptr(), libc::O_RDWR | libc::O_NOCTTY); // c:668
556 SHTTY.store(movefd(fd), Ordering::SeqCst);
557 if SHTTY.load(Ordering::SeqCst) != -1 {
558 // c:669-670 — ttystrname = ztrdup(ttyname(SHTTY));
559 if let Some(n) = ttyname_of(SHTTY.load(Ordering::SeqCst)) {
560 set_ttystrname(n);
561 }
562 }
563 }
564 if SHTTY.load(Ordering::SeqCst) == -1 {
565 // c:672-674 — failed all opens: ttystrname = "";
566 set_ttystrname(String::new());
567 } else {
568 // c:675
569 let fdflags = libc::fcntl(SHTTY.load(Ordering::SeqCst), libc::F_GETFD, 0); // c:677
570 if fdflags != -1 {
571 // c:678
572 libc::fcntl(
573 SHTTY.load(Ordering::SeqCst),
574 libc::F_SETFD, // c:680
575 fdflags | libc::FD_CLOEXEC,
576 );
577 }
578 // c:683-684 — if (!ttystrname) ttystrname = ztrdup("/dev/tty");
579 // Rust mirrors NULL-check via empty-string check on the Mutex.
580 let mut guard = crate::ported::modules::clone::ttystrname.lock().unwrap();
581 if guard.is_empty() {
582 *guard = "/dev/tty".to_string();
583 }
584 }
585 }
586
587 // c:689-694 — set up terminal output only for an interactive shell;
588 // disable the line editor (USEZLE → the special `zle` option) when the
589 // shell isn't interactive, or is interactive without a real tty.
590 if interact() {
591 // c:689
592 init_shout(); // c:690
593 // c:691-692 — `if (!SHTTY || !shout) opts[USEZLE] = 0;`. zshrs has
594 // no `shout` FILE* (it writes the tty via SHTTY / fd 2), so the C
595 // `!shout` arm collapses into the SHTTY check. The option's
596 // canonical name is `zle` (options.rs:92), NOT `usezle`.
597 if SHTTY.load(Ordering::SeqCst) == -1 {
598 crate::ported::options::opt_state_set("zle", false); // c:692 opts[USEZLE]=0
599 }
600 } else {
601 // c:693-694 — `} else opts[USEZLE] = 0;`
602 crate::ported::options::opt_state_set("zle", false); // c:694
603 }
604
605 // c:699 — `mypid = (zlong)getpid();` — no zshrs global; getpid() is
606 // called where needed (acquire_pgrp computes it locally).
607 // c:700-707 — if interactive, make sure the shell is in the foreground
608 // and is the process-group leader. Gating on MONITOR + the one-shot
609 // `!origpgrp` guard matches C exactly; the prior port called
610 // acquire_pgrp unconditionally and never recorded origpgrp, so
611 // release_pgrp at exit had no group to hand the tty back to.
612 if isset(crate::ported::zsh_h::MONITOR) {
613 // c:700
614 if SHTTY.load(Ordering::SeqCst) == -1 {
615 // c:701
616 crate::ported::options::opt_state_set("monitor", false); // c:702
617 } else {
618 // c:703 — `else if (!origpgrp)`: only acquire the first time.
619 let origpgrp_recorded = *crate::ported::jobs::ORIGPGRP
620 .get_or_init(|| std::sync::Mutex::new(0))
621 .lock()
622 .unwrap()
623 != 0;
624 if !origpgrp_recorded {
625 // c:704 — `origpgrp = GETPGRP();`
626 *crate::ported::jobs::ORIGPGRP
627 .get_or_init(|| std::sync::Mutex::new(0))
628 .lock()
629 .unwrap() = unsafe { libc::getpgrp() };
630 // c:705 — `acquire_pgrp();` (might also clear opts[MONITOR]).
631 let _ = crate::ported::jobs::acquire_pgrp();
632 }
633 }
634 }
635}
636
637/// Port of `mod_export void init_shout(void)` from Src/init.c:712.
638/// Rust idiom replacement: SHTTY atomic + `acquire_pgrp` covers the
639/// C `fdopen(SHTTY, "w")` + setpgrp dance; the FILE* stream is
640/// reconstituted on-demand by callers rather than stored as a
641/// global `shout` pointer.
642pub fn init_shout() {
643 // c:712
644 if SHTTY.load(Ordering::SeqCst) == -1 {
645 // c:712
646 // shout = stderr; return; // c:722-723
647 return;
648 }
649 // shout = fdopen(SHTTY, "w"); // c:732
650 // setvbuf(shout, shoutbuf, _IOFBF, BUFSIZ); // c:735
651 let _ = crate::ported::utils::gettyinfo(); // c:771
652}
653
654/// Port of `mod_export char *tccap_get_name(int cap)` from Src/init.c:756.
655pub fn tccap_get_name(cap: usize) -> &'static str {
656 // c:756
657 if cap >= 39
658 /* TC_COUNT */
659 {
660 // c:771
661 return ""; // c:771
662 }
663 tccapnams[cap] // c:771
664}
665
666/// Port of `mod_export int init_term(void)` from Src/init.c:771.
667///
668/// Reads `$TERM` from the param table, and on a recognised term
669/// populates `tcstr[]`/`tclen[]` from the system termcap/terminfo
670/// DB via `tgetent` + `tgetstr` over `tccapnams[]`, exactly like C.
671/// This is the substrate every `tcmultout` / `tc_leftcurs` /
672/// `tsetcap` call site reads.
673///
674/// ncurses is already linked (build.rs `rustc-link-lib=ncurses`, the
675/// same lib the zsh/terminfo module's `tigetstr` externs use), so
676/// the termcap-emulation entry points are available. The previous
677/// Rust body hardcoded ANSI/VT100 escapes — under `TERM=screen-*` /
678/// `tmux-*` that diverged from zsh on standout (`so` is `\e[3m`
679/// there, not the hardcoded `\e[7m`).
680pub fn init_term() -> i32 {
681 // c:766 — termcap emulation entry points from ncurses (the
682 // prototypes_h.rs pin forbids declaring these there; local
683 // extern block matches the modules/terminfo.rs pattern).
684 extern "C" {
685 fn tgetent(bp: *mut libc::c_char, name: *const libc::c_char) -> libc::c_int;
686 fn tgetstr(id: *const libc::c_char, area: *mut *mut libc::c_char) -> *mut libc::c_char;
687 fn tgetflag(id: *const libc::c_char) -> libc::c_int;
688 fn tgetnum(id: *const libc::c_char) -> libc::c_int;
689 }
690 use crate::ported::zsh_h::{
691 TCBACKSPACE, TCCLEARSCREEN, TCDOWN, TCFAINTBEG, TCITALICSBEG, TCITALICSEND, TCLEFT,
692 TCRESTRCURSOR, TCSAVECURSOR, TCUP, TC_COUNT,
693 };
694
695 // c:776-779 — `if (!*term) { termflags |= TERM_UNKNOWN; return 0; }`
696 let term = getsparam("TERM").unwrap_or_default();
697 if term.is_empty() {
698 TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::SeqCst);
699 return 0;
700 }
701
702 // c:782-783 — `if (!strcmp(term, "emacs")) opts[USEZLE] = 0;`
703 // C proceeds to tgetent afterwards; zshrs keeps the USEZLE-off
704 // via the option layer.
705 if term == "emacs" {
706 crate::ported::options::opt_state_set("zle", false); // c:783 opts[USEZLE] = 0
707 }
708
709 let cterm = match std::ffi::CString::new(term.as_str()) {
710 Ok(c) => c,
711 Err(_) => {
712 TERMFLAGS.fetch_or(TERM_BAD, Ordering::SeqCst);
713 return 0;
714 }
715 };
716 // c:785-797 — `if (tgetent(termbuf, term) != TGETENT_SUCCESS) {
717 // zerr(...); errflag &= ~ERRFLAG_ERROR; termflags |= TERM_BAD;
718 // return 0; }`. ncurses tgetent accepts NULL (the
719 // TGETENT_ACCEPTS_NULL arm at c:786-787).
720 let ent = unsafe { tgetent(std::ptr::null_mut(), cterm.as_ptr()) };
721 if ent != 1 {
722 // c:791 — `if (interact) zerr("can't find terminal definition
723 // for %s", term);` — interact gate keeps -fc scripts quiet.
724 if crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVE) {
725 crate::ported::utils::zerr(&format!("can't find terminal definition for {}", term));
726 // c:792
727 }
728 crate::ported::utils::errflag
729 .fetch_and(!crate::ported::utils::ERRFLAG_ERROR, Ordering::Relaxed); // c:793
730 TERMFLAGS.fetch_or(TERM_BAD, Ordering::SeqCst); // c:794
731 return 0; // c:795
732 }
733
734 // c:801-802 — `termflags &= ~TERM_BAD; termflags &= ~TERM_UNKNOWN;`
735 TERMFLAGS.fetch_and(!(TERM_BAD | TERM_UNKNOWN), Ordering::SeqCst);
736
737 // c:803-815 — `for (t0 = 0; t0 != TC_COUNT; t0++) { ... tgetstr
738 // (tccapnams[t0], &pp) ... }` — fetch every capability string.
739 {
740 let mut s = tcstr.lock().unwrap();
741 let mut l = tclen.lock().unwrap();
742 // c:798 `char tbuf[1024]` — element type must be c_char: it is
743 // i8 on macOS/x86_64-linux but u8 on aarch64-linux.
744 let mut tbuf = [0 as libc::c_char; 1024];
745 for t0 in 0..TC_COUNT as usize {
746 let cap = std::ffi::CString::new(tccapnams[t0]).unwrap();
747 let mut pp: *mut libc::c_char = tbuf.as_mut_ptr(); // c:804 `pp = tbuf;`
748 let got = unsafe { tgetstr(cap.as_ptr(), &mut pp) }; // c:808
749 if got.is_null() || got as isize == -1 {
750 // c:809 — `tcstr[t0] = NULL, tclen[t0] = 0;`
751 s[t0] = String::new();
752 l[t0] = 0;
753 } else {
754 // c:811-813 — dup the cap string + record length.
755 let bytes = unsafe { std::ffi::CStr::from_ptr(got) }.to_bytes();
756 s[t0] = String::from_utf8_lossy(bytes).into_owned();
757 l[t0] = bytes.len() as i32;
758 }
759 }
760 }
761
762 // c:817-818 — automargin / newline-glitch flags.
763 let flag = |name: &str| -> i32 {
764 let c = std::ffi::CString::new(name).unwrap();
765 unsafe { tgetflag(c.as_ptr()) }
766 };
767 let num = |name: &str| -> i32 {
768 let c = std::ffi::CString::new(name).unwrap();
769 unsafe { tgetnum(c.as_ptr()) }
770 };
771 hasam.store(flag("am"), Ordering::SeqCst); // c:818 `hasam = tgetflag("am");`
772 hasxn.store(flag("xn"), Ordering::SeqCst); // c:819 `hasxn = tgetflag("xn");`
773 tclines.store(num("li"), Ordering::SeqCst); // c:821 `tclines = tgetnum("li");`
774 tccolumns.store(num("co"), Ordering::SeqCst); // c:822 `tccolumns = tgetnum("co");`
775 tccolours.store(num("Co"), Ordering::SeqCst); // c:823 `tccolours = tgetnum("Co");`
776
777 // Post-fetch fixups — all operate on tcstr/tclen under one lock.
778 {
779 let mut s = tcstr.lock().unwrap();
780 let mut l = tclen.lock().unwrap();
781 let can = |l: &[i32; 39], cap: i32| l[cap as usize] != 0; // tccan()
782
783 // c:825-833 — no cursor-up cap → single-line mode (TERM_NOUP).
784 if can(&l, TCUP) {
785 TERMFLAGS.fetch_and(!TERM_NOUP, Ordering::SeqCst); // c:829
786 } else {
787 s[TCUP as usize] = String::new(); // c:831-832
788 l[TCUP as usize] = 0;
789 TERMFLAGS.fetch_or(TERM_NOUP, Ordering::SeqCst); // c:833
790 }
791
792 // c:836-840 — most termcaps don't define "bc"; default `\b`.
793 if !can(&l, TCBACKSPACE) {
794 s[TCBACKSPACE as usize] = "\u{8}".to_string(); // c:838
795 l[TCBACKSPACE as usize] = 1; // c:839
796 }
797
798 // c:843-847 — no cursor-left cap → use backspace.
799 if !can(&l, TCLEFT) {
800 s[TCLEFT as usize] = s[TCBACKSPACE as usize].clone(); // c:845
801 l[TCLEFT as usize] = l[TCBACKSPACE as usize]; // c:846
802 }
803
804 // c:849-853 — save-cursor without restore-cursor is useless.
805 if can(&l, TCSAVECURSOR) && !can(&l, TCRESTRCURSOR) {
806 l[TCSAVECURSOR as usize] = 0; // c:850
807 s[TCSAVECURSOR as usize] = String::new(); // c:851-852
808 }
809
810 // c:856-860 — if the down cap is `\n`, don't use it.
811 if can(&l, TCDOWN) && s[TCDOWN as usize].starts_with('\n') {
812 l[TCDOWN as usize] = 0; // c:857
813 s[TCDOWN as usize] = String::new(); // c:858-859
814 }
815
816 // c:863-867 — no clear cap → ^L.
817 if !can(&l, TCCLEARSCREEN) {
818 s[TCCLEARSCREEN as usize] = "\u{c}".to_string(); // c:865
819 l[TCCLEARSCREEN as usize] = 1; // c:866
820 }
821
822 // c:868 — `rprompt_indent = 1;` lives in the params layer
823 // (rprompt_indent_unsetfn keeps it there); no-op here.
824
825 // c:876-884 — no italics caps → CSI 3 m / CSI 23 m.
826 if !can(&l, TCITALICSBEG) {
827 s[TCITALICSBEG as usize] = "\x1b[3m".to_string(); // c:878
828 l[TCITALICSBEG as usize] = 4; // c:879
829 }
830 if !can(&l, TCITALICSEND) {
831 s[TCITALICSEND as usize] = "\x1b[23m".to_string(); // c:882
832 l[TCITALICSEND as usize] = 5; // c:883
833 }
834 // c:885-888 — no faint cap → CSI 2 m.
835 if !can(&l, TCFAINTBEG) {
836 s[TCFAINTBEG as usize] = "\x1b[2m".to_string(); // c:887
837 l[TCFAINTBEG as usize] = 4; // c:888
838 }
839 }
840
841 1 // c:890 `return 1;`
842}
843
844/// Port of `static char *getmypath(const char *name, const char *cwd)` from Src/init.c:909.
845fn getmypath(name: Option<&str>, cwd: Option<&str>) -> Option<String> {
846 // c:909
847 #[cfg(target_os = "macos")]
848 unsafe {
849 // c:914
850 let mut buf = vec![0u8; libc::PATH_MAX as usize]; // c:918
851 let mut n: u32 = libc::PATH_MAX as u32; // c:916
852 let ret = libc::_NSGetExecutablePath(
853 buf.as_mut_ptr() as *mut i8, // c:919
854 &mut n,
855 );
856 if ret < 0 {
857 // c:919
858 buf.resize(n as usize, 0); // c:921
859 let ret2 = libc::_NSGetExecutablePath(buf.as_mut_ptr() as *mut i8, &mut n); // c:922
860 if ret2 == 0 {
861 let s = std::ffi::CStr::from_ptr(buf.as_ptr() as *const i8);
862 let lossy = s.to_string_lossy().into_owned();
863 if !lossy.is_empty() {
864 return Some(lossy);
865 }
866 }
867 } else if ret == 0 {
868 // c:924
869 let s = std::ffi::CStr::from_ptr(buf.as_ptr() as *const i8);
870 let lossy = s.to_string_lossy().into_owned();
871 if !lossy.is_empty() {
872 return Some(lossy);
873 } // c:925
874 }
875 }
876 #[cfg(target_os = "linux")]
877 {
878 if let Ok(p) = std::fs::read_link("/proc/self/exe") {
879 // c:946
880 return Some(p.to_string_lossy().into_owned()); // c:949
881 }
882 }
883
884 let name = name?; // c:956-957
885 let name = if name.starts_with('-') {
886 &name[1..]
887 } else {
888 name
889 }; // c:958-959
890 let namelen = name.len(); // c:960
891 if namelen == 0 {
892 return None;
893 } // c:960-961
894 if name.ends_with('/') {
895 return None;
896 } // c:963-964
897 if name.starts_with('/') {
898 // c:965
899 return Some(name.to_string()); // c:967
900 }
901 if name.contains('/') {
902 // c:969
903 let cwd = cwd?; // c:971-972
904 return Some(format!("{}/{}", cwd, name)); // c:974
905 }
906 let path = std::env::var("PATH").ok()?; // c:984
907 if path.is_empty() {
908 return None;
909 } // c:985-986
910 for dir in path.split(':') {
911 // c:990-1000
912 let candidate = if dir.is_empty() {
913 std::path::PathBuf::from(name)
914 } else {
915 std::path::PathBuf::from(format!("{}/{}", dir, name))
916 };
917 if let Ok(real) = std::fs::canonicalize(&candidate) {
918 // c:1014
919 if real.is_file() {
920 return Some(real.to_string_lossy().into_owned());
921 }
922 }
923 }
924 None // c:1014
925}
926
927/// Bootstrap `module_path` / `MODULE_PATH` per `Src/init.c:1176`:
928/// `module_path = mkarray(ztrdup(MODULE_DIR))` plus the matching
929/// PM_TIED scalar from `Src/params.c:404` IPDEF8.
930///
931/// MODULE_DIR is a build-time `#define` from `Src/zshpaths.h`
932/// (e.g. `/opt/homebrew/Cellar/zsh/5.9.1/lib`) resolved during
933/// zsh's `./configure`. zshrs ships modules statically linked so
934/// there is no equivalent build-time anchor. Probe the running
935/// system's zsh once (cached via OnceLock) so `${module_path[1]}`
936/// / `${(j.:.)module_path}` / `${#module_path}` agree between
937/// `zshrs --zsh` and the system zsh that parity tests compare
938/// against. Falls back to an empty array when no system zsh is
939/// installed (this also makes paramtab carry an empty
940/// `module_path` array rather than no entry at all, matching the
941/// `mkarray(NULL)` shape the C code produces for an empty
942/// MODULE_DIR build).
943///
944/// Called from `setupvals` (the canonical c:1014 entry point) and
945/// from `ShellExecutor::new` (the bin entry that skips full
946/// `setupvals` per the init_bltinmods comment at vm_helper.rs).
947///
948/// **Extension** — no direct C analog. The C source inlines this
949/// at `Src/init.c:1176` inside `setupvals`. Allowlisted in
950/// `tests/data/fake_fn_allowlist.txt` so ShellExecutor can call
951/// just the module_path-init subset of setupvals without invoking
952/// the full body (which would conflict with the bin entry's
953/// piecewise paramtab init).
954pub fn module_path_init() {
955 use crate::ported::params::*;
956 use crate::ported::zsh_h::*;
957 static MODULE_DIR_CACHE: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
958 let module_dir: Vec<String> = MODULE_DIR_CACHE
959 .get_or_init(|| {
960 use std::process::Command;
961 let candidates = [
962 "/opt/homebrew/bin/zsh",
963 "/usr/local/bin/zsh",
964 "/bin/zsh",
965 "/usr/bin/zsh",
966 ];
967 for path in candidates {
968 if !std::path::Path::new(path).exists() {
969 continue;
970 }
971 let out = Command::new(path)
972 .args(["-fc", "print -r ${module_path[1]}"])
973 .output();
974 if let Ok(o) = out {
975 if o.status.success() {
976 let s = String::from_utf8_lossy(&o.stdout)
977 .trim_end_matches('\n')
978 .to_string();
979 if !s.is_empty() {
980 return vec![s];
981 }
982 }
983 }
984 }
985 Vec::new()
986 })
987 .clone();
988 let scalar_join = module_dir.join(":");
989 let mut tab = paramtab().write().unwrap();
990 // c:Src/init.c:1176 — module_path = PM_ARRAY tied to MODULE_PATH.
991 let mp = Box::new(param {
992 node: hashnode {
993 next: None,
994 nam: "module_path".to_string(),
995 flags: (PM_ARRAY | PM_SPECIAL | PM_TIED) as i32,
996 },
997 u_data: 0,
998 u_tied: None,
999 u_arr: Some(module_dir),
1000 u_str: None,
1001 u_val: 0,
1002 u_dval: 0.0,
1003 u_hash: None,
1004 gsu_s: None,
1005 gsu_i: None,
1006 gsu_f: None,
1007 gsu_a: None,
1008 gsu_h: None,
1009 base: 0,
1010 width: 0,
1011 env: None,
1012 ename: Some("MODULE_PATH".to_string()),
1013 old: None,
1014 level: 0,
1015 });
1016 tab.insert("module_path".to_string(), mp);
1017 // c:Src/params.c:404 IPDEF8 — PM_TIED scalar mirroring
1018 // module_path with `:` join.
1019 let mps = Box::new(param {
1020 node: hashnode {
1021 next: None,
1022 nam: "MODULE_PATH".to_string(),
1023 flags: (PM_SCALAR | PM_SPECIAL | PM_TIED | PM_DONTIMPORT) as i32,
1024 },
1025 u_data: 0,
1026 u_tied: None,
1027 u_arr: None,
1028 u_str: Some(scalar_join),
1029 u_val: 0,
1030 u_dval: 0.0,
1031 u_hash: None,
1032 gsu_s: None,
1033 gsu_i: None,
1034 gsu_f: None,
1035 gsu_a: None,
1036 gsu_h: None,
1037 base: 0,
1038 width: 0,
1039 env: None,
1040 ename: Some("module_path".to_string()),
1041 old: None,
1042 level: 0,
1043 });
1044 tab.insert("MODULE_PATH".to_string(), mps);
1045}
1046
1047/// Port of `void setupvals(char *cmd, char *runscript, char *zsh_name)` from Src/init.c:1014.
1048///
1049/// Initialize lots of global variables and hash tables. // c:1014
1050pub fn setupvals(cmd: Option<&str>, runscript: Option<&str>, zsh_name: &str) {
1051 // c:1014
1052 let mut close_fds = [0i32; 10]; // c:1043
1053 let mut tmppipe = [-1i32; 2]; // c:1043
1054
1055 // Workaround NIS grabbing fd's 0-9 // c:1045
1056 #[cfg(unix)]
1057 unsafe {
1058 if libc::pipe(tmppipe.as_mut_ptr()) == 0 {
1059 // c:1053
1060 let mut i: i32 = -1; // c:1060
1061 while i < 9 {
1062 // c:1061
1063 let j: i32;
1064 if i < tmppipe[0] {
1065 // c:1063
1066 j = tmppipe[0]; // c:1064
1067 } else if i < tmppipe[1] {
1068 // c:1065
1069 j = tmppipe[1]; // c:1066
1070 } else {
1071 j = libc::dup(0); // c:1068
1072 if j == -1 {
1073 break;
1074 } // c:1069-1070
1075 }
1076 if j < 10 {
1077 // c:1072
1078 close_fds[j as usize] = 1; // c:1073
1079 } else {
1080 libc::close(j); // c:1075
1081 }
1082 if i < j {
1083 i = j;
1084 } // c:1076-1077
1085 }
1086 if i < tmppipe[0] {
1087 libc::close(tmppipe[0]);
1088 } // c:1079-1080
1089 if i < tmppipe[1] {
1090 libc::close(tmppipe[1]);
1091 } // c:1081-1082
1092 }
1093 }
1094
1095 // c:1085 — `(void)addhookdefs(NULL, zshhooks, sizeof(zshhooks)/sizeof(*zshhooks));`
1096 // Registers the four well-known hookdefs (exit, before_trap,
1097 // after_trap, get_color_attr) into the global `hooktab` chain.
1098 {
1099 let base = zshhooks.load(std::sync::atomic::Ordering::SeqCst);
1100 let _ = crate::ported::module::addhookdefs(std::ptr::null(), base, 4);
1101 }
1102 // In C the `zsh/zle` module's boot_ (zle_main.c:2301) registers the
1103 // before_trap/after_trap hookfuncs AND the comphooks[] hookdefs
1104 // (insert_match, menu_start, compctl_make, compctl_cleanup,
1105 // comp_list_matches with def=ilistmatches). zshrs static-links ZLE, and
1106 // `zmodload zsh/zle` routes to features_, not boot_, so that
1107 // registration never happened — leaving `comp_list_matches` unregistered
1108 // so `list_matches` fell straight to the plain uncolored `ilistmatches`
1109 // and `zmodload zsh/complist`'s `addhookfunc` had no hookdef to attach
1110 // `complistmatches` to (every list-colors/group-colors style dropped).
1111 // Run the ZLE module boot once here at startup; addhookdef's duplicate
1112 // guard makes a later real boot_ idempotent.
1113 let _ = crate::ported::zle::zle_main::boot_(std::ptr::null());
1114 // C's zsh/complete module boot_ (complete.c:1758) attaches the funcs to
1115 // the ZLE hookdefs registered just above: complete/before_complete/
1116 // after_complete/list_matches/invalidate_list. Like zle_main::boot_ it
1117 // never ran (zmodload routes to features_), so after_complete's hook had
1118 // no func and menu-completion's menu_start (→ domenuselect) never fired.
1119 let _ = crate::ported::zle::complete::boot_(std::ptr::null());
1120 // init_eprog(); // c:1087
1121 // zero_mnumber.type = MN_INTEGER; zero_mnumber.u.l = 0; // c:1089-1090
1122
1123 // noeval = 0; // c:1092
1124 // curhist = 0; histsiz = DEFAULT_HISTSIZE; inithist(); // c:1093-1095
1125 let _ = crate::ported::hist::inithist();
1126 // cmdstack = zalloc(CMDSTACKSZ); cmdsp = 0; // c:1097-1098
1127 // bangchar = '!'; hashchar = '#'; hatchar = '^'; // c:1100-1102
1128 // termflags = TERM_UNKNOWN; // c:1103
1129 TERMFLAGS.store(TERM_UNKNOWN, Ordering::SeqCst);
1130 // curjob = prevjob = coprocin = coprocout = -1; // c:1104
1131 // zgettime_monotonic_if_available(&shtimer); // c:1105
1132 // srand((unsigned)(shtimer.tv_sec + shtimer.tv_nsec)); // c:1106
1133 #[cfg(unix)]
1134 unsafe {
1135 let mut ts: libc::timespec = std::mem::zeroed();
1136 libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts);
1137 libc::srand((ts.tv_sec as u32).wrapping_add(ts.tv_nsec as u32));
1138 }
1139
1140 // Set default path // c:1108
1141 // path = ["/bin", "/usr/bin", "/usr/ucb", "/usr/local/bin", NULL]; // c:1109-1114
1142 std::env::set_var(
1143 "PATH", // c:1109
1144 std::env::var("PATH")
1145 .unwrap_or_else(|_| "/bin:/usr/bin:/usr/ucb:/usr/local/bin".to_string()),
1146 );
1147
1148 // cdpath, manpath, fignore = mkarray(NULL) // c:1116-1118
1149 // fpath = ... // c:1132-1172
1150 // mailpath, psvar = mkarray(...) // c:1174-1175
1151 // modulestab = newmoduletable(17, "modules"); // c:1177
1152 // linkedmodules = znewlinklist(); // c:1178
1153
1154 module_path_init();
1155
1156 // Set default prompts // c:1180
1157 // prompt, prompt2, prompt3, prompt4, sprompt = ztrdup(...) // c:1181-1194
1158
1159 // ifs = EMULATION(KSH|SH) ? DEFAULT_IFS_SH : DEFAULT_IFS // c:1196-1197
1160 // wordchars = ztrdup(DEFAULT_WORDCHARS); postedit = ztrdup(""); // c:1198-1199
1161
1162 // If _ is set in environment then initialize our $_ by copying it // c:1200
1163 let underscore_val = std::env::var("_").unwrap_or_default(); // c:1201
1164 let metafied = crate::ported::utils::metafy(&underscore_val); // c:1202
1165 *zunderscore.lock().unwrap() = metafied.clone(); // c:1202
1166 underscoreused.store((metafied.len() + 1) as i32, Ordering::SeqCst); // c:1203
1167 let ulen = (metafied.len() + 1 + 31) & !31; // c:1204
1168 underscorelen.store(ulen, Ordering::SeqCst); // c:1205
1169 // zunderscore = zrealloc(zunderscore, underscorelen); // c:1205
1170
1171 // zoptarg = ""; zoptind = 1; // c:1207-1208
1172
1173 // ppid = getppid(); mypid = getpid(); // c:1210-1211
1174 // term = ztrdup(""); // c:1212
1175
1176 // nullcmd = "cat"; readnullcmd = DEFAULT_READNULLCMD; // c:1214-1215
1177
1178 // cached_uid = getuid(); // c:1219
1179 // pswd = getpwuid(cached_uid); home = pswd->pw_dir; ... // c:1222-1234
1180 if let Ok(home) = std::env::var("HOME") {
1181 // c:1225
1182 let _ = home;
1183 }
1184
1185 // PWD/OLDPWD initialization // c:1236-1259
1186 let cwd = crate::ported::compat::zgetcwd(); // c:1252
1187 std::env::set_var("PWD", &cwd); // c:1253
1188 if std::env::var("OLDPWD").is_err() {
1189 // c:1255-1257
1190 std::env::set_var("OLDPWD", &cwd); // c:1256
1191 }
1192
1193 crate::ported::utils::inittyptab(); // c:1261
1194 crate::ported::lex::initlextabs(); // c:1262
1195
1196 crate::ported::hashtable::createreswdtable(); // c:1264
1197 crate::ported::hashtable::createaliastables(); // c:1265
1198 crate::ported::hashtable::createcmdnamtable(); // c:1266
1199 crate::ported::hashtable::createshfunctable(); // c:1267
1200 let _ = crate::ported::builtin::createbuiltintable(); // c:1268
1201 crate::ported::hashnameddir::createnameddirtable(); // c:1269
1202 crate::ported::params::createparamtable(); // c:1270
1203
1204 // c:Src/init.c:1274-1276 — `#ifdef TIOCGWINSZ / adjustwinsize(0);`, the
1205 // first thing after createparamtable. Probes the tty via TIOCGWINSZ and
1206 // publishes the geometry to $COLUMNS/$LINES.
1207 //
1208 // zshrs never made this call, so it never asked the tty: $COLUMNS/$LINES
1209 // read 0 in every shell with a terminal (zsh reports 97/24 in a 97x24 pty).
1210 // Everything width-derived was then computed against 0 — prompt `%<<`
1211 // truncation, select lists, completion listing widths.
1212 //
1213 // Ordering is C's and it is load-bearing in both directions:
1214 // - It must follow init_io (c:1908), which is what sets SHTTY; while
1215 // SHTTY is -1 adjustwinsize early-returns (c:1900-1901) and does
1216 // nothing. That is also the correct behaviour with no terminal at all:
1217 // $COLUMNS keeps whatever the environment supplied, or 0.
1218 // - It must follow the environ import (createparamtable, c:1270) because
1219 // the tty geometry OVERRIDES an inherited COLUMNS: a 97-column terminal
1220 // reports 97 even when COLUMNS=10 was exported in. (C reaches that via
1221 // the c:1906-1907 "Signal missed while a job owned the tty?" promotion
1222 // of from=0 to from=1, which makes adjustcolumns take the signalled
1223 // path and overwrite zterm_columns with ws_col.)
1224 //
1225 // Note SHTTY does not require stdin/stdout to be a terminal: init_io's last
1226 // resort is `open("/dev/tty")` (c:667-670), so a piped-but-still-attached
1227 // shell gets the real width too — `zsh -fc 'print $COLUMNS | cat'` in a
1228 // 97-column terminal prints 97.
1229 let _ = crate::ported::utils::adjustwinsize(0); // c:1276
1230
1231 // c:Src/init.c:1180-1194 — default prompts. C sets the `prompt`/
1232 // `prompt2`/`prompt3`/`prompt4`/`sprompt` globals (which IPDEF7 binds
1233 // to PS1/PS2/PS3/PS4/SPROMPT) BEFORE createparamtable; zshrs creates
1234 // those special scalars empty, so set the defaults here, right after.
1235 // - prompt/prompt2 are gated on INTERACTIVE (c:1181): a
1236 // non-interactive shell keeps empty PS1/PS2. zshrs's INTERACTIVE
1237 // is isatty-based and is NOT downgraded for `-c`/script, so also
1238 // require reading from stdin (no `-c`, no runscript) — the
1239 // SHINSTDIN condition — to match zsh's "interactive" gate.
1240 // - prompt3/prompt4/sprompt are set unconditionally (c:1191-1194).
1241 // Each default only applies when the param is still empty, so an
1242 // env-imported value (e.g. `PS4` exported) wins, mirroring C where
1243 // importenv overrides the compiled defaults.
1244 {
1245 let ksh_sh = crate::ported::zsh_h::EMULATION(EMULATE_KSH | EMULATE_SH);
1246 let set_default = |name: &str, val: &str| {
1247 if getsparam(name).map_or(true, |v| v.is_empty()) {
1248 crate::ported::params::setsparam(name, val);
1249 }
1250 };
1251 // c:1181 — interactive shell reading from stdin gets the
1252 // hostname prompt; ksh/sh emulation gets the bare `$`/`#`.
1253 if isset(INTERACTIVE) && cmd.is_none() && runscript.is_none() {
1254 if ksh_sh {
1255 set_default(
1256 "PS1",
1257 if crate::ported::utils::privasserted() {
1258 "# "
1259 } else {
1260 "$ "
1261 },
1262 ); // c:1185
1263 set_default("PS2", "> "); // c:1186
1264 } else {
1265 set_default("PS1", "%m%# "); // c:1188
1266 set_default("PS2", "%_> "); // c:1189
1267 }
1268 }
1269 set_default("PS3", "?# "); // c:1191
1270 set_default("PS4", if ksh_sh { "+ " } else { "+%N:%i> " }); // c:1192-1193
1271 set_default("SPROMPT", "zsh: correct '%R' to '%r' [nyae]? "); // c:1194
1272 }
1273
1274 // condtab = NULL; wrappers = NULL; // c:1272-1273
1275
1276 let (_cols, _lines) = crate::ported::utils::adjustwinsize(0); // c:1276
1277
1278 // getrlimit loop // c:1286-1289
1279
1280 // breaks = loops = 0; // c:1292
1281 // lastmailcheck = zmonotime(NULL); // c:1293
1282 // locallevel = sourcelevel = 0; // c:1294
1283 sourcelevel.store(0, Ordering::SeqCst); // c:1294
1284 // sfcontext = SFC_NONE; trap_return = 0; // c:1295-1296
1285 // trap_state = TRAP_STATE_INACTIVE; // c:1297
1286 // noerrexit = NOERREXIT_EXIT|RETURN|SIGNAL; // c:1298
1287 // nohistsave = 1; // c:1299
1288 // dirstack = znewlinklist(); bufstack = znewlinklist(); // c:1300-1301
1289 // hsubl = hsubr = NULL; lastpid = 0; // c:1302-1303
1290
1291 // get_usage(); // c:1305
1292
1293 // Close fd's we opened to block 0-9 // c:1307
1294 #[cfg(unix)]
1295 for i in 0..10 {
1296 // c:1308
1297 if close_fds[i] != 0 {
1298 // c:1309
1299 unsafe {
1300 libc::close(i as i32);
1301 } // c:1310
1302 }
1303 }
1304
1305 crate::ported::prompt::set_default_colour_sequences(); // c:1313
1306
1307 // ZSH_EXEPATH // c:1315
1308 {
1309 let exename = argv0.lock().unwrap().clone(); // c:1318
1310 let exename = unmeta(&exename); // c:1318
1311 // c:1319 — `cwd = pwd;` (the in-shell logical cwd global).
1312 // Read paramtab; was reading OS env which can lag.
1313 let cwd = getsparam("PWD").map(|s| unmeta(&s));
1314 let mypath = getmypath(
1315 Some(&exename), // c:1320
1316 cwd.as_deref(),
1317 );
1318 if let Some(mp) = mypath {
1319 // c:1323
1320 std::env::set_var("ZSH_EXEPATH", &mp); // c:1324
1321 }
1322 }
1323 if let Some(cmd) = cmd {
1324 // c:1340
1325 std::env::set_var("ZSH_EXECUTION_STRING", cmd); // c:1340
1326 }
1327 if let Some(rs) = runscript {
1328 // c:1340
1329 std::env::set_var("ZSH_SCRIPT", rs); // c:1340
1330 }
1331 std::env::set_var("ZSH_NAME", zsh_name); // c:1340
1332}
1333
1334/// Port of `static void setupshin(char *runscript)` from Src/init.c:1340.
1335fn setupshin(runscript: Option<&str>) {
1336 // c:1340
1337 if let Some(script) = runscript {
1338 // c:1340
1339 let funmeta = unmeta(script); // c:1346
1340 let mut sfname: Option<String> = None; // c:1343
1341 if std::path::Path::new(&funmeta).is_file() {
1342 // c:1350-1352
1343 sfname = Some(script.to_string()); // c:1353
1344 }
1345 // PATHSCRIPT search omitted (depends on opts[PATHSCRIPT]) // c:1354-1360
1346 if sfname.is_none() {
1347 // c:1361
1348 crate::ported::utils::zerr(&format!(
1349 // c:1364
1350 "can't open input file: {}",
1351 script
1352 ));
1353 std::process::exit(127); // c:1365
1354 }
1355 }
1356 // lineno = 1; // c:1394
1357 // shinbufalloc(); // c:1394
1358}
1359
1360/// Port of `void init_signals(void)` from Src/init.c:1394.
1361pub fn init_signals() {
1362 // c:1394
1363 // c:1398-1399 — `sigtrapped = hcalloc(TRAPCOUNT * sizeof(int));`
1364 // and `siglists = hcalloc(TRAPCOUNT * sizeof(Eprog));`. Trap
1365 // table globals not modeled in zshrs static link path.
1366
1367 // c:1401-1406 — `if (interact) { signal_setmask(signal_mask(0));
1368 // for (i=0; i<NSIG; ++i) signal_default(i); }`. Reset to default
1369 // dispositions on every signal at startup so a parent's stale
1370 // handlers don't leak in.
1371 #[cfg(unix)]
1372 if interact() {
1373 let empty = crate::ported::signals::signal_mask(0);
1374 let _ = crate::ported::signals::signal_setmask(&empty);
1375 // c:1404 — `for (i=0; i<NSIG; ++i) signal_default(i);`. NSIG
1376 // is `<signal.h>`-provided in C; libc-rs doesn't re-export it
1377 // directly. `signals_h::SIGCOUNT` is the canonical port of
1378 // NSIG-1 (Linux=64, macOS=31).
1379 //
1380 // The previous Rust port used `1..64i32` — hardcoded, missing
1381 // signal 64 on Linux (where NSIG=65) AND iterating PAST the
1382 // valid range on macOS (where signals 32..63 don't exist).
1383 // Use `1..=SIGCOUNT` so the loop iterates exactly NSIG-1
1384 // signals on each platform, skipping signal 0 (C iterates
1385 // it but signal_default(0) is implementation-defined).
1386 for i in 1..=crate::ported::signals_h::SIGCOUNT {
1387 let _ = crate::ported::signals::signal_default(i);
1388 }
1389 }
1390
1391 // c:1407 — `sigchld_mask = signal_mask(SIGCHLD);`. Cached SIGCHLD
1392 // mask global not yet modeled — the few callers that need it
1393 // (job-reap path) re-derive on demand.
1394
1395 intr(); // c:1409
1396
1397 #[cfg(unix)]
1398 unsafe {
1399 // c:1411-1412 — detect parent-installed SIG_IGN on SIGQUIT.
1400 let mut act: libc::sigaction = std::mem::zeroed();
1401 if libc::sigaction(libc::SIGQUIT, std::ptr::null(), &mut act) == 0
1402 && act.sa_sigaction == libc::SIG_IGN
1403 {
1404 // sigtrapped[SIGQUIT] = ZSIG_IGNORED; (trap-table global
1405 // not modeled — see above).
1406 }
1407 // c:1414-1416 — `#ifndef QDEBUG signal_ignore(SIGQUIT)`.
1408 signal_ignore(libc::SIGQUIT);
1409
1410 // c:1418-1421 — SIGHUP: if parent installed SIG_IGN, clear
1411 // the HUP option; otherwise install our handler.
1412 if signal_ignore(libc::SIGHUP) == libc::SIG_IGN {
1413 dosetopt(HUP, 0, 0); // c:1419
1414 } else {
1415 install_handler(libc::SIGHUP); // c:1421
1416 }
1417 install_handler(libc::SIGCHLD); // c:1422
1418 #[cfg(not(target_os = "haiku"))]
1419 {
1420 install_handler(libc::SIGWINCH); // c:1424
1421 // c:1425 — `winch_block()`. SIGWINCH unblocked at the
1422 // prompt-display boundary (preprompt at utils.c). Not
1423 // yet modeled — leaves SIGWINCH unblocked, the safe
1424 // default for non-resize-aware redraw paths.
1425 }
1426
1427 // c:1427-1431 — interactive-only handlers.
1428 if interact() {
1429 install_handler(libc::SIGPIPE); // c:1428
1430 install_handler(libc::SIGALRM); // c:1429
1431 signal_ignore(libc::SIGTERM); // c:1430
1432 }
1433
1434 // c:1432-1436 — `if (jobbing)` job-control signal ignores.
1435 if jobbing() {
1436 signal_ignore(libc::SIGTTOU); // c:1433
1437 signal_ignore(libc::SIGTSTP); // c:1434
1438 signal_ignore(libc::SIGTTIN); // c:1435
1439 }
1440 }
1441}
1442
1443/// Port of `void run_init_scripts(void)` from Src/init.c:1445.
1444/// Runs the standard zsh init scripts (or KSH/SH-compat scripts when
1445/// emulating). Each guard mirrors the C predicate set: islogin, RCS,
1446/// GLOBALRCS, PRIVILEGED, INTERACTIVE.
1447pub fn run_init_scripts() {
1448 // c:1445
1449
1450 // c:1447 — noerrexit = NOERREXIT_EXIT | NOERREXIT_RETURN | NOERREXIT_SIGNAL;
1451 // (noerrexit global not surfaced; the C bits are
1452 // consulted by the script-source path internally.)
1453
1454 // c:1449 — if (EMULATION(EMULATE_KSH|EMULATE_SH)) { ... }
1455 let emul = emulation.load(Ordering::SeqCst);
1456 let is_posix = (emul & (EMULATE_KSH | EMULATE_SH) as i32) != 0;
1457
1458 let is_login = islogin();
1459 let interact = isset(INTERACTIVE);
1460 let privileged = isset(PRIVILEGED);
1461
1462 if is_posix {
1463 // c:1450-1451 — if (islogin) source("/etc/profile");
1464 if is_login {
1465 let _ = source("/etc/profile");
1466 }
1467 if !privileged {
1468 // c:1452
1469 // c:1454 — if (islogin) sourcehome(".profile");
1470 if is_login {
1471 sourcehome(".profile");
1472 }
1473 // c:1456-1468 — if (interact) { … getsparam("ENV"); source(s); }
1474 if interact {
1475 if let Some(s) = getsparam("ENV") {
1476 let _ = source(&s);
1477 }
1478 }
1479 } else {
1480 // c:1470 — source("/etc/suid_profile");
1481 let _ = source("/etc/suid_profile");
1482 }
1483 } else {
1484 // c:1473 — source(GLOBAL_ZSHENV);
1485 let _ = source(crate::ported::config_h::GLOBAL_ZSHENV);
1486
1487 // c:1476-1490 — if (isset(RCS) && unset(PRIVILEGED))
1488 // { newuser-probe; sourcehome(".zshenv"); }
1489 if isset(RCS) && !privileged {
1490 sourcehome(".zshenv");
1491 }
1492 // c:1491-1498 — if (islogin) { GLOBAL_ZPROFILE? + .zprofile }
1493 if is_login {
1494 if isset(RCS) && isset(GLOBALRCS) {
1495 let _ = source(crate::ported::config_h::GLOBAL_ZPROFILE);
1496 }
1497 if isset(RCS) && !privileged {
1498 sourcehome(".zprofile");
1499 }
1500 }
1501 // c:1499-1506 — if (interact) { GLOBAL_ZSHRC? + .zshrc }
1502 if interact {
1503 if isset(RCS) && isset(GLOBALRCS) {
1504 let _ = source(crate::ported::config_h::GLOBAL_ZSHRC);
1505 }
1506 if isset(RCS) && !privileged {
1507 sourcehome(".zshrc");
1508 }
1509 }
1510 // c:1507-1514 — if (islogin) { GLOBAL_ZLOGIN? + .zlogin }
1511 if is_login {
1512 if isset(RCS) && isset(GLOBALRCS) {
1513 let _ = source(crate::ported::config_h::GLOBAL_ZLOGIN);
1514 }
1515 if isset(RCS) && !privileged {
1516 sourcehome(".zlogin");
1517 }
1518 }
1519 }
1520 // c:1516-1517 — noerrexit = 0; nohistsave = 0; (not surfaced)
1521}
1522
1523/// Port of `void init_misc(char *cmd, char *zsh_name)` from Src/init.c:1524.
1524pub fn init_misc(cmd: Option<&str>, zsh_name: &str) {
1525 // c:1524
1526 if zsh_name.starts_with('r') {
1527 // c:1524
1528 crate::ported::utils::zerrnam(
1529 zsh_name, // c:1527
1530 "no support for restricted mode",
1531 );
1532 std::process::exit(1); // c:1528
1533 }
1534 if let Some(cmdstr) = cmd {
1535 // c:1530
1536 // if (SHIN >= 10) close(SHIN); // c:1531-1532
1537 // SHIN = movefd(open("/dev/null", O_RDONLY|O_NOCTTY)); // c:1551
1538 // shinbufreset(); // c:1551
1539 // execstring(cmd, 0, 1, "cmdarg"); // c:1551
1540 let _ = cmdstr;
1541 // stopmsg = 1; zexit(...); // c:1551-1537
1542 std::process::exit(0);
1543 }
1544
1545 // c:1573-1574 — `if (interact && isset(RCS))
1546 // readhistfile(NULL, 0, HFILE_USE_OPTIONS);`
1547 // — THE startup read of $HISTFILE into the history ring. Without
1548 // it an interactive shell starts with empty history (up-arrow /
1549 // fc -l see nothing from previous sessions).
1550 if crate::ported::zsh_h::interact() && isset(RCS) {
1551 crate::ported::hist::readhistfile(
1552 None,
1553 0,
1554 crate::ported::zsh_h::HFILE_USE_OPTIONS as i32,
1555 ); // c:1574
1556 }
1557}
1558
1559/// Port of `mod_export enum source_return source(char *s)` from Src/init.c:1551.
1560///
1561/// Body dispatches through `with_executor` to the fusevm bytecode
1562/// pipeline (`execute_script_zsh_pipeline`), matching the path
1563/// `bins/zshrs.rs::source_from_memory` takes. `scriptname` /
1564/// `scriptfilename` / `sourcelevel` are saved+restored around the
1565/// nested execution per the C save/restore at c:1572-1670.
1566pub fn source(s: &str) -> i32 {
1567 // c:1551
1568 let us = unmeta(s); // c:1551
1569 let path = std::path::Path::new(&us);
1570 // c:1565-1568 — `if (!s || (!(prog = try_source_file(us)) &&
1571 // (tempfd = ... open(us, ...)) == -1)) return SOURCE_NOT_FOUND;`
1572 // — a loadable `<file>.zwc` rescues a missing/unreadable plain
1573 // file; only BOTH failing is NOT_FOUND. The compiled prog itself
1574 // is fetched below (the Rust port defers it to the contents
1575 // read so the state save/restore stays in one place).
1576 let zwc_prog = crate::ported::parse::try_source_file(&us);
1577 if zwc_prog.is_none() && !path.exists() {
1578 return 1; /* SOURCE_NOT_FOUND */
1579 }
1580
1581 // c:1571-1581 — save shell state.
1582 let old_scriptname = crate::ported::utils::scriptname_get(); // c:1573
1583 let old_scriptfilename = crate::ported::utils::scriptfilename_get(); // c:1574
1584 crate::ported::utils::set_scriptname(Some(us.clone())); // c:1572
1585 crate::ported::utils::set_scriptfilename(Some(us.clone()));
1586
1587 sourcelevel.fetch_add(1, Ordering::SeqCst); // c:1606
1588
1589 // c:1610-1618 — push an FS_SOURCE funcstack frame so `$funcstack`,
1590 // `$functrace` and `$funcfiletrace` include the sourced file (the
1591 // readers at parameter.rs walk FUNCSTACK and special-case
1592 // `tp == FS_SOURCE`). FUNCSTACK is a Vec stack: the last element is
1593 // the top, so `prev` is left None (the index encodes the link, same
1594 // convention as the FS_FUNC push in exec.rs::doshfunc:5821).
1595 let oldlineno = crate::ported::lex::lineno() as i64; // c:1576 oldlineno = lineno
1596 {
1597 // c:1612-1613 — caller: the current funcstack top, else the
1598 // previous script filename, else "zsh".
1599 let caller = {
1600 let stk = crate::ported::modules::parameter::FUNCSTACK
1601 .lock()
1602 .unwrap_or_else(|e| e.into_inner());
1603 stk.last().map(|f| f.name.clone())
1604 }
1605 .or_else(|| old_scriptfilename.clone())
1606 .or_else(|| Some("zsh".to_string()));
1607 let frame = crate::ported::zsh_h::funcstack {
1608 prev: None, // c:1617 (Vec-stack index encodes link)
1609 name: us.clone(), // c:1611 fstack.name = scriptfilename
1610 filename: Some(us.clone()), // c:1616
1611 caller, // c:1612
1612 flineno: 0, // c:1614
1613 lineno: oldlineno, // c:1615
1614 tp: crate::ported::zsh_h::FS_SOURCE, // c:1618
1615 };
1616 crate::ported::modules::parameter::FUNCSTACK
1617 .lock()
1618 .unwrap_or_else(|e| e.into_inner())
1619 .push(frame); // c:1618 funcstack = &fstack
1620 }
1621
1622 // c:1618-1642 — parse-and-execute loop. Route through the
1623 // fusevm executor for the actual parse+exec; if no executor
1624 // context (out-of-band call), fall back to the partial
1625 // read-for-side-effects path so errors still surface.
1626 //
1627 // c:1566 — `try_source_file(us)` runs FIRST: a sibling
1628 // `<file>.zwc` newer than the file (or `s` itself being a
1629 // `.zwc`) supplies the compiled wordcode instead of the plain
1630 // read. Bridge wordcode → text via getpermtext (same as the
1631 // `.zwc` autoload path in exec.rs::loadautofn).
1632 let contents = match zwc_prog {
1633 Some(prog) => Ok(crate::ported::text::getpermtext(Box::new(prog), None, 0)),
1634 None => std::fs::read_to_string(path),
1635 };
1636 if let Ok(body) = contents {
1637 // c:Src/jobs.c:1878-1884 — zsh runs the sourced list through
1638 // execpline, whose per-pipeline `initjob()` caps recursion at
1639 // MAX_MAXJOBS: `zerr("job table full or recursion limit exceeded")`
1640 // then bails. The fusevm pipeline path below doesn't allocate a job
1641 // per pipeline, so without this guard runaway `. self`-style
1642 // recursion (invisible to FUNCNEST, which counts FS_FUNC frames
1643 // only) overflowed the 256 MB main-thread stack → uncatchable
1644 // SIGBUS. Reproduce the ceiling: total FUNCSTACK depth is the proxy
1645 // for zsh's concurrently-held job slots. At/over the ceiling, raise
1646 // the zsh-identical error (zerr sets ERRFLAG_ERROR so the outer
1647 // sourced lists unwind) and refuse the deeper body — the FS_SOURCE
1648 // frame just pushed is popped normally below.
1649 let over_limit = crate::ported::modules::parameter::FUNCSTACK
1650 .lock()
1651 .map(|s| s.len())
1652 .unwrap_or(0)
1653 >= crate::ported::jobs::MAX_MAXJOBS;
1654 if over_limit {
1655 crate::ported::utils::zerr("job table full or recursion limit exceeded");
1656 crate::ported::builtin::LASTVAL.store(1, Ordering::Relaxed);
1657 } else {
1658 let _ = crate::ported::exec::execute_script_zsh_pipeline(&body);
1659 }
1660 }
1661
1662 sourcelevel.fetch_sub(1, Ordering::SeqCst); // c:1644
1663
1664 // c:1664 — `funcstack = fstack.prev;` — pop our FS_SOURCE frame.
1665 crate::ported::modules::parameter::FUNCSTACK
1666 .lock()
1667 .unwrap_or_else(|e| e.into_inner())
1668 .pop();
1669
1670 // c:1646-1670 — restore shell state.
1671 crate::ported::utils::set_scriptname(old_scriptname);
1672 crate::ported::utils::set_scriptfilename(old_scriptfilename);
1673 0 /* SOURCE_OK */ // c:1679
1674}
1675
1676/// Port of `void sourcehome(char *s)` from Src/init.c:1679.
1677pub fn sourcehome(s: &str) {
1678 // c:1679
1679 queue_signals(); // c:1679
1680 let emul = emulation.load(Ordering::SeqCst);
1681 let is_posix = (emul & 6) != 0;
1682 // c:1684 — `h = is_posix ? getsparam("HOME") : (getsparam("ZDOTDIR")
1683 // ?: getsparam("HOME"))`
1684 // paramtab read; was OS env.
1685 let h = if is_posix {
1686 getsparam("HOME")
1687 } else {
1688 getsparam("ZDOTDIR").or_else(|| getsparam("HOME"))
1689 };
1690 let h = match h {
1691 // c:1685-1689
1692 Some(h) => h,
1693 None => {
1694 unqueue_signals();
1695 return;
1696 }
1697 };
1698 let buf = format!("{}/{}", h, s); // c:1713
1699 unqueue_signals(); // c:1713
1700 source(&buf); // c:1713
1701}
1702
1703/// Port of `void init_bltinmods(void)` from Src/init.c:1703.
1704pub fn init_bltinmods() {
1705 // c:1703
1706 // #include "bltinmods.list" // c:1705
1707 // load_module("zsh/main", NULL, 0); // c:1706
1708 //
1709 // C's bltinmods.list is autotools-generated and contains a
1710 // `register_module(name, setup_, ...)` line per statically-linked
1711 // module — which seeds MODULESTAB and runs each module's setup_
1712 // (and later boot_) entry points. Our equivalent is the
1713 // register_builtin_modules() call inside `modulestab::new()`, which
1714 // is reached the first time MODULESTAB is observed. Force that
1715 // observation here so default-loaded modules
1716 // (zsh/parameter, zsh/main, zsh/watch, …) register their
1717 // builtins/params/preprompt hooks before user code runs.
1718 drop(crate::ported::module::MODULESTAB.lock().unwrap());
1719}
1720
1721/// Port of `mod_export void noop_function(void)` from Src/init.c:1713.
1722pub fn noop_function() { // c:1713
1723 /* do nothing */ // c:1720
1724}
1725
1726/// Port of `mod_export void noop_function_int(int nothing)` from Src/init.c:1720.
1727pub fn noop_function_int(_nothing: i32) { // c:1720
1728 /* do nothing */ // c:1720
1729}
1730
1731/// Port of `mod_export char *zleentry(...)` from Src/init.c:1743.
1732pub fn zleentry(cmd: i32) -> Option<String> {
1733 // c:1743
1734 let mut cmd = cmd;
1735 match zle_load_state.load(Ordering::SeqCst) {
1736 // c:1755
1737 0 => {
1738 // c:1756
1739 // ZLE_CMD_TRASH=?, ZLE_CMD_RESET_PROMPT=?, ZLE_CMD_REFRESH=?
1740 if cmd != 1 && cmd != 2 && cmd != 3 {
1741 // c:1761-1762
1742 // load_module("zsh/zle", NULL, 0) // c:1764
1743 zle_load_state.store(2, Ordering::SeqCst); // c:1770
1744 }
1745 }
1746 1 => {
1747 // c:1776
1748 cmd = -1; // c:1779
1749 }
1750 2 => { /* fallback */ } // c:1782
1751 _ => {}
1752 }
1753 match cmd {
1754 // c:1788
1755 // ZLE_CMD_READ // c:1796
1756 4 => {
1757 // c:1796
1758 let _line = String::new(); // c:1808
1759 return Some(String::new());
1760 }
1761 // ZLE_CMD_GET_LINE // c:1812
1762 5 => {
1763 // c:1812
1764 return Some(String::new()); // c:1835
1765 }
1766 _ => {}
1767 }
1768 None // c:1855
1769}
1770
1771/// Port of `mod_export int fallback_compctlread(...)` from Src/init.c:1835.
1772pub fn fallback_compctlread(name: &str) -> i32 {
1773 // c:1835
1774 crate::ported::utils::zwarnnam(
1775 name, // c:1855
1776 "no loaded module provides read for completion context",
1777 );
1778 1 // c:1855
1779}
1780
1781/// Port of `mod_export int zsh_main(int argc, char **argv)` from Src/init.c:1855.
1782pub fn zsh_main(_argc: i32, argv: &[String]) -> i32 {
1783 // c:1855
1784 #[cfg(unix)]
1785 unsafe {
1786 let empty = std::ffi::CString::new("").unwrap();
1787 libc::setlocale(libc::LC_ALL, empty.as_ptr()); // c:1861
1788 }
1789
1790 // init_jobs(argv, environ); // c:1864
1791 let env: Vec<String> = std::env::vars()
1792 .map(|(k, v)| format!("{}={}", k, v))
1793 .collect();
1794 let _ = crate::ported::jobs::init_jobs(argv, &env);
1795
1796 // typtab[...] |= IMETA; // c:1871-1875
1797
1798 // Metafy each argv (already strings in Rust) // c:1877
1799
1800 let mut zsh_name = argv.first().cloned().unwrap_or_default(); // c:1879
1801 loop {
1802 // c:1880
1803 let arg0 = zsh_name.clone(); // c:1881
1804 zsh_name = match arg0.rfind('/') {
1805 // c:1882
1806 None => arg0.clone(), // c:1883
1807 Some(i) => arg0[i + 1..].to_string(), // c:1885
1808 };
1809 if zsh_name.starts_with('-') {
1810 // c:1886
1811 zsh_name = zsh_name[1..].to_string(); // c:1887
1812 }
1813 if zsh_name == "su" {
1814 // c:1888
1815 if let Ok(sh) = std::env::var("SHELL") {
1816 // c:1889
1817 if !sh.is_empty() && arg0 != sh {
1818 // c:1890
1819 zsh_name = sh; // c:1891
1820 continue; // c:1892
1821 }
1822 }
1823 break; // c:1893
1824 }
1825 break; // c:1895
1826 }
1827
1828 // fdtable_size = zopenmax(); fdtable[0..2] = FDT_EXTERNAL; // c:1898-1900
1829 let _ = crate::ported::compat::zopenmax();
1830
1831 crate::ported::options::createoptiontable(); // c:1902
1832
1833 // parseargs(zsh_name, argv, &runscript, &cmd); // c:1905
1834 let mut argv_v = argv.to_vec();
1835 let mut runscript: Option<String> = None; // c:1857
1836 let mut cmd: Option<String> = None; // c:1858
1837 parseargs(&zsh_name, &mut argv_v, &mut runscript, &mut cmd);
1838
1839 SHTTY.store(-1, Ordering::SeqCst); // c:1907
1840 init_io(cmd.as_deref()); // c:1908
1841 setupvals(cmd.as_deref(), runscript.as_deref(), &zsh_name); // c:1909
1842
1843 init_signals(); // c:1911
1844 init_bltinmods(); // c:1912
1845 crate::ported::builtin::init_builtins(); // c:1913
1846
1847 // Initialize the ZLE line editor for interactive sessions BEFORE the
1848 // rc files are sourced, so a user's `bindkey` in .zshrc/.zshenv
1849 // layers onto the populated emacs/main keymap instead of being wiped
1850 // by a later default_bindings(). C loads `zsh/zle` during init
1851 // (init.c via the module system); zshrs links ZLE statically, so the
1852 // equivalent setup — register the built-in widgets (init_thingies),
1853 // create the keymap name table, and build + select the default
1854 // keymaps (default_bindings sets curkeymap="main") — runs here once.
1855 // Guarded by zle_load_state so it never re-runs and clobbers user
1856 // bindings. Gated on `interact` so non-interactive `-c` shells skip
1857 // ZLE entirely (they read via shingetline, not the editor).
1858 if interact() && zle_load_state.load(Ordering::SeqCst) == 0 {
1859 crate::ported::zle::zle_thingy::init_thingies();
1860 crate::ported::zle::zle_keymap::createkeymapnamtab();
1861 crate::ported::zle::zle_keymap::default_bindings();
1862 zle_load_state.store(1, Ordering::SeqCst); // c:1739 — ZLE loaded
1863 }
1864
1865 // c:1914 — run_init_scripts() sources .zshenv/.zshrc/.zlogin via
1866 // source(). That runs BEFORE the loop's first execode establishes a
1867 // VM execution context, so the sourced bodies must execute under an
1868 // explicit SESSION_EXECUTOR context or they silently no-op (rc files
1869 // ignored). Scope it to JUST this call — the loop below enters its
1870 // own per-command context, and a broad/global executor scope would
1871 // re-enter on nested command substitution and hang.
1872 crate::fusevm_bridge::with_session_context(run_init_scripts); // c:1914
1873 setupshin(runscript.as_deref()); // c:1915
1874 init_misc(cmd.as_deref(), &zsh_name); // c:1916
1875
1876 loop {
1877 // c:1918
1878 let mut errexit = 0; // c:1924
1879 // maybeshrinkjobtab(); // c:1925
1880 // c:1927-1935 — `do { retflag = 0; loop(1,0); if (errflag &&
1881 // !interact && !isset(CONTINUEONERROR)) { errexit = 1; break; } }
1882 // while (tok != ENDINPUT && (tok != LEXERR || isset(SHINSTDIN)));`
1883 loop {
1884 // c:1927 do
1885 RETFLAG.store(0, Ordering::SeqCst); // c:1929 retflag = 0
1886 let _ = r#loop(1, 0); // c:1930
1887 if errflag.load(Ordering::SeqCst) != 0 && !interact() && !isset(CONTINUEONERROR) {
1888 // c:1931
1889 errexit = 1; // c:1932
1890 break; // c:1933
1891 }
1892 let tok_v = tok(); // c:1935
1893 if !(tok_v != ENDINPUT && (tok_v != LEXERR || isset(SHINSTDIN))) {
1894 break; // c:1935 while-cond false → exit do-while
1895 }
1896 }
1897 if tok() == LEXERR || errexit != 0 {
1898 // c:1936
1899 // c:1938-1939 — `if (!lastval) lastval = 1;` (fatal error → nonzero)
1900 if LASTVAL.load(Ordering::SeqCst) == 0 {
1901 LASTVAL.store(1, Ordering::SeqCst);
1902 }
1903 STOPMSG.store(1, Ordering::SeqCst); // c:1940 stopmsg = 1
1904 crate::ported::builtin::zexit(LASTVAL.load(Ordering::SeqCst), ZEXIT_NORMAL);
1905 // c:1941
1906 }
1907 if !(isset(IGNOREEOF) && interact()) {
1908 // c:1943
1909 // c:1944-1947 — interactive "logout\n"/"exit\n" echo is `#if 0`'d
1910 // out in the C source, so nothing to port there.
1911 crate::ported::builtin::zexit(LASTVAL.load(Ordering::SeqCst), ZEXIT_NORMAL); // c:1948
1912 continue; // c:1949 — only reached if zexit DEFERRED (running jobs)
1913 }
1914 noexitct.fetch_add(1, Ordering::SeqCst); // c:1951 noexitct++
1915 if noexitct.load(Ordering::SeqCst) >= 10 {
1916 // c:1952
1917 STOPMSG.store(1, Ordering::SeqCst); // c:1953 stopmsg = 1
1918 crate::ported::builtin::zexit(LASTVAL.load(Ordering::SeqCst), ZEXIT_NORMAL);
1919 // c:1954
1920 }
1921 // c:1961-1963 — IGNOREEOF interactive nudge.
1922 if use_exit_printed.load(Ordering::SeqCst) == 0 {
1923 crate::ported::utils::zerrnam(
1924 "zsh",
1925 if !islogin() {
1926 "use 'exit' to exit."
1927 } else {
1928 "use 'logout' to logout."
1929 },
1930 );
1931 }
1932 }
1933}
1934
1935// =========================================================================
1936// Functions from init.c
1937// =========================================================================
1938
1939/// Port of `enum loop_return loop(int toplevel, int justonce)` from Src/init.c:113.
1940///
1941/// Keep executing lists until EOF found. // c:109
1942///
1943/// ```c
1944/// enum loop_return
1945/// loop(int toplevel, int justonce)
1946/// {
1947/// Eprog prog;
1948/// int err, non_empty = 0;
1949/// queue_signals();
1950/// pushheap();
1951/// if (!toplevel)
1952/// zcontext_save();
1953/// for (;;) {
1954/// freeheap();
1955/// if (stophist == 3) hend(NULL);
1956/// hbegin(1);
1957/// if (isset(SHINSTDIN)) {
1958/// setblock_stdin();
1959/// if (interact && toplevel) { ... preprompt() ... }
1960/// }
1961/// use_exit_printed = 0;
1962/// intr();
1963/// lexinit();
1964/// if (!(prog = parse_event(ENDINPUT))) { ... }
1965/// if (hend(prog)) { ... preexec ... execode ... }
1966/// if (ferror(stderr)) { ... }
1967/// if (subsh) realexit();
1968/// if (((!interact || sourcelevel) && errflag) || retflag) break;
1969/// if (isset(SINGLECOMMAND) && toplevel) { dotrap(SIGEXIT); realexit(); }
1970/// if (justonce) break;
1971/// }
1972/// err = errflag;
1973/// if (!toplevel) zcontext_restore();
1974/// popheap();
1975/// unqueue_signals();
1976/// if (err) return LOOP_ERROR;
1977/// if (!non_empty) return LOOP_EMPTY;
1978/// return LOOP_OK;
1979/// }
1980/// ```
1981pub fn r#loop(toplevel: i32, justonce: i32) -> i32 {
1982 // c:113
1983
1984 // c:114 — `int subsh = subsh;` — read the global at exec.rs:160
1985 // (port of `int subsh;` from Src/exec.c). Non-zero when current
1986 // shell is a forked subshell (set by entersubsh c:1083).
1987 let subsh: i32 = crate::ported::exec::subsh.load(Ordering::Relaxed);
1988
1989 let mut prog: Option<crate::ported::parse::ZshProgram>; // c:115
1990 let err: i32; // c:116
1991 let mut non_empty: i32 = 0; // c:116
1992
1993 queue_signals(); // c:118
1994 crate::ported::mem::pushheap(); // c:119
1995 if toplevel == 0 {
1996 // c:120 !toplevel
1997 crate::ported::context::zcontext_save(); // c:121
1998 }
1999 // One HISTORY UNIT per accepted editor line. C's par_event recurses
2000 // until ENDINPUT (parse.c:635), so a pasted multi-command buffer parses
2001 // into ONE prog and hend stores ONE entry ("cmd1\ncmd2"). zshrs's
2002 // single-event interleave (parse_event returns per logical command; see
2003 // parse.rs:766-786) would cycle hbegin/hend per command and SPLIT the
2004 // pasted line into separate history entries — up-arrow then recalled
2005 // only the last command. While the previous accepted line still has
2006 // unconsumed input buffered, keep the history entry OPEN: skip the
2007 // hend/hbegin cycle (and preprompt) until the line is drained.
2008 let mut hist_open_across_events = false;
2009 loop {
2010 // c:122
2011 crate::ported::mem::freeheap(); // c:123
2012 if !hist_open_across_events {
2013 if stophist.load(Ordering::SeqCst) == 3 {
2014 // c:124 stophist == 3
2015 hend(None); // c:125 hend(NULL)
2016 }
2017 hbegin(1); // c:126 hbegin(1)
2018 }
2019 if isset(SHINSTDIN) && !hist_open_across_events {
2020 // c:127 isset(SHINSTDIN)
2021 crate::ported::utils::setblock_stdin(); // c:128
2022 if isset(INTERACTIVE) && toplevel != 0 {
2023 // c:129 interact && toplevel
2024 let hstop = stophist.load(Ordering::SeqCst); // c:130
2025 stophist.store(3, Ordering::SeqCst); // c:131
2026 // c:146-148 — drain any signals queued during the previous read
2027 // WITHOUT changing the queueing nesting level: snapshot the level,
2028 // dont_queue_signals() (whose run_queued_signals() reaps bg-job
2029 // SIGCHLDs that were queued while queueing_enabled stayed at its
2030 // baseline 1 across the ZLE read), then restore the level. This
2031 // port previously jumped straight to preprompt(), so every queued
2032 // SIGCHLD went undrained → children never reaped (zombies) and the
2033 // shell hung at the prompt. Port of Src/init.c:146-148.
2034 let q = crate::ported::signals_h::queue_signal_level();
2035 dont_queue_signals();
2036 crate::ported::signals_h::restore_queue_signals(q);
2037 // c:133-138 — reset errflag for preprompt
2038 errflag.store(0, Ordering::SeqCst); // c:139 errflag = 0
2039 crate::ported::utils::preprompt(); // c:140
2040 if stophist.load(Ordering::SeqCst) != 3 {
2041 // c:141
2042 hbegin(1); // c:142
2043 } else {
2044 // c:143
2045 stophist.store(hstop, Ordering::SeqCst); // c:144
2046 }
2047 // c:146-149 — reset errflag again
2048 errflag.store(0, Ordering::SeqCst); // c:150
2049 }
2050 }
2051 use_exit_printed.store(0, Ordering::SeqCst); // c:153
2052 intr(); // c:154
2053 crate::ported::lex::lexinit(); // c:155
2054 prog = crate::ported::parse::parse_event(ENDINPUT as i32); // c:156
2055 if prog.is_none() {
2056 // c:156
2057 hend(None); // c:158
2058 // A parse failure closes any deferred same-line history unit —
2059 // the next iteration must run its own hbegin again.
2060 hist_open_across_events = false;
2061 // c:159-161 — break on clean EOF / non-toplevel LEXERR / justonce
2062 let tok_v = tok(); // c:159 tok
2063 let errflag_v = errflag.load(Ordering::SeqCst);
2064 let lexerr_break = tok_v == LEXERR && (!isset(SHINSTDIN) || toplevel == 0);
2065 let endinput_break = tok_v == ENDINPUT && errflag_v == 0;
2066 if endinput_break || lexerr_break || justonce != 0 {
2067 // c:159-161
2068 break; // c:162
2069 }
2070 if crate::ported::hist::exit_pending.load(Ordering::SeqCst) {
2071 // c:163 exit_pending
2072 STOPMSG.store(1, Ordering::SeqCst); // c:169 stopmsg = 1
2073 crate::ported::builtin::zexit(
2074 // c:170 zexit(exit_val, ZEXIT_NORMAL)
2075 crate::ported::builtin::EXIT_VAL.load(Ordering::SeqCst),
2076 ZEXIT_NORMAL,
2077 );
2078 }
2079 if tok_v == LEXERR && LASTVAL.load(Ordering::SeqCst) == 0
2080 // c:172
2081 {
2082 LASTVAL.store(1, Ordering::SeqCst); // c:173 lastval = 1
2083 }
2084 continue; // c:174
2085 }
2086 let prog_inner = prog.take().unwrap();
2087 // c:176 — `if (hend(prog))`: passing the program in commits the
2088 // history line. zshrs's `hend` takes Option<&[u8]>; sentinel
2089 // non-empty slice signals "program present" to the commit path.
2090 //
2091 // Same-accepted-line continuation (see hist_open_across_events at
2092 // the loop head): while the input buffer still holds unconsumed
2093 // chars of the line being edited, DEFER hend — chline keeps
2094 // accumulating so the whole pasted buffer commits as one entry,
2095 // exactly as C's to-ENDINPUT par_event produces.
2096 hist_open_across_events = isset(INTERACTIVE)
2097 && toplevel != 0
2098 && crate::ported::input::inbufct.with(|c| c.get()) > 0;
2099 let hend_ret = if hist_open_across_events {
2100 1 // execute; the history commit happens when the line drains
2101 } else {
2102 let prog_bytes: Vec<u8> = format!("{:?}", prog_inner).into_bytes();
2103 hend(Some(&prog_bytes)) // c:176
2104 };
2105 if hend_ret != 0 {
2106 let _toksav = tok(); // c:177
2107 non_empty = 1; // c:179
2108 // c:180-215 — preexec hook + ZLE_CMD_PREEXEC.
2109 if toplevel != 0 {
2110 // c:180
2111 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2112 // Native p10k engine command timer (src/extensions/p10k):
2113 // stamp start-of-command for the command_execution_time
2114 // segment. The zsh theme does this in a preexec hook
2115 // (`_p9k__timer_start=EPOCHREALTIME`); the native engine
2116 // stamps here unconditionally (no-op when inactive) so
2117 // it works without any user preexec function installed.
2118 crate::p10k::note_exec_start();
2119 let preexec_fn = crate::ported::utils::getshfunc("preexec"); // c:181
2120 let preexec_hook = crate::ported::params::paramtab()
2121 .read()
2122 .ok()
2123 .and_then(|t| t.get(&format!("preexec{}", HOOK_SUFFIX)).map(|_| ())); // c:182 paramtab->getnode("preexec_functions")
2124 if preexec_fn.is_some() || preexec_hook.is_some() {
2125 let mut args: Vec<String> = Vec::new(); // c:191 newlinklist()
2126 args.push("preexec".to_string()); // c:192 addlinknode(args, "preexec")
2127 // c:195 — hist_ring && curline.histnum == curhist
2128 let hr = hist_ring.lock().unwrap();
2129 let cl = curline.lock().unwrap();
2130 let same = !hr.is_empty()
2131 && cl.as_ref().map(|c| c.histnum).unwrap_or(0)
2132 == curhist.load(Ordering::SeqCst);
2133 if same {
2134 // c:195
2135 args.push(hr.first().map(|h| h.node.nam.clone()).unwrap_or_default());
2136 // c:196
2137 } else {
2138 args.push(String::new()); // c:198
2139 }
2140 drop(hr);
2141 drop(cl);
2142 // c:199 — addlinknode(args, dupstring(getjobtext(prog, NULL)))
2143 // Eprog↔ZshProgram bridge not yet in place; pass a
2144 // freshly-allocated empty eprog so getjobtext/getpermtext
2145 // return their NULL-prog representation. Real text comes
2146 // from src/vm_helper once that bridge lands.
2147 let placeholder: Eprog = Box::new(eprog {
2148 flags: 0,
2149 len: 0,
2150 npats: 0,
2151 nref: 0,
2152 pats: Vec::new(),
2153 prog: Vec::new(),
2154 strs: None,
2155 shf: None,
2156 dump: None,
2157 strs_metafied: false, // native pool — clean UTF-8
2158 });
2159 let placeholder2: Eprog = Box::new(eprog {
2160 flags: 0,
2161 len: 0,
2162 npats: 0,
2163 nref: 0,
2164 pats: Vec::new(),
2165 prog: Vec::new(),
2166 strs: None,
2167 shf: None,
2168 dump: None,
2169 strs_metafied: false, // native pool — clean UTF-8
2170 });
2171 let job_text = crate::ported::text::getjobtext(placeholder, None); // c:199
2172 args.push(crate::ported::mem::dupstring(&job_text));
2173 // c:200 — getpermtext(prog, NULL, 0)
2174 let cmdstr = getpermtext(placeholder2, None, 0); // c:200
2175 args.push(cmdstr.clone());
2176 callhookfunc(
2177 // c:202
2178 "preexec",
2179 Some(&args),
2180 1,
2181 std::ptr::null_mut(),
2182 );
2183 crate::ported::mem::zsfree(cmdstr); // c:205
2184 errflag.fetch_and(
2185 // c:214 errflag &= ~ERRFLAG_ERROR
2186 !ERRFLAG_ERROR,
2187 Ordering::SeqCst,
2188 );
2189 }
2190 }
2191 if toplevel != 0 // c:216
2192 && zle_load_state.load(Ordering::SeqCst) == 1
2193 {
2194 let _ = zleentry(
2195 // c:217 ZLE_CMD_PREEXEC
2196 ZLE_CMD_PREEXEC,
2197 );
2198 }
2199 if STOPMSG.load(Ordering::SeqCst) != 0 {
2200 // c:218
2201 STOPMSG.fetch_sub(1, Ordering::SeqCst); // c:219
2202 }
2203 // c:220 — `execode(prog, 0, 0, toplevel ? "toplevel" : "file");`
2204 let exec_label = if toplevel != 0 { "toplevel" } else { "file" };
2205 // Save the lexer's line counter across execution. Each
2206 // statement's `SET_LINENO` (c:Src/exec.c execpline WC_PIPE_LINENO)
2207 // overwrites `lineno` to that statement's line while running;
2208 // C's execlist brackets this with `oldlineno = lineno` /
2209 // `lineno = oldlineno` (exec.c:28/292) so the value is restored
2210 // on exit. Without it, the faithful single-event loop (which
2211 // interleaves parse + execode) had the NEXT event parse from the
2212 // just-executed statement's line — `$LINENO` froze at 1 across a
2213 // piped/interactive script instead of advancing 1,2,3.
2214 let saved_lex_lineno = crate::ported::lex::LEX_LINENO.with(|c| c.get());
2215 crate::ported::exec::execode(&prog_inner, 0, 0, exec_label); // c:220
2216 crate::ported::lex::LEX_LINENO.with(|c| c.set(saved_lex_lineno));
2217 // c:Src/exec.c:1611-1618 — errexit (`set -e`) fires a shell
2218 // exit FROM execlist (`if (errexit) { errflag = 0; if
2219 // (sigtrapped[SIGEXIT]) dotrap(SIGEXIT); realexit(); }`).
2220 // zshrs's fusevm can't realexit mid-VM, so BUILTIN_ERREXIT_CHECK
2221 // defers it via EXIT_PENDING + a jump to chunk-end. Whole-program
2222 // mode then skips the rest of the single chunk via that jump, but
2223 // the faithful single-event loop runs each event as its OWN chunk
2224 // — the jump only ends the failed event, and without honoring the
2225 // pending exit here the NEXT event would still run. Mirror C's
2226 // execlist realexit at the execode boundary: zexit fires the EXIT
2227 // trap (its trap-body dispatch reaches the session executor) and
2228 // realexits with EXIT_VAL. Guarded to top level + no subshell so
2229 // a deferred subshell/cmdsub exit still propagates via its own
2230 // unwind (fusevm_bridge) instead of killing the parent here.
2231 if toplevel != 0
2232 && crate::ported::builtin::SUBSHELL_DEPTH.load(Ordering::SeqCst) == 0
2233 && crate::ported::builtin::EXIT_PENDING.load(Ordering::SeqCst) != 0
2234 {
2235 crate::ported::builtin::zexit(
2236 crate::ported::builtin::EXIT_VAL.load(Ordering::SeqCst),
2237 ZEXIT_NORMAL,
2238 );
2239 }
2240 // c:221 — `tok = toksav;` restore
2241 set_tok(_toksav);
2242 if toplevel != 0 {
2243 // c:222
2244 noexitct.store(0, Ordering::SeqCst); // c:223
2245 if zle_load_state.load(Ordering::SeqCst) == 1 {
2246 // c:224
2247 let _ = zleentry(
2248 // c:225 ZLE_CMD_POSTEXEC
2249 ZLE_CMD_POSTEXEC,
2250 );
2251 }
2252 }
2253 }
2254 // c:228-231 — `if (ferror(stderr))` write-error path. Mirror as a
2255 // best-effort flush; Rust panic on broken pipe handled upstream.
2256 // c:232 — `if (subsh) realexit();`
2257 if subsh != 0 {
2258 // c:232
2259 realexit(); // c:233
2260 }
2261 // c:234 — `if (((!interact || sourcelevel) && errflag) || retflag) break;`
2262 let errflag_v = errflag.load(Ordering::SeqCst);
2263 let interact_v = isset(INTERACTIVE);
2264 let srclvl = sourcelevel.load(Ordering::SeqCst);
2265 let retflag_v = RETFLAG.load(Ordering::SeqCst);
2266 if ((!interact_v || srclvl != 0) && errflag_v != 0) || retflag_v != 0 {
2267 // c:234
2268 break; // c:235
2269 }
2270 if isset(SINGLECOMMAND) && toplevel != 0 {
2271 // c:236
2272 dont_queue_signals(); // c:237
2273 // c:238 — sigtrapped[SIGEXIT] != 0 → dotrap(SIGEXIT)
2274 let tr = sigtrapped.lock().unwrap();
2275 let sigexit = libc::SIGINT as usize; // SIGEXIT = 0 (zsh-internal)
2276 if tr.get(sigexit).copied().unwrap_or(0) != 0 {
2277 // c:238
2278 drop(tr);
2279 let _ = dotrap(0 /* SIGEXIT */); // c:239
2280 }
2281 realexit(); // c:240
2282 }
2283 if justonce != 0 {
2284 // c:242
2285 break; // c:243
2286 }
2287 let _ = histlinect.load(Ordering::SeqCst); // silence unused alias
2288 }
2289 err = errflag.load(Ordering::SeqCst); // c:245 err = errflag
2290 if toplevel == 0 {
2291 // c:246 !toplevel
2292 zcontext_restore(); // c:247
2293 }
2294 popheap(); // c:248
2295 unqueue_signals(); // c:249
2296
2297 if err != 0 {
2298 return 2; /* LOOP_ERROR */
2299 } // c:251-252
2300 if non_empty == 0 {
2301 return 1; /* LOOP_EMPTY */
2302 } // c:253-254
2303 0 /* LOOP_OK */ // c:255
2304}
2305
2306/// Port of `static void parseopts_setemulate(...)` from Src/init.c:348.
2307fn parseopts_setemulate(nam: &str, flags: i32) {
2308 // c:350 — `emulate(nam, 1, &emulation, opts);` initialises most
2309 // options. Strip leading `-` (login-shell marker) and any path
2310 // components so the name passed to `emulate` is just the basename
2311 // (`ksh` / `sh` / `csh` / `zsh`).
2312 let bare = nam.trim_start_matches('-');
2313 let basename = std::path::Path::new(bare)
2314 .file_name()
2315 .and_then(|n| n.to_str())
2316 .unwrap_or(bare);
2317 crate::ported::options::emulate(basename, true); // c:350
2318
2319 // c:351 — `opts[LOGINSHELL] = ((flags & PARSEARGS_LOGIN) != 0);`
2320 const PARSEARGS_LOGIN: i32 = 2;
2321 crate::ported::options::opt_state_set("loginshell", (flags & PARSEARGS_LOGIN) != 0);
2322
2323 // c:352 — `opts[PRIVILEGED] = (getuid() != geteuid() || …);`
2324 let priv_on = unsafe { libc::getuid() != libc::geteuid() || libc::getgid() != libc::getegid() };
2325 crate::ported::options::opt_state_set("privileged", priv_on);
2326
2327 // c:361 — `opts[INTERACTIVE] = isatty(0) ? 2 : 0;`. The "2"
2328 // sentinel means "default-on, may be downgraded by SHINSTDIN
2329 // handling below". Rust port stores as bool — we only retain
2330 // the on/off distinction; the downgrade logic at c:end-of-fn
2331 // is a no-op once we collapse to bool.
2332 let interactive_default = unsafe { libc::isatty(0) != 0 };
2333 crate::ported::options::opt_state_set("interactive", interactive_default);
2334
2335 // c:366-368 — `opts[MONITOR] = 2; opts[HASHDIRS] = 2; opts[USEZLE] = 1;`
2336 crate::ported::options::opt_state_set("monitor", true);
2337 crate::ported::options::opt_state_set("hashdirs", true);
2338 // USEZLE's canonical name is `zle` (zsh_h.rs:4145); the prior
2339 // `"usezle"` key was never read by `isset(USEZLE)` → this default was a
2340 // silent no-op. init_io (c:691-694) downgrades it for non-tty shells.
2341 crate::ported::options::opt_state_set("zle", true); // c:368 opts[USEZLE]=1
2342 // c:369-370 — `opts[SHINSTDIN] = 0; opts[SINGLECOMMAND] = 0;`
2343 crate::ported::options::opt_state_set("shinstdin", false);
2344 crate::ported::options::opt_state_set("singlecommand", false);
2345}
2346
2347#[cfg(test)]
2348mod tests {
2349 use super::*;
2350
2351 /// source() pushes an FS_SOURCE funcstack frame (init.c:1610-1618)
2352 /// and pops it on exit (c:1664). Verify the push is balanced (no
2353 /// leak) and that a readable file returns SOURCE_OK.
2354 #[test]
2355 fn source_funcstack_push_is_balanced() {
2356 use crate::ported::modules::parameter::FUNCSTACK;
2357 let depth_before = FUNCSTACK.lock().unwrap().len();
2358
2359 let mut path = std::env::temp_dir();
2360 path.push(format!("zshrs_source_balance_{}.zsh", std::process::id()));
2361 std::fs::write(&path, ": # no-op sourced file\n").unwrap();
2362
2363 let rc = source(path.to_str().unwrap());
2364
2365 let depth_after = FUNCSTACK.lock().unwrap().len();
2366 std::fs::remove_file(&path).ok();
2367
2368 assert_eq!(rc, 0, "source of a readable file returns SOURCE_OK");
2369 assert_eq!(
2370 depth_before, depth_after,
2371 "FS_SOURCE push must be matched by a pop (no funcstack leak)"
2372 );
2373
2374 // A non-existent file is SOURCE_NOT_FOUND and pushes nothing.
2375 let missing = "/nonexistent/zshrs_source_xyz_should_not_exist";
2376 assert_ne!(source(missing), 0, "missing file returns SOURCE_NOT_FOUND");
2377 assert_eq!(
2378 FUNCSTACK.lock().unwrap().len(),
2379 depth_before,
2380 "NOT_FOUND path must not touch the funcstack"
2381 );
2382 }
2383
2384 /// `fallback_compctlread` is the dispatch target when the zle
2385 /// module isn't loaded — `compctl -K read` paths need SOMETHING
2386 /// callable. C returns 1 + zwarnnam to signal "no provider". A
2387 /// regression returning 0 would make completers believe a read
2388 /// happened when nothing did (silent data corruption in completion).
2389 #[test]
2390 fn fallback_compctlread_signals_failure() {
2391 let _g = crate::test_util::global_state_lock();
2392 assert_eq!(fallback_compctlread("test"), 1);
2393 }
2394
2395 /// c:418 — `parseopts` advances `idx` past every consumed option.
2396 /// `-c CMD` consumes 2 slots and stores CMD in cmdp. Regression
2397 /// that doesn't advance idx would loop forever or skip args.
2398 #[test]
2399 fn parseopts_dash_c_captures_command_string() {
2400 let _g = crate::test_util::global_state_lock();
2401 let mut argv = vec!["-c".to_string(), "echo hi".to_string()];
2402 let mut idx = 0usize;
2403 let mut cmd: Option<String> = None;
2404 let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2405 assert_eq!(r, 0);
2406 assert_eq!(
2407 cmd.as_deref(),
2408 Some("echo hi"),
2409 "-c must capture the next arg as the command"
2410 );
2411 assert_eq!(idx, 2, "idx must advance past both -c AND its arg");
2412 }
2413
2414 /// c:418 — `parseopts` stops at the first non-option positional.
2415 /// Regression that keeps walking past `--` or first bareword would
2416 /// silently consume the script name as an option.
2417 #[test]
2418 fn parseopts_stops_at_first_positional() {
2419 let _g = crate::test_util::global_state_lock();
2420 let mut argv = vec!["script.sh".to_string(), "arg1".to_string()];
2421 let mut idx = 0usize;
2422 let mut cmd: Option<String> = None;
2423 parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2424 assert_eq!(idx, 0, "must stop AT the first bareword (no advance)");
2425 assert!(cmd.is_none(), "no -c → cmd stays None");
2426 }
2427
2428 /// c:418 — `parseopts` on empty argv exits cleanly with idx=0
2429 /// and cmd=None. Regression panicking on empty would crash
2430 /// startup with weird args.
2431 #[test]
2432 fn parseopts_empty_argv_is_safe() {
2433 let _g = crate::test_util::global_state_lock();
2434 let mut argv: Vec<String> = Vec::new();
2435 let mut idx = 0usize;
2436 let mut cmd: Option<String> = None;
2437 assert_eq!(parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0), 0);
2438 assert_eq!(idx, 0);
2439 assert!(cmd.is_none());
2440 }
2441
2442 /// c:756 — `tccap_get_name` indexes into the terminal-capability
2443 /// name array. Out-of-range MUST NOT panic — it returns "" so
2444 /// downstream tigetstr calls fail-soft. Regression panicking
2445 /// would crash the prompt subsystem on tiny terminals.
2446 #[test]
2447 fn tccap_get_name_out_of_range_returns_empty() {
2448 let _g = crate::test_util::global_state_lock();
2449 // index 9999 is well past the cap table (~39 entries per
2450 // init.c:75). Must return safely.
2451 let n = tccap_get_name(9999);
2452 assert!(
2453 n.is_empty(),
2454 "out-of-range index must return empty (got {n:?})"
2455 );
2456 }
2457
2458 /// c:418 — `parseopts` consumes `-x` (xtrace) as a flag without
2459 /// an argument. Pin the no-arg-required path; a regression that
2460 /// always reads `argv[idx+1]` would OOB-index when -x is the
2461 /// last arg.
2462 #[test]
2463 fn parseopts_dash_x_is_single_slot_flag() {
2464 let _g = crate::test_util::global_state_lock();
2465 let mut argv = vec!["-x".to_string()];
2466 let mut idx = 0usize;
2467 let mut cmd: Option<String> = None;
2468 let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2469 assert_eq!(r, 0);
2470 assert_eq!(idx, 1, "-x consumes exactly 1 arg slot");
2471 assert!(cmd.is_none(), "-x must NOT capture a command");
2472 }
2473
2474 /// c:418 — `parseopts` -c with NO following argument. The
2475 /// contract is "no valid command captured" so the caller can
2476 /// surface a usage error.
2477 #[test]
2478 fn parseopts_dash_c_without_argument_does_not_capture_command() {
2479 let _g = crate::test_util::global_state_lock();
2480 let mut argv = vec!["-c".to_string()];
2481 let mut idx = 0usize;
2482 let mut cmd: Option<String> = None;
2483 let _ = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2484 assert!(
2485 cmd.is_none() || cmd.as_deref() == Some(""),
2486 "-c without arg must NOT capture a usable command"
2487 );
2488 }
2489
2490 /// c:418 — `parseopts` consumes multiple flags in sequence.
2491 /// Pin the per-arg loop so a regen that early-exits after the
2492 /// first flag silently drops -v.
2493 #[test]
2494 fn parseopts_consumes_multiple_flags() {
2495 let _g = crate::test_util::global_state_lock();
2496 let mut argv = vec!["-x".to_string(), "-v".to_string()];
2497 let mut idx = 0usize;
2498 let mut cmd: Option<String> = None;
2499 let r = parseopts("zsh", &mut argv, &mut idx, &mut cmd, 0);
2500 assert_eq!(r, 0);
2501 assert_eq!(idx, 2, "both flags must be consumed");
2502 }
2503
2504 /// c:756 — `tccap_get_name(0)` returns the first cap name. The
2505 /// table is non-empty per init.c:75; index 0 must be a valid
2506 /// stem. Pin the boundary so a regen that adds an "off-by-one
2507 /// slot 0" mistake gets caught.
2508 #[test]
2509 fn tccap_get_name_index_zero_is_nonempty() {
2510 let _g = crate::test_util::global_state_lock();
2511 let n = tccap_get_name(0);
2512 assert!(
2513 !n.is_empty(),
2514 "index 0 must yield a real cap name; got {:?}",
2515 n
2516 );
2517 for c in n.chars() {
2518 assert!(
2519 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_',
2520 "cap name {:?} contains non-termcap char {:?}",
2521 n,
2522 c
2523 );
2524 }
2525 }
2526
2527 /// c:1713 — `noop_function` MUST be safe to call without panics.
2528 /// C body is empty; the Rust port mirrors it. Re-entry test
2529 /// catches a regen that adds stateful side-effects.
2530 #[test]
2531 fn noop_function_is_safe_to_call_multiple_times() {
2532 let _g = crate::test_util::global_state_lock();
2533 noop_function();
2534 noop_function();
2535 noop_function();
2536 }
2537
2538 /// c:1720 — `noop_function_int(n)` ignores its argument across
2539 /// the full i32 range. Pin no-side-effect contract.
2540 #[test]
2541 fn noop_function_int_ignores_argument() {
2542 let _g = crate::test_util::global_state_lock();
2543 noop_function_int(0);
2544 noop_function_int(42);
2545 noop_function_int(-1);
2546 noop_function_int(i32::MAX);
2547 noop_function_int(i32::MIN);
2548 }
2549
2550 /// c:1743 — `zleentry(cmd)` for an unknown cmd code returns
2551 /// None (matching the C "no zle" branch). Every call site
2552 /// assumes None means "no ZLE active".
2553 #[test]
2554 fn zleentry_unknown_command_returns_none() {
2555 let _g = crate::test_util::global_state_lock();
2556 assert!(
2557 zleentry(99999).is_none(),
2558 "unknown zle command must return None, not a default string"
2559 );
2560 }
2561
2562 /// c:1551 — `source("")` on empty path must NOT segfault. The
2563 /// C source opens("") which fails with ENOENT, returning non-0.
2564 #[test]
2565 fn source_empty_path_returns_nonzero() {
2566 let _g = crate::test_util::global_state_lock();
2567 let r = source("");
2568 assert_ne!(r, 0, "source of empty path must report failure");
2569 }
2570
2571 // ─── zsh-corpus pins for init helpers ───────────────────────────
2572
2573 /// `tccap_get_name(0)` returns a non-empty cap name.
2574 #[test]
2575 fn init_corpus_tccap_index_zero_nonempty() {
2576 let _g = crate::test_util::global_state_lock();
2577 let name = tccap_get_name(0);
2578 assert!(!name.is_empty(), "cap[0] has a name, got {name:?}");
2579 }
2580
2581 /// `tccap_get_name` for index TC_COUNT or higher returns "".
2582 #[test]
2583 fn init_corpus_tccap_out_of_range_returns_empty() {
2584 let _g = crate::test_util::global_state_lock();
2585 assert_eq!(tccap_get_name(39), "", "TC_COUNT = empty");
2586 assert_eq!(tccap_get_name(999), "", "way out of range = empty");
2587 }
2588
2589 /// All cap indexes 0..TC_COUNT return non-empty distinct names.
2590 #[test]
2591 fn init_corpus_tccap_indexes_all_named() {
2592 let _g = crate::test_util::global_state_lock();
2593 for i in 0..39 {
2594 let n = tccap_get_name(i);
2595 assert!(!n.is_empty(), "cap {i} should have a name");
2596 }
2597 }
2598
2599 /// `source("/nonexistent/path/zshrs_test_xyz")` returns non-zero.
2600 #[test]
2601 fn init_corpus_source_nonexistent_returns_nonzero() {
2602 let _g = crate::test_util::global_state_lock();
2603 assert_ne!(source("/nonexistent/path/zshrs_test_xyz_zzz"), 0);
2604 }
2605
2606 /// `noop_function()` doesn't panic.
2607 #[test]
2608 fn init_corpus_noop_function_does_not_panic() {
2609 let _g = crate::test_util::global_state_lock();
2610 noop_function();
2611 }
2612
2613 /// `noop_function_int(0)` doesn't panic.
2614 #[test]
2615 fn init_corpus_noop_function_int_zero_no_panic() {
2616 let _g = crate::test_util::global_state_lock();
2617 noop_function_int(0);
2618 }
2619
2620 /// `zleentry` with negative cmd code returns None.
2621 #[test]
2622 fn init_corpus_zleentry_negative_returns_none() {
2623 let _g = crate::test_util::global_state_lock();
2624 assert!(zleentry(-1).is_none());
2625 }
2626
2627 // ═══════════════════════════════════════════════════════════════════
2628 // Additional C-parity tests for Src/init.c tccap_get_name / init_term
2629 // / noop helpers.
2630 // ═══════════════════════════════════════════════════════════════════
2631
2632 /// c:756 — `tccap_get_name(cap)` for cap >= 39 (TC_COUNT) returns
2633 /// empty string (Rust port out-of-range guard, c:771).
2634 #[test]
2635 fn tccap_get_name_out_of_range_returns_empty_pin() {
2636 let _g = crate::test_util::global_state_lock();
2637 assert_eq!(tccap_get_name(39), "", "TC_COUNT boundary returns ''");
2638 assert_eq!(tccap_get_name(100), "");
2639 assert_eq!(tccap_get_name(usize::MAX), "");
2640 }
2641
2642 /// c:756 — `tccap_get_name(0)` returns a non-empty cap name
2643 /// (first entry of tccapnams table).
2644 #[test]
2645 fn tccap_get_name_first_entry_is_nonempty() {
2646 let _g = crate::test_util::global_state_lock();
2647 let n = tccap_get_name(0);
2648 assert!(!n.is_empty(), "tccapnams[0] must be a valid cap name");
2649 }
2650
2651 /// c:756 — `tccap_get_name` is deterministic across calls.
2652 #[test]
2653 fn tccap_get_name_is_deterministic() {
2654 let _g = crate::test_util::global_state_lock();
2655 for i in 0..39 {
2656 let first = tccap_get_name(i);
2657 for _ in 0..5 {
2658 assert_eq!(tccap_get_name(i), first, "tccap_get_name({}) impure", i);
2659 }
2660 }
2661 }
2662
2663 /// c:756 — every in-range cap name fits in ASCII and has
2664 /// non-control content (termcap caps are 2-char ASCII codes).
2665 #[test]
2666 fn tccap_get_name_in_range_returns_printable_ascii() {
2667 let _g = crate::test_util::global_state_lock();
2668 for i in 0..39 {
2669 let n = tccap_get_name(i);
2670 if n.is_empty() {
2671 continue;
2672 }
2673 for c in n.chars() {
2674 assert!(
2675 c.is_ascii() && !c.is_control(),
2676 "tccap_get_name({}) = {:?} has non-printable char {:?}",
2677 i,
2678 n,
2679 c
2680 );
2681 }
2682 }
2683 }
2684
2685 /// c:1713 — `noop_function` returns nothing, has no observable effect.
2686 /// Repeated calls don't accumulate state.
2687 #[test]
2688 fn noop_function_is_idempotent() {
2689 let _g = crate::test_util::global_state_lock();
2690 for _ in 0..100 {
2691 noop_function();
2692 }
2693 }
2694
2695 /// c:1720 — `noop_function_int` accepts any i32 value without panic.
2696 #[test]
2697 fn noop_function_int_accepts_any_value() {
2698 let _g = crate::test_util::global_state_lock();
2699 noop_function_int(0);
2700 noop_function_int(-1);
2701 noop_function_int(i32::MAX);
2702 noop_function_int(i32::MIN);
2703 }
2704
2705 /// `zleentry` with very large positive cmd code returns None
2706 /// (out-of-range cmd dispatch).
2707 #[test]
2708 fn zleentry_unknown_cmd_returns_none() {
2709 let _g = crate::test_util::global_state_lock();
2710 assert!(zleentry(99999).is_none(), "unknown cmd → None");
2711 }
2712
2713 /// c:1209 — `init_bltinmods` is idempotent (safe to call multiple times).
2714 #[test]
2715 fn init_bltinmods_is_idempotent() {
2716 let _g = crate::test_util::global_state_lock();
2717 init_bltinmods();
2718 init_bltinmods();
2719 }
2720
2721 // ═══════════════════════════════════════════════════════════════════
2722 // Additional C-parity tests for Src/init.c
2723 // c:747 tccapnams / c:756 tccap_get_name / c:1713 noop_function /
2724 // c:1199 fallback_compctlread.
2725 // ═══════════════════════════════════════════════════════════════════
2726
2727 /// c:747 — `tccapnams` table has exactly 39 entries (TC_COUNT).
2728 #[test]
2729 fn tccapnams_table_size_is_tc_count() {
2730 assert_eq!(tccapnams.len(), 39, "TC_COUNT must be 39");
2731 }
2732
2733 /// c:747 — every tccapnams entry is exactly 2 ASCII chars
2734 /// (termcap 2-letter capability code convention).
2735 #[test]
2736 fn tccapnams_entries_are_two_ascii_chars() {
2737 for (i, &n) in tccapnams.iter().enumerate() {
2738 assert_eq!(n.len(), 2, "tccapnams[{}] = {:?} must be 2 chars", i, n);
2739 assert!(
2740 n.chars().all(|c| c.is_ascii_alphabetic()),
2741 "tccapnams[{}] = {:?} must be ASCII alpha",
2742 i,
2743 n
2744 );
2745 }
2746 }
2747
2748 /// c:747 — no duplicates in tccapnams.
2749 #[test]
2750 fn tccapnams_has_no_duplicates() {
2751 let mut seen = std::collections::HashSet::new();
2752 for &n in tccapnams.iter() {
2753 assert!(seen.insert(n), "duplicate tccap name: {:?}", n);
2754 }
2755 }
2756
2757 /// c:756 — `tccap_get_name(39)` is at TC_COUNT boundary → empty.
2758 #[test]
2759 fn tccap_get_name_at_tc_count_boundary_returns_empty() {
2760 assert_eq!(tccap_get_name(39), "", "TC_COUNT bound returns empty");
2761 }
2762
2763 /// c:756 — `tccap_get_name(38)` is last valid index → non-empty.
2764 #[test]
2765 fn tccap_get_name_last_valid_index_returns_nonempty() {
2766 let n = tccap_get_name(38);
2767 assert!(!n.is_empty(), "tccap_get_name(38) must be non-empty");
2768 assert_eq!(n, tccapnams[38], "matches table");
2769 }
2770
2771 /// c:756 — `tccap_get_name(usize::MAX)` is far out of range → empty.
2772 #[test]
2773 fn tccap_get_name_usize_max_returns_empty() {
2774 assert_eq!(tccap_get_name(usize::MAX), "");
2775 }
2776
2777 /// c:756 — `tccap_get_name` is a pure function.
2778 #[test]
2779 fn tccap_get_name_is_pure() {
2780 for i in [0usize, 1, 5, 10, 38, 39, 100] {
2781 let a = tccap_get_name(i);
2782 let b = tccap_get_name(i);
2783 assert_eq!(a, b, "tccap_get_name({}) must be deterministic", i);
2784 }
2785 }
2786
2787 /// c:1199 — `fallback_compctlread` returns 1 (failure) without args.
2788 #[test]
2789 fn fallback_compctlread_returns_one_pin() {
2790 let _g = crate::test_util::global_state_lock();
2791 assert_eq!(fallback_compctlread(""), 1, "fallback always fails");
2792 assert_eq!(fallback_compctlread("anything"), 1);
2793 }
2794
2795 /// c:1713 — `noop_function` is a true no-op (no panic, no state change).
2796 #[test]
2797 fn noop_function_does_nothing_observable() {
2798 for _ in 0..100 {
2799 noop_function();
2800 }
2801 }
2802
2803 /// c:1720 — `noop_function_int` accepts any i32 without panic.
2804 #[test]
2805 fn noop_function_int_accepts_full_i32_range() {
2806 for v in [i32::MIN, -1, 0, 1, 42, i32::MAX] {
2807 noop_function_int(v);
2808 }
2809 }
2810
2811 /// c:1159 — `zleentry(0)` returns None (no live ZLE for cmd=0).
2812 #[test]
2813 fn zleentry_zero_cmd_returns_none() {
2814 let _g = crate::test_util::global_state_lock();
2815 assert_eq!(zleentry(0), None, "cmd=0 unwired without live ZLE");
2816 }
2817
2818 // ═══════════════════════════════════════════════════════════════════
2819 // Additional C-parity tests for Src/init.c
2820 // c:464 tccap_get_name / c:1081 source / c:1116 sourcehome /
2821 // c:1159 zleentry / c:1199 fallback_compctlread / c:1149 noop_function /
2822 // c:1154 noop_function_int / c:1143 init_bltinmods
2823 // ═══════════════════════════════════════════════════════════════════
2824
2825 /// c:464 — `tccap_get_name` returns &'static str (compile-time type pin).
2826 #[test]
2827 fn tccap_get_name_returns_static_str_type() {
2828 let _: &'static str = tccap_get_name(0);
2829 }
2830
2831 /// c:1081 — `source("")` empty path returns i32 (compile-time pin).
2832 #[test]
2833 fn source_returns_i32_type() {
2834 let _g = crate::test_util::global_state_lock();
2835 let _: i32 = source("");
2836 }
2837
2838 /// c:1081 — `source` is deterministic for the same nonexistent path.
2839 #[test]
2840 fn source_nonexistent_is_deterministic() {
2841 let _g = crate::test_util::global_state_lock();
2842 let first = source("/__never_exists_zshrs_source__");
2843 for _ in 0..3 {
2844 assert_eq!(
2845 source("/__never_exists_zshrs_source__"),
2846 first,
2847 "source must be deterministic"
2848 );
2849 }
2850 }
2851
2852 /// c:1116 — `sourcehome("")` empty arg is safe.
2853 #[test]
2854 fn sourcehome_empty_no_panic() {
2855 let _g = crate::test_util::global_state_lock();
2856 sourcehome("");
2857 }
2858
2859 /// c:1159 — `zleentry` returns Option<String> (compile-time type pin).
2860 #[test]
2861 fn zleentry_returns_option_string_type() {
2862 let _g = crate::test_util::global_state_lock();
2863 let _: Option<String> = zleentry(0);
2864 }
2865
2866 /// c:1199 — `fallback_compctlread` returns i32.
2867 #[test]
2868 fn fallback_compctlread_returns_i32_type() {
2869 let _g = crate::test_util::global_state_lock();
2870 let _: i32 = fallback_compctlread("");
2871 }
2872
2873 /// c:1199 — `fallback_compctlread` is deterministic for any name.
2874 #[test]
2875 fn fallback_compctlread_is_deterministic() {
2876 let _g = crate::test_util::global_state_lock();
2877 for n in ["", "name", "anything"] {
2878 let first = fallback_compctlread(n);
2879 for _ in 0..3 {
2880 assert_eq!(
2881 fallback_compctlread(n),
2882 first,
2883 "fallback_compctlread({:?}) must be deterministic",
2884 n
2885 );
2886 }
2887 }
2888 }
2889
2890 /// c:1143 — `init_bltinmods` returns void (no panic check).
2891 #[test]
2892 fn init_bltinmods_no_panic_full_sweep() {
2893 let _g = crate::test_util::global_state_lock();
2894 for _ in 0..5 {
2895 init_bltinmods();
2896 }
2897 }
2898
2899 /// c:1149 — `noop_function` returns void (compile-time void type).
2900 #[test]
2901 fn noop_function_signature_void() {
2902 let _: () = noop_function();
2903 }
2904
2905 /// c:1154 — `noop_function_int(N)` returns void.
2906 #[test]
2907 fn noop_function_int_signature_void() {
2908 let _: () = noop_function_int(0);
2909 }
2910
2911 /// c:1116 — `sourcehome("/")` root dir safe.
2912 #[test]
2913 fn sourcehome_root_dir_no_panic() {
2914 let _g = crate::test_util::global_state_lock();
2915 sourcehome("/");
2916 }
2917
2918 // ═══════════════════════════════════════════════════════════════════
2919 // Additional C-parity tests for Src/init.c
2920 // c:464 tccap_get_name / c:491 init_term / c:1081 source /
2921 // c:1159 zleentry / c:1199 fallback_compctlread + lifecycle pins
2922 // ═══════════════════════════════════════════════════════════════════
2923
2924 /// c:464 — `tccap_get_name` returns `&'static str` (compile-time pin, alt).
2925 #[test]
2926 fn tccap_get_name_returns_static_str_pin_alt() {
2927 let _: &'static str = tccap_get_name(0);
2928 }
2929
2930 /// c:464 — `tccap_get_name(TC_COUNT)` and beyond return empty.
2931 /// C source: `if (cap >= 39 /* TC_COUNT */) return "";`.
2932 #[test]
2933 fn tccap_get_name_oob_returns_empty() {
2934 assert_eq!(tccap_get_name(39), "", "TC_COUNT itself returns empty");
2935 assert_eq!(tccap_get_name(100), "", "way past TC_COUNT returns empty");
2936 assert_eq!(
2937 tccap_get_name(usize::MAX),
2938 "",
2939 "MAX returns empty (no panic)"
2940 );
2941 }
2942
2943 /// c:464 — `tccap_get_name` is deterministic for in-range indices.
2944 #[test]
2945 fn tccap_get_name_deterministic_for_valid_indices() {
2946 for cap in 0..39usize {
2947 let first = tccap_get_name(cap);
2948 for _ in 0..3 {
2949 assert_eq!(
2950 tccap_get_name(cap),
2951 first,
2952 "tccap_get_name({}) must be pure",
2953 cap
2954 );
2955 }
2956 }
2957 }
2958
2959 /// c:464 — every in-range cap returns non-empty (zsh ships a name
2960 /// for every TC_* slot; an empty entry would be a corpus drift).
2961 #[test]
2962 fn tccap_get_name_all_in_range_caps_non_empty() {
2963 for cap in 0..39usize {
2964 assert!(
2965 !tccap_get_name(cap).is_empty(),
2966 "TC_{} (cap idx {}) must have a non-empty cap name",
2967 cap,
2968 cap
2969 );
2970 }
2971 }
2972
2973 /// c:491 — `init_term` returns i32 (compile-time pin).
2974 #[test]
2975 fn init_term_returns_i32_type() {
2976 let _g = crate::test_util::global_state_lock();
2977 let _: i32 = init_term();
2978 }
2979
2980 /// c:1081 — `source(empty)` returns i32 (compile-time pin, alt).
2981 #[test]
2982 fn source_returns_i32_pin_alt() {
2983 let _g = crate::test_util::global_state_lock();
2984 let _: i32 = source("");
2985 }
2986
2987 /// c:1081 — `source("/__nonexistent_xyz__")` is non-fatal (returns).
2988 #[test]
2989 fn source_nonexistent_file_no_panic() {
2990 let _g = crate::test_util::global_state_lock();
2991 let _ = source("/__definitely_not_a_file_zshrs_xyz__");
2992 }
2993
2994 /// c:1159 — `zleentry` is deterministic (pure dispatch).
2995 #[test]
2996 fn zleentry_is_deterministic() {
2997 let _g = crate::test_util::global_state_lock();
2998 for cmd in 0..5i32 {
2999 let first = zleentry(cmd);
3000 for _ in 0..3 {
3001 assert_eq!(
3002 zleentry(cmd),
3003 first,
3004 "zleentry({}) must be deterministic",
3005 cmd
3006 );
3007 }
3008 }
3009 }
3010
3011 /// c:1199 — `fallback_compctlread("")` is safe (empty name no-op).
3012 #[test]
3013 fn fallback_compctlread_empty_name_safe() {
3014 let _g = crate::test_util::global_state_lock();
3015 let _ = fallback_compctlread("");
3016 }
3017
3018 /// c:1149 — `noop_function` called repeatedly is safe.
3019 #[test]
3020 fn noop_function_repeated_calls_safe() {
3021 for _ in 0..100 {
3022 noop_function();
3023 }
3024 }
3025
3026 /// c:1154 — `noop_function_int` for various ints is safe + void.
3027 #[test]
3028 fn noop_function_int_various_inputs_safe() {
3029 for n in [-1, 0, 1, i32::MIN, i32::MAX] {
3030 let _: () = noop_function_int(n);
3031 }
3032 }
3033
3034 /// c:1116 — `sourcehome("nonexistent_file_xyz")` safe (no-op when
3035 /// the home-relative path doesn't exist).
3036 #[test]
3037 fn sourcehome_nonexistent_file_safe() {
3038 let _g = crate::test_util::global_state_lock();
3039 sourcehome("__definitely_no_such_zshrc_xyz__");
3040 }
3041
3042 // ═══════════════════════════════════════════════════════════════════
3043 // Additional C-parity pins for Src/init.c
3044 // c:464 tccap_get_name / c:491 init_term / c:1081 source /
3045 // c:1116 sourcehome / c:1143 init_bltinmods / c:1149 noop_function /
3046 // c:1159 zleentry / c:1199 fallback_compctlread
3047 // ═══════════════════════════════════════════════════════════════════
3048
3049 /// c:1143 — `init_bltinmods` is idempotent (safe to call repeatedly).
3050 #[test]
3051 fn init_bltinmods_idempotent_repeated_calls() {
3052 let _g = crate::test_util::global_state_lock();
3053 for _ in 0..10 {
3054 init_bltinmods();
3055 }
3056 }
3057
3058 /// c:464 — `tccap_get_name` for index 0 returns non-empty (first cap).
3059 #[test]
3060 fn tccap_get_name_index_zero_non_empty() {
3061 let _g = crate::test_util::global_state_lock();
3062 let _ = tccap_get_name(0);
3063 }
3064
3065 /// c:464 — `tccap_get_name(usize::MAX)` doesn't panic.
3066 #[test]
3067 fn tccap_get_name_usize_max_no_panic() {
3068 let _g = crate::test_util::global_state_lock();
3069 let r = tccap_get_name(usize::MAX);
3070 // OOB returns empty per the OOB test that already exists.
3071 assert!(r.is_empty(), "OOB must return empty, got {:?}", r);
3072 }
3073
3074 /// c:1116 — `sourcehome("")` empty name safe (no-op when path empty).
3075 #[test]
3076 fn sourcehome_empty_name_no_panic() {
3077 let _g = crate::test_util::global_state_lock();
3078 sourcehome("");
3079 }
3080
3081 /// c:1081 — `source` return type i32 (compile-time pin).
3082 #[test]
3083 fn source_returns_i32_type_compile_pin() {
3084 let _g = crate::test_util::global_state_lock();
3085 let _: i32 = source("/dev/null");
3086 }
3087
3088 /// c:1081 — `source("")` empty path doesn't panic.
3089 #[test]
3090 fn source_empty_path_no_panic() {
3091 let _g = crate::test_util::global_state_lock();
3092 let _ = source("");
3093 }
3094
3095 /// c:1159 — `zleentry` return type Option<String> (compile-time pin, alt).
3096 #[test]
3097 fn zleentry_returns_option_string_type_alt() {
3098 let _g = crate::test_util::global_state_lock();
3099 let _: Option<String> = zleentry(0);
3100 }
3101
3102 /// c:1159 — `zleentry(-1)` (invalid cmd) doesn't panic.
3103 #[test]
3104 fn zleentry_negative_cmd_no_panic() {
3105 let _g = crate::test_util::global_state_lock();
3106 let _ = zleentry(-1);
3107 }
3108
3109 /// c:1199 — `fallback_compctlread` returns i32 (compile-time pin, alt).
3110 #[test]
3111 fn fallback_compctlread_returns_i32_type_alt() {
3112 let _g = crate::test_util::global_state_lock();
3113 let _: i32 = fallback_compctlread("test");
3114 }
3115
3116 /// c:1149 — `noop_function` returns void (compile-time pin).
3117 #[test]
3118 fn noop_function_returns_void_type() {
3119 let _g = crate::test_util::global_state_lock();
3120 let _: () = noop_function();
3121 }
3122
3123 /// c:1154 — `noop_function_int` returns void (compile-time pin).
3124 #[test]
3125 fn noop_function_int_returns_void_type() {
3126 let _g = crate::test_util::global_state_lock();
3127 let _: () = noop_function_int(42);
3128 }
3129
3130 /// c:464 — `tccap_get_name` is deterministic for OOB (same empty value
3131 /// across calls).
3132 #[test]
3133 fn tccap_get_name_oob_deterministic() {
3134 let _g = crate::test_util::global_state_lock();
3135 let a = tccap_get_name(99_999);
3136 let b = tccap_get_name(99_999);
3137 assert_eq!(a, b, "OOB must be deterministic");
3138 }
3139}