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