Skip to main content

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}