zsh/ported/input.rs
1//! Input buffering and stack management for zshrs
2//!
3//! Direct port from zsh/Src/input.c
4//!
5//! the shell input fd // c:78
6//! total # of characters waiting to be read // c:88
7//! the flags controlling the input routines in input.c // c:93
8//! Reset the input buffer for SHIN, discarding any pending input // c:155
9//! stuff a whole file into memory and return it // c:610
10//! flush input queue // c:661
11//!
12//! This module handles:
13//! - Reading input from files, strings, and the line editor
14//! - Input stack for alias expansion and history substitution
15//! - Character-by-character input with push-back support
16//! - Meta-character encoding for internal tokens
17
18use crate::ported::hashtable::aliastab_lock;
19use crate::ported::hist::histbackword;
20use crate::ported::lex::{zshlex_raw_back, LEX_LEXSTOP};
21use crate::ported::signals_h::{queue_signals, unqueue_signals};
22use crate::ported::utils::{unmetafy, zerr};
23use crate::ported::zsh_h::{
24 isset, Meta, INP_ALCONT, INP_ALIAS, INP_CONT, INP_FREE, INP_HIST, INP_HISTCONT, INP_LINENO,
25 INP_RAW_KEEP, SHINSTDIN, VERBOSE,
26};
27use crate::ported::ztype_h::itok;
28use std::cell::RefCell;
29use std::collections::VecDeque;
30use std::io::{self, BufRead, BufReader, Read, Write};
31
32/// Port of `struct instacks` from `Src/input.c:109`. One frame in
33/// the input stack — pushed by `inpush()` and popped by `inpoptop()`
34/// to layer alias expansion / history-substitution / `eval`
35/// continuations over the active input.
36#[derive(Clone, Default)]
37#[allow(non_camel_case_types)]
38struct instacks {
39 // c:109
40 buf: String, // c:110 char *buf
41 bufpos: usize, // c:110 char *bufptr offset
42 bufct: i32, // c:112 int bufct — inbufct AT PUSH TIME (spans lower CONT frames)
43 flags: i32, // c:112 int flags
44 alias: Option<(String, i32)>, // c:111 Alias alias — (an->node.nam, an->node.flags)
45 /// !!! RUST-ONLY !!! — the `INBUF_SERIAL` of the frame this entry
46 /// saves, restored by `inpoptop`. See `lex::LEX_UNGET_FRAME`.
47 serial: u64,
48}
49
50/// Initial input stack size
51#[allow(dead_code)]
52const INSTACK_INITIAL: usize = 4; // c:122
53
54// `pub mod flags { … INP_* … }` deleted — Rust-only namespace with
55// values that diverged from the C `#define INP_FREE (1<<0)` etc. at
56// Src/zsh.h:467-476. The canonical mirror lives in
57// `crate::ported::zsh_h::INP_*` (matching the C bit positions
58// exactly); this file uses those constants directly.
59
60// ---------------------------------------------------------------------------
61// SHIN buffer helpers — direct ports of input.c:159/171/181/200/218/267.
62// ---------------------------------------------------------------------------
63
64/// Reset the SHIN pushback buffer.
65/// Port of `shinbufreset()` from Src/input.c:159 —
66/// `shinbufendptr = shinbufptr = shinbuffer`.
67pub fn shinbufreset() {
68 // c:159
69 shinbuffer.with(|b| b.borrow_mut().clear());
70 shinbufpos.with(|p| p.set(0));
71}
72
73// ---------------------------------------------------------------------------
74// File-scope mirrors of `Src/input.c` globals. Per-thread because each
75// worker parses independently; the C source's process-global model
76// doesn't translate directly to zshrs's parallel pipeline.
77// ---------------------------------------------------------------------------
78
79thread_local! {
80 /// Port of `int SHIN` from `Src/input.c:81`. Shell input fd
81 /// (typically 0 for stdin).
82 #[allow(non_upper_case_globals)]
83 pub static SHIN: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
84
85 /// Port of `int strin` from `Src/input.c:86`. Non-zero while
86 /// reading from a string (via `inpush` with INP_ALIAS/INP_HIST
87 /// or by `bin_eval`); short-circuits `read(2)` fallback.
88 #[allow(non_upper_case_globals)]
89 pub static strin: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
90
91 /// Port of `mod_export int inbufct` from `Src/input.c:91`.
92 /// Total characters waiting to be read across `inbuf` +
93 /// `instack` entries.
94 #[allow(non_upper_case_globals)]
95 pub static inbufct: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
96
97 /// Port of `int inbufflags` from `Src/input.c:96`. Bit-mask of
98 /// the `INP_*` flags governing the current input level.
99 #[allow(non_upper_case_globals)]
100 pub static inbufflags: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
101
102 /// Port of `static char *inbuf` from `Src/input.c:98`. Current
103 /// input buffer.
104 #[allow(non_upper_case_globals)]
105 static inbuf: RefCell<String> = const { RefCell::new(String::new()) };
106
107 /// Port of `static char *inbufptr` from `Src/input.c:99`. Offset
108 /// into `inbuf` where the next char will be read.
109 #[allow(non_upper_case_globals)]
110 static inbufpos: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
111
112 /// Byte-offset cache for `inbuf`/`inbufpos`. `inbufpos` is a CHAR
113 /// index; naively resolving it with `inbuf.chars().nth(pos)` is
114 /// O(pos), making a sequential scan of an N-char buffer O(N²) (a
115 /// 31 KB p10k function body took tens of seconds to parse). This
116 /// caches the byte offset of char index `INBUF_BYTE_POS`; a
117 /// sequential read (`pos == INBUF_BYTE_POS`) resolves in O(1).
118 /// Self-healing: on any mismatch (buffer swap, pushback reposition)
119 /// the offset is recomputed once via `char_indices().nth(pos)`.
120 /// `(0, 0)` is always valid — every `inbufpos` reset sets pos 0,
121 /// whose byte offset is 0 in any buffer.
122 #[allow(non_upper_case_globals)]
123 static inbuf_byte_char: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
124 #[allow(non_upper_case_globals)]
125 static inbuf_byte_off: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
126
127 /// Input stack — port of `static struct instacks *instack` from
128 /// `Src/input.c:114`. The instacktop pointer in C maps to the
129 /// Vec's length here.
130 static instack: RefCell<Vec<instacks>> = const { RefCell::new(Vec::new()) };
131
132 /// !!! WARNING: RUST-ONLY HELPER STATE !!! — identity of the current
133 /// `inbuf` frame (0 = none pushed yet). C identifies a frame by its
134 /// `inbuf` pointer; the lexer's pushback queue (`lex::LEX_UNGET_FRAME`)
135 /// records this so `inpoptop` can tell which queued characters were
136 /// read from the frame it pops.
137 #[allow(non_upper_case_globals)]
138 pub static INBUF_SERIAL: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
139 /// !!! WARNING: RUST-ONLY HELPER STATE !!! — next `INBUF_SERIAL` to
140 /// hand out; serials only grow, so a frame pushed later always has the
141 /// larger one.
142 #[allow(non_upper_case_globals)]
143 static INBUF_SERIAL_NEXT: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
144
145 /// `lexstop` — set when the lexer should stop pulling chars.
146 /// C mirrors this in zsh.h as an extern; per-thread here.
147 #[allow(non_upper_case_globals)]
148 pub static lexstop: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
149
150 /// Current line number for diagnostics. C `lineno` global.
151 #[allow(non_upper_case_globals)]
152 pub static lineno: std::cell::Cell<usize> = const { std::cell::Cell::new(1) };
153
154 /// SHIN read buffer — C `shinbuffer` (`char *`, a RAW BYTE
155 /// buffer: `shingetchar` hands callers `(unsigned char)
156 /// *shinbufptr++`). Held as `Vec<u8>`, not `String`: the previous
157 /// `String` had to `from_utf8_lossy` every chunk read off SHIN,
158 /// which replaced any non-UTF-8 byte with U+FFFD before the lexer
159 /// ever saw it — `echo caf\xe9` piped on stdin printed
160 /// `caf\xef\xbf\xbd`. C imposes no encoding on script input.
161 #[allow(non_upper_case_globals)]
162 static shinbuffer: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
163
164 /// SHIN read offset — C `shinbufptr`.
165 #[allow(non_upper_case_globals)]
166 static shinbufpos: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
167
168 /// SHIN save stack — C `shinsavestack`.
169 static shinsavestack: RefCell<Vec<(Vec<u8>, usize)>> = const { RefCell::new(Vec::new()) };
170
171 /// Pushback queue for `inungetc`. zshrs-specific; C inlines a
172 /// single inbufptr-decrement which can't model arbitrary-length
173 /// pushback from a different buffer.
174 static pushback: RefCell<VecDeque<char>> = const { RefCell::new(VecDeque::new()) };
175}
176
177/// Allocate a fresh SHIN buffer.
178/// Port of `shinbufalloc()` from Src/input.c:171.
179pub fn shinbufalloc() {
180 // c:171
181 shinbuffer.with(|b| {
182 *b.borrow_mut() = Vec::with_capacity(SHIN_BUF_SIZE);
183 });
184 shinbufreset();
185}
186
187/// Save the current SHIN buffer onto the save stack.
188/// Port of `shinbufsave()` from Src/input.c:181 — push the
189/// existing buffer onto a save-stack and start a fresh one for
190/// nested `eval`/`source` contexts.
191pub fn shinbufsave() {
192 // c:181
193 let (snap_buf, snap_pos) = (
194 shinbuffer.with(|b| std::mem::take(&mut *b.borrow_mut())),
195 shinbufpos.with(|p| p.replace(0)),
196 );
197 shinsavestack.with(|s| s.borrow_mut().push((snap_buf, snap_pos)));
198 shinbufalloc();
199}
200
201/// Pop the top of the SHIN save stack back into the live buffer.
202/// Port of `shinbufrestore()` from Src/input.c:200.
203pub fn shinbufrestore() {
204 // c:200
205 if let Some((buf, pos)) = shinsavestack.with(|s| s.borrow_mut().pop()) {
206 shinbuffer.with(|b| *b.borrow_mut() = buf);
207 shinbufpos.with(|p| p.set(pos));
208 }
209}
210
211// Get a character from SHIN, -1 if none available // c:218
212/// Read one byte from SHIN; returns -1 on EOF.
213/// Port of `shingetchar()` from Src/input.c:218. C source pulls
214/// from `shinbuffer` first then falls through to `read(2)` on the
215/// SHIN fd; Rust mirrors by reading from `std::io::stdin`.
216pub fn shingetchar() -> i32 {
217 // c:222-225 — `if (shinbufptr < shinbufendptr) return
218 // (unsigned char) *shinbufptr++;`. C is byte-oriented; serve the
219 // buffer one BYTE at a time (shinbufpos is a byte index).
220 let bufd = shinbuffer.with(|b| b.borrow().clone());
221 let pos = shinbufpos.with(|p| p.get());
222 if pos < bufd.len() {
223 if let Some(b) = bufd.get(pos) {
224 shinbufpos.with(|p| p.set(pos + 1));
225 return *b as i32;
226 }
227 }
228
229 // c:227 — `shinbufreset();`
230 shinbufreset();
231 let fd = SHIN.with(|s| s.get());
232 const SHINBUFSIZE: usize = 256; // c: SHINBUFSIZE
233
234 // c:228-251 — `#ifdef USE_LSEEK` fast path. Take it when SHIN is
235 // NOT the keyboard (`!isset(SHINSTDIN)`) or when SHIN is seekable
236 // (`lseek(SHIN, 0, SEEK_CUR) != -1` — a real file). A pipe /
237 // terminal returns -1 from lseek and is handled by the
238 // byte-at-a-time loop below. CRITICAL: the previous port skipped
239 // both C branches and slurped SHINBUFSIZE bytes unconditionally,
240 // so on a pipe (`cmd | zshrs -c 'read x'`) the lexer's first
241 // refill consumed every following line, leaving nothing for the
242 // `read` / `select` builtins to read off fd 0. Bug surfaced as
243 // 31 read/select parity regressions.
244 let seekable = unsafe { libc::lseek(fd, 0, libc::SEEK_CUR) != -1 };
245 if !isset(SHINSTDIN) || seekable {
246 let mut buf = [0u8; SHINBUFSIZE];
247 // c:231-234 — `do { errno=0; nread=read(...); } while (nread<0
248 // && errno==EINTR);`
249 let nread = loop {
250 let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, SHINBUFSIZE) };
251 if n < 0 && io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
252 continue;
253 }
254 break n;
255 };
256 if nread <= 0 {
257 return -1; // c:235-236
258 }
259 let nread = nread as usize;
260 // c:237-246 — when reading the keyboard (`isset(SHINSTDIN)`) and
261 // the chunk holds a newline, keep only the first line and
262 // `lseek` the fd BACK over the surplus so a later `read` / next
263 // command sees the rest. Otherwise the whole chunk is the
264 // buffer (`shinbufendptr = shinbuffer + nread`).
265 let end = if isset(SHINSTDIN) {
266 match buf[..nread].iter().position(|&b| b == b'\n') {
267 // c:239-240 — `++shinbufendptr - shinbuffer` includes
268 // the '\n'.
269 Some(nl) => {
270 let rsize = nl + 1;
271 if nread > rsize {
272 let back = (nread - rsize) as libc::off_t;
273 // c:241-244 — `lseek(SHIN, -(nread-rsize),
274 // SEEK_CUR)`; C zerr()s on failure (non-fatal).
275 if unsafe { libc::lseek(fd, -back, libc::SEEK_CUR) } < 0 {
276 crate::ported::utils::zerr(&format!(
277 "lseek({}, {}): {}",
278 fd,
279 -back,
280 io::Error::last_os_error()
281 ));
282 }
283 }
284 rsize
285 }
286 None => nread, // c:245-246
287 }
288 } else {
289 nread // c:245-246 — `shinbufendptr = shinbuffer + nread`
290 };
291 // c:247 — the buffer is raw bytes; `shingetline` above does
292 // the UTF-8 reassembly and metafication the C source defers to
293 // `metafy`. Decoding here (the old `from_utf8_lossy`) destroyed
294 // any byte that is not valid UTF-8.
295 shinbuffer.with(|b| *b.borrow_mut() = buf[..end].to_vec());
296 shinbufpos.with(|p| p.set(1));
297 return buf[0] as i32; // c:247 — `return (unsigned char) *shinbufptr++;`
298 }
299
300 // c:253-268 — non-seekable fallback (pipe / terminal): read ONE
301 // byte at a time, stopping at '\n' (`/* Use line buffering (POSIX
302 // requirement) */`) or when the buffer fills. This is what keeps
303 // the `read` builtin correct on pipes — the lexer never reads past
304 // the newline, so fd 0 still points at the next line.
305 let mut out: Vec<u8> = Vec::with_capacity(64);
306 loop {
307 let mut one = [0u8; 1];
308 // c:255-256 — `errno=0; nread=read(SHIN, shinbufendptr, 1);`
309 let n = unsafe { libc::read(fd, one.as_mut_ptr() as *mut libc::c_void, 1) };
310 if n > 0 {
311 out.push(one[0]); // c:259 — `*shinbufendptr++`
312 if one[0] == b'\n' {
313 break; // c:260 — newline terminates the line
314 }
315 if out.len() == SHINBUFSIZE {
316 break; // c:261-262 — buffer full
317 }
318 } else if n == 0 || io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
319 break; // c:263-264 — EOF or non-EINTR error
320 }
321 }
322 if out.is_empty() {
323 return -1; // c:265-266 — `if (shinbufendptr == shinbuffer) return -1;`
324 }
325 let first = out[0] as i32;
326 shinbuffer.with(|b| *b.borrow_mut() = out);
327 shinbufpos.with(|p| p.set(1));
328 first // c:267 — `return (unsigned char) *shinbufptr++;`
329}
330
331/// Read a full line from SHIN, with `\n` preserved.
332/// Port of `shingetline()` from Src/input.c:267 — calls
333/// `shingetchar` in a loop, metafies high bytes, returns NULL
334/// (`""`) on EOF.
335pub fn shingetline() -> String {
336 // c:267
337 // c:279-296 — C accumulates RAW BYTES, escaping only the reserved
338 // IMETA set (`if (imeta(c)) { *p++ = Meta; *p++ = c ^ 32; }`);
339 // every other byte, valid UTF-8 or not, lands in the buffer
340 // untouched. A Rust `String` cannot hold a byte that is not part
341 // of a UTF-8 sequence, so the collected bytes go through
342 // `script_bytes::decode_script_bytes`, which keeps valid UTF-8 as
343 // real `char`s (the lexer is Unicode-based — lex.rs:6171) and
344 // Meta-encodes only the bytes that have no `char` form. That is
345 // the same encoding `$'\xNN'` produces (lex.rs
346 // `getkeystring_dollar_quote`) and `unmetafy_str` reverses at the
347 // write boundary, so a legacy byte piped into the shell now comes
348 // back out verbatim instead of as U+FFFD.
349 let mut bytes: Vec<u8> = Vec::new();
350 // c:274 — `int q = queue_signal_level();`
351 let q = crate::ported::signals_h::queue_signal_level();
352 // c:277-278 — `winch_unblock(); dont_queue_signals();`: signals are
353 // handled while the shell sits in read(2) waiting for a line, so a job
354 // that dies meanwhile is reaped and reported (NOTIFY) at once. With a
355 // queue level left raised the SIGCHLD stayed queued and its zombie
356 // unreaped until the line arrived.
357 crate::ported::signals_h::winch_unblock();
358 crate::ported::signals_h::dont_queue_signals();
359 loop {
360 let c = shingetchar(); // c:280
361 if c < 0 || c == b'\n' as i32 {
362 // c:281-283 — `winch_block(); restore_queue_signals(q);`
363 crate::ported::signals_h::winch_block();
364 crate::ported::signals_h::restore_queue_signals(q);
365 if c == b'\n' as i32 {
366 bytes.push(b'\n'); // c:284-285 — `*p++ = '\n';`
367 }
368 break;
369 }
370 // c:293-297 — byte (metafied in decode_script_bytes below). The
371 // c:298-308 BUFSIZ flush re-queues signals only around its
372 // zrealloc; a Vec push needs no such window.
373 bytes.push(c as u8);
374 }
375 crate::script_bytes::decode_script_bytes(&bytes)
376}
377
378// ---------------------------------------------------------------------------
379// ingetc / inungetc / inpush / inpop / inpopalias — Src/input.c:318+/546/675/785/804.
380// ---------------------------------------------------------------------------
381
382/// Get the next char from the active input source.
383/// Port of `ingetc()` from Src/input.c:318 — drives the
384/// lexer; consumes pushback first, then top-of-stack input.
385pub fn ingetc() -> Option<char> {
386 // c:318
387 // c:Src/input.c:322 — `if (lexstop) return ' ';`. The C source
388 // returns the literal byte 32. The Rust port wraps the result
389 // in Option<char> where None is the canonical EOF marker (callers
390 // map None → -1 at hist.rs:392). Returning Some(' ') from this
391 // guard treated EOF as a real space byte downstream and broke
392 // every "drain past end" caller; consistent EOF means None for
393 // every post-lexstop call, matching the function's already-emitted
394 // None when buffer drains at line 293.
395 if lexstop.with(|c| c.get()) {
396 return None; // c:322 (mapped to Rust None EOF marker)
397 }
398
399 if let Some(c) = pushback.with(|p| p.borrow_mut().pop_front()) {
400 return Some(c);
401 }
402
403 loop {
404 let pos = inbufpos.with(|p| p.get());
405 // c:326 — C's `inbufptr` is a byte pointer; the Rust port keeps
406 // `inbufpos` a CHAR index into the `inbuf` String. Buffer-end is
407 // "no char at pos": the old `pos < buf.len()` compared a char index
408 // against the BYTE length, truncating multibyte `inbuf` content
409 // (eval / here-strings / cmdsubst / completion) at the first such char.
410 //
411 // O(1) resolution via the byte-offset cache instead of the old
412 // `inbuf.clone()` + `chars().nth(pos)` (both O(n) per char →
413 // O(n²) per buffer; a 31 KB `case` body took tens of seconds).
414 let byte_off = if inbuf_byte_char.with(|c| c.get()) == pos {
415 inbuf_byte_off.with(|o| o.get())
416 } else {
417 // Cache miss (buffer swap / pushback reposition): recompute
418 // this char's byte offset once, then resume O(1) advance.
419 inbuf.with(|b| {
420 b.borrow()
421 .char_indices()
422 .nth(pos)
423 .map(|(off, _)| off)
424 .unwrap_or_else(|| b.borrow().len())
425 })
426 };
427 // Extract the char + its UTF-8 width in a single borrow (no clone).
428 let ch = inbuf.with(|b| {
429 let s = b.borrow();
430 s.get(byte_off..).and_then(|rest| rest.chars().next())
431 });
432 if let Some(c) = ch {
433 inbufpos.with(|p| p.set(pos + 1));
434 // Advance the byte cache in lockstep (O(1) next read).
435 inbuf_byte_char.with(|cc| cc.set(pos + 1));
436 inbuf_byte_off.with(|o| o.set(byte_off + c.len_utf8()));
437 inbufct.with(|c| c.set(c.get().saturating_sub(1)));
438
439 // c:328 — `if (itok(lastc = (unsigned char) *inbufptr++)) continue;`
440 // Skip internal tokens via the canonical `itok()` predicate
441 // (`Src/ztype.h:52`), which tests bit `ITOK` in `typtab[c]`.
442 // Per `Src/utils.c:4198-4201` `inittyptab` sets ITOK on
443 // `Pound..LAST_NORMAL_TOK (0x84..0x9c)` AND `Snull..Nularg
444 // (0x9d..0xa1)` — canonical token range is `0x84..=0xa1`.
445 // The previous hardcoded range `0x83..=0x9b` was both too
446 // inclusive (included 0x83 = Meta lead byte, IMETA-only) and
447 // too narrow (excluded 0x9c..=0xa1 = Bang/Snull/Dnull/Bnull/
448 // Bnullkeep/Nularg). Marker (0xa2) is intentionally NOT in
449 // either set — it's IMETA-only per `c:4197`. Routing through
450 // `itok()` lets future `inittyptab` adjustments propagate
451 // automatically with zero changes here.
452 let cu32 = c as u32;
453 if crate::token_char::itok_char(c) {
454 continue;
455 }
456
457 let inp_lineno = (inbufflags.with(|f| f.get()) & INP_LINENO) != 0;
458 let is_strin = strin.with(|s| s.get()) != 0;
459 if (inp_lineno || !is_strin) && c == '\n' {
460 lineno.with(|l| l.set(l.get() + 1));
461 }
462 return Some(c);
463 }
464
465 // End of current buffer.
466 let ct = inbufct.with(|c| c.get());
467 let is_strin = strin.with(|s| s.get()) != 0;
468 let is_stop = lexstop.with(|c| c.get());
469 if ct == 0 && (is_strin || is_stop) {
470 lexstop.with(|c| c.set(true));
471 return None;
472 }
473
474 if (inbufflags.with(|f| f.get()) & INP_CONT) != 0 {
475 inpoptop();
476 continue;
477 }
478
479 // c:340-345 — `if (!inbufct && (strin || errflag)) { lexstop=1;
480 // break; }`. In C, reading a `-c` string sets `strin`, so a
481 // drained buffer stops at EOF instead of falling through to
482 // inputline() (which reads fresh SHIN input). zshrs feeds `-c`
483 // via the lexer's own LEX_INPUT window with `strin` left 0, so
484 // the c:336 gate above misses it — the distinguishing signal
485 // here is SHINSTDIN: it is SET for the interactive / stdin-script
486 // loop (which legitimately reads SHIN via inputline) and UNSET
487 // for `-c`. Without this gate, after the `-c` string drained the
488 // "last resort" inputline() swallowed the PROCESS's stdin as
489 // extra command lines — e.g. `printf data | zshrs -c 'read x'`
490 // left `read` empty and ran the data as commands (31 read/select
491 // parity regressions).
492 if !isset(SHINSTDIN) {
493 lexstop.with(|c| c.set(true));
494 return None; // c:343-344 — string input drained → EOF
495 }
496
497 // c:354-356 — `/* As a last resort, get some more input */
498 // if (inputline()) break;`. Read the next line from SHIN into
499 // `inbuf` and loop to return its first char. inputline() returns
500 // nonzero (and sets lexstop) at EOF. The lexer accumulates each
501 // returned char into its token buffer via `add()`, so input
502 // arriving through this path produces correct token text.
503 // c:339 — `if (!inbufct && (strin || errflag)) { lexstop; break; }`.
504 // We have drained the current input buffer (the char loop above
505 // fell through) and there is no INP_CONT buffer to pop. When we
506 // are reading from a pushed STRING (`strin` — e.g. the completion
507 // lexer in get_comp_string, `eval`, cmdsubst bodies), we must NOT
508 // fall through to `inputline()`, which reads fresh SHIN and, in an
509 // interactive shell, prompts PS2. C's guard also tests `!inbufct`;
510 // the Rust `inbufct` accounting can lag by one across the metafied
511 // completion buffer, so gate on the drained-buffer state we are
512 // already in instead. Without this, `l<Tab>` (whose lexer buffer
513 // drains mid-token) dropped the shell into a PS2 continuation.
514 if strin.with(|s| s.get()) != 0 {
515 lexstop.with(|c| c.set(true));
516 return None;
517 }
518 if inputline() != 0 {
519 return None; // c:356 break → EOF
520 }
521 // loop: the refilled inbuf is read at the top of the loop.
522 }
523}
524
525// Read a line from the current command stream and store it as input // c:366
526/// Read one line from the command stream and store it as the input
527/// buffer. Port of `static int inputline(void)` from Src/input.c:366.
528/// C dispatches between the zle and non-zle paths; zshrs reads via
529/// `shingetline` (no zle line editor yet). On EOF it sets `lexstop`
530/// and returns 1; on success it installs the line into the `inbuf`
531/// buffer (`inbuf = inbufptr = line; inbufleft = strlen; inbufct =
532/// …; inbufflags = 0`) and returns 0 — exactly what `ingetc`'s
533/// "as a last resort, get some more input" arm (c:355) expects.
534pub fn inputline() -> i32 {
535 // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
536 // C zsh is single-threaded, so this function can always read SHIN /
537 // drive ZLE. zshrs parses shell bodies on worker-pool threads
538 // (compinit's bytecode backfill parses ~47k autoload bodies), and
539 // `interact` / SHINSTDIN / SHTTY / USEZLE are process-globals — so a
540 // worker whose lexer buffer drained mid-construct fell through here
541 // and READ THE USER'S TERMINAL, competing with ZLE for keystrokes.
542 // Symptoms: the prompt after `compinit -C` rendered PS2 (`> `) and
543 // swallowed every following line, stray empty `zsh:N:` diagnostics,
544 // eaten/duplicated characters, and bogus parse errors. Background
545 // threads get EOF here, which is what C's `strin` gate (input.c:339)
546 // produces for string input.
547 if crate::worker::in_worker_thread() {
548 lexstop.with(|c| c.set(true));
549 return 1; // EOF — never prompt or read SHIN off the main thread
550 }
551 // c:371-384 — if reading code interactively, work out the prompt: PS1
552 // on the first line of a command, PS2 (continuation) otherwise.
553 // `ingetcpmptl` is the SOURCE prompt string fed to promptexpand.
554 let mut ingetcpmptl: Option<String> = None;
555 let mut ingetcpmptr: Option<String> = None;
556 if crate::ported::zsh_h::interact() && isset(SHINSTDIN) {
557 // c:372
558 // RPS1/RPROMPT (and RPS2/RPROMPT2) are documented as equivalent
559 // right-prompt names but are SEPARATE parameters (unlike PS1/PROMPT,
560 // which zshrs aliases): `RPROMPT=x` does NOT set `$RPS1` and vice
561 // versa, matching zsh's readback. zsh nonetheless renders the right
562 // prompt from whichever is set, so read RPS1 first and fall back to
563 // RPROMPT when it's unset/empty. Without the fallback, a config that
564 // sets only RPROMPT (the classic name — e.g. zpwr's vim-mode keymap
565 // indicator) produced no right prompt at all. (Both set is
566 // contradictory config; RPS1 wins here vs zsh's last-assigned wins.)
567 // RPS1/RPROMPT (and RPS2/RPROMPT2) are documented as equivalent
568 // right-prompt names but are SEPARATE parameters (unlike PS1/PROMPT,
569 // which zshrs aliases): `RPROMPT=x` does NOT set `$RPS1` and vice
570 // versa, matching zsh's readback. zsh nonetheless renders the right
571 // prompt from whichever is set, so read the modern RPS1/RPS2 name
572 // first and fall back to the classic RPROMPT/RPROMPT2 when it's
573 // unset/empty. Without the fallback a config that sets only RPROMPT
574 // (the classic name — e.g. zpwr's vim-mode keymap indicator)
575 // produced no right prompt at all. An empty primary is treated as
576 // "not set" so a bare `RPS1=` doesn't blank a populated RPROMPT.
577 // (Both set is contradictory config; RPS1 wins here vs zsh's
578 // last-assigned wins.) Bug #654.
579 let rprompt_effective = |primary: &str, legacy: &str| -> Option<String> {
580 match crate::ported::params::getsparam(primary) {
581 Some(s) if !s.is_empty() => Some(s),
582 _ => crate::ported::params::getsparam(legacy),
583 }
584 };
585 if !crate::ported::lex::LEX_ISFIRSTLN.with(|c| c.get()) {
586 // c:373-377 — continuation line → PS2 / RPS2 (or RPROMPT2).
587 ingetcpmptl = crate::ported::params::getsparam("PS2");
588 ingetcpmptr = rprompt_effective("RPS2", "RPROMPT2");
589 } else {
590 // c:379-382 — first line → PS1 / RPS1 (or RPROMPT).
591 ingetcpmptl = crate::ported::params::getsparam("PS1");
592 ingetcpmptr = rprompt_effective("RPS1", "RPROMPT");
593 }
594 }
595 // c:385 — `if (!(interact && isset(SHINSTDIN) && SHTTY != -1 &&
596 // isset(USEZLE)))`: read via the ZLE line editor when interactive
597 // on a real tty with USEZLE; otherwise read straight from the input
598 // file (printing a prompt first).
599 // The ZLE line editor is on for any interactive shell on a real tty
600 // with USEZLE set (zsh's standard gate) — `unsetopt zle` turns it off
601 // and falls back to the cooked reader below, exactly as in zsh.
602 let use_zle = crate::ported::zsh_h::interact()
603 && isset(SHINSTDIN)
604 && crate::ported::init::SHTTY.load(std::sync::atomic::Ordering::Relaxed) != -1
605 && isset(crate::ported::zsh_h::USEZLE);
606 let line = if !use_zle {
607 // c:391-406 — non-ZLE: print the expanded prompt to fd 2 (only
608 // when still interactive, e.g. running under emacs), then
609 // shingetline. Gated on `interact && SHINSTDIN` so piped /
610 // non-interactive input prints no prompt.
611 if crate::ported::zsh_h::interact() && isset(SHINSTDIN) {
612 // c:401-403 — `promptexpand(*ingetcpmptl)` → `write_loop(2, …)`.
613 let (expanded, _, _) = crate::ported::prompt::promptexpand(
614 ingetcpmptl.as_deref().unwrap_or(""), // c:401
615 0,
616 None,
617 );
618 let pptbuf = crate::ported::utils::unmetafy_str(&expanded); // c:401 unmetafy
619 let _ = crate::ported::utils::write_loop(2, &pptbuf); // c:403
620 }
621 shingetline() // c:406 ingetcline = shingetline()
622 } else {
623 // c:413-423 — ZLE path. `int flags = ZLRF_HISTORY|ZLRF_NOSETTY;
624 // if (isset(IGNOREEOF)) flags |= ZLRF_IGNOREEOF;
625 // ingetcline = zleentry(ZLE_CMD_READ, ingetcpmptl, ingetcpmptr,
626 // flags, context); histdone |= HISTFLAG_SETTY;`
627 // zleentry dispatches to the ZLE module's zle_main_entry; zshrs
628 // links ZLE in so it calls the entry point directly — but the
629 // module-registration half of zleentry still has to run, because
630 // it is what marks `zsh/zle`/`zsh/complete`/`zsh/compctl` loaded
631 // (init.c:1764-1765). `_default` keys off `zmodload -e
632 // zsh/compctl` to decide whether the legacy compctl engine is
633 // available, so skipping this made interactive completion
634 // diverge from zsh. One-shot, on the first ZLE read, exactly
635 // where C's lazy `load_module` fires.
636 // c:init.c:1764-1765 — `if (load_module("zsh/zle", NULL, 0) != 1)
637 // (void)load_module("zsh/compctl", NULL, 0);`. `zsh/compctl`
638 // pulls in `zsh/complete` through the compctl.mdd moddeps edge.
639 // ZLE_CMD_READ is not one of the three commands c:1761-1762
640 // exempts, so the load always fires here on the first read.
641 if !crate::ported::init::zle_modules_loaded.swap(true, std::sync::atomic::Ordering::SeqCst)
642 {
643 let mut tab = crate::ported::module::MODULESTAB.lock().unwrap();
644 if tab.load_module("zsh/zle", None, false) != 1 {
645 tab.load_module("zsh/compctl", None, false); // c:init.c:1765
646 }
647 }
648 // c:Src/Zle/zle_main.c:2244-2252 `setup_()` — loading zsh/zle runs
649 // the module's setup, which registers the widgets
650 // (`init_thingies()`, c:2253) and sets `zle_load_state = 1`
651 // (c:2250). zshrs normally does that eagerly in `zsh_main`, but
652 // that path is skipped when the shell starts with `+Z`; if an rc
653 // file then turns the editor back on with `setopt zle`, this read
654 // is where C's lazy `load_module("zsh/zle")` would fire, so run
655 // the same one-shot setup here.
656 //
657 // `zle_load_state` is the ONLY gate, exactly as in C — so every
658 // zshrs stand-in for "zsh/zle just got loaded" has to set it, or
659 // this block performs a SECOND module load and the DESTRUCTIVE
660 // `default_bindings()` (c:Src/Zle/zle_keymap.c:1309) rebuilds
661 // every keymap from fresh structs, discarding the bindings the
662 // user made in between. `bin_bindkey` (zle_keymap.rs) is that
663 // stand-in — in C `bindkey` is a builtin OF zsh/zle, so calling
664 // it autoloads the module first — and it now stores
665 // `zle_load_state = 1` the way C's `setup_` does.
666 if crate::ported::init::zle_load_state.load(std::sync::atomic::Ordering::SeqCst) == 0 {
667 crate::ported::zle::zle_thingy::init_thingies(); // c:2253
668 crate::ported::zle::zle_keymap::createkeymapnamtab();
669 crate::ported::zle::zle_keymap::default_bindings();
670 crate::ported::init::zle_load_state.store(1, std::sync::atomic::Ordering::SeqCst);
671 // c:2250
672 }
673 let mut flags = crate::ported::zsh_h::ZLRF_HISTORY | crate::ported::zsh_h::ZLRF_NOSETTY;
674 if isset(crate::ported::zsh_h::IGNOREEOF) {
675 flags |= crate::ported::zsh_h::ZLRF_IGNOREEOF;
676 }
677 let mut lp = ingetcpmptl.clone();
678 let mut rp = ingetcpmptr.clone();
679 let mut args = crate::ported::zle::zle_main::zle_main_entry_args::Read {
680 lp: &mut lp,
681 rp: &mut rp,
682 flags,
683 context: 0, // ZLCON_LINE_START — normal command line read
684 };
685 let r = crate::ported::zle::zle_main::zle_main_entry(
686 crate::ported::zsh_h::ZLE_CMD_READ,
687 &mut args,
688 );
689 // c:423 — `histdone |= HISTFLAG_SETTY;` (defer tty restore to the
690 // end of the current input via the history mechanism).
691 crate::ported::hist::histdone.fetch_or(
692 crate::ported::zsh_h::HISTFLAG_SETTY,
693 std::sync::atomic::Ordering::Relaxed,
694 );
695 // zleread returns "" on EOF (^D) and "…\n" otherwise, matching the
696 // C NULL-vs-line distinction the empty-check below relies on.
697 r.unwrap_or_default()
698 };
699 // c:425-427 — `if (!ingetcline) { ... return lexstop = 1; }`.
700 // shingetline returns "" only at real EOF (a blank line is "\n").
701 if line.is_empty() {
702 lexstop.with(|c| c.set(true)); // c:426 lexstop = 1
703 return 1;
704 }
705 // c:433-437 — `if (isset(VERBOSE)) { zputs(ingetcline, stderr);
706 // fflush(stderr); }`. The `-v` / VERBOSE option echoes each input
707 // line to stderr as it is read. zputs writes the UNmetafied bytes;
708 // the line already carries its trailing newline so no extra one is
709 // added.
710 if isset(VERBOSE) {
711 use std::io::Write;
712 // c:435 — `zputs(ingetcline, stderr)` writes the unmetafied raw
713 // bytes (not a re-encoded String), so write the byte buffer.
714 let _ = io::stderr().write_all(&crate::ported::utils::unmetafy_str(&line));
715 let _ = io::stderr().flush(); // c:436 fflush(stderr)
716 }
717 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
718 // `@ <code>` at the prompt runs stryke when the linking binary
719 // registered a handler (`zsh::set_stryke_handler`). It has to be taken
720 // here, as raw text, before the lexer sees it: stryke syntax is not
721 // shell syntax (`1:10 |> sum` is a pipe into a redirect, `$_` would be
722 // expanded), so parsing it as a command named `@` cannot work.
723 let line = crate::stryke_line(line);
724 // c:498-500 — install the line as the live input buffer.
725 let len = line.chars().count() as i32; // c:500 inbufleft (char count — inbufpos/inbufct are char-based)
726 inbuf.with(|b| *b.borrow_mut() = line);
727 inbufpos.with(|p| p.set(0)); // c:499 inbufptr = inbuf
728 inbufct.with(|c| c.set(len)); // c:501 inbufct = inbufleft
729 inbufflags.with(|f| f.set(0)); // c:502 inbufflags = 0
730 // Fresh input arrived — clear any stale EOF latch so ingetc reads it.
731 lexstop.with(|c| c.set(false));
732 0 // c:508 return 0
733}
734
735/// Replace the current input line.
736/// Port of `inputsetline(char *str, int flags)` from Src/input.c:510.
737pub fn inputsetline(str: &str, flags: i32) {
738 // c:510
739 inbuf.with(|b| *b.borrow_mut() = str.to_string());
740 inbufpos.with(|p| p.set(0));
741 let len = str.chars().count() as i32; // char count — inbufct is char-based
742 if (flags & INP_CONT) != 0 {
743 inbufct.with(|c| c.set(c.get() + len));
744 } else {
745 inbufct.with(|c| c.set(len));
746 }
747 inbufflags.with(|f| f.set(flags));
748 // c:Src/input.c — inputsetline is the "fresh input arrives" entry
749 // point. In C, ingetc's lexstop guard is reset by every grammar-
750 // boundary call (zshlex/getfirsttok/zshlex_raw_back at lex.c:455,
751 // 519, etc.) BEFORE the next ingetc fires. zshrs has no such
752 // reset surface yet, so a previously-drained buffer leaves
753 // lexstop=true and the new content is unreadable. Reset here so
754 // the contract "inputsetline(s) makes s readable" holds.
755 lexstop.with(|c| c.set(false));
756}
757
758/// Push a character back onto the input stream.
759/// Port of `inungetc(int c)` from Src/input.c:546.
760pub fn inungetc(c: char) {
761 // c:546
762 if lexstop.with(|c| c.get()) {
763 return;
764 }
765 let pos = inbufpos.with(|p| p.get());
766 if pos > 0 {
767 inbufpos.with(|p| p.set(pos - 1));
768 inbufct.with(|cell| cell.set(cell.get() + 1));
769 let inp_lineno = (inbufflags.with(|f| f.get()) & INP_LINENO) != 0;
770 let is_strin = strin.with(|s| s.get()) != 0;
771 if (inp_lineno || !is_strin) && c == '\n' {
772 lineno.with(|l| l.set(l.get().saturating_sub(1)));
773 }
774 } else {
775 pushback.with(|p| p.borrow_mut().push_front(c));
776 }
777}
778
779/// Read entire file into memory.
780/// Port of `mod_export off_t zstuff(char **out, const char *fn)`
781/// from Src/input.c:614. C body opens via `unmeta(fn)`, fseeks to
782/// end for size, fread()s the body, queues signals around the IO,
783/// zerr()s on open/read failures, returns byte count or -1.
784///
785/// Rust signature: `(path: &str) -> Result<(String, i64), i32>`
786/// — Ok((contents, byte_count)) or Err(-1) on open/read fail.
787/// Path is unmetafied to match C's `unmeta(fn)` step before open.
788/// WARNING: param names don't match C — Rust=(path) vs C=(out, fn).
789pub fn zstuff(path: &str) -> Result<(String, i64), i32> {
790 // c:614
791 use std::io::Read;
792 // c:621 — `unmeta(fn)`: de-metafy the path before open(2).
793 let mut path_bytes = path.as_bytes().to_vec();
794 unmetafy(&mut path_bytes);
795 let real_path = String::from_utf8_lossy(&path_bytes);
796 // c:621 — `fopen(unmeta(fn), "r")`. Rust File::open mirrors fopen
797 // with read-only mode; failure path zerrs and returns -1.
798 let mut file = match std::fs::File::open(real_path.as_ref()) {
799 // c:621
800 Ok(f) => f,
801 Err(_) => {
802 // c:622
803 zerr(&format!("can't open {}", path)); // c:622
804 return Err(-1); // c:623
805 }
806 };
807 // c:625 — `queue_signals();` block syscalls from the trap fast path
808 // for the duration of the read.
809 queue_signals();
810 // c:626-628 — `fseek(end); ftell; fseek(start);` to size the file
811 // without consuming the stream. Use stream metadata in Rust.
812 let len = match file.metadata() {
813 // c:627
814 Ok(m) => m.len() as i64,
815 Err(_) => 0,
816 };
817 // c:629 — `buf = zalloc(len + 1);` C slurps RAW BYTES; the caller
818 // (`stuff`, c:647) pushes them onto the input stack where the lexer
819 // metafies. Reading as UTF-8 rejected any file with a legacy byte.
820 let mut raw: Vec<u8> = Vec::new();
821 // c:630-635 — `fread(buf, len, 1, in)` failure arm zerrs read error.
822 if file.read_to_end(&mut raw).is_err() {
823 // c:630
824 zerr(&format!("read error on {}", path)); // c:631
825 unqueue_signals(); // c:633
826 return Err(-1); // c:634
827 }
828 unqueue_signals(); // c:640
829 let buf = crate::script_bytes::decode_script_bytes(&raw);
830 Ok((buf, len)) // c:642
831}
832
833// `input_has_alias` / `take_raw_input` deleted — Rust-only helpers
834// with zero callers in this tree. C uses different mechanisms
835// (the lexer walks `instack` inline for alias detection; raw input
836// for history accumulates through `chline` / `addtoline`).
837
838/// Stuff a whole file into the input queue.
839/// Port of `stuff(char *fn)` from Src/input.c:647 — read the file, echo
840/// it to stderr, push onto the input stack.
841/// WARNING: param names don't match C — Rust=(filename) vs C=(fn)
842pub fn stuff(filename: &str) -> i32 {
843 // c:647
844 // c:651 — `read(fd, buf, ...)`: raw bytes, metafied by the caller
845 // path, never rejected for encoding.
846 let buf = match crate::script_bytes::read_script_file(filename) {
847 Ok(b) => b,
848 Err(_) => return 1,
849 };
850 let _ = std::io::stderr().write_all(buf.as_bytes());
851 let _ = std::io::stderr().flush();
852 inpush(&buf, INP_FREE, None);
853 0
854}
855
856/*
857 * Check if the current input line, including continuations, is
858 * expanding an alias. This does not detect alias expansions that
859 * have been fully processed and popped from the input stack.
860 * If there is an alias, the most recently expanded is returned,
861 * else NULL.
862 */
863// c:822-828 — comment copied verbatim from Src/input.c.
864//
865// !!! WARNING: RUST-ONLY HELPER !!!
866//
867// C's `inpoptop` (c:759) only moves `instacktop` DOWN — the popped frame
868// stays addressable, so `inungetc` can walk BACK UP over exhausted alias
869// continuations when the lexer backs the terminator up to a segment start
870// (c:587-605: `do { if (instacktop->alias) …->inuse = 1; instacktop++; }
871// while (instacktop->flags & (INP_ALCONT|INP_HISTCONT) && !…->bufleft);`).
872// That walk-up is what leaves the alias frames visible to
873// `input_hasalias()` after the alias body has been fully lexed.
874//
875// zshrs's `instack` is a `Vec` whose `pop()` DESTROYS the frame, so the
876// walk-up cannot be reconstructed. `crate::alias_input_frames` mirrors the observable:
877// `inpoptop` records the alias name of every frame it restores that carries
878// `INP_ALCONT`, and the list is dropped as soon as a pop lands on flags
879// without `INP_ALCONT` — i.e. exactly when the continuation chain that
880// `inungetc` would have restored has really ended.
881
882/// Port of `char *input_hasalias(void)` from `Src/input.c:831`.
883pub fn input_hasalias() -> Option<String> {
884 // c:831
885 let mut flags = inbufflags.with(|f| f.get()); // c:833
886 let mut instackptr = instack.with(|st| st.borrow().len()); // c:834 instacktop
887 let mut alias_nam: Option<String> = None; // c:835
888
889 loop {
890 // c:837
891 if (flags & INP_CONT) == 0 {
892 // c:839
893 break; // c:840
894 }
895 // c:841 — `DPUTS(instackptr == instack, "BUG: continuation at
896 // bottom of instack");` — Rust can't index below 0, so bail.
897 if instackptr == 0 {
898 break;
899 }
900 instackptr -= 1; // c:842
901 instack.with(|st| {
902 let st = st.borrow();
903 let frame = &st[instackptr];
904 if frame.alias.is_some() {
905 // c:843
906 alias_nam = frame.alias.as_ref().map(|(nam, _)| nam.clone()); // c:844
907 }
908 flags = frame.flags; // c:845
909 });
910 }
911
912 // RUST-ONLY fallback (see crate::alias_input_frames above): the alias frames C
913 // would still have on the stack were destroyed by `Vec::pop`. The
914 // bottom-most recorded name is the outermost alias of the chain, which
915 // is what C's downward walk ends up with for nested expansions
916 // (`secondalias` → `firstalias` → body reports `secondalias`).
917 if alias_nam.is_none() && (inbufflags.with(|f| f.get()) & INP_ALCONT) != 0 {
918 alias_nam = crate::alias_input_frames::outermost();
919 }
920
921 alias_nam // c:848
922}
923
924/// Discard pending input after a parse error.
925/// Port of `inerrflush()` from Src/input.c:665.
926pub fn inerrflush() {
927 // c:665
928 while !lexstop.with(|c| c.get()) && inbufct.with(|c| c.get()) > 0 {
929 let _ = ingetc();
930 }
931}
932
933// Set some new input onto a new element of the input stack // c:675
934/// Push a new input source onto the stack.
935/// Port of `inpush(char *str, int flags, Alias inalias)` from Src/input.c:675 — used for `eval`/
936/// `source`, alias expansion, and process substitution to layer a
937/// new input on top of the current one.
938pub fn inpush(str: &str, flags: i32, inalias: Option<(String, i32)>) {
939 // c:675
940 // c:687 — `inbufflags &= ~(INP_ALCONT|INP_HISTCONT);` — the
941 // continuation markers describe the frame BELOW; strip them from
942 // the flags being saved so a pop doesn't re-run the alias-unwind
943 // arm for a frame that isn't an alias continuation.
944 let saved_flags = inbufflags.with(|f| f.get()) & !(INP_ALCONT | INP_HISTCONT); // c:687
945 let saved = instacks {
946 buf: inbuf.with(|b| std::mem::take(&mut *b.borrow_mut())),
947 bufpos: inbufpos.with(|p| p.replace(0)),
948 // c:686 — `instacktop->bufct = inbufct;` — the count AT PUSH
949 // TIME spans this frame's remainder plus every CONT frame
950 // below it; inpoptop restores it verbatim (c:764). The old
951 // port didn't save it and recomputed only the restored
952 // frame's remainder on pop — undercounting made ingetc's
953 // `!inbufct && strin` EOF gate (c:342) fire at an
954 // intermediate frame boundary, dropping everything below
955 // (`alias g='echo A'; alias t='g x'; eval t` lost ` x`).
956 bufct: inbufct.with(|c| c.get()), // c:686
957 flags: saved_flags,
958 alias: None,
959 serial: INBUF_SERIAL.with(|s| s.get()),
960 };
961 instack.with(|st| st.borrow_mut().push(saved));
962 INBUF_SERIAL.with(|s| {
963 s.set(INBUF_SERIAL_NEXT.with(|n| n.replace(n.get() + 1)))
964 });
965
966 inbuf.with(|b| *b.borrow_mut() = str.to_string());
967 inbufpos.with(|p| p.set(0));
968
969 let mut combined = flags;
970 if (flags & (INP_ALIAS | INP_HIST)) != 0 {
971 combined |= INP_CONT | INP_ALIAS;
972 if let Some(a) = inalias {
973 instack.with(|st| {
974 if let Some(last) = st.borrow_mut().last_mut() {
975 last.alias = Some(a);
976 if (flags & INP_HIST) != 0 {
977 last.flags |= INP_HISTCONT;
978 } else {
979 last.flags |= INP_ALCONT;
980 }
981 }
982 });
983 }
984 } else if (saved_flags & INP_ALIAS) != 0 && (flags & INP_CONT) != 0 {
985 // c:707-709 — `if (((instacktop->flags = inbufflags) & INP_ALIAS)
986 // && (flags & INP_CONT)) flags |= INP_ALIAS;` — a plain
987 // INP_CONT push layered over an alias frame continues the
988 // alias expansion: mark the new frame INP_ALIAS too so
989 // history stays off for its chars.
990 combined |= INP_ALIAS;
991 }
992
993 let new_len = inbuf.with(|b| b.borrow().chars().count()) as i32; // char count — inbufct is char-based
994 if (combined & INP_CONT) != 0 {
995 inbufct.with(|c| c.set(c.get() + new_len));
996 } else {
997 inbufct.with(|c| c.set(new_len));
998 }
999 inbufflags.with(|f| f.set(combined));
1000 // c:Src/input.c — same lexstop reset as inputsetline (input.rs:336).
1001 // Without this, an inpush after the buffer drained leaves lexstop=true
1002 // and ingetc returns None immediately, so the just-pushed alias body
1003 // (e.g. `inpush("echo hello")` after lexing `hi`) never gets read.
1004 // Symptom: `alias hi='echo hello'; eval hi` → empty output, because
1005 // eval's inner `hi` token drained the buffer, set lexstop, then
1006 // exalias inpushed `echo hello` but the next ingetc still saw
1007 // lexstop=true and returned None → tok=ENDINPUT.
1008 //
1009 // BOTH lexstop globals must reset: input.rs's local `lexstop` gates
1010 // ingetc (line 244); lex.rs's `LEX_LEXSTOP` gates gettok. zshrs
1011 // duplicates the C single `lexstop` global across two modules.
1012 lexstop.with(|c| c.set(false));
1013 LEX_LEXSTOP.with(|c| c.set(false));
1014}
1015
1016// Remove the top element of the stack // c:736
1017/// Pop one input-stack frame off the top.
1018/// Port of `inpoptop()` from Src/input.c:736.
1019pub fn inpoptop() {
1020 // c:736
1021 // c:738 — if (!lexstop) {
1022 if !LEX_LEXSTOP.with(|c| c.get()) {
1023 // c:739 — inbufflags &= ~(INP_ALCONT|INP_HISTCONT);
1024 inbufflags.with(|f| f.set(f.get() & !(INP_ALCONT | INP_HISTCONT)));
1025 // c:740-753 — `while (inbufptr > inbuf) { inbufptr--; …
1026 // if ((inbufflags & (INP_ALIAS|INP_HIST|INP_RAW_KEEP)) == INP_ALIAS)
1027 // zshlex_raw_back(); }` — walk back over every character ALREADY
1028 // READ from the frame, and for an alias frame take each one back out
1029 // of the raw record, so a command substitution's word keeps the alias
1030 // NAME its source spelled, not the expansion (c:Src/lex.c:2072-2077).
1031 // `inbufpos` is that count: the lexer's own pushback lives in
1032 // `LEX_UNGET_BUF`, which `hgetc` drains before `ingetc` can exhaust a
1033 // frame. The port used to count the UNREAD rest (zero for an exhausted
1034 // frame), so `x=$(ll 5)` with `alias ll='print aliased'` recorded
1035 // `llprint aliased 5` once skipcomm parsed the body.
1036 let was_alias =
1037 (inbufflags.with(|f| f.get()) & (INP_ALIAS | INP_HIST | INP_RAW_KEEP)) == INP_ALIAS;
1038 if was_alias {
1039 // C's `inungetc` already moved `inbufptr` back over every
1040 // character pushed back into this frame (c:558) and took it out
1041 // of the raw record then (c:609). Here those characters sit in
1042 // `LEX_UNGET_BUF`, tagged with this frame's serial; they must not
1043 // be taken out a second time. (Tags are trusted only while the
1044 // queue and its tags are the same length.)
1045 let serial = INBUF_SERIAL.with(|s| s.get());
1046 let backed = crate::ported::lex::LEX_UNGET_FRAME.with_borrow(|f| {
1047 let in_step =
1048 crate::ported::lex::LEX_UNGET_BUF.with_borrow(|b| b.len()) == f.len();
1049 if in_step {
1050 f.iter().filter(|&&t| t == serial).count()
1051 } else {
1052 0
1053 }
1054 });
1055 for _ in 0..inbufpos.with(|p| p.get()).saturating_sub(backed) {
1056 zshlex_raw_back(); // c:752
1057 }
1058 }
1059 }
1060
1061 // c:756-757 — if (inbuf && (inbufflags & INP_FREE)) free(inbuf);
1062 // Rust Drop covers the heap-string free when entry is replaced.
1063 // In C the characters `inungetc` pushed back into this alias frame, or
1064 // into the INP_CONT frames it stacked above it (c:571-603), are part of
1065 // what is freed here: popping an alias expansion abandons its unread
1066 // text. zshrs holds that pushback in `LEX_UNGET_BUF`, so drop the queued
1067 // characters that belong to this frame or to a frame pushed after it
1068 // (serials only grow). They sit at the front: they were read last.
1069 if (inbufflags.with(|f| f.get()) & INP_ALIAS) != 0 {
1070 let serial = INBUF_SERIAL.with(|s| s.get());
1071 use crate::ported::lex::{LEX_UNGET_BUF, LEX_UNGET_FRAME, LEX_UNGET_HPTR, LEX_UNGET_RAW};
1072 let in_step = LEX_UNGET_BUF.with_borrow(|b| b.len())
1073 == LEX_UNGET_FRAME.with_borrow(|f| f.len());
1074 while in_step && LEX_UNGET_FRAME.with_borrow(|f| f.front().is_some_and(|&t| t >= serial)) {
1075 LEX_UNGET_FRAME.with_borrow_mut(|f| f.pop_front());
1076 LEX_UNGET_BUF.with_borrow_mut(|b| b.pop_front());
1077 LEX_UNGET_HPTR.with_borrow_mut(|b| b.pop_front());
1078 LEX_UNGET_RAW.with_borrow_mut(|b| b.pop_front());
1079 crate::funcdef_capture::LEX_UNGET_SRCCAP.with_borrow_mut(|b| b.pop_front());
1080 }
1081 }
1082
1083 // c:759-765 — pop and restore from instacktop->{buf,bufptr,bufleft,bufct,flags}
1084 if let Some(entry) = instack.with(|st| st.borrow_mut().pop()) {
1085 // c:770-778 — if (instacktop->alias) { alias->inuse = 0; if trailing
1086 // space → inalmore=1; histbackword(); }
1087 if let Some((name, alias_flags)) = &entry.alias {
1088 // c:771 — `char *t = instacktop->alias->text;` — the check
1089 // below is against the ALIAS BODY, not the saved outer
1090 // buffer (`entry.buf` is the frame being restored; the
1091 // drained alias text was in `inbuf`).
1092 let alias_text: Option<String> = {
1093 // READ guard: `inuse` is atomic, so clearing it never
1094 // splits the copy-on-write table (see `alias::inuse`).
1095 // `instacktop->alias` points into whichever table the node
1096 // came from. A suffix alias (ALIAS_SUFFIX, set by `alias -s`,
1097 // c:Src/builtin.c:4480) lives in sufaliastab; looking only in
1098 // aliastab left it marked in use after its first expansion, so
1099 // `!an->inuse` (c:Src/lex.c:1936) refused it from then on.
1100 let lock = if (alias_flags & crate::ported::zsh_h::ALIAS_SUFFIX) != 0 {
1101 crate::ported::hashtable::sufaliastab_lock()
1102 } else {
1103 aliastab_lock()
1104 };
1105 let tab = lock.read().expect("aliastab poisoned");
1106 match tab.get(name) {
1107 Some(a) => {
1108 a.inuse.store(0, std::sync::atomic::Ordering::Relaxed); // c:773
1109 Some(a.text.clone())
1110 }
1111 None => None,
1112 }
1113 };
1114 // c:774-777 — `if (*t && t[strlen(t) - 1] == ' ')
1115 // { inalmore = 1; histbackword(); }`
1116 // — a trailing-space alias body marks the NEXT word
1117 // alias-eligible (input.c:63 comment; consumed by
1118 // checkalias at lex.c:1917).
1119 if alias_text.is_some_and(|t| t.ends_with(' ')) {
1120 crate::ported::lex::LEX_INALMORE.with(|f| f.set(1)); // c:775
1121 histbackword(); // c:776
1122 }
1123 }
1124 // RUST-ONLY bookkeeping for `input_hasalias` — see the crate::alias_input_frames
1125 // comment there. Record the alias of a frame restored WITH
1126 // INP_ALCONT: C would keep it reachable above `instacktop` for
1127 // `inungetc` (c:587-605) to walk back onto.
1128 if (entry.flags & INP_ALCONT) != 0 {
1129 if let Some((name, _)) = &entry.alias {
1130 crate::alias_input_frames::push(name);
1131 }
1132 } else {
1133 crate::alias_input_frames::clear();
1134 }
1135 inbuf.with(|b| *b.borrow_mut() = entry.buf);
1136 inbufpos.with(|p| p.set(entry.bufpos));
1137 inbufflags.with(|f| f.set(entry.flags));
1138 INBUF_SERIAL.with(|s| s.set(entry.serial));
1139 // c:764 — `inbufct = instacktop->bufct;` — restore the count
1140 // saved at push time. It spans the restored frame's remainder
1141 // PLUS every CONT frame below; the previous recompute-from-
1142 // this-frame-only undercounted, tripping ingetc's
1143 // `!inbufct && strin` EOF gate (c:342) with unread CONT
1144 // frames still stacked.
1145 inbufct.with(|c| c.set(entry.bufct)); // c:764
1146 }
1147}
1148
1149// Remove the top element of the stack and all its continuations. // c:785
1150/// Pop the topmost input-stack frame plus any continuations.
1151/// Port of `inpop()` from Src/input.c:785.
1152pub fn inpop() {
1153 // c:785
1154 loop {
1155 let was_cont = (inbufflags.with(|f| f.get()) & INP_CONT) != 0;
1156 inpoptop();
1157 if !was_cont {
1158 break;
1159 }
1160 }
1161}
1162
1163/// Pop the top input level only if it's an alias frame.
1164/// Port of `inpopalias()` from Src/input.c:804 — used to unwind
1165/// alias expansion without disturbing the underlying source.
1166pub fn inpopalias() {
1167 // c:804
1168 while (inbufflags.with(|f| f.get()) & INP_ALIAS) != 0 {
1169 inpoptop();
1170 }
1171}
1172
1173/// Get a slice of the unread portion of the current input.
1174/// Port of `ingetptr()` from Src/input.c:817.
1175pub fn ingetptr() -> String {
1176 // c:817
1177 let pos = inbufpos.with(|p| p.get());
1178 inbuf.with(|b| {
1179 b.borrow()
1180 .get(pos..)
1181 .map(str::to_string)
1182 .unwrap_or_default()
1183 })
1184}
1185
1186// Size of buffer for non-interactive command input // c:127
1187/// Size of the shell input buffer
1188const SHIN_BUF_SIZE: usize = 8192;
1189
1190/// Re-export of `META` from zsh_h.rs (canonical port of `Src/zsh.h:144`).
1191/// Duplicate declarations of the Meta byte invite drift; keep one
1192/// source of truth.
1193
1194/// Check if a character needs Meta-encoding in the SHIN buffer.
1195///
1196/// Port of `imeta(c)` from `Src/ztype.h:60` — `zistype(c, IMETA)`.
1197/// Per `Src/utils.c:4195-4201`, the IMETA typtab bits are set for:
1198/// - `'\0'` (0x00)
1199/// - `Meta` (0x83)
1200/// - `Pound..=LAST_NORMAL_TOK` (`Bang`) (0x84..=0x9c, ITOK+IMETA)
1201/// - `Snull..=Nularg` (0x9d..=0xa1, ITOK+IMETA+INULL)
1202/// - `Marker` (0xa2)
1203///
1204/// The previous Rust port used `b < 32 || (0x83..=0x9b).contains(&b)`
1205/// which was BOTH:
1206/// - too inclusive (0x01..=0x1f are NOT IMETA per the typtab —
1207/// only 0x00 is); reading control chars from stdin would have
1208/// been spuriously Meta-encoded, corrupting the input buffer
1209/// for SHIN clients that pass them through literally; and
1210/// - too narrow (0x9c, 0x9d..=0xa1, 0xa2 all needed Meta-encoding
1211/// but escaped untouched, then later token-byte readers would
1212/// mis-interpret them as live tokens rather than literal user
1213/// bytes).
1214/// Route through the canonical `ztype_h::imeta` typtab predicate so
1215/// every IMETA test in the codebase agrees.
1216fn imeta(c: char) -> bool {
1217 // c:60 (Src/ztype.h)
1218 let b = c as u32;
1219 if b > 0xff {
1220 return false;
1221 }
1222 crate::ported::ztype_h::imeta(b as u8)
1223}
1224
1225// `InputBuffer` aggregate + thread_local INPUT singleton deleted.
1226// Every field has been split into a thread_local file-scope static
1227// matching the corresponding C global in `Src/input.c`. Every
1228// previously-method-bound fn (`ingetc`, `inungetc`, `inpush`, etc.)
1229// is now a free fn with the C signature. `StringInput` (Rust-only
1230// convenience wrapper) was deleted in a previous commit.
1231
1232#[cfg(test)]
1233mod tests {
1234 use super::*;
1235 use crate::ported::utils::inittyptab;
1236 use crate::ported::ztype_h::TYPTAB_TEST_LOCK;
1237
1238 /// Test-only reset: clear all input statics so per-test setup
1239 /// starts from a clean slate (tests run in the same thread by
1240 /// default and would otherwise leak state between them).
1241 fn reset_input() {
1242 super::inbuf.with(|b| b.borrow_mut().clear());
1243 super::inbufpos.with(|p| p.set(0));
1244 super::inbufct.with(|c| c.set(0));
1245 super::inbufflags.with(|f| f.set(0));
1246 super::lexstop.with(|c| c.set(false));
1247 super::lineno.with(|l| l.set(1));
1248 super::instack.with(|st| st.borrow_mut().clear());
1249 super::pushback.with(|p| p.borrow_mut().clear());
1250 }
1251
1252 #[test]
1253 fn test_input_buffer_basic() {
1254 let _g = crate::test_util::global_state_lock();
1255 reset_input();
1256 inputsetline("hello", 0);
1257 assert_eq!(ingetc(), Some('h'));
1258 assert_eq!(ingetc(), Some('e'));
1259 assert_eq!(ingetc(), Some('l'));
1260 assert_eq!(ingetc(), Some('l'));
1261 assert_eq!(ingetc(), Some('o'));
1262 assert_eq!(ingetc(), None);
1263 }
1264
1265 #[test]
1266 fn test_input_ungetc() {
1267 let _g = crate::test_util::global_state_lock();
1268 reset_input();
1269 inputsetline("abc", 0);
1270 assert_eq!(ingetc(), Some('a'));
1271 assert_eq!(ingetc(), Some('b'));
1272 inungetc('b');
1273 assert_eq!(ingetc(), Some('b'));
1274 assert_eq!(ingetc(), Some('c'));
1275 }
1276
1277 #[test]
1278 fn test_input_stack() {
1279 let _g = crate::test_util::global_state_lock();
1280 reset_input();
1281 inputsetline("outer", 0);
1282 assert_eq!(ingetc(), Some('o'));
1283 inpush("inner", INP_CONT, None);
1284 assert_eq!(ingetc(), Some('i'));
1285 assert_eq!(ingetc(), Some('n'));
1286 assert_eq!(ingetc(), Some('n'));
1287 assert_eq!(ingetc(), Some('e'));
1288 assert_eq!(ingetc(), Some('r'));
1289 assert_eq!(ingetc(), Some('u'));
1290 assert_eq!(ingetc(), Some('t'));
1291 }
1292
1293 #[test]
1294 fn test_line_number_tracking() {
1295 let _g = crate::test_util::global_state_lock();
1296 reset_input();
1297 inputsetline("a\nb\nc", INP_LINENO);
1298 assert_eq!(lineno.with(|l| l.get()), 1);
1299 ingetc(); // a
1300 ingetc(); // \n
1301 assert_eq!(lineno.with(|l| l.get()), 2);
1302 ingetc(); // b
1303 ingetc(); // \n
1304 assert_eq!(lineno.with(|l| l.get()), 3);
1305 }
1306
1307 /// Pin: `imeta(c)` matches the canonical IMETA typtab population
1308 /// at `Src/utils.c:4195-4201`. IMETA is set for:
1309 /// - `'\0'` (c:4195)
1310 /// - `Meta` (0x83) (c:4196)
1311 /// - `Marker` (0xa2) (c:4197)
1312 /// - `Pound..=LAST_NORMAL_TOK` = `0x84..=0x9c` (c:4198, Bang)
1313 /// - `Snull..=Nularg` = `0x9d..=0xa1` (c:4200)
1314 ///
1315 /// Non-IMETA: every ASCII letter, every ASCII digit, AND every
1316 /// control char EXCEPT NUL (0x01..=0x1f are NOT IMETA — the
1317 /// previous Rust port spuriously Meta-encoded them).
1318 #[test]
1319 fn test_meta_encoding() {
1320 let _g = crate::test_util::global_state_lock();
1321 // Tests must initialise the typtab — without `inittyptab()`
1322 // every byte's IMETA bit reads as 0. Serialise against other
1323 // typtab-mutating tests via the canonical lock.
1324 let _g = TYPTAB_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1325 inittyptab();
1326
1327 // c:4195 — '\0' is IMETA.
1328 assert!(imeta('\x00'));
1329 // c:4196 — Meta (0x83) is IMETA.
1330 assert!(imeta('\u{83}'));
1331 // c:4197 — Marker (0xa2) is IMETA.
1332 assert!(imeta('\u{a2}'));
1333 // c:4198 — Pound (0x84) is IMETA via the NORMAL_TOK loop.
1334 assert!(imeta('\u{84}'));
1335 // c:4198 — LAST_NORMAL_TOK = Bang (0x9c) is IMETA.
1336 assert!(imeta('\u{9c}'));
1337 // c:4200 — Snull (0x9d) is IMETA via the NULL_TOK loop.
1338 assert!(imeta('\u{9d}'));
1339 // c:4200 — Nularg (0xa1) is IMETA at the top of the range.
1340 assert!(imeta('\u{a1}'));
1341
1342 // Non-IMETA: ASCII letters / digits.
1343 assert!(!imeta('a'));
1344 assert!(!imeta('Z'));
1345 assert!(!imeta('0'));
1346
1347 // Non-IMETA: control chars OTHER than NUL. The previous
1348 // Rust port hardcoded `b < 32` which erroneously included
1349 // these; canonical C IMETA covers only NUL among control
1350 // chars (c:4195 has `typtab['\0'] |= IMETA;` and nothing
1351 // else in the 0x01..=0x1f range).
1352 assert!(!imeta('\x01'));
1353 assert!(!imeta('\x1f'));
1354 // Above 0xa2 (e.g. 0xa3 onward) is NOT IMETA either.
1355 assert!(!imeta('\u{a3}'));
1356
1357 // Verify the inlined metafy XOR (Src/utils.c:4856 c ^ 32) is
1358 // self-inverting — encode then decode round-trips to the input.
1359 let encoded = char::from_u32(('\x00' as u32) ^ 32).unwrap_or('\x00');
1360 let decoded = char::from_u32((encoded as u32) ^ 32).unwrap_or(encoded);
1361 assert_eq!(decoded, '\x00');
1362 }
1363
1364 #[test]
1365 fn test_ingetptr() {
1366 let _g = crate::test_util::global_state_lock();
1367 reset_input();
1368 inputsetline("hello world", 0);
1369 ingetc(); // h
1370 ingetc(); // e
1371 ingetc(); // l
1372 ingetc(); // l
1373 ingetc(); // o
1374 assert_eq!(ingetptr(), " world");
1375 }
1376
1377 #[test]
1378 fn test_inerrflush() {
1379 let _g = crate::test_util::global_state_lock();
1380 reset_input();
1381 inputsetline("remaining input", 0);
1382 ingetc();
1383 inerrflush();
1384 assert!(lexstop.with(|c| c.get()) || inbufct.with(|c| c.get()) == 0);
1385 }
1386
1387 /// `Src/input.c:159-162` — `shinbufreset` body is
1388 /// `shinbufendptr = shinbufptr = shinbuffer;` — reset both
1389 /// pointers to the start of the buffer. Rust port clears
1390 /// `shinbuffer` (the buffer storage) and zeros `shinbufpos`.
1391 /// Pin both: post-condition is empty buffer + pos==0.
1392 #[test]
1393 fn shinbufreset_clears_buffer_and_zeros_pos() {
1394 let _g = crate::test_util::global_state_lock();
1395 super::shinbuffer.with(|b| {
1396 *b.borrow_mut() = b"leftover".to_vec();
1397 });
1398 super::shinbufpos.with(|p| p.set(7));
1399 super::shinbufreset();
1400 super::shinbuffer.with(|b| {
1401 assert!(
1402 b.borrow().is_empty(),
1403 "c:161 — shinbuffer must be empty after reset"
1404 );
1405 });
1406 assert_eq!(
1407 super::shinbufpos.with(|p| p.get()),
1408 0,
1409 "c:161 — shinbufpos must be 0 after reset"
1410 );
1411 }
1412
1413 /// `Src/input.c:171-175` — `shinbufalloc` body is
1414 /// `shinbuffer = zalloc(SHINBUFSIZE); shinbufreset();`. The
1415 /// Rust port replaces the buffer with a fresh `Vec<u8>` of
1416 /// `SHIN_BUF_SIZE` capacity, then calls `shinbufreset`.
1417 /// Pin the post-condition: empty buffer + pos==0 + capacity
1418 /// hint set.
1419 #[test]
1420 fn shinbufalloc_resets_and_capacity_hints() {
1421 let _g = crate::test_util::global_state_lock();
1422 super::shinbuffer.with(|b| {
1423 *b.borrow_mut() = b"stale".to_vec();
1424 });
1425 super::shinbufpos.with(|p| p.set(3));
1426 super::shinbufalloc();
1427 super::shinbuffer.with(|b| {
1428 assert!(
1429 b.borrow().is_empty(),
1430 "c:173 — fresh shinbuffer must be empty"
1431 );
1432 // Capacity hint is at least 1 (default `String::with_capacity` semantics).
1433 // Not pinning exact value — different libstd versions may round.
1434 assert!(b.borrow().capacity() >= 1);
1435 });
1436 assert_eq!(super::shinbufpos.with(|p| p.get()), 0);
1437 }
1438
1439 /// `Src/input.c:181-194` — `shinbufsave` snapshots the current
1440 /// buffer onto `shinsavestack` and reinitialises via
1441 /// `shinbufalloc`. Pin the round-trip: save with buf="abc",
1442 /// pos=2 → buf reset to "" + stack contains ("abc", 2). Then
1443 /// `shinbufrestore` restores the saved state.
1444 #[test]
1445 fn shinbufsave_restore_round_trip() {
1446 let _g = crate::test_util::global_state_lock();
1447 // Clear stack from any prior test.
1448 super::shinsavestack.with(|s| s.borrow_mut().clear());
1449 super::shinbuffer.with(|b| *b.borrow_mut() = b"abc".to_vec());
1450 super::shinbufpos.with(|p| p.set(2));
1451 super::shinbufsave();
1452 super::shinbuffer.with(|b| {
1453 assert!(
1454 b.borrow().is_empty(),
1455 "c:193 — shinbufsave invokes shinbufalloc (resets to empty)"
1456 );
1457 });
1458 assert_eq!(
1459 super::shinbufpos.with(|p| p.get()),
1460 0,
1461 "c:193 — pos must be 0 after save"
1462 );
1463 super::shinbufrestore();
1464 super::shinbuffer.with(|b| {
1465 assert_eq!(
1466 *b.borrow(),
1467 b"abc",
1468 "c:200-209 — shinbufrestore restores saved buffer"
1469 );
1470 });
1471 assert_eq!(
1472 super::shinbufpos.with(|p| p.get()),
1473 2,
1474 "c:200-209 — shinbufrestore restores saved pos"
1475 );
1476 }
1477
1478 /// `Src/input.c:200` — `shinbufrestore` on an empty save stack
1479 /// must NOT panic (C dereferences `shinsavestack` which would be
1480 /// NULL — UB in C; the Rust port chose to no-op for safety).
1481 /// Pin the no-op so a regression doesn't add a panic.
1482 #[test]
1483 fn shinbufrestore_on_empty_stack_is_noop() {
1484 let _g = crate::test_util::global_state_lock();
1485 super::shinsavestack.with(|s| s.borrow_mut().clear());
1486 // Pre-seed a non-empty buffer.
1487 super::shinbuffer.with(|b| *b.borrow_mut() = b"persist".to_vec());
1488 super::shinbufrestore();
1489 super::shinbuffer.with(|b| {
1490 assert_eq!(
1491 *b.borrow(),
1492 b"persist",
1493 "empty-stack restore must leave buffer untouched"
1494 );
1495 });
1496 }
1497
1498 /// `Src/input.c:328` — `ingetc` skips bytes for which `itok()`
1499 /// (the canonical predicate at `Src/ztype.h:52`) returns true.
1500 /// Per `Src/utils.c:4198-4201`, `inittyptab` sets ITOK on
1501 /// `Pound..LAST_NORMAL_TOK (0x84..0x9c)` and `Snull..Nularg
1502 /// (0x9d..0xa1)` — i.e. the canonical token range is `0x84..=0xa1`.
1503 /// A previous hardcoded `0x83..=0x9b` range was both too inclusive
1504 /// (included 0x83 = Meta lead byte) and too narrow (excluded
1505 /// 0x9c..=0xa1 = Bang/Snull/Dnull/Bnull/Bnullkeep/Nularg).
1506 /// Pin the canonical itok-driven skip via two endpoints:
1507 /// * 0x9c (Bang) — ITOK, must be skipped.
1508 /// * 0xa1 (Nularg) — ITOK, must be skipped.
1509 #[test]
1510 fn ingetc_skips_token_bytes_via_itok_predicate() {
1511 let _g = crate::test_util::global_state_lock();
1512 reset_input();
1513 // Make sure typtab is populated (without this the per-thread
1514 // typtab default may have ITOK bits unset).
1515 inittyptab();
1516
1517 let bang: char = crate::ported::zsh_h::Bang; // LAST_NORMAL_TOK
1518 let nularg: char = crate::ported::zsh_h::Nularg; // last ITOK byte
1519 let mut s = String::new();
1520 s.push('a');
1521 s.push(bang);
1522 s.push('b');
1523 s.push(nularg);
1524 s.push('c');
1525 // Real U+009C text is not a token (crate::token_char): kept.
1526 s.push('\u{009c}');
1527 inputsetline(&s, 0);
1528 // c:328 — itok bytes must be silently skipped; visible
1529 // sequence is "abc".
1530 assert_eq!(ingetc(), Some('a'));
1531 assert_eq!(
1532 ingetc(),
1533 Some('b'),
1534 "c:328 — Bang (0x9c) must be skipped (ITOK bit set per inittyptab)"
1535 );
1536 assert_eq!(
1537 ingetc(),
1538 Some('c'),
1539 "c:328 — Nularg (0xa1) must be skipped (ITOK bit set per inittyptab)"
1540 );
1541 assert_eq!(ingetc(), Some('\u{009c}'), "data U+009C is not the Bang token");
1542 }
1543
1544 /// `Src/input.c:328` — non-token bytes (e.g. Meta=0x83) must NOT
1545 /// be skipped by `ingetc`. Meta is IMETA-only, never ITOK; treating
1546 /// it as a token would corrupt every metafied character read by
1547 /// the lexer. Same for Marker (0xa2) which is IMETA-only per
1548 /// `Src/utils.c:4197` (`typtab[Marker] |= IMETA`, NOT ITOK).
1549 #[test]
1550 fn ingetc_does_not_skip_imeta_only_bytes() {
1551 let _g = crate::test_util::global_state_lock();
1552 reset_input();
1553 inittyptab();
1554 let meta: char = '\u{0083}'; // Meta lead byte — IMETA only
1555 let marker: char = crate::ported::zsh_h::Marker; // IMETA only per c:4197
1556 let mut s = String::new();
1557 s.push('x');
1558 s.push(meta);
1559 s.push('y');
1560 s.push(marker);
1561 s.push('z');
1562 inputsetline(&s, 0);
1563 assert_eq!(ingetc(), Some('x'));
1564 assert_eq!(
1565 ingetc(),
1566 Some(meta),
1567 "c:328 — Meta (0x83) is IMETA-only, NOT ITOK; must pass through"
1568 );
1569 assert_eq!(ingetc(), Some('y'));
1570 assert_eq!(
1571 ingetc(),
1572 Some(marker),
1573 "c:328 / c:4197 — Marker (0xa2) is IMETA-only, NOT ITOK; must pass through"
1574 );
1575 assert_eq!(ingetc(), Some('z'));
1576 }
1577
1578 // ═══════════════════════════════════════════════════════════════════
1579 // Input-buffer behavior — additional edge cases for ingetc / inungetc
1580 // / inputsetline / inpush. These pin behavior that existing tests
1581 // don't cover: empty input, multi-byte ungetc, stack pop sequences,
1582 // line-number accumulation across pushes.
1583 // ═══════════════════════════════════════════════════════════════════
1584
1585 /// Empty input → first ingetc returns None.
1586 #[test]
1587 fn input_empty_line_yields_none_first_read() {
1588 let _g = crate::test_util::global_state_lock();
1589 reset_input();
1590 inputsetline("", 0);
1591 assert_eq!(ingetc(), None);
1592 }
1593
1594 /// Reading past end keeps returning None (eventually — zshrs may
1595 /// append a trailing separator like space/newline before the buffer
1596 /// drains, then None thereafter). Pin: eventually None, no panic.
1597 #[test]
1598 fn input_repeated_reads_past_end_keep_returning_none_eventually() {
1599 let _g = crate::test_util::global_state_lock();
1600 reset_input();
1601 inputsetline("a", 0);
1602 assert_eq!(ingetc(), Some('a'));
1603 // Skip any trailing separator (zshrs's inputsetline may append).
1604 let mut seen_none = false;
1605 for _ in 0..10 {
1606 if ingetc().is_none() {
1607 seen_none = true;
1608 break;
1609 }
1610 }
1611 assert!(seen_none, "should reach None within 10 reads past end");
1612 // After None, subsequent reads must stay None.
1613 for _ in 0..3 {
1614 assert_eq!(ingetc(), None);
1615 }
1616 }
1617
1618 /// inungetc on a fresh buffer makes the ungot char the next read.
1619 #[test]
1620 fn input_inungetc_before_any_read() {
1621 let _g = crate::test_util::global_state_lock();
1622 reset_input();
1623 inputsetline("xyz", 0);
1624 inungetc('Q');
1625 assert_eq!(ingetc(), Some('Q'));
1626 assert_eq!(ingetc(), Some('x'));
1627 }
1628
1629 /// Multiple inungetc calls stack LIFO (last in first out).
1630 #[test]
1631 fn input_inungetc_lifo_order() {
1632 let _g = crate::test_util::global_state_lock();
1633 reset_input();
1634 inputsetline("z", 0);
1635 inungetc('A');
1636 inungetc('B');
1637 inungetc('C');
1638 // Last pushed comes out first.
1639 assert_eq!(ingetc(), Some('C'));
1640 assert_eq!(ingetc(), Some('B'));
1641 assert_eq!(ingetc(), Some('A'));
1642 assert_eq!(ingetc(), Some('z'));
1643 }
1644
1645 /// inpush then inpoptop returns to the outer buffer.
1646 #[test]
1647 fn input_inpush_then_pop_returns_to_outer() {
1648 let _g = crate::test_util::global_state_lock();
1649 reset_input();
1650 inputsetline("outer", 0);
1651 assert_eq!(ingetc(), Some('o'));
1652 inpush("inner", INP_CONT, None);
1653 assert_eq!(ingetc(), Some('i'));
1654 inpoptop();
1655 // After popping the inner buffer, the next read continues from
1656 // the outer buffer where it left off.
1657 assert_eq!(ingetc(), Some('u'));
1658 }
1659
1660 /// inputsetline with non-empty replaces buffer cleanly after empty.
1661 /// (Skip the post-empty read since zshrs leaves the empty-set buffer
1662 /// state opaque — just drain any residue, then set new buffer.)
1663 #[test]
1664 fn input_set_empty_then_set_nonempty_reads_new_content() {
1665 let _g = crate::test_util::global_state_lock();
1666 reset_input();
1667 inputsetline("", 0);
1668 // Drain whatever zshrs left in the empty-set buffer.
1669 for _ in 0..5 {
1670 if ingetc().is_none() {
1671 break;
1672 }
1673 }
1674 inputsetline("xyz", 0);
1675 // Now the next reads should yield 'x', 'y', 'z' in order.
1676 assert_eq!(ingetc(), Some('x'));
1677 assert_eq!(ingetc(), Some('y'));
1678 assert_eq!(ingetc(), Some('z'));
1679 }
1680
1681 /// Reads return exactly the input bytes for ASCII content.
1682 #[test]
1683 fn input_ascii_passthrough_byte_for_byte() {
1684 let _g = crate::test_util::global_state_lock();
1685 reset_input();
1686 let src = "the quick brown fox 0123!";
1687 inputsetline(src, 0);
1688 let mut got = String::new();
1689 while let Some(c) = ingetc() {
1690 got.push(c);
1691 }
1692 assert_eq!(got, src);
1693 }
1694
1695 /// Line-number tracking gate: per `c:Src/input.c:330` —
1696 /// `if (((inbufflags & INP_LINENO) || !strin) && lastc == '\n') lineno++;`
1697 /// The gate fires when EITHER the flag is set OR we're not in
1698 /// string-input mode. The "no INP_LINENO" path only suppresses
1699 /// the advance when `strin != 0` (string-input mode like eval).
1700 /// Test pins both the strin=1 suppression (this test) and the
1701 /// strin=0 always-advance shape — the latter is covered by
1702 /// input_lineno_flag_increments_on_each_newline below.
1703 #[test]
1704 fn input_no_lineno_flag_means_lineno_unchanged_on_newline_anchored() {
1705 let _g = crate::test_util::global_state_lock();
1706 reset_input();
1707 // c:330 gate: !strin path triggers the advance even when the
1708 // INP_LINENO flag is unset. Set strin=1 so the gate's `||`
1709 // short-circuit relies on the flag alone; with flag=0 lineno
1710 // must stay put.
1711 let saved_strin = strin.with(|s| s.get());
1712 strin.with(|s| s.set(1));
1713 let start = lineno.with(|l| l.get());
1714 inputsetline("a\nb\n", 0);
1715 while ingetc().is_some() {}
1716 let end = lineno.with(|l| l.get());
1717 strin.with(|s| s.set(saved_strin));
1718 assert_eq!(end, start, "c:330 — strin && !INP_LINENO → lineno stable");
1719 }
1720
1721 /// INP_LINENO flag → lineno advances by the number of `\n`s.
1722 #[test]
1723 fn input_lineno_flag_increments_on_each_newline() {
1724 let _g = crate::test_util::global_state_lock();
1725 reset_input();
1726 inputsetline("x\ny\nz\n", INP_LINENO);
1727 let start = lineno.with(|l| l.get());
1728 while ingetc().is_some() {}
1729 let end = lineno.with(|l| l.get());
1730 assert_eq!(end - start, 3, "three `\\n`s should advance lineno by 3");
1731 }
1732
1733 /// Pushing on top of a partially-read buffer reads inner first.
1734 #[test]
1735 fn input_inpush_priorities_inner_over_outer() {
1736 let _g = crate::test_util::global_state_lock();
1737 reset_input();
1738 inputsetline("abc", 0);
1739 // Don't read anything yet
1740 inpush("123", INP_CONT, None);
1741 // Inner exhausts first
1742 assert_eq!(ingetc(), Some('1'));
1743 assert_eq!(ingetc(), Some('2'));
1744 assert_eq!(ingetc(), Some('3'));
1745 // Then outer
1746 assert_eq!(ingetc(), Some('a'));
1747 assert_eq!(ingetc(), Some('b'));
1748 assert_eq!(ingetc(), Some('c'));
1749 }
1750
1751 /// Multi-byte UTF-8 char passes through correctly.
1752 #[test]
1753 fn input_multibyte_utf8_char_passes_through() {
1754 let _g = crate::test_util::global_state_lock();
1755 reset_input();
1756 inputsetline("日", 0);
1757 // The char `日` is a single Unicode codepoint; ingetc should
1758 // return it as a single Option<char>.
1759 let c = ingetc();
1760 assert_eq!(c, Some('日'));
1761 assert_eq!(ingetc(), None);
1762 }
1763
1764 // ─── zsh-corpus pins for input buffer behavior ─────────────────
1765
1766 /// Reading all of "abc" returns characters in order then None.
1767 #[test]
1768 fn input_corpus_ascii_returns_chars_in_order() {
1769 let _g = crate::test_util::global_state_lock();
1770 reset_input();
1771 inputsetline("abc", 0);
1772 assert_eq!(ingetc(), Some('a'));
1773 assert_eq!(ingetc(), Some('b'));
1774 assert_eq!(ingetc(), Some('c'));
1775 assert_eq!(ingetc(), None);
1776 }
1777
1778 /// `inungetc` when `inbufpos > 0` just rewinds position — the
1779 /// passed char is IGNORED in favor of the buffer content at
1780 /// `pos - 1`. Per `Src/input.c:546-555`, zsh assumes callers
1781 /// unget the char they just got. Pin this quirk.
1782 #[test]
1783 fn input_corpus_inungetc_after_read_rewinds_to_buf_char() {
1784 let _g = crate::test_util::global_state_lock();
1785 reset_input();
1786 inputsetline("xy", 0);
1787 let _ = ingetc(); // consume 'x' → pos=1
1788 inungetc('Z'); // pos→0; 'Z' silently dropped per C contract
1789 assert_eq!(
1790 ingetc(),
1791 Some('x'),
1792 "buf[pos-1] returned, NOT the unget char"
1793 );
1794 assert_eq!(ingetc(), Some('y'));
1795 }
1796
1797 /// `inungetc` of multiple chars: LIFO order.
1798 #[test]
1799 fn input_corpus_inungetc_multiple_lifo() {
1800 let _g = crate::test_util::global_state_lock();
1801 reset_input();
1802 inputsetline("end", 0);
1803 inungetc('1');
1804 inungetc('2');
1805 inungetc('3');
1806 assert_eq!(ingetc(), Some('3'), "last-pushed first");
1807 assert_eq!(ingetc(), Some('2'));
1808 assert_eq!(ingetc(), Some('1'));
1809 assert_eq!(ingetc(), Some('e'), "then original buffer");
1810 }
1811
1812 /// Empty inputsetline → ingetc returns None immediately.
1813 #[test]
1814 fn input_corpus_empty_line_returns_none() {
1815 let _g = crate::test_util::global_state_lock();
1816 reset_input();
1817 inputsetline("", 0);
1818 assert_eq!(ingetc(), None);
1819 }
1820
1821 /// Multi-codepoint UTF-8 string: each codepoint returned once.
1822 #[test]
1823 fn input_corpus_multibyte_two_codepoints() {
1824 let _g = crate::test_util::global_state_lock();
1825 reset_input();
1826 inputsetline("日本", 0);
1827 assert_eq!(ingetc(), Some('日'));
1828 assert_eq!(ingetc(), Some('本'));
1829 assert_eq!(ingetc(), None);
1830 }
1831
1832 // ═══════════════════════════════════════════════════════════════════
1833 // C-parity tests pinning Src/input.c. Tests that capture KNOWN
1834 // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
1835 // ═══════════════════════════════════════════════════════════════════
1836
1837 /// `ingetc` after `inputsetline("")` returns None (empty input).
1838 /// C `Src/input.c:247` returns -1 (EOF) when input is exhausted.
1839 #[test]
1840 fn ingetc_empty_input_returns_none() {
1841 let _g = crate::test_util::global_state_lock();
1842 reset_input();
1843 inputsetline("", 0);
1844 assert_eq!(ingetc(), None, "empty input → None (EOF)");
1845 }
1846
1847 /// `inungetc` puts a char back so the next `ingetc` returns it.
1848 /// C `Src/input.c:360`.
1849 #[test]
1850 fn inungetc_then_ingetc_returns_pushed_char() {
1851 let _g = crate::test_util::global_state_lock();
1852 reset_input();
1853 inputsetline("XY", 0);
1854 let first = ingetc().unwrap();
1855 assert_eq!(first, 'X');
1856 inungetc(first);
1857 assert_eq!(ingetc(), Some('X'), "inungetc'd byte comes back");
1858 assert_eq!(ingetc(), Some('Y'), "then the normal next byte");
1859 }
1860
1861 /// `ingetc` after EOF returns None on repeated calls (not stuck).
1862 #[test]
1863 fn ingetc_repeated_after_eof_stays_none() {
1864 let _g = crate::test_util::global_state_lock();
1865 reset_input();
1866 inputsetline("A", 0);
1867 assert_eq!(ingetc(), Some('A'));
1868 assert_eq!(ingetc(), None);
1869 assert_eq!(ingetc(), None, "repeated EOF → still None");
1870 assert_eq!(ingetc(), None);
1871 }
1872
1873 /// `inerrflush` clears any pending input buffer without panic.
1874 /// C `Src/input.c:455` — idempotent flush.
1875 #[test]
1876 fn inerrflush_is_safe_to_call_multiple_times() {
1877 let _g = crate::test_util::global_state_lock();
1878 reset_input();
1879 inerrflush();
1880 inerrflush();
1881 inerrflush();
1882 }
1883
1884 // ═══════════════════════════════════════════════════════════════════
1885 // Additional C-parity tests for Src/input.c ingetc + inungetc +
1886 // inputsetline round-trip.
1887 // ═══════════════════════════════════════════════════════════════════
1888
1889 /// c:318 — `ingetc` on empty buffer returns None.
1890 #[test]
1891 fn ingetc_empty_buffer_returns_none() {
1892 let _g = crate::test_util::global_state_lock();
1893 reset_input();
1894 inputsetline("", 0);
1895 let _ = ingetc(); // may consume or return None
1896 // No panic = pass.
1897 }
1898
1899 /// c:318 — `inputsetline(s) + ingetc` walks every byte in order.
1900 #[test]
1901 fn inputsetline_ingetc_round_trip_three_chars() {
1902 let _g = crate::test_util::global_state_lock();
1903 reset_input();
1904 inputsetline("abc", 0);
1905 assert_eq!(ingetc(), Some('a'));
1906 assert_eq!(ingetc(), Some('b'));
1907 assert_eq!(ingetc(), Some('c'));
1908 assert_eq!(ingetc(), None, "after exhausting buffer → None");
1909 }
1910
1911 /// c:546 — `inungetc` when pos > 0 REWINDS the buffer position;
1912 /// the argument `c` is IGNORED in that branch (matches C's
1913 /// `inbufptr--, inbufct++` rewind semantics). The buffer char
1914 /// at the rewound position comes back, not the argument.
1915 #[test]
1916 fn inungetc_when_pos_positive_rewinds_buffer_ignoring_arg() {
1917 let _g = crate::test_util::global_state_lock();
1918 reset_input();
1919 inputsetline("X", 0);
1920 assert_eq!(ingetc(), Some('X'));
1921 // pos > 0 now → inungetc('Y') rewinds, putting 'X' back.
1922 inungetc('Y');
1923 assert_eq!(
1924 ingetc(),
1925 Some('X'),
1926 "inungetc with pos>0 rewinds buf; arg 'Y' is ignored — original 'X' returns"
1927 );
1928 }
1929
1930 /// c:546 — `inungetc` while lexstop is set is a no-op (early return).
1931 /// Subsequent ingetc still returns None.
1932 #[test]
1933 fn inungetc_while_lexstop_is_noop() {
1934 let _g = crate::test_util::global_state_lock();
1935 reset_input();
1936 inputsetline("a", 0);
1937 let _ = ingetc(); // consume 'a'
1938 let _ = ingetc(); // hit EOF → lexstop set
1939 // inungetc must not panic; subsequent ingetc still None.
1940 inungetc('Z');
1941 // Result depends on whether port restored lexstop; pin no panic.
1942 }
1943
1944 /// c:546 — multiple inungetc calls preserve LIFO order.
1945 #[test]
1946 fn inungetc_multiple_chars_lifo_order() {
1947 let _g = crate::test_util::global_state_lock();
1948 reset_input();
1949 inputsetline("end", 0);
1950 inungetc('A');
1951 inungetc('B'); // pushed last → comes out first
1952 assert_eq!(ingetc(), Some('B'), "LIFO: B pushed last comes first");
1953 assert_eq!(ingetc(), Some('A'));
1954 // Then original buffer continues.
1955 assert_eq!(ingetc(), Some('e'));
1956 }
1957
1958 /// c:510 — `inputsetline("", _)` sets empty buffer; ingetc → None.
1959 #[test]
1960 fn inputsetline_empty_then_ingetc_returns_none() {
1961 let _g = crate::test_util::global_state_lock();
1962 reset_input();
1963 inputsetline("", 0);
1964 assert_eq!(ingetc(), None);
1965 }
1966
1967 /// c:455 — `inerrflush` after partial consumption is safe (doesn't
1968 /// panic or leave inconsistent state).
1969 #[test]
1970 fn inerrflush_after_partial_consume_is_safe() {
1971 let _g = crate::test_util::global_state_lock();
1972 reset_input();
1973 inputsetline("hello", 0);
1974 let _ = ingetc();
1975 let _ = ingetc();
1976 inerrflush();
1977 // No panic = pass.
1978 }
1979
1980 /// c:510 — `inputsetline` resets lexstop so the new buffer is readable
1981 /// after a prior EOF.
1982 #[test]
1983 fn inputsetline_resets_lexstop_for_new_buffer() {
1984 let _g = crate::test_util::global_state_lock();
1985 reset_input();
1986 inputsetline("a", 0);
1987 let _ = ingetc(); // consume a
1988 let _ = ingetc(); // EOF, sets lexstop
1989 // New inputsetline should make new content readable.
1990 inputsetline("b", 0);
1991 assert_eq!(ingetc(), Some('b'), "lexstop reset by inputsetline");
1992 }
1993
1994 // ═══════════════════════════════════════════════════════════════════
1995 // Additional C-parity tests for Src/input.c
1996 // c:63 shinbufreset / c:247 ingetc / c:326 inputline / c:441 stuff /
1997 // c:467 inpush / c:594 inpopalias / etc.
1998 // ═══════════════════════════════════════════════════════════════════
1999
2000 /// c:247 — `ingetc` returns Option<char> (compile-time type pin).
2001 #[test]
2002 fn ingetc_returns_option_char_type() {
2003 let _g = crate::test_util::global_state_lock();
2004 reset_input();
2005 inputsetline("x", 0);
2006 let _: Option<char> = ingetc();
2007 }
2008
2009 /// c:366 — `inputline` returns int (0=line installed, 1=EOF).
2010 #[test]
2011 fn inputline_returns_int_status() {
2012 let _g = crate::test_util::global_state_lock();
2013 reset_input();
2014 let _: i32 = inputline();
2015 }
2016
2017 /// c:603 — `ingetptr` returns String.
2018 #[test]
2019 fn ingetptr_returns_string_type() {
2020 let _g = crate::test_util::global_state_lock();
2021 reset_input();
2022 let _: String = ingetptr();
2023 }
2024
2025 /// c:594 — `inpopalias()` is safe on empty stack.
2026 #[test]
2027 fn inpopalias_empty_stack_no_panic() {
2028 let _g = crate::test_util::global_state_lock();
2029 reset_input();
2030 inpopalias();
2031 inpopalias();
2032 }
2033
2034 /// c:467 — `inpush(empty, 0, None)` is safe.
2035 #[test]
2036 fn inpush_empty_string_no_panic() {
2037 let _g = crate::test_util::global_state_lock();
2038 reset_input();
2039 inpush("", 0, None);
2040 inpush("", 0, Some(("alias_name".to_string(), 0)));
2041 }
2042
2043 /// c:63 — `shinbufreset` is idempotent.
2044 #[test]
2045 fn shinbufreset_idempotent() {
2046 let _g = crate::test_util::global_state_lock();
2047 for _ in 0..10 {
2048 shinbufreset();
2049 }
2050 }
2051
2052 /// c:144 — `shinbufalloc` is idempotent.
2053 #[test]
2054 fn shinbufalloc_idempotent() {
2055 let _g = crate::test_util::global_state_lock();
2056 for _ in 0..10 {
2057 shinbufalloc();
2058 }
2059 }
2060
2061 /// c:156-168 — shinbufsave + shinbufrestore round-trip safe.
2062 #[test]
2063 fn shinbufsave_restore_round_trip_safe() {
2064 let _g = crate::test_util::global_state_lock();
2065 reset_input();
2066 shinbufsave();
2067 shinbufrestore();
2068 }
2069
2070 /// c:441 — `stuff(nonexistent)` returns nonzero (file not found).
2071 #[test]
2072 fn stuff_nonexistent_path_returns_nonzero() {
2073 let _g = crate::test_util::global_state_lock();
2074 let r = stuff("/__nonexistent_zshrs_xyz__");
2075 assert_ne!(r, 0, "nonexistent path → error");
2076 }
2077
2078 /// c:392 — `zstuff(nonexistent)` returns Err.
2079 #[test]
2080 fn zstuff_nonexistent_path_returns_err() {
2081 let _g = crate::test_util::global_state_lock();
2082 let r = zstuff("/__nonexistent_zshrs_xyz__");
2083 assert!(r.is_err(), "nonexistent path → Err");
2084 }
2085
2086 /// c:213 — `shingetline` returns String.
2087 #[test]
2088 fn shingetline_returns_string_type() {
2089 let _g = crate::test_util::global_state_lock();
2090 reset_input();
2091 let _: String = shingetline();
2092 }
2093
2094 // ═══════════════════════════════════════════════════════════════════
2095 // Additional C-parity tests for Src/input.c
2096 // c:326 inputline / c:337 inputsetline / c:360 inungetc /
2097 // c:392 zstuff / c:441 stuff / c:455 inerrflush / c:467 inpush /
2098 // c:181 shingetchar / c:603 ingetptr
2099 // ═══════════════════════════════════════════════════════════════════
2100
2101 /// c:181 — `shingetchar` returns i32 (compile-time type pin, alt name).
2102 #[test]
2103 fn shingetchar_returns_i32_alt_pin() {
2104 let _g = crate::test_util::global_state_lock();
2105 reset_input();
2106 let _: i32 = shingetchar();
2107 }
2108
2109 /// c:337 — `inputsetline("", 0)` is safe + resets lexstop.
2110 #[test]
2111 fn inputsetline_empty_no_panic() {
2112 let _g = crate::test_util::global_state_lock();
2113 reset_input();
2114 inputsetline("", 0);
2115 }
2116
2117 /// c:337 — `inputsetline(s, INP_CONT)` accumulates inbufct
2118 /// vs the non-CONT replacement variant. Pin both paths exist.
2119 #[test]
2120 fn inputsetline_cont_vs_replace_both_safe() {
2121 let _g = crate::test_util::global_state_lock();
2122 reset_input();
2123 inputsetline("hello", 0);
2124 inputsetline("world", INP_CONT);
2125 }
2126
2127 /// c:360 — `inungetc` on fresh state without prior get pushes back.
2128 #[test]
2129 fn inungetc_on_empty_pushes_back_no_panic() {
2130 let _g = crate::test_util::global_state_lock();
2131 reset_input();
2132 inungetc('x');
2133 inungetc('\n');
2134 }
2135
2136 /// c:360 — `inungetc` is safe across many calls (no overflow).
2137 #[test]
2138 fn inungetc_many_calls_no_panic() {
2139 let _g = crate::test_util::global_state_lock();
2140 reset_input();
2141 for c in "abcdefghij".chars() {
2142 inungetc(c);
2143 }
2144 }
2145
2146 /// c:441 — `stuff` returns i32 (compile-time pin).
2147 #[test]
2148 fn stuff_returns_i32_type() {
2149 let _g = crate::test_util::global_state_lock();
2150 let _: i32 = stuff("/__nonexistent__");
2151 }
2152
2153 /// c:441 — `stuff(empty)` returns nonzero (empty path is invalid).
2154 #[test]
2155 fn stuff_empty_path_returns_nonzero() {
2156 let _g = crate::test_util::global_state_lock();
2157 assert_ne!(stuff(""), 0, "empty path must be an error");
2158 }
2159
2160 /// c:392 — `zstuff(empty)` returns Err.
2161 #[test]
2162 fn zstuff_empty_path_returns_err() {
2163 let _g = crate::test_util::global_state_lock();
2164 assert!(zstuff("").is_err(), "empty path → Err");
2165 }
2166
2167 /// c:455 — `inerrflush` is idempotent (callable repeatedly).
2168 #[test]
2169 fn inerrflush_idempotent() {
2170 let _g = crate::test_util::global_state_lock();
2171 for _ in 0..10 {
2172 inerrflush();
2173 }
2174 }
2175
2176 /// c:467 — `inpush(s, 0, None)` with various strings is safe.
2177 #[test]
2178 fn inpush_various_strings_safe() {
2179 let _g = crate::test_util::global_state_lock();
2180 reset_input();
2181 for s in &["a", "longer text", "\n", "\t", " "] {
2182 inpush(s, 0, None);
2183 }
2184 }
2185
2186 /// c:467 — `inpush` with alias name doesn't panic.
2187 #[test]
2188 fn inpush_with_alias_name_no_panic() {
2189 let _g = crate::test_util::global_state_lock();
2190 reset_input();
2191 inpush("expanded", 0, Some(("ll".to_string(), 0)));
2192 }
2193
2194 /// c:523/580 — `inpoptop` + `inpop` are no-ops on empty stack.
2195 #[test]
2196 fn inpoptop_inpop_empty_stack_no_panic() {
2197 let _g = crate::test_util::global_state_lock();
2198 reset_input();
2199 inpoptop();
2200 inpop();
2201 inpoptop();
2202 inpop();
2203 }
2204
2205 // ═══════════════════════════════════════════════════════════════════
2206 // Additional C-parity pins for Src/input.c
2207 // c:63 shinbufreset / c:144 shinbufalloc / c:156 shinbufsave /
2208 // c:213 shingetline / c:326 inputline / c:392 zstuff /
2209 // c:467 inpush / c:594 inpopalias / c:603 ingetptr / c:455 inerrflush
2210 // ═══════════════════════════════════════════════════════════════════
2211
2212 /// c:63 — `shinbufreset` is idempotent (safe to call repeatedly).
2213 #[test]
2214 fn shinbufreset_idempotent_repeated_calls() {
2215 let _g = crate::test_util::global_state_lock();
2216 for _ in 0..10 {
2217 shinbufreset();
2218 }
2219 }
2220
2221 /// c:144 — `shinbufalloc` is idempotent.
2222 #[test]
2223 fn shinbufalloc_idempotent_repeated_calls() {
2224 let _g = crate::test_util::global_state_lock();
2225 for _ in 0..10 {
2226 shinbufalloc();
2227 }
2228 }
2229
2230 /// c:156/168 — save/restore round-trip is safe (alt).
2231 #[test]
2232 fn shinbufsave_restore_round_trip_safe_alt() {
2233 let _g = crate::test_util::global_state_lock();
2234 shinbufsave();
2235 shinbufrestore();
2236 shinbufsave();
2237 shinbufrestore();
2238 }
2239
2240 /// c:213 — `shingetline` returns String type.
2241 #[test]
2242 fn shingetline_returns_string_type_alt() {
2243 let _g = crate::test_util::global_state_lock();
2244 let _: String = shingetline();
2245 }
2246
2247 /// c:366 — `inputline` returns int type (alt).
2248 #[test]
2249 fn inputline_returns_int_status_alt() {
2250 let _g = crate::test_util::global_state_lock();
2251 let _: i32 = inputline();
2252 }
2253
2254 /// c:366 — `inputline` is deterministic on idle state.
2255 #[test]
2256 fn inputline_deterministic_on_idle_state() {
2257 let _g = crate::test_util::global_state_lock();
2258 reset_input();
2259 let a = inputline();
2260 let b = inputline();
2261 assert_eq!(a, b, "inputline must be deterministic on idle state");
2262 }
2263
2264 /// c:392 — `zstuff` returns Result<(String, i64), i32>.
2265 #[test]
2266 fn zstuff_returns_result_type_compile_pin() {
2267 let _g = crate::test_util::global_state_lock();
2268 let _: Result<(String, i64), i32> = zstuff("");
2269 }
2270
2271 /// c:392 — `zstuff("/dev/null")` returns Ok with empty content.
2272 #[test]
2273 fn zstuff_dev_null_returns_ok_empty() {
2274 let _g = crate::test_util::global_state_lock();
2275 let r = zstuff("/dev/null");
2276 match r {
2277 Ok((s, _)) => assert!(s.is_empty(), "/dev/null content must be empty"),
2278 Err(_) => {} // /dev/null may not exist on some CI; accept either
2279 }
2280 }
2281
2282 /// c:467 — `inpush("")` empty string safe (alt).
2283 #[test]
2284 fn inpush_empty_string_no_panic_alt() {
2285 let _g = crate::test_util::global_state_lock();
2286 reset_input();
2287 inpush("", 0, None);
2288 }
2289
2290 /// c:594 — `inpopalias` on empty stack safe (alt).
2291 #[test]
2292 fn inpopalias_empty_stack_no_panic_alt() {
2293 let _g = crate::test_util::global_state_lock();
2294 reset_input();
2295 inpopalias();
2296 inpopalias();
2297 }
2298
2299 /// c:603 — `ingetptr` returns String type (alt).
2300 #[test]
2301 fn ingetptr_returns_string_type_alt() {
2302 let _g = crate::test_util::global_state_lock();
2303 let _: String = ingetptr();
2304 }
2305
2306 /// c:455 — `inerrflush` returns void (compile-time pin).
2307 #[test]
2308 fn inerrflush_returns_void_type() {
2309 let _g = crate::test_util::global_state_lock();
2310 let _: () = inerrflush();
2311 }
2312}