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}