Skip to main content

zsh/ported/
utils.rs

1//! Utility functions for zshrs
2//!
3//! Port from zsh/Src/utils.c
4//!
5//! Provides miscellaneous utilities: error handling, file operations,
6//! string utilities, and character classification.
7
8use std::ffi::CString;
9use std::fs;
10use std::io::{self, Read, Write};
11use std::os::unix::fs::{MetadataExt, PermissionsExt};
12use std::os::unix::io::{AsRawFd, RawFd};
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicI64, Ordering};
15use std::sync::Mutex;
16use std::time::UNIX_EPOCH;
17
18use crate::init::zleentry;
19use crate::params::getsparam_u;
20use crate::ported::builtin::{BUILTINS, SFCONTEXT, STOPMSG};
21use crate::ported::compat::{u9_iswprint, zchdir, zgetdir};
22use crate::ported::hashnameddir::{nameddirtab, removenameddirnode};
23use crate::ported::hashtable::shfunctab_lock;
24use crate::ported::hist::{bangchar, chrealpath};
25use crate::ported::init::SHTTY;
26use crate::ported::modules::clone::{coprocin, coprocout, mypgrp};
27use crate::{DPUTS, DPUTS1};
28use libc::{
29    S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP,
30    S_IXOTH, S_IXUSR,
31};
32// SHTTY imported under an alias to avoid collision with the
33// `SHTTY: i32` function parameters at fdsettyinfo/fdgettyinfo
34// (Rule E — C uses SHTTY as both the global and the parameter name).
35use crate::ported::lex::lineno;
36use crate::ported::options::{dosetopt, opt_state_set};
37use crate::ported::params::{
38    assignsparam, convbase as convbase_param, getaparam, getsparam, homesetfn, ifsgetfn, ifssetfn,
39    isident, locallevel as LOCALLEVEL, setaparam, setiparam, setsparam, wordcharsgetfn,
40    wordcharssetfn,
41};
42use crate::ported::signals::{queue_signals, unqueue_signals};
43use crate::ported::string::dupstrpfx;
44use crate::ported::zsh_h::{
45    dirsav, hashnode, interact, isset, jobbing, nameddir, opt_name, shfunc, unset, Dash, Marker,
46    Meta, Nularg, Pound, Snull, AUTONAMEDIRS, BANGHIST, BEEP, CHASELINKS, CSHJUNKIEQUOTES,
47    DEFAULT_IFS, DVORAK, EMULATE_KSH, EMULATE_SH, EMULATION, EXTENDEDGLOB, FDT_EXTERNAL, FDT_FLOCK,
48    FDT_FLOCK_EXEC, FDT_INTERNAL, FDT_MODULE, FDT_UNUSED, GLOBDOTS, HISTFLAG_NOEXEC,
49    LAST_NORMAL_TOK, MAGICEQUALSUBST, MULTIBYTE, ND_NOABBREV, ND_USERNAME, NICEFLAG_HEAP,
50    NICEFLAG_NODUP, NICEFLAG_QUOTE, OCTALZEROES, PATCHARS, POSIXIDENTIFIERS, PRINTEIGHTBIT,
51    QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_BACKTICK, QT_DOLLARS, QT_DOUBLE,
52    QT_NONE, QT_SINGLE, QT_SINGLE_OPTIONAL, RCQUOTES, RMSTARWAIT, SFC_SUBST, SHINSTDIN, SPECCHARS,
53    XTRACE, ZLE_CMD_TRASH,
54};
55use crate::ported::zsh_h::{
56    Inbrace, Inbrack, Inpar, Inparmath, Outbrace, Outbrack, Outpar, Outparmath, Qstring, Qtick,
57    Stringg, Tick,
58};
59use crate::ported::zsh_system_h::DEFAULT_WORDCHARS;
60use crate::ported::ztype_h::{
61    imeta, itok, iwsep, IALNUM, IALPHA, IBLANK, ICNTRL, IDIGIT, IIDENT, IMETA, INBLANK, INULL,
62    IPATTERN, ISEP, ISPECIAL, ITOK, IUSER, IWORD, IWSEP, TYPTAB, TYPTAB_FLAGS, ZISPRINT,
63    ZTF_BANGCHAR, ZTF_INIT, ZTF_INTERACT, ZTF_SP_COMMA,
64};
65
66/// Set a wide-char array from a multibyte source string.
67/// Port of `set_widearray(char *mb_array, Widechar_array wca)` from `Src/utils.c:69`.
68///
69/// ```c
70/// static void set_widearray(char *mb_array, Widechar_array wca) {
71///     if (wca->chars) free(wca->chars);
72///     wca->len = 0;
73///     if (mb_array) {
74///         while (*mb_array) {
75///             if (unsigned char *mb_array <= 0x7f) {
76///                 *wcptr++ = (wchar_t)*mb_array++;
77///                 continue;
78///             }
79///             mblen = mb_metacharlenconv(mb_array, &wci);
80///             if (!mblen) break;
81///             if (wci == WEOF) return;  // any non-convertible aborts
82///             *wcptr++ = (wchar_t)wci;
83///             mb_array += mblen;
84///         }
85///         wca->chars = malloc(...); wca->len = wcptr - tmpwcs;
86///     }
87/// }
88/// ```
89///
90/// Build a wide-char array from a metafied multibyte source string.
91/// C uses `mb_metacharlenconv()` to walk Meta-encoded sequences;
92/// Rust port unmetafies first, then collects chars (the
93/// equivalent: walk Unicode codepoints).
94///
95/// Returns the new vec; caller assigns to the appropriate slot
96/// (`WORDCHARS_w`, `IFS_w`, etc.). C aborts on non-convertible
97/// chars (returns without setting `wca->chars`); Rust port mirrors
98/// by returning empty Vec when conversion fails.
99/// WARNING: param names don't match C — Rust=(mb_array) vs C=(mb_array, wca)
100// Rust idiom replacement: `unmetafy` + `str::chars` covers the C
101// `mbsrtowcs`+`mbstate_t` conversion; the C `wca` out-param drops
102// since the Vec is returned by value.
103/// Port of `set_widearray()` from `Src/utils.c:69`.
104///
105/// `set_widearray` — see implementation.
106/// `static struct widechar_array ifs_wide;` (c:Src/utils.c:64) — `$IFS` as
107/// wide characters, rebuilt by `inittyptab` under MULTIBYTE (c:4207-4213).
108/// `wcsitype(c, ISEP)` matches a non-ASCII character against it (c:4367);
109/// ASCII characters use the typtab ISEP bit.
110pub static IFS_WIDE: std::sync::RwLock<Vec<char>> = std::sync::RwLock::new(Vec::new());
111
112pub fn set_widearray(mb_array: &str) -> Vec<char> {
113    let mut bytes = mb_array.as_bytes().to_vec();
114    unmetafy(&mut bytes);
115    match std::str::from_utf8(&bytes) {
116        Ok(s) => s.chars().collect(),
117        Err(_) => Vec::new(),
118    }
119}
120
121// =====================================================================
122// Port of `zwarning(const char *cmd, const char *fmt, va_list ap)` from `Src/utils.c:142`.
123// =====================================================================
124
125/// Port of `zwarning()` from `Src/utils.c:142` — C decl `zwarning(const char *cmd, const char *fmt, va_list ap)`.
126///
127/// Internal helper that builds the diagnostic prefix and emits it +
128/// the formatted message to stderr. Direct C body translation:
129///
130/// ```c
131/// if (isatty(2)) zleentry(ZLE_CMD_TRASH);
132/// char *prefix = scriptname ? scriptname : (argzero ? argzero : "");
133/// if (cmd) {
134///     if (unset(SHINSTDIN) || locallevel) {
135///         nicezputs(prefix, stderr);
136///         fputc(':', stderr);
137///     }
138///     nicezputs(cmd, stderr);
139///     fputc(':', stderr);
140/// } else {
141///     nicezputs((isset(SHINSTDIN) && !locallevel) ? "zsh" : prefix, stderr);
142///     fputc(':', stderr);
143/// }
144/// zerrmsg(stderr, fmt, ap);
145/// ```
146/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd, fmt, ap)
147fn zwarning(cmd: Option<&str>, msg: &str) {
148    // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
149    // C zsh has one thread, so every diagnostic here belongs to the
150    // command the user just ran. zshrs parses/compiles shell bodies on
151    // worker-pool threads (compinit's bytecode backfill), and a body that
152    // fails to parse there is BACKGROUND noise, not a user-visible error:
153    // printing it stomps the live ZLE line (`zsh: unmatched "` landing in
154    // the middle of the prompt) and the ZLE_CMD_TRASH repaint below runs
155    // off-thread. Route worker-thread diagnostics to the log instead.
156    if crate::worker::in_worker_thread() {
157        tracing::debug!(cmd = ?cmd, msg = %msg, "background-thread diagnostic (not shown)");
158        return;
159    }
160    // c:96 — `if (isatty(2)) zleentry(ZLE_CMD_TRASH);`
161    // Flush any in-flight ZLE redraw state before the warning lands
162    // on stderr — without this, half-painted edit lines bleed into
163    // the diagnostic. Previously: `let _ = isatty(2);` (the result
164    // was discarded; the canonical zleentry port at init.rs:905 was
165    // never actually called from the warning path).
166    if unsafe { libc::isatty(2) } != 0 {
167        // c:96
168        let _ = zleentry(
169            // c:96
170            ZLE_CMD_TRASH, // c:96
171        );
172    }
173    let scriptname = scriptname_lock().lock().unwrap().clone();
174    let argzero = argzero_lock().lock().unwrap().clone();
175    // c:150 reads the `locallevel` global. `here()` rather than `load()`:
176    // C's counter belongs to its single thread of execution, so it can only
177    // ever describe the frame that raised THIS diagnostic. zshrs runs
178    // `async_precmd` hook functions on a pool worker against the shared
179    // counter, and a hook's scope made a top-level error print with a
180    // function-name prefix and a line number zsh does not print —
181    //     spin_hook:4: parse error near `done'   vs   zsh: parse error near `done'
182    // for a line typed while the hook ran. See
183    // `crate::thread_shell_state::LocalLevelCell`.
184    let locallevel = LOCALLEVEL.here();
185    let prefix: String = scriptname.or(argzero).unwrap_or_default();
186    let stderr_handle = io::stderr();
187    let mut stderr_lock = stderr_handle.lock();
188    if let Some(cmd) = cmd {
189        // c:107-110 — `if (unset(SHINSTDIN) || locallevel) {
190        //                 nicezputs(prefix, stderr); fputc(':', stderr); }`
191        if unset(SHINSTDIN) || locallevel != 0 {
192            let _ = nicezputs(&prefix, &mut stderr_lock); // c:108
193            let _ = stderr_lock.write_all(b":");
194        }
195        let _ = nicezputs(cmd, &mut stderr_lock); // c:111
196        let _ = stderr_lock.write_all(b":");
197    } else {
198        // c:114 — `nicezputs((isset(SHINSTDIN) && !locallevel) ? "zsh" : prefix, stderr);`
199        let to_emit = if isset(SHINSTDIN) && locallevel == 0 {
200            "zsh"
201        } else {
202            prefix.as_str()
203        };
204        let _ = nicezputs(to_emit, &mut stderr_lock); // c:114
205        let _ = stderr_lock.write_all(b":");
206    }
207    // c:116 — `zerrmsg(stderr, fmt, ap)` — lineno prefix + message.
208    // Pre-built `msg: &str` covers C's va_list; zerrmsg port hasn't
209    // had its `(file, fmt, ap)` signature wired yet so the lineno
210    // prefix + write is inlined here against the same `unset(SHINSTDIN)`
211    // gate C uses at c:301.
212    let lineno = lineno() as i32;
213    if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
214        let _ = stderr_lock.write_all(format!("{}: ", lineno).as_bytes());
215    } else {
216        let _ = stderr_lock.write_all(b" ");
217    }
218    let _ = stderr_lock.write_all(msg.as_bytes());
219    let _ = stderr_lock.write_all(b"\n");
220    let _ = stderr_lock.flush();
221}
222
223// =====================================================================
224// Port of `zerr(VA_ALIST1(const char *fmt))` / `zerrnam` / `zwarn` / `zwarnnam` from utils.c:173
225// onward. Each is a thin wrapper: check errflag/noerrs guards, set
226// `ERRFLAG_ERROR` on the fatal variants, call `zwarning`.
227// =====================================================================
228
229/// Port of `zerr()` from `Src/utils.c:173` — C decl `zerr(VA_ALIST1(const char *fmt))`.
230///
231/// ```c
232/// if (errflag || noerrs) {
233///     if (noerrs < 2) errflag |= ERRFLAG_ERROR;
234///     return;
235/// }
236/// errflag |= ERRFLAG_ERROR;
237/// zwarning(NULL, fmt, ap);
238/// ```
239/// WARNING: param names don't match C — Rust=(msg) vs C=()
240pub fn zerr(msg: &str) {
241    // c:173
242    let noerrs = *noerrs_lock().lock().unwrap();
243    if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
244        // c:175
245        if noerrs < 2 {
246            // c:176
247            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:176
248        }
249        return; // c:177
250    }
251    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:194
252    zwarning(None, msg); // c:194
253}
254
255/// Port of `zerrnam()` from `Src/utils.c:194` — C decl `zerrnam(VA_ALIST2(const char *cmd, const char *fmt))`.
256///
257/// ```c
258/// if (errflag || noerrs) return;
259/// errflag |= ERRFLAG_ERROR;
260/// zwarning(cmd, fmt, ap);
261/// ```
262/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd)
263pub fn zerrnam(cmd: &str, msg: &str) {
264    // c:194
265    let noerrs = *noerrs_lock().lock().unwrap();
266    if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
267        // c:196
268        return;
269    }
270    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:214
271    zwarning(Some(cmd), msg); // c:214
272}
273
274/// Port of `zwarn()` from `Src/utils.c:214` — C decl `zwarn(VA_ALIST1(const char *fmt))`.
275///
276/// ```c
277/// if (errflag || noerrs) return;
278/// zwarning(NULL, fmt, ap);
279/// ```
280/// WARNING: param names don't match C — Rust=(msg) vs C=()
281pub fn zwarn(msg: &str) {
282    // c:214
283    let noerrs = *noerrs_lock().lock().unwrap();
284    if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
285        // c:216
286        return;
287    }
288    zwarning(None, msg); // c:231
289}
290
291/// Port of `zwarnnam()` from `Src/utils.c:231` — C decl `zwarnnam(VA_ALIST2(const char *cmd, const char *fmt))`.
292///
293/// ```c
294/// if (errflag || noerrs) return;
295/// zwarning(cmd, fmt, ap);
296/// ```
297/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd)
298pub fn zwarnnam(cmd: &str, msg: &str) {
299    // c:231
300    let noerrs = *noerrs_lock().lock().unwrap();
301    if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
302        // c:233
303        return;
304    }
305    zwarning(Some(cmd), msg); // c:235
306}
307
308/// Port of `dputs()` from `Src/utils.c:253` — C decl `dputs(VA_ALIST1(const char *message))`.
309///
310/// C body (c:253-270):
311/// ```c
312/// mod_export void dputs(const char *message)
313/// {
314///     char *filename;
315///     FILE *file;
316///     if ((filename = getsparam_u("ZSH_DEBUG_LOG")) != NULL &&
317///         (file = fopen(filename, "a")) != NULL) {
318///         zerrmsg(file, message, ap);
319///         fclose(file);
320///     } else
321///         zerrmsg(stderr, message, ap);
322/// }
323/// ```
324///
325/// Previous Rust port was a FAKE — `eprintln!("BUG: {}", msg)` ignored
326/// `$ZSH_DEBUG_LOG` entirely (which is the whole point of dputs — to
327/// route debug output to a user-specified log file). Re-port now
328/// consults paramtab via `getsparam_u`, opens the log in append mode,
329/// and routes the message there; falls back to stderr per c:268.
330///
331/// Rust signature drift: takes `&str` instead of va_args; callers
332/// pre-format via Rust's `format!` (same pattern as `zerrmsg`).
333pub fn dputs(msg: &str) {
334    // c:253
335    // c:263 — `getsparam_u("ZSH_DEBUG_LOG")`. The `_u` variant
336    // unmetafies the result (utils.rs's getsparam_u port at
337    // params.rs:3831 wraps getsparam + unmeta).
338    let log_file = getsparam_u("ZSH_DEBUG_LOG"); // c:263
339                                                 // c:264 — `fopen(filename, "a")` — append mode.
340    let opened = log_file.as_ref().and_then(|p| {
341        // c:264
342        fs::OpenOptions::new()
343            .create(true)
344            .append(true)
345            .open(p)
346            .ok()
347    });
348    // Shared format logic: lineno prefix + msg + newline, matching
349    // zerrmsg at c:296-308. Built once, written to file or stderr.
350    let lineno = lineno() as i32;
351    let shinstdin = isset(SHINSTDIN);
352    // `here()` not `load()` — the depth of the thread raising this message;
353    // see the matching note in `zwarning`.
354    let locallevel = LOCALLEVEL.here();
355    let prefix = if (!shinstdin || locallevel != 0) && lineno != 0 {
356        format!("{}: ", lineno)
357    } else {
358        String::new()
359    };
360    let line = format!("{}{}\n", prefix, msg);
361    if let Some(mut f) = opened {
362        // c:265 zerrmsg(file, ...)
363        let _ = f.write_all(line.as_bytes()); // c:265
364                                              // c:266 — `fclose(file)` — handled by Drop when `f` goes out of scope.
365    } else {
366        // c:267 else stderr
367        let _ = io::stderr().write_all(line.as_bytes()); // c:268 zerrmsg(stderr, ...)
368    }
369}
370
371// ---------------------------------------------------------------------------
372// Remaining 33 missing utils.c functions
373// ---------------------------------------------------------------------------
374
375// `zwarning` is defined earlier in this file as the real port of
376// utils.c:142 (private helper invoked by `zerr`/`zerrnam`/`zwarn`/
377// `zwarnnam`). The duplicate stub previously here has been deleted
378// — callers use the four public entry points instead.
379
380/// Port of `zz_plural_z_alpha()` from `Src/utils.c:282` — C decl `void zz_plural_z_alpha(void)`.
381///
382/// Cygwin-only no-op symbol the C source emits to work around a
383/// dllwrap bug that drops the last alphabetically-sorted exported
384/// symbol (zwarnnam). The Rust port has no equivalent linker
385/// problem; this is preserved as a no-op for symbol-table parity.
386pub fn zz_plural_z_alpha() {} // c:282
387
388/// Port of `zerrmsg(FILE *file, const char *fmt, va_list ap)` from `Src/utils.c:289`.
389///
390/// C body emits the formatted message + (when locallevel > 0 or
391/// SHINSTDIN unset) the line number prefix + "\n". The Rust port
392/// is invoked indirectly through `zwarning` — direct callers pass
393/// pre-formatted strings.
394/// WARNING: param names don't match C — Rust=(msg, errno) vs C=(file, fmt, ap)
395// Rust idiom replacement: pre-formatted `msg: &str` covers the C
396// va_list expansion; the C `file`+`fmt`+`ap` triplet collapses
397// because callers (zerr/zwarning) pre-format via Rust's `format!`.
398/// Port of `zerrmsg()` from `Src/utils.c:289`.
399///
400/// `zerrmsg` — see implementation.
401pub fn zerrmsg(msg: &str, errno: Option<i32>) {
402    // c:289
403    // c:296 — `lineno` is the parser-advanced line counter. The
404    // previous Rust port read `lineno_lock` (a Mutex<i32> in utils.rs)
405    // that ONLY fusevm_bridge::set_lineno wrote to; the actual
406    // parser uses lex::LEX_LINENO (thread_local Cell<u64>) updated
407    // by every newline scan. Result: error messages emitted from
408    // parse-time always printed line 0 (or last fusevm_bridge value)
409    // rather than the actual line of the syntax error.
410    //
411    // Route through lex::lineno() so the parser-advanced counter
412    // drives the error prefix.
413    let lineno = lineno() as i32;
414    // `here()` not `load()` — the depth of the thread raising this message;
415    // see the matching note in `zwarning`.
416    let locallevel = LOCALLEVEL.here();
417    // c:301-308 — `if ((unset(SHINSTDIN) || locallevel) && lineno)
418    //                 fprintf(file, "%d: ", lineno); else fputc(' ', file);`
419    if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
420        eprint!("{}: ", lineno);
421    } else {
422        eprint!(" ");
423    }
424    if let Some(e) = errno {
425        // c:348-365 — `%e`: render the errno exactly as C's zerrmsg does,
426        // through `zsh_errno_msg` (EINTR→"interrupt", EIO verbatim, else
427        // strerror with a lowercased first letter). The previous
428        // `io::Error::from_raw_os_error` kept the capital and appended
429        // " (os error N)" — the byte-diff tracked as docs/BUGS.md #316.
430        if e == libc::EINTR {
431            // c:351-354 — EINTR sets the error flag and stops formatting.
432            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
433        }
434        eprintln!("{}: {}", msg, zsh_errno_msg(e));
435    } else {
436        eprintln!("{}", msg);
437    }
438}
439
440/// Port of `zsetupterm()` from `Src/utils.c:390` — C decl `void zsetupterm(void)`.
441///
442/// Reference-counts terminfo's `cur_term` setup. The C source
443/// guards with `#ifdef HAVE_SETUPTERM`; without terminfo this is a
444/// pure no-op. Rust's `term`-style state lives elsewhere (modules
445/// like `zle/termcap` initialize on demand), so this is a no-op
446/// counter for symbol-table parity.
447pub fn zsetupterm() {
448    // c:390
449    static TERM_COUNT: std::sync::atomic::AtomicI32 = // c:402
450        std::sync::atomic::AtomicI32::new(0);
451    let tc = TERM_COUNT.load(Ordering::Relaxed);
452    // c:395-396 — DPUTS(term_count < 0 || (term_count > 0 && !cur_term),
453    //                   "inconsistent term_count and/or cur_term");
454    // The Rust port has no `cur_term` analogue (terminfo state lives
455    // in zle/termcap module), so the second condition reduces to
456    // `term_count > 0 && true` if we treat cur_term as the count
457    // itself. Simplified to the bare `term_count < 0` invariant.
458    DPUTS!(tc < 0, "inconsistent term_count and/or cur_term"); // c:395-396
459    TERM_COUNT.fetch_add(1, Ordering::Relaxed); // c:402
460}
461
462/// Port of `zdeleteterm()` from `Src/utils.c:409` — C decl `void zdeleteterm(void)`.
463pub fn zdeleteterm() {
464    // c:409
465    static TERM_COUNT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
466    let tc = TERM_COUNT.load(Ordering::Relaxed);
467    // c:412-413 — DPUTS(term_count < 1 || !cur_term,
468    //                   "inconsistent term_count and/or cur_term");
469    DPUTS!(tc < 1, "inconsistent term_count and/or cur_term"); // c:412-413
470    if tc > 0 {
471        TERM_COUNT.fetch_sub(1, Ordering::Relaxed); // c:414
472    }
473}
474
475/// Port of `putraw()` from `Src/utils.c:424` — C decl `int putraw(int c)`. Writes a
476/// single byte to stdout for the termcap library, returning 0.
477///
478/// Declared `extern "C"` with C's exact `int (int)` signature because
479/// it is handed to `tputs(3)` as a function pointer — that is the only
480/// thing C uses it for (`Src/Modules/termcap.c:132`,
481/// `tputs(t, 1, putraw)`), and a Rust-ABI `fn(char) -> i32` cannot be
482/// passed there. `src/ported/modules/termcap.rs` used to carry its own
483/// copy under this name for exactly that reason; the copy is gone and
484/// termcap now passes this one.
485///
486/// !!! RUST-ONLY DEVIATION !!! C is `putc(c, stdout)` — buffered through
487/// the FILE*. This writes the byte straight to fd 1, so the caller
488/// controls interleaving with Rust's own buffered stdout by flushing
489/// (termcap.rs does, right after the `tputs` call).
490pub extern "C" fn putraw(c: libc::c_int) -> libc::c_int {
491    // c:424
492    let byte = c as u8; // c:426 — `putc(c, stdout);`
493    unsafe {
494        libc::write(1, &byte as *const u8 as *const libc::c_void, 1);
495    }
496    0 // c:427
497}
498
499/// Port of `putshout()` from `Src/utils.c:434` — C decl `int putshout(int c)`. Writes a
500/// single byte to `shout` (zsh's interactive stdout — falls back
501/// to stdout in zshrs's static-link path), returning 0.
502pub fn putshout(c: char) -> i32 {
503    // c:434
504    print!("{}", c); // c:434
505    0 // c:437
506}
507
508/// Port of `nicechar_sel()` from `Src/utils.c:462` — C decl `nicechar_sel(int c, int quotable)`.
509/// Renders one byte as its `^X` / `M-X` / `\\n` / `\\t` display form;
510/// `quotable=true` emits `\\C-X` instead of `^X` so the result is
511/// shell-quotable.
512pub fn nicechar_sel(c: char, quotable: bool) -> String {
513    // c:462
514    // c:466 — `c &= 0xff;` mask to byte before any classification.
515    let mut c = (c as u32) & 0xff;
516    let mut out = String::new();
517    // c:467 — `if (ZISPRINT(c)) goto done;`. Use the canonical
518    // `ZISPRINT` predicate (port of ztype.h:89 — IPRINT typtab bit
519    // AND != 0x7f). Previously used a custom `is_print` closure that
520    // wrongly accepted 0xa0+ as printable — diverged from C ZISPRINT
521    // for every high-bit byte under PRINTEIGHTBIT-off, breaking
522    // `\M-X` escape generation.
523    if !ZISPRINT(c as u8) {
524        // c:467
525        if c & 0x80 != 0 {
526            // c:469
527            if isset(PRINTEIGHTBIT) { // c:470
528                 // c:471 — goto done (output raw); c unchanged.
529            } else {
530                out.push_str("\\M-"); // c:472-474
531                c &= 0x7f; // c:475
532                if ZISPRINT(c as u8) {
533                    // c:476-477
534                    // c:477 — goto done after writing \M- + ASCII char.
535                    if let Some(ch) = char::from_u32(c) {
536                        out.push(ch);
537                    }
538                    return out;
539                }
540            }
541        }
542        if c == 0x7f {
543            // c:479
544            out.push_str(if quotable { "\\C-" } else { "^" }); // c:481-486
545            c = b'?' as u32;
546        } else if c == b'\n' as u32 {
547            // c:487
548            out.push('\\');
549            c = b'n' as u32;
550        } else if c == b'\t' as u32 {
551            // c:490
552            out.push('\\');
553            c = b't' as u32;
554        } else if c < 0x20 {
555            // c:493
556            out.push_str(if quotable { "\\C-" } else { "^" }); // c:494-499
557            c += 0x40;
558        }
559    }
560    if let Some(ch) = char::from_u32(c) {
561        // c:511
562        out.push(ch);
563    }
564    out
565}
566
567/// Port of `nicechar()` from `Src/utils.c:520` — C decl `nicechar(int c)`. C body:
568///     `return nicechar_sel(c, 0);`
569pub fn nicechar(c: char) -> String {
570    // c:520
571    nicechar_sel(c, false) // c:523
572}
573
574/// Port of `is_nicechar()` from `Src/utils.c:531` — C decl `is_nicechar(int c)`.
575/// ```c
576/// c &= 0xff;
577/// if (ZISPRINT(c)) return 0;
578/// if (c & 0x80)    return !isset(PRINTEIGHTBIT);
579/// return (c == 0x7f || c == '\n' || c == '\t' || c < 0x20);
580/// ```
581/// "Nice" means "needs escape-formatting when printed" — so
582/// returns true for control chars + (under PRINTEIGHTBIT off)
583/// high-bit bytes. The previous Rust port treated all `!c.is_ascii()`
584/// as nice unconditionally — divergent for users running with
585/// `setopt printeightbit` (very common for non-ASCII filenames).
586pub fn is_nicechar(c: char) -> bool {
587    let cu = (c as u32) & 0xff;
588    // c:534 — `if (ZISPRINT(c)) return 0;` — printable ASCII is not nice.
589    if ZISPRINT(cu as u8) {
590        return false;
591    }
592    // c:536 — high-bit byte path.
593    if (cu & 0x80) != 0 {
594        return !isset(PRINTEIGHTBIT);
595    }
596    // c:538 — ASCII control chars (DEL/\n/\t/<0x20).
597    cu == 0x7f || cu == b'\n' as u32 || cu == b'\t' as u32 || cu < 0x20
598}
599
600/// Initialize multibyte state (from utils.c mb_charinit) - no-op in Rust
601/// Port of `mb_charinit` from `Src/utils.c:553`.
602pub fn mb_charinit() {
603    // Rust handles UTF-8 natively
604}
605
606/// Scratch space for a libc `mbstate_t`.
607///
608/// C declares `mbstate_t mbs;` on the stack wherever it converts
609/// between the multibyte and wide representations (`Src/utils.c:543`
610/// `mb_shiftstate`, `Src/Zle/zle_utils.c:381`, `Src/Zle/zle_utils.c:202`,
611/// `Src/Zle/zle_refresh.c:625`, `Src/Zle/zle_main.c:995`). POSIX leaves
612/// the struct opaque and its size is platform-specific — 8 bytes on
613/// glibc/musl, 128 bytes on macOS and the BSDs — so the port hands libc
614/// an over-sized, 8-byte-aligned, zeroed buffer instead of restating the
615/// layout. `MBSTATE_ZERO` is the `memset(&mbs, 0, sizeof mbs)` those C
616/// sites perform before their first conversion.
617pub type MbStateBuf = [u64; 16];
618
619/// A zeroed [`MbStateBuf`] — the `memset(&mbs, '\0', sizeof mbs)` that
620/// precedes every C conversion loop (e.g. `Src/Zle/zle_utils.c:444`).
621pub const MBSTATE_ZERO: MbStateBuf = [0u64; 16];
622
623// !!! WARNING: RUST-ONLY HELPER !!!
624//
625// C has no counterpart: `zsh_main` calls `setlocale(LC_ALL, "")` at
626// `Src/init.c:1861` before anything else runs, so by the time any
627// conversion happens the process locale is already the environment's.
628// A Rust binary never calls `setlocale`, and a library consumer of this
629// crate (unit tests, embedded entry points, the daemon) may reach the
630// `mbrtowc`/`wcrtomb` wrappers above without ever going through
631// `zsh_main`. Under the startup default ("C") those are single-byte
632// codecs, so a UTF-8 terminal line would be shredded into raw bytes.
633//
634// Forcing the locale once, lazily, from the environment restores the C
635// precondition for those consumers. `zsh_main` touches it right after
636// its own `setlocale` (`Src/init.c:1861`) so the lazy path can never
637// fire later and undo an `LC_*` parameter assignment made from an rc
638// file.
639pub static MB_LOCALE_READY: std::sync::LazyLock<()> = std::sync::LazyLock::new(|| {
640    #[cfg(unix)]
641    unsafe {
642        let empty = std::ffi::CString::new("").unwrap();
643        libc::setlocale(libc::LC_CTYPE, empty.as_ptr());
644    }
645});
646
647/// `(size_t)-2` — `mbrtowc` "the bytes so far are a valid but incomplete
648/// character". zsh spells this `MB_INCOMPLETE` (`Src/zsh.h:3313`).
649pub const MB_INCOMPLETE: usize = usize::MAX - 1;
650
651/// `(size_t)-1` — `mbrtowc` "invalid multibyte sequence". zsh spells this
652/// `MB_INVALID` (`Src/zsh.h:3314`).
653pub const MB_INVALID: usize = usize::MAX;
654
655// The libc conversion primitives themselves. The whole point of routing
656// through libc rather than Rust's native UTF-8 codecs is that these are
657// LOCALE-DRIVEN: in a single-byte locale (`MB_CUR_MAX == 1`) `mbrtowc`
658// consumes exactly one byte and yields that byte's value as the wide
659// character (under the ASCII C locale only for bytes up to 0x7f, below),
660// while under a UTF-8 locale it decodes the full sequence. zsh's
661// byte-vs-multibyte behaviour is entirely this distinction — no C caller
662// in `stringaszleline`/`zlelineasstring`/`getrestchar`/`zwcputc` tests
663// `MB_CUR_MAX` or `isset(MULTIBYTE)` itself. The `mbstate_t` argument is
664// typed as an opaque pointer; pass `&mut MBSTATE_ZERO`-initialised
665// [`MbStateBuf`]. The libc crate does not re-export these on unix.
666//
667// `mbrtowc`/`wcrtomb` come through `crate::c_locale`, which gives the C
668// locale (ASCII codeset) glibc's answer on every platform: a byte or wide
669// character above 0x7f does not convert. macOS libc would convert it, and
670// zsh's own tests expect the glibc output (E02xtrace.ztst `\M-c`). Every
671// other locale is libc unchanged. Returns: bytes consumed / written, `0` for
672// a NUL, [`MB_INCOMPLETE`] or [`MB_INVALID`].
673pub use crate::c_locale::{mbrtowc, wcrtomb};
674extern "C" {
675    /// libc `MB_CUR_MAX`: the maximum number of bytes in a multibyte
676    /// character in the CURRENT locale — 1 in a single-byte codeset
677    /// (`LC_ALL=C`), 4 under UTF-8, 2 under `zh_CN.GB2312`. It is a
678    /// MACRO in C (`<stdlib.h>`), so the libc crate cannot re-export
679    /// it; each platform's underlying function is declared here, the
680    /// same way `crate::c_locale` declares `mbrtowc`/`wcrtomb`. Darwin spells it
681    /// `___mb_cur_max()` (SDK `_stdlib.h:133`), glibc
682    /// `__ctype_get_mb_cur_max()`.
683    #[cfg(target_vendor = "apple")]
684    #[link_name = "___mb_cur_max"]
685    pub fn mb_cur_max_raw() -> libc::c_int;
686
687    /// glibc/musl spelling of the same accessor — see the Darwin arm.
688    #[cfg(not(target_vendor = "apple"))]
689    #[link_name = "__ctype_get_mb_cur_max"]
690    pub fn mb_cur_max_raw() -> libc::size_t;
691}
692
693/// Port of `wcs_nicechar_sel()` from `Src/utils.c:593`.
694///
695/// Port of `wcs_nicechar_sel(wchar_t c, size_t *widthp, char **swidep,
696/// int quotable)` from `Src/utils.c:593-705`. Four branches per C:
697///   1. c < 0x80 and not printable: control-char escape (\n, \t, ^X, \C-X)
698///      — same as `nicechar_sel`.
699///   2. c >= 0x80 printable AND PRINTEIGHTBIT set: emit raw UTF-8 bytes.
700///   3. c >= 0x80 fits in UTF-8: emit UTF-8 bytes (default-on for MULTIBYTE).
701///   4. c >= 0x10000: `\U%.8x` 8-digit hex; c >= 0x100: `\u%.4x` 4-digit hex.
702///
703/// Param mapping (Rule S1, faithful to C):
704/// - `widthp: Option<&mut usize>` ← C `size_t *widthp` (`None` ≡ `NULL`).
705///   Set to the display column width of the produced sequence.
706/// - `swidep: Option<&mut usize>` ← C `char **swidep` (`None` ≡ `NULL`).
707///   Set to the byte index in the returned string where the post-wide
708///   meta-prefixed bytes begin (i.e. boundary between display-width
709///   characters and trailing Meta+X encoding bytes that don't add to
710///   column position).
711pub fn wcs_nicechar_sel(
712    c: char,
713    widthp: Option<&mut usize>,
714    swidep: Option<&mut usize>,
715    quotable: bool,
716) -> String {
717    // c:593
718    let cv = c as u32;
719    // c:616 — `if (!WC_ISPRINT(c) && (c < 0x80 || !isset(PRINTEIGHTBIT)))`.
720    // The non-printable + (low-ASCII or PRINTEIGHTBIT-off) branch.
721    let print_eightbit = isset(PRINTEIGHTBIT);
722    let is_printable = u9_iswprint(c);
723    let buf: String;
724    if !is_printable && (cv < 0x80 || !print_eightbit) {
725        if cv == 0x7f {
726            // c:617-624 — DEL: `^?` / `\C-?`
727            buf = if quotable {
728                "\\C-?".to_string()
729            } else {
730                "^?".to_string()
731            };
732            // c:686 widthp = (s - buf) + wcw (wcw for '?' = 1).
733            if let Some(wp) = widthp {
734                *wp = buf.chars().count();
735            }
736            if let Some(sp) = swidep {
737                *sp = buf.len();
738            }
739            return buf;
740        } else if c == '\n' {
741            // c:625-627 — `\n` literal.
742            buf = "\\n".to_string();
743            if let Some(wp) = widthp {
744                *wp = 2;
745            }
746            if let Some(sp) = swidep {
747                *sp = buf.len();
748            }
749            return buf;
750        } else if c == '\t' {
751            // c:628-630 — `\t` literal.
752            buf = "\\t".to_string();
753            if let Some(wp) = widthp {
754                *wp = 2;
755            }
756            if let Some(sp) = swidep {
757                *sp = buf.len();
758            }
759            return buf;
760        } else if cv < 0x20 {
761            // c:631-638 — ^X / \C-X for controls (excluding \n, \t).
762            let cc = (cv + 0x40) as u8 as char;
763            buf = if quotable {
764                format!("\\C-{}", cc)
765            } else {
766                format!("^{}", cc)
767            };
768            if let Some(wp) = widthp {
769                *wp = buf.chars().count();
770            }
771            if let Some(sp) = swidep {
772                *sp = buf.len();
773            }
774            return buf;
775        }
776        // c:639-641 — c >= 0x80 non-printable falls through to ret=-1
777        // path (hex escape).
778    } else if cv < 0x80 {
779        // c:644-704 — printable ASCII: emit raw char. wcw=1.
780        buf = c.to_string();
781        if let Some(wp) = widthp {
782            *wp = 1;
783        }
784        if let Some(sp) = swidep {
785            *sp = buf.len();
786        }
787        return buf;
788    }
789    // c:644-678 — high-bit char: try UTF-8 encode first.
790    if u9_iswprint(c) {
791        // c:681-693 — printable wide char: emit raw UTF-8.
792        buf = c.to_string();
793        let wcw = zwcwidth(c) as usize;
794        if let Some(wp) = widthp {
795            *wp = wcw;
796        }
797        if let Some(sp) = swidep {
798            *sp = buf.len();
799        }
800        return buf;
801    }
802    // c:656-678 — non-printable wide: hex escape (or fall back to byte
803    // nicechar for c < 0x100).
804    if cv >= 0x10000 {
805        // c:656-659 — `\U%.8x` (10 chars).
806        buf = format!("\\U{:08x}", cv);
807        if let Some(wp) = widthp {
808            *wp = 10;
809        }
810        if let Some(sp) = swidep {
811            *sp = buf.len();
812        }
813        buf
814    } else if cv >= 0x100 {
815        // c:660-663 — `\u%.4x` (6 chars).
816        buf = format!("\\u{:04x}", cv);
817        if let Some(wp) = widthp {
818            *wp = 6;
819        }
820        if let Some(sp) = swidep {
821            *sp = buf.len();
822        }
823        buf
824    } else {
825        // c:664-674 — fall back to byte nicechar_sel.
826        buf = nicechar_sel(c, quotable);
827        // c:670-671 — `*widthp = ztrlen(buf);` (display width respects
828        // metafied chars). `ztrlen` counts visible cells.
829        if let Some(wp) = widthp {
830            *wp = ztrlen(&buf);
831        }
832        // c:672-673 — `*swidep = buf + strlen(buf);` (no trailing meta
833        // since nicechar_sel ASCII output).
834        if let Some(sp) = swidep {
835            *sp = buf.len();
836        }
837        buf
838    }
839}
840
841/// Port of `wcs_nicechar()` from `Src/utils.c:709` — C decl `wcs_nicechar(wchar_t c, size_t *widthp, char **swidep)`.
842/// C body: `return wcs_nicechar_sel(c, widthp, swidep, 0);`
843pub fn wcs_nicechar(c: char, widthp: Option<&mut usize>, swidep: Option<&mut usize>) -> String {
844    // c:709
845    wcs_nicechar_sel(c, widthp, swidep, false) // c:711
846}
847
848/// Port of `is_wcs_nicechar()` from `Src/utils.c:720` — C decl `int is_wcs_nicechar(wchar_t c)`.
849///
850/// "Return 1 if wcs_nicechar() would reformat this character for
851/// display." Mirrors the C condition: non-printable AND (low ASCII
852/// OR PRINTEIGHTBIT unset) for control chars; for high bytes, true
853/// when ≥0x100 or `is_nicechar` says so.
854pub fn is_wcs_nicechar(c: char) -> bool {
855    // c:720
856    let cv = c as u32;
857    // c:722 `WC_ISPRINT(c)` — u9_iswprint in a --enable-unicode9 build
858    // (Src/ztype.h:77), as wcs_nicechar_sel uses. Rust's is_control() is
859    // only Cc, so U+200B / U+00AD / U+FEFF read as printable and
860    // `${(q+)v}` left them bare where zsh writes $'\u200b'.
861    let printable = crate::ported::compat::u9_iswprint(c);
862    let print_eight = isset(PRINTEIGHTBIT); // c:722
863    if !printable && (cv < 0x80 || !print_eight) {
864        if cv == 0x7f || c == '\n' || c == '\t' || cv < 0x20 {
865            // c:734
866            return true;
867        }
868        if cv >= 0x80 {
869            // c:734
870            return cv >= 0x100 || is_nicechar(c); // c:734
871        }
872    }
873    false // c:734
874}
875
876/// Port of `zwcwidth()` from `Src/utils.c:734` — C decl `int zwcwidth(wint_t wc)`.
877///
878/// Get wide character width (from utils.c zwcwidth)
879///
880/// C body (c:734-745):
881/// ```c
882/// int wcw;
883/// /* assume a single-byte character if not valid */
884/// if (wc == WEOF || unset(MULTIBYTE))                  // c:738
885///     return 1;
886/// wcw = WCWIDTH(wc);                                   // c:740
887/// /* if not printable, assume width 1 */
888/// if (wcw < 0)                                         // c:742
889///     return 1;
890/// return wcw;                                          // c:744
891/// ```
892///
893/// The previous Rust port skipped the `unset(MULTIBYTE)` early-
894/// return (c:738). When the MULTIBYTE option is OFF (set via
895/// `setopt nomultibyte` / `set +o multibyte`), C zsh treats every
896/// codepoint as a single-byte char and returns width 1 unconditionally.
897/// Without the option check, the Rust port would still report the
898/// Unicode-width-table answer (2 for CJK, 0 for combining marks)
899/// in single-byte mode — diverging from prompt/refresh layout that
900/// relies on the option as the source of truth.
901pub fn zwcwidth(wc: char) -> usize {
902    // c:734
903    // c:738 — `if (wc == WEOF || unset(MULTIBYTE)) return 1;`. WEOF
904    // path is Rust-impossible (char is always a valid scalar). The
905    // MULTIBYTE option gate maps to the canonical option register.
906    if !isset(MULTIBYTE) {
907        // c:738
908        return 1; // c:739
909    }
910    // c:740-744 — WCWIDTH(wc); negative result → width 1.
911    unicode_width::UnicodeWidthChar::width(wc).unwrap_or(1) // c:740-744
912}
913
914/// Port of `pathprog()` from `Src/utils.c:761` — C decl `pathprog(char *prog, char **namep)`.
915/// ```c
916/// for (pp = path; *pp; pp++) {
917///     sprintf(buf, "%s/%s", *pp, prog);
918///     funmeta = unmeta(buf);
919///     if (access(funmeta, F_OK) == 0 && stat(funmeta, &st) >= 0 &&
920///         !S_ISDIR(st.st_mode)) {
921///         return funmeta;
922///     }
923/// }
924/// return NULL;
925/// ```
926/// C checks: (1) F_OK = exists, (2) stat succeeds, (3) NOT a directory.
927/// NO executable-bit check. Used by autoload / `which` paths that
928/// need to find any file in PATH, not just executables.
929///
930/// Previously the Rust port added a `mode & 0o111 != 0` executable
931/// check — divergent, made every `pathprog` lookup miss non-executable
932/// files (e.g. autoload-function plaintext scripts that don't have
933/// +x set).
934/// WARNING: param names don't match C — Rust=(prog) vs C=(prog, namep)
935pub fn pathprog(prog: &str) -> Option<PathBuf> {
936    // c:760
937    // The early-return on `prog` containing '/' is a Rust-port
938    // convenience NOT in C's pathprog. C unconditionally walks $PATH
939    // and prefixes each entry. The convenience is harmless since
940    // C's caller (`findcmd`) handles slashes separately before
941    // calling pathprog.
942    if prog.contains('/') {
943        let p = PathBuf::from(prog);
944        return if p.exists() { Some(p) } else { None };
945    }
946    if let Some(path_var) = getsparam("PATH") {
947        for dir in path_var.split(':') {
948            // c:773
949            let full_path = PathBuf::from(dir).join(prog);
950            // c:776 — `funmeta = unmeta(buf)`. The previous Rust port
951            // passed the raw composed path to `fs::metadata`, missing
952            // the unmeta step. Paths containing Meta-encoded bytes
953            // (from PATH entries or prog name with metafy lead bytes)
954            // would silently miss valid executables.
955            let unmeta_path = unmeta(full_path.to_str().unwrap_or("")); // c:776 unmeta(buf)
956                                                                        // c:777-779 — `access(F_OK) == 0 && stat >= 0 && !S_ISDIR`.
957                                                                        // is_file() folds existence + stat + not-dir into one.
958            if let Ok(meta) = fs::metadata(&unmeta_path) {
959                if meta.is_file() {
960                    // Return the unmeta'd path since that's what
961                    // C does (funmeta is the returned value).
962                    return Some(PathBuf::from(unmeta_path));
963                }
964            }
965        }
966    }
967    None
968}
969
970/// Port of `findpwd(char *s)` from `Src/utils.c:792`.
971///
972/// ```c
973/// char *findpwd(char *s)
974/// {
975///     char *t;
976///     if (*s == '/')
977///         return xsymlink(s, 0);
978///     s = tricat((pwd[1]) ? pwd : "", "/", s);
979///     t = xsymlink(s, 0);
980///     zsfree(s);
981///     return t;
982/// }
983/// ```
984///
985/// Resolve `s` to its canonical form. Absolute paths route through
986/// `xsymlink` directly; relative paths get prefixed with the
987/// current `pwd` first.
988///
989/// Signature note: C takes `s: char *`. The previous Rust port had
990/// no parameter (returning the cwd) — completely wrong. New port
991/// matches C: takes `&str`, returns `Option<String>` (xsymlink can
992/// return NULL).
993// get a symlink-free pathname for s relative to PWD                        // c:792
994/// Port of `findpwd()` from `Src/utils.c:792`.
995///
996/// `findpwd` — see implementation.
997pub fn findpwd(s: &str) -> Option<String> {
998    // c:792
999    if s.starts_with('/') {
1000        // c:792
1001        return xsymlink(s); // c:797
1002    }
1003    // C: tricat((pwd[1]) ? pwd : "", "/", s) — uses the global
1004    // `pwd` (logical cwd; differs from realpath when chasing
1005    // symlinks is disabled). The Rust port reads `$PWD` since
1006    // shell-set `PWD` mirrors C's `pwd` global; falls back to
1007    // `getcwd()` when unset.
1008    let pwd = getsparam("PWD")
1009        .or_else(|| {
1010            std::env::current_dir()
1011                .ok()
1012                .map(|p| p.to_string_lossy().into_owned())
1013        })
1014        .unwrap_or_default(); // c:798
1015    let prefix: &str = if pwd.len() > 1 { &pwd } else { "" }; // c:798 pwd[1]
1016    let combined = format!("{}/{}", prefix, s); // c:798
1017    xsymlink(&combined) // c:799
1018}
1019
1020/// Port of `ispwd()` from `Src/utils.c:809`.
1021///
1022/// Validate an inherited `$PWD` exactly like zsh's ispwd() at
1023/// src/zsh/Src/utils.c:809: PWD must be absolute, must stat to the
1024/// same dev+inode as ".", and must contain no `.` or `..` components.
1025/// When this returns false, callers should fall back to `getcwd()`.
1026pub(crate) fn ispwd(pwd: &str) -> bool {
1027    if !pwd.starts_with('/') {
1028        return false;
1029    }
1030    let pwd_meta = match fs::metadata(pwd) {
1031        Ok(m) => m,
1032        Err(_) => return false,
1033    };
1034    let dot_meta = match fs::metadata(".") {
1035        Ok(m) => m,
1036        Err(_) => return false,
1037    };
1038    if pwd_meta.dev() != dot_meta.dev() || pwd_meta.ino() != dot_meta.ino() {
1039        return false;
1040    }
1041    // Reject any component that is exactly `.` or `..` — the same loop
1042    // zsh runs after the dev/ino check.
1043    for comp in pwd.split('/') {
1044        if comp == "." || comp == ".." {
1045            return false;
1046        }
1047    }
1048    true
1049}
1050
1051// ---------------------------------------------------------------------------
1052// Missing utility functions ported from utils.c
1053// ---------------------------------------------------------------------------
1054
1055/// Port of `slashsplit()` from `Src/utils.c:837` — C decl `static char **slashsplit(char *s)`.
1056///
1057/// Split path into components (from utils.c slashsplit).
1058///
1059///
1060/// C body (c:837-863):
1061/// ```c
1062/// if (!*s) return zshcalloc(...);            // c:842 — empty input → empty
1063/// for (t = s, t0 = 0; *t; t++)               // c:845 — count slashes
1064///     if (*t == '/') t0++;
1065/// q = r = zalloc(sizeof(char*) * (t0 + 2));
1066/// while ((t = strchr(s, '/'))) {             // c:850
1067///     *q++ = ztrduppfx(s, t - s);            // c:851 — emit prefix
1068///     while (*t == '/') t++;                 // c:852-853 — collapse runs
1069///     if (!*t) { *q = NULL; return r; }      // c:854-857 — trailing `/` ends
1070///     s = t;
1071/// }
1072/// *q++ = ztrdup(s);                          // c:860 — final tail
1073/// ```
1074///
1075/// Three behaviors that the previous `split('/').filter(non_empty)`
1076/// Rust port got WRONG:
1077///   1. **Leading `/` keeps an empty segment** (c:851 with `t == s`).
1078///      C: `slashsplit("/usr")` → `["", "usr"]`; previous Rust dropped
1079///      the empty, returning `["usr"]`. Caller `xsymlinks` (c:879)
1080///      iterates the result building `xbuf` byte-by-byte — the
1081///      empty leading segment is how it knows to start from `/`.
1082///      Without it, absolute-path resolution silently became relative.
1083///   2. **Consecutive slashes collapse** (c:852-853 inner while-loop).
1084///      C: `"a//b"` → `["a", "b"]` (drop empty between). Filter-on-
1085///      empty Rust gets this right by coincidence.
1086///   3. **Trailing `/` drops** (c:854-857). C: `"a/b/"` → `["a", "b"]`.
1087///      Filter-on-empty Rust gets this right by coincidence.
1088///
1089/// Pin: the leading-empty-segment behavior IS the C contract, not
1090/// an oversight to filter out. Matches `xsymlinks` and any future
1091/// path-walker port that reads slashsplit output.
1092pub fn slashsplit(s: &str) -> Vec<String> {
1093    // c:837
1094    if s.is_empty() {
1095        // c:842
1096        return Vec::new();
1097    }
1098    let mut result = Vec::new();
1099    let mut rest = s;
1100    // c:850 — `while ((t = strchr(s, '/')))`.
1101    while let Some(pos) = rest.find('/') {
1102        result.push(rest[..pos].to_string()); // c:851 ztrduppfx
1103        rest = &rest[pos..];
1104        // c:852-853 — `while (*t == '/') t++;` collapse run.
1105        while let Some(rest_after) = rest.strip_prefix('/') {
1106            rest = rest_after;
1107        }
1108        // c:854-857 — if walked off end, return without emitting tail.
1109        if rest.is_empty() {
1110            return result;
1111        }
1112    }
1113    // c:860 — `*q++ = ztrdup(s);` emit final tail.
1114    result.push(rest.to_string());
1115    result
1116}
1117
1118/// Port of `xsymlinks()` from `Src/utils.c:872` — C decl `static int xsymlinks(char *s)`.
1119///
1120/// Expands `.` and `..` components AND follows ONE LEVEL of
1121/// symlinks (per C source comment at c:865-867: "expands .. or .
1122/// expressions and one level of symlinks"). Used by the `:A`/`:P`
1123/// modifier paths via `subst.rs:6896`.
1124///
1125/// The previous Rust port did `.`/`..` normalization ONLY, with a
1126/// stale doc-comment claiming "Does NOT follow symlinks (matches
1127/// the `physical = 0` mode in C)." That claim is FALSE: C
1128/// `xsymlinks` IS the symlink-following form (it calls `readlink(2)`
1129/// at c:908). The `physical = 0` no-symlink path is a different
1130/// fn (`xsymlink` at utils.c:971, without the trailing `s`). Result:
1131/// `:A` modifier output never resolved actual symlinks — `:A` on
1132/// `/tmp/link -> /usr` returned `/tmp/link` instead of `/usr`.
1133///
1134/// Rewrite: walk components; for each non-`.`/`..` component, try
1135/// `readlink` on the accumulated path. If it succeeds, the target
1136/// replaces (or prepends to) the accumulator; otherwise the
1137/// component appends as-is. C handles re-rooting (absolute symlink
1138/// target replaces buf) and component-level concat.
1139pub fn xsymlinks(s: &str) -> io::Result<String> {
1140    // c:872
1141    if s.is_empty() {
1142        return Ok(String::new());
1143    }
1144
1145    let path = if !s.starts_with('/') {
1146        // c:879 — slashsplit
1147        let cwd = std::env::current_dir()?;
1148        format!("{}/{}", cwd.display(), s)
1149    } else {
1150        s.to_string()
1151    };
1152
1153    let components: Vec<&str> = path.split('/').collect();
1154    // c:877 — `xbuflen = strlen(xbuf)`. Start with empty xbuf for
1155    // absolute paths (the leading "" from slashsplit handles that).
1156    let mut xbuf = String::new();
1157    for comp in components {
1158        match comp {
1159            "" | "." => continue, // c:881
1160            ".." => {
1161                // c:883
1162                if xbuf == "/" || xbuf.is_empty() {
1163                    // c:886-889
1164                    continue;
1165                }
1166                // c:891-895 — walk back one `/`-delimited component.
1167                if let Some(pos) = xbuf.rfind('/') {
1168                    xbuf.truncate(pos);
1169                }
1170            }
1171            c => {
1172                // c:905-907 — `memcpy xbuf2, xbuf` then append `/comp`.
1173                let candidate = format!("{}/{}", xbuf, c); // c:907
1174                                                           // c:908 — `readlink(unmeta(xbuf2), xbuf3, PATH_MAX)`.
1175                #[cfg(unix)]
1176                {
1177                    match fs::read_link(&candidate) {
1178                        // c:908
1179                        Ok(target) => {
1180                            // c:918-933 — successful readlink: target is
1181                            // either absolute (replaces xbuf wholesale) or
1182                            // relative (appends with `/`).
1183                            let t = target.to_string_lossy().into_owned();
1184                            if t.starts_with('/') {
1185                                // c:927
1186                                xbuf = t; // c:928
1187                            } else {
1188                                xbuf = format!("{}/{}", xbuf, t); // c:930-931
1189                            }
1190                            continue;
1191                        }
1192                        Err(_) => {
1193                            // c:909-916 — readlink failed (not a symlink),
1194                            // append the component verbatim.
1195                            xbuf = candidate; // c:910-912
1196                        }
1197                    }
1198                }
1199                #[cfg(not(unix))]
1200                {
1201                    xbuf = candidate;
1202                }
1203            }
1204        }
1205    }
1206
1207    if xbuf.is_empty() {
1208        Ok(if path.starts_with('/') {
1209            "/".to_string()
1210        } else {
1211            ".".to_string()
1212        })
1213    } else {
1214        Ok(xbuf)
1215    }
1216}
1217
1218/// Port of `xsymlink()` from `Src/utils.c:971` — C decl `xsymlink(char *s, int heap)`.
1219///
1220/// ```c
1221/// mod_export char *
1222/// xsymlink(char *s, int heap)
1223/// {
1224///     if (*s != '/')
1225///         return NULL;
1226///     *xbuf = '\0';
1227///     if (!chrealpath(&s, 'P', heap)) {
1228///         zwarn("path expansion failed, using root directory");
1229///         return heap ? dupstring("/") : ztrdup("/");
1230///     }
1231///     return s;
1232/// }
1233/// ```
1234///
1235/// Returns `Some(resolved)` on success, `None` if the path isn't
1236/// absolute (C: returns NULL). On resolve failure emits the same
1237/// "path expansion failed, using root directory" warning and
1238/// returns `Some("/")`.
1239///
1240/// C body (c:971-980):
1241/// ```c
1242/// char *xsymlink(char *s, int heap)
1243/// {
1244///     if (*s != '/') return NULL;
1245///     *xbuf = '\0';
1246///     if (!chrealpath(&s, 'P', heap)) {
1247///         zwarn("path expansion failed, using root directory");
1248///         return heap ? dupstring("/") : ztrdup("/");
1249///     }
1250///     return s;
1251/// }
1252/// ```
1253///
1254/// Previous Rust port was a FAKE — called `std::fs::canonicalize`
1255/// directly with the rationale "same semantics for symlink-
1256/// resolution". That bypassed the canonical `chrealpath` port
1257/// (hist.rs:2311) which handles the partial-prefix walk + xbuf
1258/// state that `fs::canonicalize` doesn't replicate. Re-port now
1259/// matches C line-by-line.
1260///
1261/// Rust signature drift: `heap` param dropped — Rust strings always
1262/// live on the heap; the C `heap` flag toggles between zhalloc
1263/// (heap arena) and ztrdup (process-wide) allocation which has no
1264/// distinction in Rust. Always defaults to `false` (ztrdup arm) at
1265/// the chrealpath call.
1266pub fn xsymlink(path: &str) -> Option<String> {
1267    // c:971
1268    // c:973 — `if (*s != '/') return NULL;`
1269    if !path.starts_with('/') {
1270        // c:973
1271        return None;
1272    }
1273    // c:974 — `*xbuf = '\0';`  Reset the xbuf cursor; no-op in Rust
1274    // (xbuf is an internal chrealpath buffer not exposed at this level).
1275    // c:975 — `if (!chrealpath(&s, 'P', heap))`
1276    match chrealpath(path, b'P', false) {
1277        // c:975
1278        Some(r) => Some(r), // c:979 return s
1279        None => {
1280            // c:976 failure arm
1281            // c:977 — `zwarn("path expansion failed, using root directory");`
1282            zwarn("path expansion failed, using root directory"); // c:977
1283                                                                  // c:978 — `return heap ? dupstring("/") : ztrdup("/");`
1284            Some("/".to_string()) // c:978
1285        }
1286    }
1287}
1288
1289/// Port of `print_if_link()` from `Src/utils.c:985` — C decl `void print_if_link(char *s, int all)`.
1290///
1291/// "Print arrow + symlink target(s) iff `s` is an absolute path
1292/// pointing through symlinks." When `all` is set, follows the
1293/// chain (`xsymlinks` loop, c:992); otherwise emits just the final
1294/// realpath if it differs from the input. Always relative to
1295/// stdout. The Rust port writes via `print!` to mirror C's
1296/// `printf`/`zputs(stdout)` calls.
1297pub fn print_if_link(s: &str, all: bool) {
1298    // c:985
1299    if !s.starts_with('/') {
1300        // c:987
1301        return;
1302    }
1303    if all {
1304        // c:988
1305        let mut start = s.to_string();
1306        loop {
1307            // c:992
1308            match xsymlinks(&start) {
1309                Ok(target) if !target.is_empty() && target != start => {
1310                    print!(" -> "); // c:994
1311                    print!("{}", if target.is_empty() { "/" } else { &target }); // c:995
1312                    start = target; // c:998-999
1313                }
1314                _ => break, // c:1002
1315            }
1316        }
1317    } else {
1318        // c:1006
1319        // c:1007-1011 — HAVE_MEMCCPY arm: copy s into a PATH_MAX-1 buffer
1320        // and DPUTS1 if the input overflows. Rust's String has no fixed
1321        // PATH_MAX limit but the C parity-check still fires when a
1322        // pathological caller passes a path that wouldn't fit C's
1323        // s_at_entry[PATH_MAX+1] buffer.
1324        DPUTS1!(
1325            // c:1009
1326            s.len() >= libc::PATH_MAX as usize, // c:1008 memccpy returns NULL
1327            "path longer than PATH_MAX: {}",
1328            s // c:1009
1329        );
1330        let s_at_entry = s.to_string(); // c:1013
1331                                        // c:1015 — `if (chrealpath(&s, 'P', 0) && strcmp(s, s_at_entry))`
1332                                        // The previous Rust port called std::fs::canonicalize directly —
1333                                        // a fake that bypassed the canonical chrealpath port (hist.rs:2311).
1334        let mut resolved = s.to_string(); // c:1015 &s in/out
1335        if let Some(r) = chrealpath(&resolved, b'P', false) {
1336            // c:1015
1337            resolved = r;
1338            if resolved != s_at_entry {
1339                // c:1015 strcmp(s, s_at_entry)
1340                print!(" -> "); // c:1016
1341                print!("{}", if resolved.is_empty() { "/" } else { &resolved });
1342                // c:1017
1343            }
1344        }
1345    }
1346    let _ = io::stdout().flush();
1347}
1348
1349/// Port of `fprintdir()` from `Src/utils.c:1031` — C decl `void fprintdir(char *s, FILE *f)`.
1350///
1351/// "print a directory" — abbreviates `s` via `finddir` (so a path
1352/// matching `$HOME` or a `nameddirtab` entry shows as `~name/...`)
1353/// and returns the rendering. The C source writes to a FILE*; Rust
1354/// returns the string for the caller to print.
1355pub fn fprintdir(s: &str) -> String {
1356    // c:1031
1357    match finddir(s) {
1358        // c:1031
1359        None => unmeta(s),          // c:1036
1360        Some(rendered) => rendered, // c:1038-1040
1361    }
1362}
1363
1364/// Port of `substnamedir()` from `Src/utils.c:1053` — C decl `char *substnamedir(char *s)`.
1365///
1366/// C body (c:1053-1061):
1367/// ```c
1368/// Nameddir d = finddir(s);
1369/// if (!d)
1370///     return quotestring(s, QT_BACKSLASH);
1371/// return zhtricat("~", d->node.nam, quotestring(s + strlen(d->dir),
1372///                                                QT_BACKSLASH));
1373/// ```
1374///
1375/// The previous Rust port was a FAKE — it called `finddir(s)` (Rust
1376/// signature returns `Option<String>` of the already-formatted
1377/// `~name/rest`) and returned that string unchanged in the Some-arm,
1378/// missing the `quotestring(..., QT_BACKSLASH)` C applies to the
1379/// residue. C's `finddir` returns a `Nameddir` pointer; the
1380/// `~name` + quoted-residue join lives here in `substnamedir`.
1381///
1382/// This re-port duplicates the HOME-first + nameddirtab-scan logic
1383/// (finddir_scan at c:1106) so it can take the (name, dir_prefix)
1384/// split BEFORE pre-joining, then apply quotestring to the residue
1385/// per c:1059.
1386pub fn substnamedir(s: &str) -> String {
1387    // c:1053
1388    // C `finddir` at c:1127 runs the homenode through the SAME
1389    // best-diff scan as the nameddirtab (c:1164-1165) — home COMPETES
1390    // with `hash -d` entries by `diff`, it does not preempt them.
1391    // Duplicate that competition here without the pre-format, so we
1392    // can apply quotestring on just the residue (c:1059).
1393    let home = getsparam("HOME").unwrap_or_default(); // c:1133
1394    let mut finddir_best: i32 = 0; // c:1163
1395    let mut best: Option<(String, String)> = None; // finddir_last
1396    let home_diff = if home.len() == 1 {
1397        0
1398    } else {
1399        home.len() as i32
1400    }; // c:1140-1141
1401    if home_diff > 0 {
1402        if let Some(rest) = s.strip_prefix(&home) {
1403            if rest.is_empty() || rest.starts_with('/') {
1404                finddir_best = home_diff;
1405                // HOME is the implicit homenode (no name).
1406                best = Some((String::new(), rest.to_string()));
1407            }
1408        }
1409    }
1410    // c:1106 finddir_scan — best-diff walk over nameddirtab.
1411    if let Some((name, rest, diff)) = finddir_scan(s) {
1412        if diff > finddir_best {
1413            best = Some((name, rest));
1414        }
1415    }
1416    match best {
1417        // c:1059 — `zhtricat("~", d->node.nam, quotestring(s + strlen(d->dir), QT_BACKSLASH))`
1418        Some((name, rest)) => format!("~{}{}", name, quotestring(&rest, QT_BACKSLASH)),
1419        None => quotestring(s, QT_BACKSLASH), // c:1058
1420    }
1421}
1422
1423// ===========================================================
1424// Direct ports of utility entries from Src/utils.c.
1425// ===========================================================
1426
1427/// Cached current-user lookup.
1428/// Port of `get_username()` from Src/utils.c:1075 — `getpwuid(3)`
1429/// against the current real uid, with the result cached so a
1430/// re-call after setuid sees the new identity. The C source uses
1431/// a `cached_uid`+`cached_username` pair guarded by a uid match;
1432/// the Rust port uses an `OnceLock` keyed on uid for the same
1433/// invalidate-on-uid-change behaviour.
1434pub fn get_username() -> String {
1435    static CACHE: Mutex<Option<(u32, String)>> = Mutex::new(None);
1436
1437    let current_uid = unsafe { libc::getuid() };
1438    let mut guard = CACHE.lock().unwrap();
1439    if let Some((uid, name)) = &*guard {
1440        if *uid == current_uid {
1441            return name.clone();
1442        }
1443    }
1444    let name = unsafe {
1445        let pw = libc::getpwuid(current_uid);
1446        if pw.is_null() {
1447            String::new() // c:1088 — `ztrdup("")` fallback
1448        } else {
1449            // c:1086 — `cached_username = ztrdup_metafy(pswd->pw_name);`.
1450            // `ztrdup_metafy` calls `metafy((char *)s, -1, META_DUP)`
1451            // (utils.c:4929). The previous Rust port returned the raw
1452            // pw_name verbatim — fine for ASCII usernames, but high-bit
1453            // bytes (e.g. an `émile` username on some systems) would
1454            // surface to callers un-metafied. zsh's downstream pipeline
1455            // (param expansion, prompt rendering) assumes paramtab
1456            // entries are metafied; an un-metafied high-bit byte breaks
1457            // the Meta-escape contract.
1458            let raw = std::ffi::CStr::from_ptr((*pw).pw_name)
1459                .to_string_lossy()
1460                .into_owned();
1461            metafy(&raw) // c:1086 ztrdup_metafy
1462        }
1463    };
1464    *guard = Some((current_uid, name.clone()));
1465    name
1466}
1467
1468/// Port of `finddir_scan()` from `Src/utils.c:1106`.
1469///
1470/// Port of `finddir_scan(HashNode hn, UNUSED(int flags))` from Src/utils.c:1106-1112 — ScanFunc the
1471/// C source registers with `scanhashtable(nameddirtab, …)`. Keeps the
1472/// entry with the LARGEST `diff` (zsh.h:2152 — `strlen(dir) -
1473/// strlen(nam)`, the abbreviation savings — NOT the raw dir length)
1474/// whose dir is a boundary-prefix of the path (`!dircmp`, c:1075-1086)
1475/// and which is not ND_NOABBREV (PWD/OLDPWD entries, c:1108).
1476/// WARNING: param names don't match C — Rust=(path) vs C=(hn, flags);
1477/// the C statics finddir_full/finddir_last/finddir_best are threaded
1478/// as the argument and the `(name, rest, diff)` return instead.
1479pub fn finddir_scan(path: &str) -> Option<(String, String, i32)> {
1480    // c:1106
1481    let table = nameddirtab().lock().ok()?;
1482    let mut best: Option<(String, String, i32)> = None;
1483    for (name, nd) in table.iter() {
1484        // c:1107-1108 — `nd->diff > finddir_best && !dircmp(nd->dir,
1485        // finddir_full) && !(nd->node.flags & ND_NOABBREV)`.
1486        if (nd.node.flags & crate::ported::zsh_h::ND_NOABBREV) != 0 {
1487            continue;
1488        }
1489        if best.as_ref().is_some_and(|b| nd.diff <= b.2) {
1490            continue;
1491        }
1492        if let Some(rest) = path.strip_prefix(nd.dir.as_str()) {
1493            if rest.is_empty() || rest.starts_with('/') {
1494                best = Some((name.clone(), rest.to_string(), nd.diff));
1495            }
1496        }
1497    }
1498    best
1499}
1500
1501/// Port of `finddir()` from `Src/utils.c:1127` — C decl `Nameddir finddir(char *s)`.
1502///
1503/// "See if a path has a named directory as its prefix." Compares
1504/// `s` against `$HOME` first (longest implicit named dir), then
1505/// scans the global `nameddirtab`, then falls back to
1506/// `subst_string_by_hook("zsh_directory_name", "d", s)`.
1507///
1508/// Rust signature returns `Option<String>` (the abbreviated path
1509/// `~name/rest`) instead of the C `Nameddir` pointer.
1510pub fn finddir(path: &str) -> Option<String> {
1511    // c:1127
1512    // c:1138 — `homenode.dir = home ? home : "";`. Reads the C global
1513    // `char *home` (params.c:91) DIRECTLY — not via paramtab. zshrs
1514    // ports the global as `params::home_lock()` accessed by
1515    // `homegetfn` (which ignores its &param arg per
1516    // `c:5109 UNUSED(Param pm)`). Pass a default param so the read
1517    // works even before paramtab["HOME"] has been hydrated (during
1518    // early init / unit-test environments).
1519    let _default_pm = crate::ported::zsh_h::param::default();
1520    let home = crate::ported::params::homegetfn(&_default_pm); // c:1138 home
1521                                                               // c:1139-1141 + 1164-1165 — the home node (nam="", diff =
1522                                                               // strlen(home), root's 1 → 0) runs through the SAME best-diff scan
1523                                                               // as the table (`finddir_scan(&homenode.node, 0);
1524                                                               // scanhashtable(nameddirtab, …)`): it COMPETES with `hash -d`
1525                                                               // entries instead of preempting them, so a named dir with a larger
1526                                                               // diff wins (e.g. ~ZPWR over ~/.zpwr). The previous early return
1527                                                               // on the $HOME prefix hid every named dir under $HOME.
1528    let mut finddir_best: i32 = 0; // c:1163
1529    let mut best: Option<(String, String)> = None; // finddir_last
1530    let home_diff = if home.len() == 1 {
1531        0
1532    } else {
1533        home.len() as i32
1534    }; // c:1140-1141
1535    if home_diff > 0 {
1536        if let Some(rest) = path.strip_prefix(&home) {
1537            if rest.is_empty() || rest.starts_with('/') {
1538                finddir_best = home_diff;
1539                best = Some((String::new(), rest.to_string()));
1540            }
1541        }
1542    }
1543    if let Some((name, rest, diff)) = finddir_scan(path) {
1544        // c:1165 — scanhashtable: only a strictly larger diff replaces
1545        // the home candidate (finddir_scan's `nd->diff > finddir_best`).
1546        if diff > finddir_best {
1547            finddir_best = diff;
1548            best = Some((name, rest));
1549        }
1550    }
1551    // c:1167-1175 — zsh_directory_name hook — returns ["name", "len"];
1552    // wins only when its match length beats finddir_best (c:1169).
1553    if let Some(reply) = subst_string_by_hook("zsh_directory_name", Some("d"), path) {
1554        if reply.len() >= 2 {
1555            // c:1170
1556            if let Ok(len) = reply[1].parse::<i32>() {
1557                if len > finddir_best && (len as usize) <= path.len() {
1558                    return Some(format!("~[{}]{}", reply[0], &path[len as usize..]));
1559                }
1560            }
1561        }
1562    }
1563    best.map(|(name, rest)| format!("~{}{}", name, rest)) // c:1177
1564}
1565
1566/// Port of `adduserdir()` from `Src/utils.c:1187`.
1567///
1568/// Port of `void adduserdir(char *s, char *t, int flags, int always)`
1569/// from Src/utils.c:1187.
1570///
1571/// Adds (or removes when `t` is empty / non-absolute) an entry in
1572/// the global `nameddirtab`. ND_USERNAME entries from `getpwnam`
1573/// don't override explicit assignments. AUTONAMEDIRS gating, the
1574/// trailing-slash trim, and PWD/OLDPWD ND_NOABBREV stamp are all
1575/// preserved. Routes through `crate::ported::hashnameddir`.
1576pub fn adduserdir(name: &str, dir: &str, flags: i32, always: bool) {
1577    // c:1187
1578
1579    if !interact() {
1580        return;
1581    } // c:1193
1582    if let Ok(t) = nameddirtab().lock() {
1583        if (flags & ND_USERNAME) != 0 && t.contains_key(name) {
1584            // c:1199
1585            return;
1586        }
1587        if !always && !isset(AUTONAMEDIRS) && !t.contains_key(name) {
1588            // c:1207
1589            return;
1590        }
1591    }
1592    // c:1211 — `if (!t || *t != '/' || strlen(t) >= PATH_MAX)`. C
1593    // rejects paths >= PATH_MAX as too long (would overflow path-
1594    // expansion buffers downstream). PATH_MAX is platform-dependent
1595    // (4096 on Linux, 1024 on macOS); libc::PATH_MAX exposes it.
1596    if dir.is_empty() || !dir.starts_with('/') || dir.len() >= libc::PATH_MAX as usize
1597    // c:1211 strlen(t) >= PATH_MAX
1598    {
1599        let _ = removenameddirnode(name); // c:1214
1600        return;
1601    }
1602    let mut trimmed = dir.trim_end_matches('/').to_string(); // c:1224-1226
1603    if trimmed.is_empty() {
1604        trimmed = dir.to_string(); // c:1227-1233
1605    }
1606    let final_flags = if name == "PWD" || name == "OLDPWD" {
1607        // c:1237
1608        flags | ND_NOABBREV
1609    } else {
1610        flags
1611    };
1612    // c:1239 — `nd = (Nameddir) zshcalloc(sizeof *nd); nd->node.flags = …;
1613    //           nd->dir = ztrdup(t); addnode(nameddirtab, ztrdup(s), nd);`
1614    let nd = nameddir {
1615        node: hashnode {
1616            next: None,
1617            nam: name.to_string(),
1618            flags: final_flags,
1619        },
1620        dir: trimmed,
1621        diff: 0,
1622    };
1623    crate::ported::hashnameddir::addnameddirnode(name, nd);
1624}
1625
1626/// Port of `getnameddir()` from `Src/utils.c:1247` — C decl `char *getnameddir(char *name)`.
1627///
1628/// Looks up `name` in `nameddirtab`; if absent, checks for a
1629/// scalar parameter whose value starts with `/` and registers it
1630/// via `adduserdir`; finally falls back to `getpwnam(name)` for
1631/// `~user`-style lookups when USE_GETPWNAM is enabled.
1632pub fn getnameddir(name: &str) -> Option<String> {
1633    // c:1247
1634    if let Ok(t) = nameddirtab().lock() {
1635        if let Some(nd) = t.get(name) {
1636            // c:1254
1637            return Some(nd.dir.clone());
1638        }
1639    }
1640    // c:1257-1264 — `if ((pm = realparamtab->getnode2(realparamtab, name)) &&
1641    // !pm->level && (PM_TYPE(pm->node.flags) == PM_SCALAR) &&
1642    // (str = getsparam(name)) && *str == '/') { pm->node.flags |=
1643    // PM_NAMEDDIR; adduserdir(name, str, 0, 1); return str; }`. Only a
1644    // GLOBAL string parameter names a directory (54328); a reference is
1645    // PM_NAMEREF, not PM_SCALAR, so `~ref` does not resolve through it.
1646    let global_scalar = crate::ported::params::paramtab()
1647        .read()
1648        .ok()
1649        .and_then(|t| {
1650            t.get(name).map(|pm| {
1651                pm.level == 0
1652                    && crate::ported::zsh_h::PM_TYPE(pm.node.flags as u32)
1653                        == crate::ported::zsh_h::PM_SCALAR
1654            })
1655        })
1656        .unwrap_or(false);
1657    if global_scalar {
1658        if let Some(s) = getsparam(name) {
1659            if s.starts_with('/') {
1660                if let Ok(mut t) = crate::ported::params::paramtab().write() {
1661                    if let Some(pm) = t.get_mut(name) {
1662                        pm.node.flags |= crate::ported::zsh_h::PM_NAMEDDIR as i32; // c:1261
1663                    }
1664                }
1665                adduserdir(name, &s, 0, true); // c:1262
1666                return Some(s); // c:1263
1667            }
1668        }
1669    }
1670    #[cfg(unix)]
1671    {
1672        // c:1268 — getpwnam fallback.
1673        let cn = CString::new(name).ok()?;
1674        let pw = unsafe { libc::getpwnam(cn.as_ptr()) };
1675        if !pw.is_null() {
1676            let raw_dir = unsafe {
1677                std::ffi::CStr::from_ptr((*pw).pw_dir)
1678                    .to_string_lossy()
1679                    .into_owned()
1680            };
1681            // c:1273-1274 — `isset(CHASELINKS) ? xsymlink(pw->pw_dir, 0)
1682            // : ztrdup(pw->pw_dir);`. Resolve symlinks when the option
1683            // is set. Previously omitted in the Rust port — silently
1684            // returned the raw passwd-db dir even when the user had
1685            // `setopt chaselinks`.
1686            let dir = if isset(CHASELINKS) {
1687                xsymlink(&raw_dir).unwrap_or(raw_dir)
1688            } else {
1689                raw_dir
1690            };
1691            // c:1276 — `adduserdir(name, dir, ND_USERNAME, 1);`
1692            // Cache the lookup so subsequent `~user` expansions hit the
1693            // nameddirtab fast-path at c:1254 instead of round-tripping
1694            // through getpwnam every time.
1695            adduserdir(name, &dir, ND_USERNAME, true); // c:1276
1696            return Some(dir);
1697        }
1698    }
1699    None
1700}
1701
1702/// Port of `dircmp()` from `Src/utils.c:1296` — C decl `dircmp(char *s, char *t)`.
1703///
1704/// Compare directory paths (from utils.c dircmp)
1705pub fn dircmp(s: &str, t: &str) -> bool {
1706    let s = s.trim_end_matches('/');
1707    let t = t.trim_end_matches('/');
1708    s == t
1709}
1710
1711// Add a function to the list of pre-prompt functions.                     // c:1332
1712/// Port of `addprepromptfn()` from `Src/utils.c:1319` — C decl `addprepromptfn(voidvoidfnptr_t func)`.
1713///
1714/// Register a callback to run before each prompt.
1715pub fn addprepromptfn(func: fn()) {
1716    // c:1319
1717    PREPROMPT_FNS.lock().unwrap().push(func);
1718}
1719
1720// Remove a function from the list of pre-prompt functions.                // c:1332
1721/// Port of `delprepromptfn()` from `Src/utils.c:1332` — C decl `delprepromptfn(voidvoidfnptr_t func)`.
1722///
1723/// Remove a previously-registered pre-prompt callback.
1724pub fn delprepromptfn(func: fn()) {
1725    // c:1332
1726    let mut list = PREPROMPT_FNS.lock().unwrap();
1727    if let Some(pos) = list.iter().position(|f| *f as usize == func as usize) {
1728        list.remove(pos);
1729    }
1730}
1731
1732// Add a function to the list of timed functions.                           // c:1367
1733/// Port of `addtimedfn()` from `Src/utils.c:1371`.
1734///
1735/// Port of `void addtimedfn(voidvoidfnptr_t func, time_t when)` from
1736/// `Src/utils.c:1371`. Faithful walk of the timedfns LinkList:
1737/// allocate a `Timedfn`, lazy-init the list when empty, otherwise scan
1738/// from `firstnode` and insert BEFORE the first node whose `when` is
1739/// greater than ours. The standard linklist API only inserts AFTER a
1740/// node, so the C loop carries `ln` as the previous node and inserts
1741/// before `next` once `when < next->when`. Note: zsh's `time_t` is
1742/// signed and historically negative-`when` was supported, so we keep
1743/// `i64`.
1744pub fn addtimedfn(func: fn(), when: i64) {
1745    // c:1371
1746    let mut list = TIMED_FNS.lock().unwrap(); // c:1365 timedfns
1747    let tfdat: (i64, fn()) = (when, func); // c:1373-1375 Timedfn tfdat
1748    if list.is_empty() {
1749        // c:1377 !timedfns
1750        list.push(tfdat); // c:1378-1379 znewlinklist + zaddlinknode
1751        return;
1752    }
1753    if list.is_empty() {
1754        // c:1394 !ln (firstnode of empty list)
1755        list.push(tfdat); // c:1395 zaddlinknode
1756        return; // c:1396
1757    }
1758    let mut idx: usize = 0; // c:1381 LinkNode ln = firstnode(timedfns)
1759    loop {
1760        // c:1398 for(;;)
1761        let next = idx + 1; // c:1400 LinkNode next = nextnode(ln)
1762        if next >= list.len() {
1763            // c:1401 !next
1764            list.push(tfdat); // c:1402 zaddlinknode
1765            return; // c:1403
1766        }
1767        let tfdat2_when = list[next].0; // c:1405 tfdat2 = getdata(next)
1768        if when < tfdat2_when {
1769            // c:1406 when < tfdat2->when
1770            list.insert(next, tfdat); // c:1407 zinsertlinknode(timedfns, ln, tfdat)
1771            return; // c:1408
1772        }
1773        idx = next; // c:1410 ln = next
1774    }
1775}
1776
1777/// Port of `deltimedfn()` from `Src/utils.c:1430` — C decl `deltimedfn(voidvoidfnptr_t func)`.
1778///
1779/// Remove a registered timed function (first occurrence only).
1780pub fn deltimedfn(func: fn()) {
1781    // c:1430
1782    let mut list = TIMED_FNS.lock().unwrap();
1783    if let Some(pos) = list.iter().position(|(_, f)| *f as usize == func as usize) {
1784        list.remove(pos);
1785    }
1786}
1787
1788/// Port of `callhookfunc()` from `Src/utils.c:1469`.
1789///
1790/// Invoke a hook function by name plus any `<name>_functions` array.
1791/// Port of `callhookfunc(char *name, LinkList lnklst, int arrayp, int *retval)` from Src/utils.c:1469. Returns 0 if at
1792/// least one hook ran, 1 otherwise — the C source uses the same
1793/// stat semantics so the prompt machinery can detect "did periodic
1794/// fire". Hook dispatch goes through the executor singleton (which
1795/// owns the function table); we look up `name` and then walk the
1796/// `<name>_functions` array exactly as the C source does at
1797/// Src/utils.c:1469.
1798/// Direct port of
1799/// `int callhookfunc(char *name, LinkList lnklst, int arrayp, int *retval)`
1800/// at `Src/utils.c:1469`. C signature mapping:
1801///   - `char *name`     → `name: &str`
1802///   - `LinkList lnklst` → `lnklst: Option<&[String]>` (Rust-shape
1803///     stand-in for the C-shape `LinkList` — both are an ordered list
1804///     of meta-fied strings; the LinkList port itself diverges)
1805///   - `int arrayp`     → `arrayp: i32`
1806///   - `int *retval`    → `retval: *mut i32` (out-param; NULL is fine
1807///     when the caller doesn't need the doshfunc return value)
1808///
1809/// Returns `stat` — 0 if at least one shfunc fired, 1 otherwise
1810/// (matches `c:1495 stat = 0` after every dispatch). When `retval` is
1811/// non-null, `*retval` is set to the most recent `doshfunc`-returned
1812/// status, mirroring the C body's `*retval = ret` semantics.
1813pub fn callhookfunc(name: &str, lnklst: Option<&[String]>, arrayp: i32, retval: *mut i32) -> i32 {
1814    let mut stat: i32 = 1; // c:1475
1815    let mut ret: i32 = 0; // c:1475
1816
1817    // Build the doshfunc arg list `[fname ($0), $1, $2, …]`. A provided
1818    // `lnklst` already carries the ORIGINAL hook name at [0] and the real
1819    // positional args at [1..] (matching C, where the caller does
1820    // `addlinknode(args, name)` before the args). So substitute `fname` for
1821    // [0] — `fname` is the hook name on the direct path (c:1487, identity)
1822    // and a `${name}_functions` member on the array path (c:1504-1508,
1823    // `arg0 = [*arrptr] + argarr[1..]`) — and KEEP [1..].
1824    //
1825    // The prior code prepended `fname` to the WHOLE list (name included),
1826    // so every hook invoked with args saw `$1` == its own name (e.g.
1827    // `preexec`'s `$1` was "preexec" instead of the command line). c:1483.
1828    let mk_args = |fname: &str| -> Vec<String> {
1829        let mut v: Vec<String> = vec![fname.to_string()];
1830        if let Some(extra) = lnklst {
1831            if extra.len() > 1 {
1832                v.extend_from_slice(&extra[1..]);
1833            }
1834        }
1835        v
1836    };
1837
1838    // c:1495 — `if ((shfunc = getshfunc(name))) { doshfunc(...); stat = 0; }`
1839    let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
1840        .read()
1841        .ok()
1842        .and_then(|t| t.get(name).cloned());
1843    if let Some(mut shf) = shf_clone {
1844        let args = mk_args(name);
1845        // c:1487 — `ret = doshfunc(shfunc, lnklst, 1);`. Direct
1846        // doshfunc invocation; body_runner routes through the host
1847        // body-only entry to avoid double-wrapping the scope.
1848        let name_for_body = name.to_string();
1849        let body_args = args.clone();
1850        let body_runner = move || -> i32 {
1851            crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
1852        };
1853        ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
1854        stat = 0; // c:1504
1855    }
1856
1857    if arrayp != 0 {
1858        // c:1507-1525 — fire every `${name}_functions` hook in order.
1859        let arr_name = format!("{}_functions", name); // c:1511
1860        let arr = crate::ported::params::paramtab()
1861            .read()
1862            .ok()
1863            .and_then(|t| t.get(&arr_name).and_then(|p| p.u_arr.clone()))
1864            .unwrap_or_default(); // c:1512 getaparam
1865        for fn_name in arr {
1866            // c:1514
1867            let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
1868                .read()
1869                .ok()
1870                .and_then(|t| t.get(&fn_name).cloned());
1871            if let Some(mut shf) = shf_clone {
1872                // c:1518
1873                let args = mk_args(&fn_name);
1874                // c:1508 — `newret = doshfunc(shfunc, arg0, 1);`.
1875                let name_for_body = fn_name.clone();
1876                let body_args = args.clone();
1877                let body_runner = move || -> i32 {
1878                    crate::ported::exec::run_function_body(&name_for_body, &body_args[1..])
1879                        .unwrap_or(0)
1880                };
1881                ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
1882                stat = 0; // c:1520
1883            }
1884        }
1885    }
1886
1887    // c:1528 — `if (retval) *retval = ret;`
1888    if !retval.is_null() {
1889        unsafe {
1890            *retval = ret;
1891        }
1892    }
1893    let _ = ret;
1894
1895    stat // c:1530
1896}
1897
1898// do pre-prompt stuff                                                      // c:1530
1899/// Run pre-prompt machinery: precmd, periodic, prepromptfns.
1900/// Port of `preprompt()` from Src/utils.c:1530: the `PROMPT_SP` heuristic, the
1901/// pending-job scan, the `precmd` hook + `precmd_functions` array, the
1902/// `periodic` hook on its PERIOD-second cadence, the mail check and the
1903/// prepromptfns registry.
1904pub fn preprompt() {
1905    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1906    // Stamp duration + exit status onto the SQLite history row hend()
1907    // added for the just-finished command (no-op when nothing is
1908    // pending). preprompt runs immediately after execode returns and
1909    // before the next ZLE read, so the elapsed time is command
1910    // wall-time, not prompt idle.
1911    crate::history::history_sqlite_finish(crate::ported::builtin::LASTVAL.load(Ordering::Relaxed));
1912    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1913    // Flush the autoload bytecode cache. `put_one` buffers rather than
1914    // rewriting the whole shard per autoload, and this is the batch
1915    // boundary: every function the just-finished command (or a <TAB>
1916    // completion) autoloaded goes out in ONE write. Cheap when nothing
1917    // is buffered, which is the common case.
1918    crate::autoload_cache::try_flush_pending();
1919    crate::deparse_cache::try_flush_pending();
1920    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1921    // Native p10k engine (src/extensions/p10k): snapshot `$?` and close
1922    // the command timer BEFORE the precmd hook runs, so precmd's own
1923    // commands can't clobber what the status / command_execution_time
1924    // segments show. Mirrors p10k's `_p9k_save_status` precmd ordering.
1925    crate::p10k::note_command_finished(
1926        crate::ported::builtin::LASTVAL.load(Ordering::Relaxed) as i64
1927    );
1928    // c:1532 `static time_t lastperiodic;` — periodic-hook last-fire timestamp.
1929    static LAST_PERIODIC: AtomicI64 = AtomicI64::new(0);
1930    // `lastmailcheck` is module-scoped (LAST_MAILCHECK below) because C makes it
1931    // a file-static shared between checkmail() and checkmailpath() (c:1447).
1932
1933    // c:1535-1536 — `zlong period = getiparam("PERIOD"); zlong mailcheck
1934    //                = getiparam("MAILCHECK");`
1935    let period = crate::ported::params::getiparam("PERIOD");
1936    let mailcheck = crate::ported::params::getiparam("MAILCHECK");
1937
1938    // c:1538-1543 — `winch_unblock(); ... winch_block();` — let any
1939    // pending SIGWINCH fire so the prompt picks up the latest column
1940    // count, then re-block to prevent the resize-handler from
1941    // interrupting prompt rendering mid-paint.
1942    crate::ported::signals_h::winch_unblock();
1943    crate::ported::signals_h::winch_block();
1944
1945    // c:1543-1565 — PROMPT_SP heuristic: when the last command left
1946    // dangling output (no trailing newline), print `$PROMPT_EOL_MARK`,
1947    // pad to the right margin, then `\r` + w spaces + `\r`. On a line that
1948    // already ended cleanly the marker is overwritten by the padding and
1949    // nothing shows; on a partial line the pad wraps to the next row, so
1950    // the prompt starts fresh and the truncated output stays visible with
1951    // the inverse `%` marker.
1952    //
1953    // Was previously left as a no-op on the grounds that `countprompt`
1954    // could not measure zshrs's expanded prompt (which carries readline
1955    // `\x01`/`\x02` ignore markers rather than C's Inpar/Outpar). That is
1956    // stale: countprompt accepts BOTH conventions (prompt.rs:3458).
1957    //
1958    // Without this, `printf 100` (or any output whose last line is
1959    // partial) had its tail overwritten by the next prompt — zsh shows
1960    // `100%` then a fresh prompt line, zshrs showed the prompt painted
1961    // over the `100`.
1962    // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
1963    // In C, `preprompt` runs only from loop() between commands, with the
1964    // cursor at a known baseline. zshrs also reaches it while ZLE owns the
1965    // screen — e.g. with a completion listing still displayed — and the mark
1966    // plus its right-margin padding then lands INSIDE that listing, blanking
1967    // the row from the cursor rightwards (`-z  -- push arguments%` … `stack`).
1968    // Only write the mark when ZLE is not holding the display.
1969    let zle_owns_screen = crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0
1970        || crate::ported::zle::zle_refresh::LISTSHOWN.load(Ordering::Relaxed) != 0;
1971    if isset(crate::ported::zsh_h::PROMPTSP)
1972        && isset(crate::ported::zsh_h::PROMPTCR)
1973        && crate::ported::init::use_exit_printed.load(Ordering::SeqCst) == 0
1974        // c:1543 `&& shout` — init_shout (c:Src/init.c:735-739) points
1975        // `shout` at stderr for an interactive shell without a tty
1976        // (SHTTY == -1), and init_io only calls it when interactive
1977        // (c:Src/init.c:705-706).
1978        && (crate::ported::init::SHTTY.load(Ordering::Relaxed) >= 0
1979            || isset(crate::ported::zsh_h::INTERACTIVE))
1980        && !zle_owns_screen
1981    {
1982        // c:1550-1554 — `$PROMPT_EOL_MARK`, default `%B%S%#%s%b`.
1983        let eolmark = crate::ported::params::getsparam("PROMPT_EOL_MARK")
1984            .filter(|s| !s.is_empty())
1985            .unwrap_or_else(|| "%B%S%#%s%b".to_string());
1986        // c:1555 — the mark is always `%`-expanded, whatever PROMPT_PERCENT
1987        // is set to; restore the option afterwards (c:1559).
1988        let percents = crate::ported::options::opt_state_get("promptpercent");
1989        crate::ported::options::opt_state_set("promptpercent", true);
1990        // c:1556 — `txtunknownattrs = TXT_ATTR_ALL;`
1991        crate::ported::prompt::txtunknownattrs
1992            .store(crate::ported::zsh_h::TXT_ATTR_ALL, Ordering::Relaxed);
1993        let (mark, _, _) = crate::ported::prompt::promptexpand(&eolmark, 1, None); // c:1557
1994        let mut w: i32 = 0; // c:1552
1995        let mut h: i32 = 1;
1996        crate::ported::prompt::countprompt(&mark, &mut w, &mut h, -1); // c:1558
1997        if let Some(p) = percents {
1998            crate::ported::options::opt_state_set("promptpercent", p);
1999        }
2000        // c:1560 `zputs(str, shout)` — the tty, or stderr (c:Src/init.c:738).
2001        let fd = match crate::ported::init::SHTTY.load(Ordering::Relaxed) {
2002            -1 => 2,
2003            tty => tty,
2004        };
2005        // c:1561 reads the `zterm_columns` global as it stands. With no tty
2006        // it is never resampled (adjustwinsize returns early on SHTTY == -1,
2007        // c:Src/utils.c:1900), so it can be 0; `adjustcolumns()` would
2008        // instead seed 80 into it, changing $COLUMNS as a side effect.
2009        let columns = ZTERM_COLUMNS.load(Ordering::SeqCst);
2010        // c:1561-1562 — `fprintf(shout, "%*s\r%*s\r",
2011        //   zterm_columns - w - !hasxn, "", w, "")`. A negative `%*s`
2012        // width means left-justify in a field of its magnitude (C99
2013        // 7.21.6.1p5), so the pad is |n| spaces, never clamped to zero.
2014        let pad = (columns
2015            - w
2016            - if crate::ported::init::hasxn.load(Ordering::SeqCst) != 0 {
2017                0
2018            } else {
2019                1
2020            })
2021        .unsigned_abs() as usize;
2022        // zshrs's `promptexpand` keeps C's Inpar/Outpar non-printing spans as
2023        // readline RL_PROMPT_*_IGNORE bytes (\x01/\x02) — `countprompt` reads
2024        // them (prompt.rs:3458) but the TERMINAL must never see them, exactly
2025        // as zle_refresh filters them before every write (zle_refresh.rs:2274).
2026        let mut out: String = mark
2027            .chars()
2028            .filter(|&c| c != '\u{1}' && c != '\u{2}')
2029            .collect();
2030        out.push_str(&" ".repeat(pad));
2031        out.push('\r');
2032        out.push_str(&" ".repeat(w.max(0) as usize));
2033        out.push('\r');
2034        let _ = write_loop(fd, out.as_bytes()); // c:1560-1563
2035    }
2036
2037    // Reap any children that exited while the shell sat idle. zsh's
2038    // SIGCHLD handler reaps asynchronously during the ZLE read (signals
2039    // are unqueued there), but zshrs runs the interactive event loop
2040    // inside a queued-signal window (init.rs `queue_signals()` at loop
2041    // top), so zhandler QUEUES each SIGCHLD instead of reaping it
2042    // (signals.rs:373). Disowned / background jobs (`&!`, `&`, and
2043    // `<(…)` process substitutions) therefore piled up as zombies
2044    // between prompts — every zpwr/zinit/p10k async `&!` per prompt
2045    // leaked a zombie, slowing startup and eventually exhausting the
2046    // process table. Mirror zhandler's reap+route here, right before
2047    // the scanjobs notification pass, so `waitpid(-1)` clears the
2048    // zombies and the DONE bits land for scanjobs to print/delete. No
2049    // foreground job is live at preprompt, so reaping -1 can't steal a
2050    // fg wait's status.
2051    {
2052        let reaped = crate::ported::signals::wait_for_processes();
2053        if !reaped.is_empty() {
2054            if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
2055                if let Ok(mut guard) = jt.lock() {
2056                    for (pid, status) in reaped {
2057                        let _ = crate::ported::jobs::update_bg_job(&mut guard, pid, status);
2058                    }
2059                }
2060            }
2061        }
2062        // c:Src/jobs.c:651-652 — the `dotrap(SIGCHLD)` update_job owes for a
2063        // child that is not `thisjob`, run now that JOBTAB is unlocked.
2064        for _ in 0..crate::ported::jobs::CHLD_TRAP_PENDING.swap(0, std::sync::atomic::Ordering::SeqCst) {
2065            crate::ported::signals::dotrap(libc::SIGCHLD);
2066        }
2067        // c:Src/jobs.c:658-678 — the trapped SIGINT/SIGQUIT pseudo-delivery
2068        // update_job parked in PSEUDO_SIG_TRAP_PENDING while it held the
2069        // JOBTAB guard: `dotrap(sig); if (errflag) breaks = loops;
2070        // check_cursh_sig(sig);`, run now that JOBTAB is unlocked.
2071        let pseudo = crate::ported::jobs::PSEUDO_SIG_TRAP_PENDING.swap(0, std::sync::atomic::Ordering::SeqCst);
2072        if pseudo != 0 {
2073            crate::ported::signals::dotrap(pseudo); // c:663
2074            if errflag.load(Ordering::Relaxed) != 0 {
2075                crate::ported::builtin::BREAKS.store(
2076                    crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
2077                    Ordering::Relaxed,
2078                ); // c:671-672
2079            }
2080            if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
2081                crate::ported::jobs::check_cursh_sig(&jt.lock().unwrap_or_else(|e| e.into_inner()), pseudo); // c:677
2082            }
2083        }
2084    }
2085
2086    // c:1569-1572 — `if (unset(NOTIFY)) scanjobs();` — sync job-status
2087    // print before prompt, via the canonical scanjobs port
2088    // (Src/jobs.c:1993).
2089    if !crate::ported::zsh_h::isset(crate::ported::zsh_h::NOTIFY) {
2090        if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
2091            let mut guard = jt.lock().unwrap();
2092            crate::ported::jobs::scanjobs(&mut guard);
2093        }
2094    }
2095
2096    // c:1573-1574 — `if (errflag) return;` — bail if a previous error
2097    // already set the global flag.
2098    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
2099    // the C line cited above tests the WHOLE errflag, so masking here let an
2100    // interrupted shell keep going where zsh stops.
2101    if errflag.load(Ordering::Relaxed) != 0 {
2102        return;
2103    }
2104
2105    // c:1576-1580 — `callhookfunc("precmd", NULL, 1, NULL);` + errflag bail.
2106    callhookfunc("precmd", None, 1, std::ptr::null_mut());
2107    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
2108        return;
2109    }
2110
2111    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2112    // Native p10k engine (src/extensions/p10k): rebuild PROMPT/RPROMPT
2113    // after the precmd hook so user precmd state is fresh. Equivalent
2114    // to the zsh theme's `_p9k_precmd` running as the last precmd
2115    // hook (p10k.zsh registers _p9k_precmd via add-zsh-hook). No-op
2116    // unless the powerlevel10k theme source was intercepted.
2117    crate::p10k::preprompt_render();
2118
2119    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2120    // Fire the `async_precmd` hooks on the worker pool AFTER the prompt is
2121    // built/rendered, so they never block the paint. Non-blocking: submits to
2122    // the pool and returns immediately; the hooks write into the shared param
2123    // table which the NEXT prompt render reads. (Same call shape as the p10k
2124    // render above — the ShellExecutor-using logic lives in the extension.)
2125    crate::async_precmd::fire_async_precmd();
2126
2127    // c:1582-1589 — periodic-hook dispatch on PERIOD cadence.
2128    if period > 0 {
2129        let now = std::time::SystemTime::now()
2130            .duration_since(UNIX_EPOCH)
2131            .map(|d| d.as_secs() as i64)
2132            .unwrap_or(0);
2133        if now > LAST_PERIODIC.load(Ordering::Relaxed) + period
2134            && callhookfunc("periodic", None, 1, std::ptr::null_mut()) == 0
2135        {
2136            LAST_PERIODIC.store(now, Ordering::Relaxed);
2137        }
2138    }
2139    // c:1588-1589 — `if (errflag) return;` post-periodic bail.
2140    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
2141    // the C line cited above tests the WHOLE errflag, so masking here let an
2142    // interrupted shell keep going where zsh stops.
2143    if errflag.load(Ordering::Relaxed) != 0 {
2144        return;
2145    }
2146
2147    // c:1591-1611 — mail check: `if (mailcheck && difftime(now,
2148    // lastmailcheck) > mailcheck)` walk MAILPATH (or scalar MAIL) via
2149    // checkmailpath. Faithful port — checkmailpath already exists at
2150    // utils.rs:1623, getaparam exists for the MAILPATH array.
2151    let currentmailcheck = std::time::SystemTime::now()
2152        .duration_since(UNIX_EPOCH)
2153        .map(|d| d.as_secs() as i64)
2154        .unwrap_or(0);
2155    if mailcheck > 0 && (currentmailcheck - LAST_MAILCHECK.load(Ordering::Relaxed)) > mailcheck {
2156        // c:1597 — `if (mailpath && *mailpath && **mailpath)`
2157        let mailpath = crate::ported::params::getaparam("MAILPATH");
2158        let has_mailpath = mailpath
2159            .as_ref()
2160            .map(|p| !p.is_empty() && p.first().map(|s| !s.is_empty()).unwrap_or(false))
2161            .unwrap_or(false);
2162        if has_mailpath {
2163            // c:1598 `checkmailpath(mailpath);` — emit each message to the
2164            // terminal (C writes directly to `shout`).
2165            for m in checkmailpath(mailpath.as_deref().unwrap()) {
2166                println!("{}", m);
2167            }
2168        } else {
2169            // c:1600-1608 — `if ((mailfile = getsparam("MAIL")) && *mailfile)
2170            //                  { x[0]=mailfile; x[1]=NULL; checkmailpath(x); }`
2171            crate::ported::signals::queue_signals(); // c:1600
2172            if let Some(mailfile) = crate::ported::params::getsparam("MAIL") {
2173                if !mailfile.is_empty() {
2174                    let x = vec![mailfile]; // c:1604-1605
2175                    for m in checkmailpath(&x) {
2176                        println!("{}", m); // c:1606
2177                    }
2178                }
2179            }
2180            crate::ported::signals::unqueue_signals(); // c:1608
2181        }
2182        LAST_MAILCHECK.store(currentmailcheck, Ordering::Relaxed); // c:1610
2183    }
2184
2185    // c:1613-1618 — `if (prepromptfns) for (...) ppnode->func();`
2186    let snapshot: Vec<fn()> = PREPROMPT_FNS.lock().unwrap().clone();
2187    for f in snapshot {
2188        f();
2189    }
2190}
2191
2192/// `static time_t lastmailcheck;` (Src/utils.c:1447) — the timestamp of the
2193/// previous mail check. `checkmail()` reads it (mtime/atime comparisons happen
2194/// against the PREVIOUS check) and writes it after walking the paths.
2195pub static LAST_MAILCHECK: AtomicI64 = AtomicI64::new(0);
2196
2197/// Port of `checkmailpath()` from `Src/utils.c:1623` — C decl `void checkmailpath(char **s)`.
2198/// Walks each `PATH` / `PATH?message` MAILPATH component:
2199///   - empty component → `zerr` (c:1634-1636)
2200///   - `mailstat` failure → `zerr` unless ENOENT (c:1637-1639)
2201///   - directory → list entries (`PATH/entry[?message]`) and recurse
2202///     (c:1640-1669)
2203///   - file, interactive (`shout`) only: "You have new mail." or the
2204///     `?`-suffixed message expanded with `$_` bound to the file
2205///     (`parsestr`+`singsub`, c:1670-1699); and, under `MAILWARNING`,
2206///     "The mail in PATH has been read." (c:1700-1704)
2207///
2208/// **Signature note:** C is `void` and writes to `shout`; the zshrs caller
2209/// drives output, so this returns the messages (in C emission order) instead.
2210/// The new-mail / read-warning conditions are now ported faithfully — the
2211/// previous adhoc port used an unfaithful "mtime within 60s" heuristic and its
2212/// result was discarded by the caller, so the feature did nothing.
2213pub fn checkmailpath(s: &[String]) -> Vec<String> {
2214    // c:1620
2215    let mut messages = Vec::new();
2216    let interactive = *crate::ported::init::shout.lock().unwrap() != 0; // c:1670 `else if (shout)`
2217    let lastmailcheck = LAST_MAILCHECK.load(Ordering::Relaxed);
2218
2219    for path in s {
2220        // c:1627-1633 — split on the first '?': `file` before, `u` after (or None).
2221        let (file, u) = match path.find('?') {
2222            Some(p) => (&path[..p], Some(&path[p + 1..])),
2223            None => (path.as_str(), None),
2224        };
2225
2226        if file.is_empty() {
2227            // c:1634-1636 — `if (**s == 0) zerr("empty MAILPATH component: %s", *s);`
2228            zerr(&format!("empty MAILPATH component: {}", path));
2229            continue;
2230        }
2231
2232        let mut st: libc::stat = unsafe { std::mem::zeroed() };
2233        let real = unmeta(file); // c:1637 — mailstat(unmeta(*s), &st)
2234        if mailstat(&real, &mut st) == -1 {
2235            // c:1638-1639 — report anything other than "no such file".
2236            let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2237            if e != libc::ENOENT {
2238                zerr(&format!("{}: {}", zsh_errno_msg(e), path));
2239            }
2240        } else if (st.st_mode as libc::mode_t & libc::S_IFMT) == libc::S_IFDIR {
2241            // c:1640-1669 — a directory: enumerate entries and recurse.
2242            match fs::read_dir(&real) {
2243                Err(_) => {
2244                    let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2245                    zerr(&format!("{}: {}", zsh_errno_msg(e), path)); // c:1647
2246                }
2247                Ok(mut rd) => {
2248                    let mut arr = Vec::new();
2249                    // c:1653-1662 — `while ((fn = zreaddir(lock, 1)) && !errflag)`.
2250                    while let Some(fname) = zreaddir(&mut rd, 1) {
2251                        if errflag.load(Ordering::Relaxed) != 0 {
2252                            break;
2253                        }
2254                        // c:1654-1657 — keep the `?message` suffix on each entry.
2255                        arr.push(match u {
2256                            Some(u) => format!("{}/{}?{}", path, fname, u),
2257                            None => format!("{}/{}", path, fname),
2258                        });
2259                    }
2260                    messages.extend(checkmailpath(&arr)); // c:1667 — recurse
2261                }
2262            }
2263        } else if interactive {
2264            // c:1671-1672 — new mail: non-empty, not yet read, newer than the
2265            // last check.
2266            if st.st_size != 0 && st.st_atime <= st.st_mtime && st.st_mtime >= lastmailcheck {
2267                match u {
2268                    // c:1673-1675 — `fprintf(shout, "You have new mail.\n");`
2269                    None => messages.push("You have new mail.".to_string()),
2270                    // c:1676-1698 — custom message: bind `$_` to the file, then
2271                    // parsestr + singsub the message (prompt-style expansion).
2272                    Some(u) => {
2273                        let usav = crate::ported::init::zunderscore.lock().unwrap().clone();
2274                        crate::ported::exec::setunderscore(path); // c:1685 setunderscore(*s)
2275                        if let Ok(parsed) = crate::ported::lex::parsestr(u) {
2276                            // c:1688-1691 — parse ok → singsub then emit.
2277                            messages.push(crate::ported::subst::singsub(&parsed));
2278                        }
2279                        crate::ported::exec::setunderscore(&usav); // c:1694-1697 restore $_
2280                    }
2281                }
2282            }
2283            // c:1700-1704 — MAILWARNING: file read since the last check.
2284            if crate::ported::zsh_h::isset(crate::ported::zsh_h::MAILWARNING)
2285                && st.st_atime > st.st_mtime
2286                && st.st_atime > lastmailcheck
2287                && st.st_size != 0
2288            {
2289                messages.push(format!("The mail in {} has been read.", unmeta(file)));
2290            }
2291        }
2292    }
2293    messages
2294}
2295
2296/// Port of `FILE *xtrerr` from `Src/utils.c:1714` — the stream xtrace
2297/// output goes to. Held as the fd behind the FILE: `2` is `stderr`
2298/// (c:Src/init.c:637 `xtrerr = stderr`). `execcmd_exec` points it at a
2299/// private copy of stderr for the duration of a simple command
2300/// (c:Src/exec.c:3766-3772), so the trace
2301/// of `print x 2>file` — and of whatever a redirected `.` runs — reaches
2302/// the stderr the command was started with, not the file.
2303#[allow(non_upper_case_globals)]
2304pub static xtrerr: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(2); // c:1714
2305
2306/// Port of `printprompt4()` from `Src/utils.c:1718`.
2307///
2308/// Render the PS4 / PROMPT4 prefix and write it to stderr. zsh's
2309/// implementation reads `prompt4` global, suppresses XTRACE during
2310/// promptexpand (so subshells inside `%(?…)` don't recursively trace),
2311/// then fprintf's the expanded prefix to xtrerr. zshrs uses the same
2312/// suppress-XTRACE-around-expand pattern; ksh/sh emulation defaults
2313/// to `+ ` per Src/init.c:1192, zsh default to `+%N:%i> `.
2314///
2315/// The C source's caller (Src/exec.c::tracingcond etc.) follows this
2316/// with the per-line/per-arg fprintf — same shape mirrored at the two
2317/// zshrs call sites in fusevm_bridge.rs (BUILTIN_XTRACE_LINE / ARGS).
2318pub(crate) fn printprompt4() {
2319    // c:1720-1721 — `if (!xtrerr) xtrerr = stderr;`. There is NO XTRACE
2320    // test here: every XTRACE caller gates itself, and source()'s
2321    // SOURCE_TRACE line (c:Src/init.c:1627-1630) prints PS4 with XTRACE off.
2322    // c:utils.c:1722-1724 — `if (prompt4) { ... s = dupstring(prompt4);`
2323    // C `prompt4` is a global initialized in init.c:1192 from the
2324    // emulation bits; PS4/PROMPT4 paramtab entries alias it via
2325    // IPDEF7R/IPDEF7 (params.c:381, 421). Read from paramtab to
2326    // honour user-set values, fall back to the same emulation
2327    // default C uses at init.c:1192.
2328    let posix = EMULATION(EMULATE_KSH | EMULATE_SH); // c:init.c:1192
2329    let prefix_template = getsparam("PS4")
2330        .or_else(|| getsparam("PROMPT4"))
2331        .unwrap_or_else(|| {
2332            if posix {
2333                "+ ".to_string() // c:init.c:1192
2334            } else {
2335                "+%N:%i> ".to_string() // c:init.c:1193
2336            }
2337        });
2338    // c:utils.c:1723,1726,1730 — `t = opts[XTRACE]; opts[XTRACE] = 0;
2339    //                              promptexpand(...);  opts[XTRACE] = t;`
2340    let saved = isset(XTRACE);
2341    opt_state_set(&opt_name(XTRACE), false);
2342    let prefix = crate::prompt::expand_prompt(&prefix_template);
2343    opt_state_set(&opt_name(XTRACE), saved);
2344    // c:utils.c:1736 — `fprintf(xtrerr, "%s", s)`. Append to the xtrerr
2345    // line buffer rather than writing straight to stderr; the emitter
2346    // appends the command/args and flushes the whole line in one write,
2347    // matching C's single `fflush(xtrerr)` (so concurrent pipeline-stage
2348    // trace lines never interleave).
2349    crate::fusevm_bridge::xtrerr_fputs(&prefix);
2350}
2351
2352/// Port of `freestr()` from `Src/utils.c:1739` — C decl `freestr(void *a)`.
2353///
2354/// C body:
2355/// ```c
2356/// void freestr(void *a) { zsfree(a); }
2357/// ```
2358/// The C function is registered as the `freenode` callback for
2359/// hashtables holding plain string values. The Rust port consumes
2360/// `a` by value; Rust's `Drop` runs the equivalent of `zsfree` when
2361/// the `String` is moved into this fn and falls out of scope at the
2362/// closing brace — the no-op body is the correct port. Param name
2363/// `a` matches C exactly per Rule E.
2364pub fn freestr(_a: String) {}
2365
2366/// Port of `gettempfile()` from `Src/utils.c:2231`.
2367///
2368/// Port of `int gettempfile(const char *prefix, int use_heap, char **tempname)`
2369/// from Src/utils.c:2231. Creates a fresh tempfile with `O_RDWR|O_CREAT|O_EXCL`
2370/// and mode 0600 under umask 0177; returns `(fd, path)` on success.
2371///
2372/// C signature uses an out-param for the filename and returns the fd; Rust
2373/// returns both as a tuple. Matches the C control-flow: open with O_EXCL,
2374/// retry up to 16 times on EEXIST (the non-mkstemp branch, c:2255-2269).
2375pub fn gettempfile(prefix: Option<&str>) -> Option<(i32, String)> {
2376    // c:2231
2377    #[cfg(unix)]
2378    {
2379        queue_signals(); // c:2239
2380        let old_umask = unsafe { libc::umask(0o177) }; // c:2240
2381        let mut failures = 0; // c:2255
2382        let mut result: Option<(i32, String)> = None;
2383        loop {
2384            let fn_ = match gettempname(prefix, false) {
2385                // c:2260
2386                Some(n) => n,
2387                None => break,
2388            };
2389            let cn = match CString::new(fn_.clone()) {
2390                Ok(c) => c,
2391                Err(_) => break,
2392            };
2393            let fd = unsafe {
2394                libc::open(
2395                    cn.as_ptr(),
2396                    libc::O_RDWR | libc::O_CREAT | libc::O_EXCL, // c:2264
2397                    0o600 as libc::c_int,
2398                )
2399            };
2400            if fd >= 0 {
2401                result = Some((fd, fn_));
2402                break;
2403            }
2404            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2405            if err != libc::EEXIST {
2406                // c:2269
2407                break;
2408            }
2409            failures += 1;
2410            if failures >= 16 {
2411                // c:2269
2412                break;
2413            }
2414        }
2415        unsafe {
2416            libc::umask(old_umask);
2417        } // c:2273
2418        unqueue_signals(); // c:2274
2419        result
2420    }
2421    #[cfg(not(unix))]
2422    {
2423        let _ = prefix;
2424        None
2425    }
2426}
2427
2428/// Port of `void gettyinfo(struct ttyinfo *ti)` from Src/utils.c:1746.
2429///
2430/// The saved baseline terminal mode — C's `struct ttyinfo shttyinfo`
2431/// (Src/init.c). Captured ONCE while the terminal is still in its
2432/// original (cooked / canonical) mode, before ZLE ever puts it into
2433/// raw mode via `zsetterm()`. `trashzle()` restores this baseline via
2434/// `settyinfo(&shttyinfo)` so that external commands the user runs
2435/// (`cat`, `less`, …) execute with a cooked terminal — where `^D` is
2436/// EOF, `^C` is SIGINT, and line editing works. Without it the tty
2437/// stayed in ZLE raw mode across command execution, so `cat` never
2438/// saw EOF on `^D`.
2439#[cfg(unix)]
2440pub static SHTTYINFO: std::sync::Mutex<Option<libc::termios>> = std::sync::Mutex::new(None);
2441
2442/// Port of `gettyinfo()` from `Src/utils.c:1746`.
2443///
2444/// Reads the current termios from the global `SHTTY`. Returns
2445/// `None` when SHTTY is closed or the call fails (matching C's
2446/// silent return when SHTTY == -1).
2447/// C body (single statement): `fdgettyinfo(SHTTY, ti);`
2448/// Rust returns `Option<termios>` instead of taking an out-ptr;
2449/// the SHTTY=-1 case naturally returns Err from fdgettyinfo → None.
2450#[cfg(unix)]
2451pub fn gettyinfo() -> Option<libc::termios> {
2452    // c:1746
2453    fdgettyinfo(SHTTY.load(Ordering::Relaxed)).ok() // c:1748
2454}
2455
2456/// Port of `fdgettyinfo()` from `Src/utils.c:1753`.
2457///
2458/// Emit the `$PS4` xtrace prefix to stderr.
2459/// Read terminal mode from a file descriptor.
2460/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1753. C source uses
2461/// `tcgetattr(SHTTY, &ti->tio)`; we return the populated termios
2462/// or an io::Error on failure (caller equivalent to zsh's `zerr`).
2463#[cfg(unix)]
2464/// WARNING: param names don't match C — Rust=(fd) vs C=(SHTTY, ti)
2465pub fn fdgettyinfo(fd: i32) -> io::Result<libc::termios> {
2466    let mut tio: libc::termios = unsafe { std::mem::zeroed() };
2467    if unsafe { libc::tcgetattr(fd, &mut tio) } == -1 {
2468        Err(io::Error::last_os_error())
2469    } else {
2470        Ok(tio)
2471    }
2472}
2473
2474/// Port of `fdgettyinfo()` from `Src/utils.c:1753` — C decl `fdgettyinfo(int SHTTY, struct ttyinfo *ti)`.
2475#[cfg(not(unix))]
2476/// WARNING: param names don't match C — Rust=(_fd) vs C=(SHTTY, ti)
2477pub fn fdgettyinfo(_fd: i32) -> std::io::Result<()> {
2478    Err(std::io::Error::new(
2479        std::io::ErrorKind::Unsupported,
2480        "no termios",
2481    ))
2482}
2483
2484/// Port of `settyinfo()` from `Src/utils.c:1778` — C decl `void settyinfo(struct ttyinfo *ti)`.
2485///
2486/// Restores the termios state on the global `SHTTY` with EINTR
2487/// retry; no-op when SHTTY is closed.
2488/// C body (single statement): `fdsettyinfo(SHTTY, ti);`
2489/// Rust returns bool; SHTTY=-1 yields Err → false from fdsettyinfo.
2490#[cfg(unix)]
2491pub fn settyinfo(ti: &libc::termios) -> bool {
2492    // c:1778
2493    fdsettyinfo(SHTTY.load(Ordering::Relaxed), ti).is_ok() // c:1780
2494}
2495
2496/// Port of `fdsettyinfo()` from `Src/utils.c:1785`.
2497///
2498/// Apply terminal mode to a file descriptor, with EINTR retry.
2499/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1785. C source loops
2500/// `while (tcsetattr(SHTTY, TCSADRAIN, &ti->tio) == -1 && errno
2501/// == EINTR)` — same retry shape here.
2502#[cfg(unix)]
2503pub fn fdsettyinfo(SHTTY2: i32, ti: &libc::termios) -> io::Result<()> {
2504    loop {
2505        if unsafe { libc::tcsetattr(SHTTY2, libc::TCSADRAIN, ti) } != -1 {
2506            return Ok(());
2507        }
2508        let err = io::Error::last_os_error();
2509        if err.kind() != io::ErrorKind::Interrupted {
2510            return Err(err);
2511        }
2512    }
2513}
2514
2515/// Port of `fdsettyinfo()` from `Src/utils.c:1785` — C decl `fdsettyinfo(int SHTTY, struct ttyinfo *ti)`.
2516#[cfg(not(unix))]
2517pub fn fdsettyinfo(SHTTY: i32, ti: &()) -> std::io::Result<()> {
2518    Err(std::io::Error::new(
2519        std::io::ErrorKind::Unsupported,
2520        "no termios",
2521    ))
2522}
2523
2524/// Port of the `zterm_lines` global declared at `Src/params.c:105`
2525/// (`zterm_lines,	/* $LINES       */`).
2526///
2527/// C aliases the `$LINES` parameter to this very storage —
2528/// `Src/params.c:363` `IPDEF5("LINES", &zterm_lines, zlevar_gsu)` — so
2529/// a shell script's `$LINES` and every C reader of `zterm_lines` are
2530/// one number, and it changes only where `adjustwinsize` writes it.
2531pub static ZTERM_LINES: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
2532
2533/// Port of the `zterm_columns` global declared at `Src/params.c:104`
2534/// (`zterm_columns,	/* $COLUMNS     */`), aliased to `$COLUMNS` by
2535/// `Src/params.c:362` `IPDEF5("COLUMNS", &zterm_columns, zlevar_gsu)`.
2536/// Same single-storage contract as `ZTERM_LINES` above.
2537pub static ZTERM_COLUMNS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
2538
2539// window size changed                                                     // c:1831
2540/// Reads the `zterm_lines` global — `Src/utils.c:1833` `int oldlines =
2541/// zterm_lines;` is the shape every C caller uses.
2542///
2543/// C's `adjustlines(int signalled)` (c:1831) is a FILE-STATIC helper
2544/// that only `adjustwinsize` calls; it refreshes `zterm_lines` from the
2545/// already-probed `shttyinfo.winsize` and returns whether the value
2546/// changed. Every other C site — `Src/Zle/compresult.c:1932`,
2547/// `Src/loop.c:375`, `Src/Zle/zle_refresh.c` — just reads the bare
2548/// global. This function stands for that read, which is why it returns
2549/// the row count rather than C's changed-flag.
2550///
2551/// The TIOCGWINSZ probe lives in `adjustwinsize` (c:1902) and NOWHERE
2552/// else. It used to live here, re-sampling the tty on every call, and
2553/// that broke C's single-storage contract: `$LINES`/`$COLUMNS` (updated
2554/// only when the SIGWINCH handler runs) and this function (live) gave
2555/// two different answers for the whole window between a resize and the
2556/// handler — which C blocks SIGWINCH across by default
2557/// (`Src/init.c:1458` `winch_block()`). A completion listing built its
2558/// display strings from `$COLUMNS` and then had `calclist` count them
2559/// against the live width: `git <TAB>` across a 24x80 -> 24x60 resize
2560/// asked "see all 164 possibilities (251 lines)?" for 164 one-line
2561/// matches.
2562///
2563/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2564pub fn adjustlines() -> usize {
2565    // c:1833
2566    let cached = ZTERM_LINES.load(Ordering::SeqCst);
2567    if cached > 0 {
2568        return cached as usize;
2569    }
2570    // c:1841-1845 — `if (zterm_lines <= 0) zterm_lines = tclines > 0 ?
2571    //                tclines : 24;`
2572    //
2573    // !!! RUST-ONLY SEEDING !!! C can reach this with `shttyinfo.winsize`
2574    // already filled by `setupvals`'s `adjustwinsize(0)` (Src/init.c:1276);
2575    // zshrs has callers that run before that (see prompt.rs:3385), and
2576    // for them a one-shot probe is the only source of a real geometry.
2577    // Once seeded the value is cached like C's global, so the split
2578    // between "what the shell says" and "what the tty says" cannot
2579    // reopen.
2580    #[cfg(unix)]
2581    {
2582        unsafe {
2583            // c:1902 probes SHTTY, not fd 1; fall back to fd 1 only while
2584            // SHTTY is still unset (-1), which is where the previous
2585            // always-probe port read from.
2586            let shtty = SHTTY.load(Ordering::Relaxed);
2587            let fd = if shtty >= 0 { shtty } else { 1 };
2588            let mut ws: libc::winsize = std::mem::zeroed();
2589            if libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_row > 0 {
2590                ZTERM_LINES.store(ws.ws_row as i32, Ordering::SeqCst);
2591                return ws.ws_row as usize;
2592            }
2593        }
2594    }
2595    // paramtab `$LINES`, not OS env. See adjustcolumns for why the `> 0`
2596    // filter is load-bearing.
2597    let n = getsparam("LINES")
2598        .and_then(|s| s.parse().ok())
2599        .filter(|&n: &usize| n > 0)
2600        .unwrap_or(24); // c:1844
2601    ZTERM_LINES.store(n as i32, Ordering::SeqCst);
2602    n
2603}
2604
2605/// Reads the `zterm_columns` global — `Src/utils.c:1858` `int
2606/// oldcolumns = zterm_columns;`.
2607///
2608/// See `adjustlines` above for why this is a cached read and not the
2609/// TIOCGWINSZ probe it used to be; C's `adjustcolumns(int signalled)`
2610/// (c:1856) is the file-static half that only `adjustwinsize` calls.
2611///
2612/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2613pub fn adjustcolumns() -> usize {
2614    // c:1858
2615    let cached = ZTERM_COLUMNS.load(Ordering::SeqCst);
2616    if cached > 0 {
2617        return cached as usize;
2618    }
2619    // c:1866-1870 — `if (zterm_columns <= 0) zterm_columns = tccolumns >
2620    //                0 ? tccolumns : 80;`. Same Rust-only seeding note as
2621    // `adjustlines`.
2622    #[cfg(unix)]
2623    {
2624        unsafe {
2625            let shtty = SHTTY.load(Ordering::Relaxed);
2626            let fd = if shtty >= 0 { shtty } else { 1 };
2627            let mut ws: libc::winsize = std::mem::zeroed();
2628            if libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
2629                ZTERM_COLUMNS.store(ws.ws_col as i32, Ordering::SeqCst);
2630                return ws.ws_col as usize;
2631            }
2632        }
2633    }
2634    // C consults `getsparam("COLUMNS")` (paramtab), not OS env.
2635    // The `> 0` filter is C's `<= 0` clamp: a literal `COLUMNS=0` (what a
2636    // TERM=dumb pty leaves behind) parses fine, so without it callers that
2637    // divide by the column count panic instead of falling back to 80.
2638    let n = getsparam("COLUMNS")
2639        .and_then(|s| s.parse().ok())
2640        .filter(|&n: &usize| n > 0)
2641        .unwrap_or(80); // c:1869
2642    ZTERM_COLUMNS.store(n as i32, Ordering::SeqCst);
2643    n
2644}
2645
2646// window size changed                                                      // c:1824
2647/// Port of `adjustwinsize()` from `Src/utils.c:1889` — C decl `void adjustwinsize(int from)`.
2648/// SIGWINCH handler + LINES/COLUMNS-update entry. Reads the tty's
2649/// current geometry via `TIOCGWINSZ` ioctl, updates the cached
2650/// `zterm_lines`/`zterm_columns`, and writes `$LINES` / `$COLUMNS`
2651/// when they're already set in the environment.
2652/// ```c
2653/// void
2654/// adjustwinsize(int from)
2655/// {
2656///     static int getwinsz = 1;
2657///     int ttyrows = shttyinfo.winsize.ws_row;
2658///     int ttycols = shttyinfo.winsize.ws_col;
2659///     int resetzle = 0;
2660///     if (getwinsz || from == 1) {
2661///         if (SHTTY == -1) return;
2662///         if (ioctl(SHTTY, TIOCGWINSZ, &shttyinfo.winsize) == 0) {
2663///             resetzle = (ttyrows != ... || ttycols != ...);
2664///             ...
2665///         } else {
2666///             shttyinfo.winsize.ws_row = zterm_lines;
2667///             shttyinfo.winsize.ws_col = zterm_columns;
2668///         }
2669///     }
2670///     switch (from) {
2671///     case 0: case 1:
2672///         getwinsz = 0;
2673///         if (adjustlines(from) && zgetenv("LINES")) setiparam("LINES", zterm_lines);
2674///         if (adjustcolumns(from) && zgetenv("COLUMNS")) setiparam("COLUMNS", zterm_columns);
2675///         getwinsz = 1;
2676///         break;
2677///     case 2: resetzle = adjustlines(0); break;
2678///     case 3: resetzle = adjustcolumns(0); break;
2679///     }
2680///     if (interact && resetzle) zleentry(ZLE_CMD_REFRESH);
2681/// }
2682/// ```
2683pub fn adjustwinsize(from: i32) {
2684    // c:1889
2685    //
2686    // Returns nothing, like C's `void adjustwinsize(int from)` (c:1888-1889).
2687    // This used to return `(usize, usize)`, and it built that pair by calling
2688    // `adjustcolumns()` / `adjustlines()` again on the way out — a Rust-only
2689    // return value that no caller ever read (every call site is `let _ =`).
2690    // Those trailing re-entries DEADLOCKED the shell: `assignnparam` takes the
2691    // paramtab WRITE guard and still holds it when it dispatches the c:2874
2692    // gsu setfn (`intsetfn` on its PM_INTEGER arm), and the setfn behind
2693    // `$LINES` is `zlevarsetfn`, which calls back in here with from=2
2694    // (c:4232). Exiting through `adjustcolumns()` then reached
2695    // `getsparam("COLUMNS")` — its last-resort fallback, below — which wants a
2696    // READ lock on the very table the caller still holds for writing. std's
2697    // `RwLock` is not reentrant, so the shell
2698    // parked in `lock_contended` forever. C cannot hit this: `adjustcolumns`
2699    // (c:1856-1878) reads `shttyinfo.winsize` and `tccolumns` and never once
2700    // touches the parameter table.
2701    //
2702    // The window it needed was a terminal reporting 0 columns, because only
2703    // then does `adjustcolumns` fall past its ioctl to the parameter fallback.
2704    // A `zsh/zpty` slave is exactly that whenever the shell that spawned it is
2705    // NOT interactive: the stamp that would give the slave a real geometry,
2706    // `Src/Modules/zpty.c:374-377`, sits inside an `if (interact)` at
2707    // zpty.c:371, so a pty opened from a script keeps the 0x0 the kernel gave
2708    // it. Measured: both shells report 24x80 (their fallbacks, not a real
2709    // window) inside a zpty spawned from `zsh -f <script>`, and giving the
2710    // OUTER shell a 40x100 terminal changes nothing, because it is still not
2711    // interactive. That is every pty the test harness opens, which is why this
2712    // took out the zpty parity suite wholesale while leaving hand-run
2713    // interactive shells alone.
2714
2715    // c:1891 — `static int getwinsz = 1;`
2716    let getwinsz = ADJUSTWINSIZE_GETWINSZ.load(Ordering::SeqCst);
2717
2718    // c:1893-1894 — `int ttyrows = shttyinfo.winsize.ws_row;` /
2719    // `ttycols = shttyinfo.winsize.ws_col;` — the PREVIOUSLY cached
2720    // geometry, read before the ioctl overwrites it, so c:1903 can tell
2721    // whether the window actually changed size.
2722    //
2723    // !!! RUST-ONLY SOURCING !!! zshrs's `SHTTYINFO` (utils.rs) holds only
2724    // the `termios` half of C's `struct ttyinfo`; there is no cached
2725    // `winsize` to read. The last geometry this function published lives
2726    // in `ZTERM_LINES` / `ZTERM_COLUMNS`, which the c:1837/c:1862 stores
2727    // below keep in lock-step with `shttyinfo.winsize` — C's c:1839/c:1864
2728    // else-arms copy the globals straight back into that struct, so the
2729    // two are the same number. Same values, same comparison.
2730    let mut ttyrows: i32 = ZTERM_LINES.load(Ordering::SeqCst); // c:1893
2731    let mut ttycols: i32 = ZTERM_COLUMNS.load(Ordering::SeqCst); // c:1894
2732    let mut resetzle = 0i32; // c:1896
2733
2734    // c:1898-1917 — TIOCGWINSZ probe.
2735    if getwinsz != 0 || from == 1 {
2736        // c:1898
2737        let shtty = SHTTY.load(Ordering::Relaxed);
2738        if shtty == -1 {
2739            // c:1900 — no tty: `zterm_lines` / `zterm_columns` keep what
2740            // setupvals imported from the environment (c:Src/init.c:1294-1305),
2741            // or 0. `zsh -fc 'print $COLUMNS $LINES' </dev/null` with no
2742            // controlling tty prints `0 0`.
2743            return; // c:1901
2744        }
2745        #[cfg(unix)]
2746        unsafe {
2747            let mut ws: libc::winsize = std::mem::zeroed();
2748            if libc::ioctl(shtty, libc::TIOCGWINSZ, &mut ws as *mut _) == 0 {
2749                // c:1902
2750                // c:1903-1904 — `resetzle = (ttyrows != ws_row ||
2751                //                            ttycols != ws_col);`
2752                // This assignment was MISSING: `resetzle` was left at 0 on
2753                // the `from == 1` (SIGWINCH) path, so the c:1954 redraw
2754                // never fired and a resize left the screen as the terminal
2755                // had mangled it. With a completion listing displayed and
2756                // the window SHRUNK, the terminal drops the top rows and
2757                // nothing repaints them — zsh kept all 3 non-blank rows on
2758                // a 24x80 -> 14x80 shrink, zshrs kept 0.
2759                resetzle = (ttyrows != ws.ws_row as i32 || ttycols != ws.ws_col as i32) as i32;
2760                ttyrows = ws.ws_row as i32; // c:1907
2761                ttycols = ws.ws_col as i32; // c:1908
2762            }
2763        }
2764    }
2765
2766    match from {
2767        // c:1921
2768        0 | 1 => {
2769            // c:1922-1923
2770            ADJUSTWINSIZE_GETWINSZ.store(0, Ordering::SeqCst); // c:1924
2771                                                               // c:1931-1934 — C: `if (adjustlines(from) && zgetenv("LINES"))
2772                                                               // setiparam("LINES", zterm_lines);` (same for COLUMNS). The
2773                                                               // zgetenv gate only guards re-publishing to an ENV-exported
2774                                                               // LINES — in C the param itself is ALWAYS live because its
2775                                                               // valptr aliases the `zterm_lines` global that adjustlines
2776                                                               // just wrote (createparamtable IPDEF5 → zlevarsetfn/intvargetfn
2777                                                               // on &zterm_lines). zshrs params store their own u_val copy
2778                                                               // (no valptr aliasing), so the probe result must be written
2779                                                               // through setiparam unconditionally — gating on the env left
2780                                                               // `$LINES`/`$COLUMNS` at 0 for every interactive shell
2781                                                               // (LINES is not in the environment; zsh doesn't export it).
2782                                                               // Effect: `$(( LINES / 3 ))` = 0 → division-by-zero in
2783                                                               // history-search-multi-word pagination (_hsmw_main:81), and
2784                                                               // every LINES/COLUMNS-derived layout was wrong. Recursion is
2785                                                               // safe: setiparam → zlevarsetfn → adjustwinsize(2|3) never
2786                                                               // re-enters this arm.
2787            let lines = if ttyrows > 0 {
2788                ttyrows as i64 // c:1837 — zterm_lines = shttyinfo.winsize.ws_row
2789            } else {
2790                adjustlines() as i64 // c:1844 — tclines/24 fallback chain
2791            };
2792            // c:1837 — `zterm_lines = shttyinfo.winsize.ws_row;`, the ONE
2793            // place the row count is published. It has to land in the
2794            // global BEFORE setiparam, because setiparam's zlevarsetfn
2795            // recursion (c:1932 → params.c:4232 → this function, from=2)
2796            // reads it back.
2797            ZTERM_LINES.store(lines as i32, Ordering::SeqCst); // c:1837
2798                                                               // c:1847-1850 — `if (zterm_lines > 2) termflags &= ~TERM_SHORT;
2799                                                               //                else termflags |= TERM_SHORT;`
2800            if lines > 2 {
2801                crate::ported::params::TERMFLAGS
2802                    .fetch_and(!crate::ported::zsh_h::TERM_SHORT, Ordering::SeqCst); // c:1848
2803            } else {
2804                crate::ported::params::TERMFLAGS
2805                    .fetch_or(crate::ported::zsh_h::TERM_SHORT, Ordering::SeqCst); // c:1850
2806            }
2807            setiparam("LINES", lines); // c:1932
2808            let cols = if ttycols > 0 {
2809                ttycols as i64 // c:1862 — zterm_columns = ws_col
2810            } else {
2811                adjustcolumns() as i64 // c:1869 — tccolumns/80 fallback chain
2812            };
2813            ZTERM_COLUMNS.store(cols as i32, Ordering::SeqCst); // c:1862
2814                                                                // c:1872-1875 — the TERM_NARROW half of the same clamp.
2815            if cols > 2 {
2816                crate::ported::params::TERMFLAGS
2817                    .fetch_and(!crate::ported::zsh_h::TERM_NARROW, Ordering::SeqCst); // c:1873
2818            } else {
2819                crate::ported::params::TERMFLAGS
2820                    .fetch_or(crate::ported::zsh_h::TERM_NARROW, Ordering::SeqCst); // c:1875
2821            }
2822            setiparam("COLUMNS", cols); // c:1934
2823            ADJUSTWINSIZE_GETWINSZ.store(1, Ordering::SeqCst); // c:1935
2824        }
2825        2 => {
2826            // c:1937-1938 — `resetzle = adjustlines(0);`
2827            //
2828            // C arrives here from `zlevarsetfn` (Src/params.c:4232), which
2829            // has ALREADY done `*p = x` with `p == &zterm_lines`
2830            // (Src/params.c:363 IPDEF5) — that write is mirrored into
2831            // `ZTERM_LINES` by the zshrs `zlevarsetfn` port. So `oldlines`
2832            // here is the value the assignment just published, and
2833            // c:1836's `(signalled && …) || zterm_lines <= 0` is false for
2834            // any positive value: the else arm copies the global into
2835            // shttyinfo and leaves it alone, making c:1852's
2836            // `zterm_lines != oldlines` zero. Only an explicit `LINES=0`
2837            // falls through to the ws_row refresh.
2838            let oldlines = ZTERM_LINES.load(Ordering::SeqCst); // c:1833
2839            if oldlines <= 0 && ttyrows > 0 {
2840                ZTERM_LINES.store(ttyrows, Ordering::SeqCst); // c:1837
2841            }
2842            resetzle = (ZTERM_LINES.load(Ordering::SeqCst) != oldlines) as i32; // c:1852
2843        }
2844        3 => {
2845            // c:1940-1941 — `resetzle = adjustcolumns(0);` — the COLUMNS
2846            // half of the same shape (Src/utils.c:1858-1877).
2847            let oldcolumns = ZTERM_COLUMNS.load(Ordering::SeqCst); // c:1858
2848            if oldcolumns <= 0 && ttycols > 0 {
2849                ZTERM_COLUMNS.store(ttycols, Ordering::SeqCst); // c:1862
2850            }
2851            resetzle = (ZTERM_COLUMNS.load(Ordering::SeqCst) != oldcolumns) as i32; // c:1877
2852        }
2853        _ => {}
2854    }
2855
2856    // c:1945-1952 — the `interact && from >= 2` block is entirely commented
2857    // out in the C (the TIOCSWINSZ write-back is disabled), so nothing to
2858    // port. Keep the reads alive for the c:1954 gate below.
2859    let _ = ttyrows;
2860    let _ = ttycols;
2861
2862    // c:1954-1961 —
2863    // ```c
2864    //     if (zleactive && resetzle) {
2865    //         winchanged = resetneeded = 1;
2866    //         zleentry(ZLE_CMD_RESET_PROMPT);
2867    //         zleentry(ZLE_CMD_REFRESH);
2868    //     }
2869    // ```
2870    // The whole block was absent, which is why a SIGWINCH never repainted
2871    // anything.
2872    if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0 && resetzle != 0 {
2873        // !!! RUST-ONLY GUARD !!! C protects this re-entry with
2874        // `queue_signals()` around every critical section (Src/signals.h:90),
2875        // so a SIGWINCH arriving inside zrefresh is deferred rather than
2876        // recursing. zshrs has no equivalent coverage over the ZLE refresh
2877        // path and its video state is behind non-reentrant `std::sync::Mutex`
2878        // handles, so a signal landing mid-refresh would DEADLOCK rather than
2879        // recurse. Serialise instead: a resize that arrives while this
2880        // repaint is in flight is dropped, exactly as C's queued signal is
2881        // superseded by the pending full redraw.
2882        if !ADJUSTWINSIZE_IN_ZLE.swap(true, Ordering::SeqCst) {
2883            WINCHANGED.store(1, Ordering::SeqCst); // c:1956
2884            RESETNEEDED.store(1, Ordering::SeqCst); // c:1958
2885            crate::ported::zle::zle_refresh::RESETNEEDED.store(1, Ordering::SeqCst); // c:1958
2886            crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_RESET_PROMPT); // c:1959
2887            crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_REFRESH); // c:1960
2888            ADJUSTWINSIZE_IN_ZLE.store(false, Ordering::SeqCst);
2889        }
2890    }
2891    // c:1962 — C falls off the end here. The `(adjustcolumns(), adjustlines())`
2892    // that used to stand in this spot was the deadlock described on the
2893    // signature above; nothing needs it, because every arm that can leave a
2894    // geometry unpublished is followed by a caller that seeds it (the from=0/1
2895    // arm stores both globals itself, and the SHTTY==-1 branch seeds both
2896    // before returning).
2897}
2898
2899/// Port of `static int getwinsz` from `Src/utils.c:1891`. Local
2900/// reentry guard inside adjustwinsize — bumped to 0 around the
2901/// setiparam recursion so the recursive call short-circuits.
2902pub static ADJUSTWINSIZE_GETWINSZ: std::sync::atomic::AtomicI32 =
2903    std::sync::atomic::AtomicI32::new(1); // c:1891
2904
2905/// !!! WARNING: RUST-ONLY GUARD !!! No C counterpart.
2906///
2907/// Set while `adjustwinsize`'s c:1954 branch is inside
2908/// `zleentry(ZLE_CMD_RESET_PROMPT)` / `zleentry(ZLE_CMD_REFRESH)`.
2909/// C reaches that branch from a signal handler and relies on
2910/// `queue_signals()` (`Src/signals.h:90`) to defer a SIGWINCH that lands
2911/// during a refresh; zshrs's ZLE video state sits behind non-reentrant
2912/// `std::sync::Mutex` handles, where the same re-entry hangs instead of
2913/// recursing. This flag drops the nested repaint; the outer one already
2914/// re-reads the geometry.
2915static ADJUSTWINSIZE_IN_ZLE: std::sync::atomic::AtomicBool =
2916    std::sync::atomic::AtomicBool::new(false);
2917
2918/// Port of `check_fd_table()` from `Src/utils.c:1969` — C decl `check_fd_table(int fd)`.
2919/// ```c
2920/// if (fd <= max_zsh_fd) return;
2921/// if (fd >= fdtable_size) {
2922///     int old_size = fdtable_size;
2923///     while (fd >= fdtable_size)
2924///         fdtable = zrealloc(fdtable, (fdtable_size *= 2) * sizeof(*fdtable));
2925///     memset(fdtable + old_size, 0, (fdtable_size - old_size) * sizeof(*fdtable));
2926/// }
2927/// max_zsh_fd = fd;
2928/// ```
2929/// C semantics: GROW the `fdtable` array so it can index `fd`, then
2930/// update `max_zsh_fd`. Returns void.
2931///
2932/// The fdtable global is `fdtable_lock()`; the body grows it with
2933/// `Vec::resize` and updates `MAX_ZSH_FD`. It does not validate the fd
2934/// (C does not either); the bool result is Rust-only and is false for a
2935/// negative `fd` beyond the current maximum.
2936pub fn check_fd_table(fd: i32) -> bool {
2937    // c:1969
2938    // c:1971-1972 — `if (fd <= max_zsh_fd) return;`
2939    let cur_max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:1971
2940    if fd <= cur_max {
2941        // c:1971
2942        return true; // c:1972 (return; in C)
2943    }
2944    if fd < 0 {
2945        // defensive — fdtable index must be ≥0
2946        return false;
2947    }
2948    // c:1974-1981 — `if (fd >= fdtable_size) { while (fd >= fdtable_size)
2949    //                fdtable_size *= 2; zrealloc; memset(new_slots, 0); }`.
2950    // Rust Vec::resize handles the realloc + zero-fill in one call (the
2951    // expansion bumps capacity geometrically too).
2952    {
2953        let mut g = fdtable_lock().lock().unwrap();
2954        if (fd as usize) >= g.len() {
2955            g.resize((fd as usize) + 1, FDT_UNUSED); // c:1975-1979 grow
2956        }
2957    }
2958    // c:1982 — `max_zsh_fd = fd;`.
2959    MAX_ZSH_FD.store(fd, Ordering::Relaxed); // c:1982
2960    true
2961}
2962
2963/// Port of `movefd()` from `Src/utils.c:1990` — C decl `movefd(int fd)`.
2964/// ```c
2965/// if (fd != -1 && fd < 10) {
2966///     int fe = fcntl(fd, F_DUPFD, 10);
2967///     zclose(fd);          // unconditional close, even when fe == -1
2968///     fd = fe;
2969/// }
2970/// if (fd != -1) {
2971///     check_fd_table(fd);
2972///     fdtable[fd] = FDT_INTERNAL;
2973/// }
2974/// return fd;
2975/// ```
2976/// Two divergences fixed this iteration:
2977///   1. Old Rust closed the source fd ONLY on success; C closes
2978///      unconditionally per the c:1999-2004 comment "probably better
2979///      to avoid a leak."
2980///   2. Old Rust added `FD_CLOEXEC` after the dup; C does NOT —
2981///      CLOEXEC is added later by `addmodulefd`/`addlockfd` callers
2982///      that need it. movefd itself does not.
2983pub fn movefd(fd: i32) -> i32 {
2984    #[cfg(unix)]
2985    {
2986        let mut fd = fd;
2987        if fd != -1 && fd < 10 {
2988            // c:1992
2989            let fe = unsafe { libc::fcntl(fd, libc::F_DUPFD, 10) }; // c:1994
2990            unsafe { libc::close(fd) }; // c:2004 zclose(fd) — unconditional
2991            fd = fe; // c:2005
2992        }
2993        // c:2007-2010 — `if (fd != -1) { check_fd_table(fd); fdtable[fd]
2994        // = FDT_INTERNAL; }`. The fdtable global IS now modeled (port
2995        // of utils.c:63 — `Vec<u8>` behind `fdtable_lock`). Mark the
2996        // new fd as FDT_INTERNAL so the rest of the shell knows the
2997        // shell is using it (a later `addmodulefd` / `addlockfd`
2998        // upgrade may reclassify; raw external code shouldn't touch
2999        // it). The previous port skipped this so internal-fd tracking
3000        // (which `closeallelse` and forkexec rely on) silently
3001        // never saw zshrs-internal fds.
3002        if fd != -1 {
3003            // c:2007
3004            check_fd_table(fd); // c:2008
3005            fdtable_set(fd, FDT_INTERNAL); // c:2009
3006        }
3007        fd
3008    }
3009    #[cfg(not(unix))]
3010    {
3011        fd
3012    }
3013}
3014
3015/// Port of `redup()` from `Src/utils.c:2021` — C decl `redup(int x, int y)`.
3016///
3017/// C signature: `int redup(int x, int y)`.
3018/// "Move fd x to y. If x == -1, fd y is closed. Returns y for
3019/// success, -1 for failure." (c:2014-2016 docstring.)
3020///
3021/// Body mirrors c:2023-2068: when `x == -1`, close `y` (return `y`);
3022/// when `x == y`, no-op (return `y`); otherwise `dup2(x, y)` +
3023/// `close(x)`.
3024///
3025/// C body fdtable updates (c:2053-2063), which are load-bearing:
3026///   * `fdtable[y] = fdtable[x]` — the new fd inherits the old fd's
3027///     ownership category (FDT_INTERNAL / FDT_MODULE / etc.).
3028///   * If the inherited type is `FDT_FLOCK` / `FDT_FLOCK_EXEC`, promote
3029///     to `FDT_INTERNAL` (the dup'd fd doesn't carry the flock).
3030///   * If `fdtable[x] == FDT_FLOCK`, decrement `fdtable_flocks` (the
3031///     original lock-holding fd is about to be closed).
3032///
3033/// Without these updates, redup'd fds would carry stale `FDT_UNUSED`
3034/// ownership and `closeallelse(FDT_EXTERNAL)` etc. could not classify them.
3035pub fn redup(x: i32, y: i32) -> i32 {
3036    // c:2021
3037    let mut ret = y; // c:2023
3038    #[cfg(unix)]
3039    {
3040        if x < 0 {
3041            // c:2047
3042            zclose(y); // c:2048
3043        } else if x != y {
3044            // c:2049
3045            // c:2053-2057 — successful dup2: copy fdtable + FLOCK promote.
3046            if unsafe { libc::dup2(x, y) } == -1 {
3047                // c:2050
3048                ret = -1; // c:2051
3049            } else {
3050                check_fd_table(y); // c:2053
3051                let kind_x = fdtable_get(x); // c:2054
3052                let kind_y = if kind_x == FDT_FLOCK || kind_x == FDT_FLOCK_EXEC {
3053                    FDT_INTERNAL // c:2055-2056
3054                } else {
3055                    kind_x // c:2054
3056                };
3057                fdtable_set(y, kind_y);
3058            }
3059            // c:2062-2063 — `if (fdtable[x] == FDT_FLOCK) fdtable_flocks--;`
3060            // The original lock-holding fd is about to be closed, so the
3061            // flock-count tracker must drop. Even on dup2 failure C still
3062            // closes x, so this runs in both arms.
3063            if fdtable_get(x) == FDT_FLOCK {
3064                // c:2062
3065                FDTABLE_FLOCKS.fetch_sub(1, Ordering::SeqCst); // c:2063
3066            }
3067            zclose(x); // c:2064
3068        }
3069    }
3070    #[cfg(not(unix))]
3071    {
3072        let _ = (x, y);
3073    }
3074    ret // c:2067
3075}
3076
3077/// Port of `addmodulefd()` from `Src/utils.c:2091` — C decl `addmodulefd(int fd, int fdt)`.
3078/// ```c
3079/// if (fd >= 0) {
3080///     check_fd_table(fd);
3081///     fdtable[fd] = fdt;
3082/// }
3083/// ```
3084/// Two divergences fixed this iteration:
3085///   1. C accepts an `fdt` parameter (FDT_MODULE / FDT_INTERNAL /
3086///      FDT_EXTERNAL — see callers in Src/Modules/{random,socket,tcp,
3087///      db_gdbm}.c). Rust port hardcoded `FDT_MODULE` — silently
3088///      ignored caller intent. Now takes `fdt` per C.
3089///   2. C does NOT set `FD_CLOEXEC`. Rust port was adding it
3090///      unconditionally — divergent. CLOEXEC is the caller's
3091///      responsibility based on the fdt semantics.
3092pub fn addmodulefd(fd: i32, fdt: i32) {
3093    // c:2091
3094    // c:2093 — `if (fd >= 0)`.
3095    if fd >= 0 {
3096        // c:2094 — `check_fd_table(fd)` — grow fdtable to cover fd.
3097        check_fd_table(fd); // c:2094
3098                            // c:2095 — `fdtable[fd] = fdt`. Routes through the canonical
3099                            // helper which mirrors C's direct `fdtable[fd] = fdt` write.
3100        fdtable_set(fd, fdt); // c:2095
3101    }
3102}
3103
3104/// Port of `addlockfd()` from `Src/utils.c:2112` — C decl `addlockfd(int fd, int cloexec)`.
3105/// ```c
3106/// if (cloexec) {
3107///     if (fdtable[fd] != FDT_FLOCK)
3108///         fdtable_flocks++;
3109///     fdtable[fd] = FDT_FLOCK;
3110/// } else {
3111///     fdtable[fd] = FDT_FLOCK_EXEC;
3112/// }
3113/// ```
3114/// Critical divergence fixed: C updates `fdtable[fd]` to FDT_FLOCK
3115/// (cloexec=true) or FDT_FLOCK_EXEC (cloexec=false). Previous Rust
3116/// port did the OPPOSITE — called `fcntl(F_SETFD, FD_CLOEXEC)` when
3117/// `cloexec` was true. That's wrong on two counts:
3118///   1. C's "cloexec" parameter selects the FDT category, not a
3119///      libc fcntl flag.
3120///   2. FDT_FLOCK means "lock survives across exec via fd inheritance"
3121///      — the OPPOSITE of close-on-exec. The Rust port was adding
3122///      CLOEXEC for the very case where the fd should be inheritable.
3123pub fn addlockfd(fd: i32, cloexec: bool) {
3124    // c:2112
3125    if cloexec {
3126        // c:2114
3127        // c:2115-2117 — track flock count, set FDT_FLOCK.
3128        if fdtable_get(fd) != FDT_FLOCK {
3129            FDTABLE_FLOCKS.fetch_add(1, Ordering::SeqCst);
3130        }
3131        fdtable_set(fd, FDT_FLOCK);
3132    } else {
3133        // c:2118
3134        // c:2119 — FDT_FLOCK_EXEC means flock inherited by exec.
3135        fdtable_set(fd, FDT_FLOCK_EXEC);
3136    }
3137}
3138
3139// FDTABLE_FLOCKS canonical declaration lives at utils.rs:5219
3140// (the exec.c global with same name). Reuse, don't redeclare.
3141
3142/// Port of `zclose()` from `Src/utils.c:2127` — C decl `int zclose(int fd)`.
3143///
3144/// Close the given fd, and clear it from fdtable.                          // c:2123
3145pub fn zclose(fd: i32) -> i32 {
3146    // c:2127
3147    if fd >= 0 {
3148        // c:2129
3149        // c:2130-2133 — comment carry: "Careful: we allow closing of
3150        // arbitrary fd's, beyond max_zsh_fd. In that case we don't
3151        // try anything clever."
3152        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed); // c:2134
3153        if fd <= max_fd {
3154            // c:2134
3155            // !!! ZSHRS THREADING ADAPTATION — deviates from C here !!!
3156            // C closes blindly; it is single-threaded, so a shell-recorded
3157            // fd number can never be concurrently recycled by someone else.
3158            // zshrs runs 18 worker threads whose Rust-owned fds (ReadDir
3159            // dirfds, SQLite, File) recycle freed numbers immediately — a
3160            // stale zclose then closes a FOREIGN fd, and std panics in
3161            // ReadDir::drop ("unexpected error during closedir: EBADF",
3162            // observed from the compinit bg scan). An in-range fd the
3163            // fdtable does NOT own is by definition not ours to close.
3164            if fdtable_get(fd) == FDT_UNUSED {
3165                tracing::debug!(
3166                    fd,
3167                    "zclose: skipping unowned in-range fd (thread-safety guard)"
3168                );
3169                // The guard exists for an fd a foreign thread has OPEN; an fd
3170                // nobody has open is what C's blind `close(fd)` (c:2145)
3171                // reports as -1/EBADF, and `{varid}<&-` on an already-closed
3172                // descriptor surfaces that as "failed to close file
3173                // descriptor N" (c:Src/exec.c:3928-3930). fcntl leaves errno
3174                // at EBADF for the caller's `%e`.
3175                #[cfg(unix)]
3176                if unsafe { libc::fcntl(fd, libc::F_GETFD) } < 0 {
3177                    return -1;
3178                }
3179                return 0;
3180            }
3181            if fdtable_get(fd) == FDT_FLOCK {
3182                // c:2135
3183                FDTABLE_FLOCKS.fetch_sub(1, Ordering::Relaxed);
3184                // c:2136
3185            }
3186            fdtable_set(fd, FDT_UNUSED); // c:2137
3187                                         // c:2138-2139 — shrink max_zsh_fd past trailing UNUSED slots.
3188            let mut m = MAX_ZSH_FD.load(Ordering::Relaxed);
3189            while m > 0 && fdtable_get(m) == FDT_UNUSED {
3190                m -= 1;
3191            }
3192            MAX_ZSH_FD.store(m, Ordering::Relaxed);
3193            // c:2140-2143 — coproc fd tracking.
3194            if fd == coprocin.load(Ordering::Relaxed) {
3195                coprocin.store(-1, Ordering::Relaxed);
3196            }
3197            if fd == coprocout.load(Ordering::Relaxed) {
3198                coprocout.store(-1, Ordering::Relaxed);
3199            }
3200        }
3201        #[cfg(unix)]
3202        unsafe {
3203            return libc::close(fd);
3204        } // c:2145
3205        #[cfg(not(unix))]
3206        return 0;
3207    }
3208    -1 // c:2147
3209}
3210
3211/// Port of `zcloselockfd()` from `Src/utils.c:2156` — C decl `int zcloselockfd(int fd)`.
3212/// ```c
3213/// if (fd > max_zsh_fd) return -1;
3214/// if (fdtable[fd] != FDT_FLOCK && fdtable[fd] != FDT_FLOCK_EXEC)
3215///     return -1;
3216/// zclose(fd);
3217/// return 0;
3218/// ```
3219/// "Close an fd returning 0 if used for locking; return -1 if it
3220/// isn't." Caller (`bin_zsystem_flock -u`) uses the -1 return to
3221/// distinguish "fd not in the flock table" from "successfully
3222/// released a lock."
3223///
3224/// Previously the Rust port skipped the FDT_FLOCK / FDT_FLOCK_EXEC
3225/// check entirely and always returned 0 — meaning `zsystem flock -u
3226/// <unlocked-fd>` reported success on any fd. Now that the
3227/// `addlockfd` port (this iteration) populates the fdtable with the
3228/// canonical FDT_FLOCK / FDT_FLOCK_EXEC slots, the check can fire
3229/// faithfully.
3230pub fn zcloselockfd(fd: i32) -> i32 {
3231    // c:2156
3232    let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
3233    // c:2158 — `if (fd > max_zsh_fd) return -1;`.
3234    if fd > max_fd {
3235        return -1;
3236    }
3237    // c:2160-2161 — `if (fdtable[fd] != FDT_FLOCK && != FDT_FLOCK_EXEC)
3238    //                   return -1;`.
3239    let slot = fdtable_get(fd);
3240    if slot != FDT_FLOCK && slot != FDT_FLOCK_EXEC {
3241        return -1;
3242    }
3243    zclose(fd); // c:2162
3244    0 // c:2163
3245}
3246
3247/// Port of `gettempname()` from `Src/utils.c:2178` — C decl `char *gettempname(const char *prefix, int use_heap)`.
3248///
3249/// Returns a unique tempfile name templated like C `mktemp(3)` —
3250/// `{prefix}.XXXXXX`. Falls back to `getsparam("TMPPREFIX")` and
3251/// then `DEFAULT_TMPPREFIX` when `prefix` is None. Does NOT create
3252/// the file (matches C — only `gettempfile` creates).
3253pub fn gettempname(prefix: Option<&str>, _use_heap: bool) -> Option<String> {
3254    // c:2178
3255    let suffix = if prefix.is_some() {
3256        ".XXXXXX"
3257    } else {
3258        "XXXXXX"
3259    }; // c:2178
3260    queue_signals(); // c:2182
3261    let prefix_owned: String = match prefix {
3262        // c:2183
3263        Some(p) => p.to_string(),
3264        // c:2184 — `getsparam("TMPPREFIX")`. Read from paramtab (not OS
3265        //          env); fall back to compile-time default when unset.
3266        None => getsparam("TMPPREFIX")
3267            .unwrap_or_else(|| crate::ported::config_h::DEFAULT_TMPPREFIX.to_string()),
3268    };
3269    let template = format!("{}{}", prefix_owned, suffix); // c:2186-2188
3270                                                          // C uses mktemp(3) which mutates the X's into a unique name              // c:2192/2219
3271                                                          // without creating the file. Rust has no mktemp; emulate with
3272                                                          // pid+timestamp. Caller is responsible for O_EXCL open.
3273    let pid = std::process::id();
3274    let nanos = std::time::SystemTime::now()
3275        .duration_since(UNIX_EPOCH)
3276        .map(|d| d.as_nanos())
3277        .unwrap_or(0);
3278    // c:2192 — mkstemp(3) replaces the six X's with EXACTLY six
3279    // characters from [A-Za-z0-9], so every zsh temp name has a fixed
3280    // width (`/tmp/zshDYKzyg`). The `{pid:x}{nanos:x}` form here was 8-12
3281    // characters wide, which shows anywhere the name is displayed: the
3282    // `fc -` parity cell compares the editor's status line, where zsh
3283    // shows /private/tmp/zshDYKzyg and zshrs showed
3284    // /private/tmp/zshdb0ff10c48. Caller retries on EEXIST (c:2255-2269),
3285    // so a collision is handled rather than fatal.
3286    let mut mix: u128 = ((pid as u128) << 47) ^ nanos;
3287    let mut unique = String::with_capacity(6);
3288    for _ in 0..6 {
3289        let idx = (mix % 62) as usize;
3290        unique.push(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[idx] as char);
3291        mix /= 62;
3292    }
3293    let name = template.replace("XXXXXX", &unique);
3294    unqueue_signals(); // c:2221
3295    Some(name)
3296}
3297
3298/// Check if metafied - port from zsh/Src/utils.c has_token()
3299// Check if a string contains a token                                       // c:2282
3300/// Port of `has_token()` from `Src/utils.c:2282` — C decl `int has_token(const char *s)`.
3301///
3302/// Port of `has_token(const char *s)` from `Src/utils.c:2282` — used by
3303/// `ecstrcode` (parse.rs:989) to flip the token-marker bit on the
3304/// encoded string offset. Token markers live in 0x83..=0x9f (Pound,
3305/// Stringg, Hat, Star, ..., Bnull, Nularg). Earlier impl checked
3306/// only 0x83 (Meta) which missed Dash/Equals/Inbrack/Inbrace/etc.,
3307/// so any string containing those (e.g. `dart-lang/dart` →
3308/// `dart\u{e19b}lang/dart`) got encoded with the no-token bit set,
3309/// breaking byte parity with C's wordcode-emitter output.
3310/// ```c
3311/// while (*s)
3312///     if (itok(*s++)) return 1;
3313/// return 0;
3314/// ```
3315/// Routes through the canonical `itok()` typtab-driven predicate.
3316/// Previous Rust port used hardcoded `0x83..=0x9f` which was:
3317///   - Wrong on the low end: includes `0x83` (Meta, NOT a token).
3318///   - Wrong on the high end: missing `0xa0..=0xa1` (part of the
3319///     Snull..Nularg range that `itok` legitimately covers).
3320/// The full canonical ITOK range per `Src/zsh.h:152-159` is
3321/// `0x84..=0xa1` (Pound..Nularg), with possible gaps in the middle
3322/// (Snull at 0x9d follows Bang at 0x9c).
3323pub fn has_token(s: &str) -> bool {
3324    // c:2282
3325    // C scans BYTES (c:2284-2285), which is safe THERE only because zsh
3326    // metafies: a raw byte >= 0x80 is stored escaped (Meta, then byte ^ 32),
3327    // so a bare 0x84..=0xA1 in a metafied string is always a real token.
3328    // zshrs does not metafy — it keeps UTF-8 `str` and stores tokens as
3329    // PUA CHARS (crate::token_char). Scanning bytes here therefore mistakes a UTF-8
3330    // CONTINUATION byte for a token: `—` U+2014 encodes as E2 80 94 and
3331    // `→` U+2192 as E2 86 92 — 0x94, 0x86 and 0x92 all answer `itok`, so
3332    // every em-dash, smart quote and arrow reported "has tokens".
3333    //
3334    // `lex.rs:6165` already carries this fix (it was made there because the
3335    // false positive never clears `execcmd_exec`'s
3336    // `while has_token(&args[0]) { zglob(..) }` sweep, c:3315-3318, and the
3337    // shell spins at 100% CPU); the copy here — the one at C's own home for
3338    // this function, and the one `cond.rs:856`/`cond.rs:893`/`text.rs:1272`
3339    // import — never inherited it.
3340    s.chars().any(|c| {
3341        // c:2285 itok(*s++)
3342        crate::token_char::itok_char(c)
3343    })
3344}
3345
3346// Delete a character in a string                                           // c:2294
3347/// Port of `chuck()` from `Src/utils.c:2294`.
3348///
3349/// Remove character from string (from utils.c chuck)
3350pub fn chuck(s: &mut String, pos: usize) {
3351    // c:2294
3352    if pos < s.len() {
3353        s.remove(pos);
3354    }
3355}
3356
3357/// Port of `tulower()` from `Src/utils.c:2302` — C decl `tulower(int c)`.
3358///
3359/// Convert character to lowercase
3360/// To-lowercase that respects locale.
3361pub fn tulower(c: char) -> char {
3362    // c:2302
3363    c.to_lowercase().next().unwrap_or(c)
3364}
3365
3366/// Port of `tuupper()` from `Src/utils.c:2310` — C decl `tuupper(int c)`.
3367///
3368/// Convert character to uppercase
3369/// To-uppercase that respects locale.
3370pub fn tuupper(c: char) -> char {
3371    // c:2310
3372    c.to_uppercase().next().unwrap_or(c)
3373}
3374
3375/// Port of `ztrncpy()` from `Src/utils.c:2320` — C decl `void ztrncpy(char *s, char *t, int len)`.
3376///
3377/// C body (c:2320-2326):
3378/// ```c
3379/// while (len--)
3380///     *s++ = *t++;
3381/// *s = '\0';
3382/// ```
3383///
3384/// Copy `len` bytes from `t` into `s`, then NUL-terminate. C does no
3385/// bounds check; the caller must ensure `s` has `len + 1` bytes of
3386/// capacity. Rust port preserves the (s, t, len) parameter shape:
3387/// `s: &mut String` (output), `t: &str` (source), `len: usize` (copy
3388/// count). The C NUL terminator at c:2325 is implicit in Rust's
3389/// length-prefixed String. The Rust port clamps `len` to `t.len()` to
3390/// avoid the out-of-bounds read C's `*s++ = *t++` would perform when
3391/// `len > strlen(t)` (UB in C; explicitly bounded in Rust).
3392pub fn ztrncpy(s: &mut String, t: &str, len: usize) {
3393    // c:2320
3394    s.clear(); // C overwrites from start of *s
3395    let take = len.min(t.len()); // c:2322 while (len--)
3396    s.push_str(&t[..take]); // c:2322 *s++ = *t++
3397                            // c:2325 — `*s = '\0';` (implicit in Rust String length)
3398}
3399
3400/// Port of `strucpy()` from `Src/utils.c:2331` — C decl `void strucpy(char **s, char *t)`.
3401/// ```c
3402/// char *u = *s;
3403/// while ((*u++ = *t++));
3404/// *s = u - 1;       // leave *s pointing at the NUL terminator
3405/// ```
3406/// Body: `strcpy(*s, t)` + advance `*s` to point at the new
3407/// NUL terminator. The "u" in the name is a C convention for the
3408/// local pointer-walker, NOT "upper-case" — the function does NOT
3409/// change case.
3410///
3411/// Rust API: param name `s` matches C exactly per Rule E (despite Rust
3412/// convention of `dest` for output buffers). The C pointer-advance
3413/// `*s = u - 1` translates to "the new end of s is at s.len()", which
3414/// is implicit in Rust's owning-String model.
3415pub fn strucpy(s: &mut String, t: &str) {
3416    // c:2331
3417    s.push_str(t); // c:2335 `*u++ = *t++` loop
3418                   // c:2336 — `*s = u - 1;` — pointer-advance is implicit in Rust
3419                   // (`s.len()` is the new end-of-string position).
3420}
3421
3422/// Port of `struncpy()` from `Src/utils.c:2341` — C decl `void struncpy(char **s, char *t, int n)`.
3423/// ```c
3424/// char *u = *s;
3425/// while (n-- && (*u = *t++)) u++;
3426/// *s = u;
3427/// if (n > 0) *u = '\0';
3428/// ```
3429/// Body: copy up to `n` bytes from `t` to `*s`, NUL-terminate.
3430/// Note c:2348 — "just one null-byte will do, unlike strncpy(3)";
3431/// doesn't pad with NULs.
3432///
3433/// Param names match C exactly per Rule E.
3434pub fn struncpy(s: &mut String, t: &str, n: usize) {
3435    // c:2341
3436    // c:2345 — `while (n-- && (*u = *t++)) u++;` — copy up to n
3437    // bytes, stop at NUL (which in Rust &str is the end-of-string).
3438    let take = n.min(t.len());
3439    s.push_str(&t[..take]);
3440}
3441
3442/// Port of `arrlen()` from `Src/utils.c:2357` — C decl `arrlen(char **s)`.
3443///
3444/// Array length - port from arrlen()
3445pub fn arrlen<T>(s: &[T]) -> usize {
3446    // c:2357
3447    s.len()
3448}
3449
3450// Return TRUE iff arrlen(s) >= lower_bound, but more efficiently.          // c:2382
3451/// Port of `arrlen_ge()` from `Src/utils.c:2369`.
3452///
3453/// Check if array length >= n (from utils.c arrlen_ge)
3454pub fn arrlen_ge<T>(arr: &[T], n: usize) -> bool {
3455    // c:2369
3456    arr.len() >= n
3457}
3458
3459// Return TRUE iff arrlen(s) > lower_bound, but more efficiently.           // c:2400
3460/// Port of `arrlen_gt()` from `Src/utils.c:2382`.
3461///
3462/// Check if array length > n (from utils.c arrlen_gt)
3463pub fn arrlen_gt<T>(arr: &[T], n: usize) -> bool {
3464    // c:2382
3465    arr.len() > n
3466}
3467
3468// Return TRUE iff arrlen(s) <= upper_bound, but more efficiently.          // c:2409
3469/// Port of `arrlen_le()` from `Src/utils.c:2391`.
3470///
3471/// Check if array length <= n (from utils.c arrlen_le)
3472pub fn arrlen_le<T>(arr: &[T], n: usize) -> bool {
3473    // c:2391
3474    arr.len() <= n
3475}
3476
3477// Return TRUE iff arrlen(s) < upper_bound, but more efficiently.           // c:2400
3478/// Port of `arrlen_lt()` from `Src/utils.c:2400`.
3479///
3480/// Check if array length < n (from utils.c arrlen_lt)
3481pub fn arrlen_lt<T>(arr: &[T], n: usize) -> bool {
3482    // c:2400
3483    arr.len() < n
3484}
3485
3486/// Port of `skipparens()` from `Src/utils.c:2409`.
3487///
3488/// Direct port of `int skipparens(char inpar, char outpar, char **s)`
3489/// from `Src/utils.c:2409`. Walks `*s` past a balanced `inpar`/`outpar`
3490/// pair, advancing the input slice in place. Returns 0 on full
3491/// balance, `-1` when `**s != inpar` (no opening paren), or positive
3492/// when the scan ran off the end of `*s` with `level > 0`
3493/// (unbalanced).
3494///
3495/// Rust signature: `s: &mut &str` mirrors C's `char **s` out-arg —
3496/// the caller's slice handle is rewritten to point past the closing
3497/// paren (or to the empty tail on unbalanced input).
3498/// WARNING: param names don't match C — Rust=(open, close, s) vs C=(inpar, outpar, s)
3499pub fn skipparens(open: char, close: char, s: &mut &str) -> i32 {
3500    // c:2409
3501    let mut chars = s.char_indices();
3502    // c:2413-2414 — `if (**s != inpar) return -1;`
3503    match chars.next() {
3504        Some((_, c)) if c == open => {}
3505        _ => return -1,
3506    }
3507    // c:2416 — `for (level = 1; *++*s && level;)`
3508    let mut level: i32 = 1;
3509    let mut consumed = open.len_utf8();
3510    for (i, c) in chars {
3511        consumed = i + c.len_utf8();
3512        if c == open {
3513            level += 1; // c:2417-2418
3514        } else if c == close {
3515            level -= 1; // c:2419-2420
3516        }
3517        if level == 0 {
3518            break;
3519        }
3520    }
3521    *s = &s[consumed..];
3522    // c:2422 — `return level;`. 0 on balanced match, positive on
3523    // ran-off-end (unbalanced input).
3524    level
3525}
3526
3527/// Port of `zstrtol()` from `Src/utils.c:2427` — C decl `zstrtol(const char *s, char **t, int base)`.
3528///
3529/// zlong with base autodetection. Returns the parsed value AND a
3530/// `&str` slice pointing to the first unconsumed character —
3531/// matching C's `char **t` out-arg.
3532///
3533/// C signature: `zlong zstrtol(const char *s, char **t, int base)`.
3534/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
3535/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Otherwise `base`
3536/// must be in [2, 36]; values outside that range emit `zerr` and
3537/// return 0.
3538///
3539/// On overflow, mirrors C's "number truncated after N digits"
3540/// behaviour: emits a `zwarn` and returns the truncated value
3541/// (last in-range computation).
3542/// WARNING: param names don't match C — Rust=(s, base) vs C=(s, t, base)
3543pub fn zstrtol(s: &str, base: i32) -> (i64, &str) {
3544    // c:2427
3545    zstrtol_underscore(s, base, false) // c:2427
3546}
3547
3548/// Port of `zstrtol_underscore()` from `Src/utils.c:2438` — C decl `zstrtol_underscore(const char *s, char **t, int base, int underscore)`.
3549///
3550/// C signature: `zlong zstrtol_underscore(const char *s, char **t,
3551///                                         int base, int underscore)`.
3552///
3553/// Convert string to zlong with optional `_` digit-separator support.
3554/// `underscore != 0` allows underscores to appear inside the digit
3555/// run (skipped during accumulation). Returns the parsed value AND
3556/// the unconsumed-tail slice (matching C's `*t = (char *)s` writeback
3557/// at line 2516).
3558///
3559/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
3560/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Bases outside [2, 36]
3561/// emit `zerr` and return `(0, original_s)`. Overflow truncates and
3562/// emits a `zwarn` per c:2510-2512.
3563pub fn zstrtol_underscore(s: &str, base: i32, underscore: bool) -> (i64, &str) {
3564    // c:2438
3565    let bytes = s.as_bytes();
3566    let mut i = 0usize;
3567    // c:2444-2445 — skip leading `inblank` whitespace.
3568    while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
3569        i += 1;
3570    }
3571    // c:2447-2450 — handle sign.
3572    let neg = if i < bytes.len() && (bytes[i] == b'-') {
3573        // c:2447 IS_DASH
3574        i += 1;
3575        true
3576    } else if i < bytes.len() && bytes[i] == b'+' {
3577        // c:2449
3578        i += 1;
3579        false
3580    } else {
3581        false
3582    };
3583
3584    // c:2452-2461 — base autodetect when base == 0.
3585    let mut base = base;
3586    if base == 0 {
3587        // c:2452
3588        if i >= bytes.len() || bytes[i] != b'0' {
3589            // c:2453
3590            base = 10; // c:2454
3591        } else {
3592            i += 1; // c:2455 ++s
3593            if i < bytes.len() && (bytes[i] == b'x' || bytes[i] == b'X') {
3594                // c:2455
3595                base = 16;
3596                i += 1;
3597            } else if i < bytes.len() && (bytes[i] == b'b' || bytes[i] == b'B') {
3598                // c:2457
3599                base = 2;
3600                i += 1;
3601            } else {
3602                // c:2459
3603                base = 8; // c:2460
3604            }
3605        }
3606    }
3607    let inp_idx = i; // c:2462 inp = s
3608    if base < 2 || base > 36 {
3609        // c:2463
3610        zerr(&format!(
3611            "invalid base (must be 2 to 36 inclusive): {}",
3612            base
3613        )); // c:2464
3614        return (0, s); // c:2465 return 0
3615    }
3616
3617    let mut calc: u64 = 0; // c:2441
3618    let mut trunc_idx: Option<usize> = None; // c:2440 trunc = NULL
3619    if base <= 10 {
3620        // c:2466
3621        // c:2467-2479 — digit accumulator, low bases.
3622        let max_d = b'0' + base as u8;
3623        while i < bytes.len() {
3624            let c = bytes[i];
3625            if c >= b'0' && c < max_d {
3626                if trunc_idx.is_none() {
3627                    let newcalc = calc
3628                        .wrapping_mul(base as u64)
3629                        .wrapping_add((c - b'0') as u64);
3630                    if newcalc < calc {
3631                        // c:2473
3632                        trunc_idx = Some(i); // c:2474
3633                    } else {
3634                        calc = newcalc;
3635                    }
3636                }
3637                i += 1;
3638            } else if underscore && c == b'_' {
3639                // c:2467 underscore && *s == '_'
3640                i += 1;
3641            } else {
3642                break;
3643            }
3644        }
3645    } else {
3646        // c:2480
3647        // c:2481-2495 — digit accumulator, high bases (>10).
3648        while i < bytes.len() {
3649            let c = bytes[i];
3650            let digit = if c.is_ascii_digit() {
3651                // c:2481 idigit
3652                Some((c - b'0') as u32)
3653            } else if c >= b'a' && c < b'a' + base as u8 - 10 {
3654                // c:2482
3655                Some(((c & 0x1f) + 9) as u32) // c:2486 (*s & 0x1f) + 9
3656            } else if c >= b'A' && c < b'A' + base as u8 - 10 {
3657                // c:2483
3658                Some(((c & 0x1f) + 9) as u32)
3659            } else if underscore && c == b'_' {
3660                // c:2484
3661                i += 1;
3662                continue;
3663            } else {
3664                None
3665            };
3666            match digit {
3667                Some(d) => {
3668                    if trunc_idx.is_none() {
3669                        let newcalc = calc.wrapping_mul(base as u64).wrapping_add(d as u64);
3670                        if newcalc < calc {
3671                            // c:2489
3672                            trunc_idx = Some(i); // c:2490
3673                        } else {
3674                            calc = newcalc;
3675                        }
3676                    }
3677                    i += 1;
3678                }
3679                None => break,
3680            }
3681        }
3682    }
3683
3684    // c:2504-2511 — special-case: signed-overflow into top bit.
3685    // The lowest negative number triggers the first test but is
3686    // representable correctly; check it explicitly.
3687    if trunc_idx.is_none() && (calc as i64) < 0 {
3688        // c:2504
3689        let top_bit_only = calc & !(1u64 << 63); // c:2506
3690        if !neg || top_bit_only != 0 {
3691            // c:2505
3692            trunc_idx = Some(i.saturating_sub(1)); // c:2508
3693            calc /= base as u64; // c:2509
3694        }
3695    }
3696
3697    if let Some(t) = trunc_idx {
3698        // c:2512
3699        let digits = t.saturating_sub(inp_idx);
3700        let inp_str = &s[inp_idx..];
3701        zwarn(&format!(
3702            "number truncated after {} digits: {}",
3703            digits, inp_str
3704        )); // c:2513
3705    }
3706
3707    let result = if neg {
3708        // c:2517
3709        -(calc as i64)
3710    } else {
3711        calc as i64
3712    };
3713    (result, &s[i..])
3714}
3715
3716/// Port of `zstrtoul_underscore()` from `Src/utils.c:2529`.
3717///
3718/// Parse unsigned integer with underscore support
3719/// Port from zsh/Src/utils.c zstrtoul_underscore() lines 2528-2575
3720/// Port of `zstrtoul_underscore(const char *s, zulong *retval)` from
3721/// `Src/utils.c:2529-2590`. C body:
3722/// ```c
3723/// if (*s == '+') s++;
3724/// if      (*s != '0')                    base = 10;
3725/// else if (*++s == 'x' || *s == 'X')     base = 16, s++;
3726/// else if (*s == 'b' || *s == 'B')       base = 2,  s++;
3727/// else                                   base = isset(OCTALZEROES) ? 8 : 10;
3728/// ```
3729/// The leading-zero case is option-gated on OCTALZEROES — without
3730/// the option (default), `0777` parses as decimal 777, NOT octal 511.
3731/// Previously the Rust port always treated leading-zero as octal —
3732/// divergent for the default shell setup.
3733pub fn zstrtoul_underscore(s: &str) -> Option<u64> {
3734    // c:2529
3735    let s = s.trim();
3736    let s = s.strip_prefix('+').unwrap_or(s); // c:2533-2534
3737
3738    let (base, rest) = if s.starts_with("0x") || s.starts_with("0X") {
3739        // c:2538
3740        (16, &s[2..])
3741    } else if s.starts_with("0b") || s.starts_with("0B") {
3742        // c:2540
3743        (2, &s[2..])
3744    } else if s.starts_with('0') && s.len() > 1 && isset(OCTALZEROES)
3745    // c:2543
3746    {
3747        (8, &s[1..])
3748    } else {
3749        (10, s)
3750    };
3751
3752    let rest = rest.replace('_', "");
3753    u64::from_str_radix(&rest, base).ok()
3754}
3755
3756/// Port of `setblock_fd()` from `Src/utils.c:2579`.
3757///
3758/// Port of `int setblock_fd(int turnonblocking, int fd, long *modep)`
3759/// from `Src/utils.c:2579`. Toggles O_NONBLOCK on `fd`.
3760///
3761/// **Signature matches C exactly** (param order `turnonblocking, fd`):
3762/// - `turnonblocking=1` → clear O_NONBLOCK (enable blocking).
3763/// - `turnonblocking=0` → set O_NONBLOCK (disable blocking).
3764///
3765/// Returns:
3766/// - `(true, mode)` if the fd's state was changed.
3767/// - `(false, -1)` if the fd is a regular file (C skips regular
3768///   files entirely — only operates on pipes/sockets/ttys per
3769///   `c:2599 if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`).
3770/// - `(false, mode)` if state was already correct.
3771///
3772/// Rust returns `(state_changed, modep_value)` as a tuple to mirror
3773/// C's `int` return + `long *modep` out-param.
3774pub fn setblock_fd(turnonblocking: bool, fd: i32) -> (bool, libc::c_long) {
3775    // c:2579
3776    #[cfg(unix)]
3777    unsafe {
3778        // c:2598-2600 — `if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`.
3779        let mut st: libc::stat = std::mem::zeroed();
3780        if libc::fstat(fd, &mut st) != 0 {
3781            return (false, -1);
3782        }
3783        // c:2599 — skip regular files (no nonblock concept).
3784        let mode_bits = st.st_mode as u32;
3785        if (mode_bits & libc::S_IFMT as u32) == libc::S_IFREG as u32 {
3786            return (false, -1); // c:2614 *modep = -1; return 0
3787        }
3788        // c:2601 — `*modep = fcntl(fd, F_GETFL, 0);`
3789        let modep = libc::fcntl(fd, libc::F_GETFL, 0) as libc::c_long;
3790        if modep < 0 {
3791            return (false, -1); // c:2602 if (*modep != -1)
3792        }
3793        const NONBLOCK: libc::c_long = libc::O_NONBLOCK as libc::c_long;
3794        if !turnonblocking {
3795            // c:2603-2606 — want to KNOW if blocking was off; set it on.
3796            if (modep & NONBLOCK) != 0 {
3797                return (true, modep); // already nonblock — no-op, but "off"
3798            }
3799            if libc::fcntl(fd, libc::F_SETFL, modep | NONBLOCK) == 0 {
3800                return (true, modep); // c:2606
3801            }
3802        } else {
3803            // c:2607-2611 — want to clear NONBLOCK if currently set.
3804            if (modep & NONBLOCK) != 0 && libc::fcntl(fd, libc::F_SETFL, modep & !NONBLOCK) == 0 {
3805                return (true, modep); // c:2611 state changed
3806            }
3807        }
3808        (false, modep)
3809    }
3810    #[cfg(not(unix))]
3811    {
3812        let _ = (turnonblocking, fd);
3813        (false, -1)
3814    }
3815}
3816
3817/// Port of `setblock_stdin()` from `Src/utils.c:2622` — C decl `int setblock_stdin(void)`.
3818/// ```c
3819/// long mode;
3820/// return setblock_fd(1, 0, &mode);
3821/// ```
3822/// `turnonblocking=1, fd=0` — turn ON blocking on stdin (clear
3823/// O_NONBLOCK).
3824pub fn setblock_stdin() -> i32 {
3825    // c:2624 — `return setblock_fd(1, 0, &mode);`.
3826    let (changed, _mode) = setblock_fd(true, 0); // c:2624
3827    changed as i32
3828}
3829
3830/// Port of `read_poll()` from `Src/utils.c:2645`.
3831///
3832/// Port of `int read_poll(int fd, int *readchar, int polltty, zlong microseconds)`
3833/// from `Src/utils.c:2645-2738`.
3834///
3835/// Check whether input is pending on `fd` within `timeout_us`
3836/// microseconds. Faithful port of all three C paths:
3837/// 1. The `polltty` non-canonical-tty path (VMIN/VTIME termios
3838///    setup so a raw tty can be polled — c:2665-2696).
3839/// 2. The `HAVE_SELECT` wait (c:2697-2705). Rust uses `poll(2)`
3840///    as the idiom substitution for `select(2)`; `poll` return
3841///    semantics are mapped onto C's `ret` so the `ret < 0`
3842///    error-fallback still triggers.
3843/// 3. The final `setblock_fd` + non-blocking `read` fallback that
3844///    captures a byte when `select`/`poll` errored (c:2718-2729).
3845///
3846/// `readchar` is C's `int *readchar` out-param: when a byte is
3847/// actually consumed during the poll it is written through
3848/// `readchar` (so `read -t -k` on a raw tty gets the byte). `polltty`
3849/// is C's `int polltty` truthiness. Returns C's `(ret > 0)`.
3850///
3851/// WARNING: param order differs from C — Rust=(fd, readchar,
3852/// polltty, timeout_us) vs C=(fd, readchar, polltty, microseconds);
3853/// C's `microseconds` is `timeout_us` here.
3854pub fn read_poll(fd: i32, readchar: &mut i32, polltty: bool, timeout_us: i64) -> bool {
3855    // c:2645
3856    #[cfg(unix)]
3857    {
3858        let mut ret: i32 = -1; // c:2647
3859        let mut mode: libc::c_long = -1; // c:2648
3860                                         // C reassigns the `polltty` param; hold it as the VMIN-derived int.
3861        let mut polltty: i32 = polltty as i32; // c:2645
3862        let mut ti: Option<libc::termios> = None; // c:2659 `struct ttyinfo ti;`
3863
3864        // c:2662-2663 — `if (fd < 0 || (polltty && !isatty(fd))) polltty = 0;`
3865        if fd < 0 || (polltty != 0 && unsafe { libc::isatty(fd) } == 0) {
3866            polltty = 0; // no tty to poll
3867        }
3868
3869        // c:2665-2696 — HAS_TIO && !__CYGWIN__: non-canonical VMIN poll setup.
3870        if polltty != 0 && fd >= 0 {
3871            // c:2686 — `gettyinfo(&ti);` (operates on global SHTTY, as in C).
3872            if let Some(mut t) = gettyinfo() {
3873                // c:2687 — `if ((polltty = ti.tio.c_cc[VMIN]))`
3874                polltty = t.c_cc[libc::VMIN] as i32;
3875                if polltty != 0 {
3876                    t.c_cc[libc::VMIN] = 0; // c:2688
3877                                            // c:2690 — termios timeout is 10ths of a second.
3878                    t.c_cc[libc::VTIME] = (timeout_us / 100_000) as libc::cc_t; // c:2690
3879                    settyinfo(&t); // c:2691
3880                }
3881                ti = Some(t);
3882            } else {
3883                // gettyinfo failed (SHTTY closed) — nothing to poll via VMIN.
3884                polltty = 0;
3885            }
3886        }
3887
3888        // c:2697-2705 — HAVE_SELECT wait, using poll(2) as the select(2) idiom.
3889        let timeout_ms = (timeout_us / 1000) as i32; // c:2698-2699 (expire_tv)
3890        if fd > -1 {
3891            // c:2701-2703
3892            let mut fds = [libc::pollfd {
3893                fd: fd as RawFd,
3894                events: libc::POLLIN,
3895                revents: 0,
3896            }];
3897            let raw = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
3898            ret = if raw > 0 {
3899                // Map poll revents onto select's "fd is readable" ret>0.
3900                if (fds[0].revents & libc::POLLIN) != 0 {
3901                    1
3902                } else {
3903                    0
3904                }
3905            } else {
3906                raw // 0 = timeout, -1 = error (triggers fallback below)
3907            };
3908        } else {
3909            // c:2705 — `select(0, NULL, NULL, NULL, &expire_tv)`: pure sleep.
3910            ret = unsafe { libc::poll(std::ptr::null_mut(), 0, timeout_ms) };
3911        }
3912
3913        // c:2718 — `if (fd >= 0 && ret < 0 && !errflag)`
3914        if fd >= 0 && ret < 0 && errflag.load(Ordering::Relaxed) == 0 {
3915            // c:2723 — final attempt: non-blocking read of a single char.
3916            // `(polltty || setblock_fd(0, fd, &mode))`
3917            let can_read = if polltty != 0 {
3918                true // polltty short-circuits setblock_fd; mode stays -1
3919            } else {
3920                let (changed, m) = setblock_fd(false, fd); // c:2723
3921                mode = m; // C writes *modep unconditionally
3922                changed
3923            };
3924            if can_read {
3925                let mut c: u8 = 0;
3926                let n = unsafe { libc::read(fd, &mut c as *mut u8 as *mut libc::c_void, 1) };
3927                if n > 0 {
3928                    *readchar = c as i32; // c:2724
3929                    ret = 1; // c:2725
3930                }
3931            }
3932            // c:2727-2728 — `if (mode != -1) fcntl(fd, F_SETFL, mode);`
3933            if mode != -1 {
3934                unsafe {
3935                    libc::fcntl(fd, libc::F_SETFL, mode);
3936                }
3937            }
3938        }
3939
3940        // c:2730-2736 — HAS_TIO: restore canonical VMIN=1 / VTIME=0.
3941        if polltty != 0 {
3942            if let Some(mut t) = ti {
3943                t.c_cc[libc::VMIN] = 1; // c:2732
3944                t.c_cc[libc::VTIME] = 0; // c:2733
3945                settyinfo(&t); // c:2734
3946            }
3947        }
3948
3949        ret > 0 // c:2737
3950    }
3951    #[cfg(not(unix))]
3952    {
3953        let _ = (fd, readchar, polltty, timeout_us);
3954        false
3955    }
3956}
3957
3958/// Port of `timespec_diff_us()` from `Src/utils.c:2752` — C decl `timespec_diff_us(const struct timespec *t1, const struct timespec *t2)`.
3959///
3960/// Compute time difference in microseconds (from utils.c timespec_diff_us)
3961/// Rust idiom replacement: `Instant::duration_since().as_micros()`
3962/// replaces the C tv_sec/tv_nsec arithmetic + sign-flip dance.
3963pub fn timespec_diff_us(t1: &std::time::Instant, t2: &std::time::Instant) -> i64 {
3964    if *t2 > *t1 {
3965        t2.duration_since(*t1).as_micros() as i64
3966    } else {
3967        -(t1.duration_since(*t2).as_micros() as i64)
3968    }
3969}
3970
3971/// Port of `zmonotime()` from `Src/utils.c:2780` — C decl `int zmonotime(time_t *tloc)`.
3972///
3973/// "Like time(), but uses the monotonic clock." Reads CLOCK_MONOTONIC
3974/// and returns `tv_sec`; writes the same value through `tloc` if
3975/// non-NULL (Rust: `Some(&mut t)`).
3976pub fn zmonotime(tloc: Option<&mut i64>) -> i64 {
3977    // c:2780
3978    #[cfg(unix)]
3979    {
3980        // c:2782 — `struct timespec ts;`
3981        let mut ts = libc::timespec {
3982            // c:2782
3983            tv_sec: 0,
3984            tv_nsec: 0,
3985        };
3986        // c:2783 — `zgettime_monotonic_if_available(&ts);`
3987        unsafe {
3988            libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); // c:2783
3989        }
3990        // c:2784-2785 — `if (tloc) *tloc = ts.tv_sec;`
3991        if let Some(t) = tloc {
3992            // c:2784
3993            *t = ts.tv_sec as i64; // c:2785
3994        }
3995        ts.tv_sec as i64 // c:2786 return ts.tv_sec
3996    }
3997    #[cfg(not(unix))]
3998    {
3999        let _ = tloc;
4000        0
4001    }
4002}
4003
4004/// Port of `zsleep()` from `Src/utils.c:2797` — C decl `int zsleep(long us)`.
4005///
4006/// Check if string looks like a number
4007/// Check whether a string parses as a decimal integer.
4008/// Sleep for a given number of seconds (fractional)
4009/// Sleep for a fractional number of seconds.
4010///
4011/// "Sleep for the given number of microseconds." Wraps
4012/// `nanosleep(2)` with EINTR retry, returning 1 on completion, 0
4013/// on permanent error.
4014pub fn zsleep(us: i64) -> i32 {
4015    // c:2797
4016    let mut sleeptime = libc::timespec {
4017        // c:2797-2803
4018        tv_sec: (us / 1_000_000) as libc::time_t,
4019        tv_nsec: ((us % 1_000_000) * 1000) as libc::c_long,
4020    };
4021    #[cfg(unix)]
4022    {
4023        loop {
4024            // c:2804
4025            let mut rem = libc::timespec {
4026                tv_sec: 0,
4027                tv_nsec: 0,
4028            };
4029            let ret = unsafe { libc::nanosleep(&sleeptime, &mut rem) }; // c:2806
4030            if ret == 0 {
4031                // c:2808
4032                return 1;
4033            }
4034            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
4035            if err != libc::EINTR {
4036                // c:2810
4037                return 0; // c:2811
4038            }
4039            sleeptime = rem; // c:2812
4040        }
4041    }
4042    #[cfg(not(unix))]
4043    {
4044        let _ = sleeptime;
4045        std::thread::sleep(std::time::Duration::from_micros(us.max(0) as u64));
4046        1
4047    }
4048}
4049
4050/// Port of `zsleep_random()` from `Src/utils.c:2833`.
4051///
4052/// Sleep random amount up to max microseconds (from utils.c zsleep_random)
4053/// Port of `int zsleep_random(long max_us, time_t end_time)`
4054/// from Src/utils.c:2833.
4055///
4056/// "Sleep for time (fairly) randomly up to max_us microseconds.
4057/// Don't let the time extend beyond end_time. end_time is compared
4058/// to the current *monotonic* clock time. Return 1 if that seemed
4059/// to work, else 0."
4060pub fn zsleep_random(max_us: i64, end_time: i64) -> i32 {
4061    // c:2833
4062    let now = zmonotime(None); // c:2833
4063    let r16 = unsafe { libc::rand() } & 0xFFFF; // c:2845
4064    let mut r: i64 = (max_us >> 16) * (r16 as i64); // c:2852
4065    while r != 0 && now + (r / 1_000_000) > end_time {
4066        // c:2858
4067        r >>= 1; // c:2859
4068    }
4069    if r != 0 {
4070        // c:2860
4071        zsleep(r) // c:2861
4072    } else {
4073        0 // c:2862
4074    }
4075}
4076
4077/// Port of `checkrmall()` from `Src/utils.c:2867` — C decl `int checkrmall(char *s)`.
4078///
4079/// C body (c:2867-2919): count files in directory `s` (capped at 100,
4080/// honoring GLOBDOTS), emit one of 4 prompts based on count, optionally
4081/// sleep 10s under RMSTARWAIT, then `getquery("ny", 1)` for confirm.
4082///
4083/// Previous Rust port was a FAKE — single generic prompt, no file
4084/// count, no shout/errflag checks, no RMSTARWAIT, wrong valid-chars
4085/// order ("yn" instead of "ny" so 'n' isn't the default-first-arm).
4086/// Lost 9 of the 10 C semantic branches.
4087pub fn checkrmall(s: &str) -> bool {
4088    // c:2867
4089    // c:2871-2872 — `if (!shout) return 1;` — no controlling tty for
4090    // prompt output, default to yes. Rust analogue: if stderr isn't
4091    // a tty, skip the prompt and approve.
4092    let shout_is_tty = unsafe { libc::isatty(2) != 0 }; // c:2871
4093    if !shout_is_tty {
4094        // c:2871
4095        return true; // c:2872
4096    }
4097    // c:2873-2878 — `if (*s != '/')` build absolute via pwd prefix.
4098    let s_owned: String; // c:2873
4099    let s_abs: &str = if !s.starts_with('/') {
4100        // c:2873
4101        let pwd = getsparam("PWD").unwrap_or_default(); // c:2874 pwd[1]
4102        s_owned = if pwd.len() > 1 {
4103            // c:2874 if (pwd[1])
4104            crate::ported::string::tricat(&pwd, "/", s) // c:2875 zhtricat
4105        } else {
4106            // c:2876
4107            crate::ported::string::dyncat("/", s) // c:2877
4108        };
4109        s_owned.as_str()
4110    } else {
4111        s
4112    };
4113    let max_count: i32 = 100; // c:2879
4114    let mut count: i32 = 0; // c:2870 count = 0
4115                            // c:2880-2892 — opendir + readdir loop, count entries (skip
4116                            // dotfiles when !GLOBDOTS), cap at max_count.
4117    let ignoredots = !isset(GLOBDOTS); // c:2881
4118                                       // c:2880 — `if (!(dir = opendir(unmeta(s)))) return;` then
4119                                       //          `while ((fn = zreaddir(dir, 1))) { ... }`.
4120    if let Ok(mut dir) = fs::read_dir(s_abs) {
4121        while let Some(fname) = zreaddir(&mut dir, 1) {
4122            if ignoredots && fname.starts_with('.') {
4123                // c:2885
4124                continue; // c:2886
4125            }
4126            count += 1; // c:2887
4127            if count > max_count {
4128                // c:2888
4129                break; // c:2889
4130            }
4131        }
4132        // c:closedir(dir) auto on drop
4133    }
4134    // c:2893-2904 — four-arm prompt based on file count.
4135    let stderr_h = io::stderr();
4136    let mut stderr_w = stderr_h.lock();
4137    if count > max_count {
4138        // c:2893
4139        let _ = write!(
4140            stderr_w,
4141            "zsh: sure you want to delete more than {} files in ",
4142            max_count
4143        ); // c:2894
4144    } else if count == 1 {
4145        // c:2896
4146        let _ = write!(stderr_w, "zsh: sure you want to delete the only file in ");
4147    // c:2897
4148    } else if count > 0 {
4149        // c:2898
4150        let _ = write!(
4151            stderr_w,
4152            "zsh: sure you want to delete all {} files in ",
4153            count
4154        ); // c:2899
4155    } else {
4156        // c:2901
4157        // c:2902 — `We don't know how many files the glob will expand to`
4158        let _ = write!(stderr_w, "zsh: sure you want to delete all the files in ");
4159        // c:2903
4160    }
4161    // c:2905 — `nicezputs(s, shout)` — escape non-printables in path.
4162    let _ = nicezputs(s_abs, &mut stderr_w); // c:2905
4163                                             // c:2906-2912 — RMSTARWAIT: print "? (waiting ten seconds)",
4164                                             // flush, beep, sleep 10, then newline.
4165    if isset(RMSTARWAIT) {
4166        // c:2906
4167        let _ = write!(stderr_w, "? (waiting ten seconds)"); // c:2907
4168        let _ = stderr_w.flush(); // c:2908
4169        drop(stderr_w); // release lock around zbeep + sleep
4170        zbeep(); // c:2909
4171        std::thread::sleep(std::time::Duration::from_secs(10)); // c:2910
4172        let _ = writeln!(io::stderr()); // c:2911 fputc('\n', shout)
4173    } else {
4174        drop(stderr_w);
4175    }
4176    // c:2913-2914 — `if (errflag) return 0;`
4177    if errflag.load(Ordering::Relaxed) != 0 {
4178        // c:2913
4179        return false; // c:2914
4180    }
4181    // c:2915-2917 — emit "[yn]? ", flush, beep.
4182    let _ = io::stderr().write_all(b" [yn]? "); // c:2915 nicezputs of " [yn]? "
4183    let _ = io::stderr().flush(); // c:2916
4184    zbeep(); // c:2917
4185             // c:2918 — `return (getquery("ny", 1) == 'y');`. Default-first-
4186             // char is 'n' (no) — getquery's first valid char is the default
4187             // returned when the user just hits Enter.
4188    getquery(Some("ny"), 1) == b'y' as i32 // c:2918
4189}
4190
4191/// Port of `read_loop()` from `Src/utils.c:2923`.
4192///
4193/// Port of `ssize_t read_loop(int fd, char *buf, size_t len)` from
4194/// `Src/utils.c:2923`.
4195///
4196/// C body (c:2923-2945):
4197/// ```c
4198/// while (1) {
4199///     ssize_t ret = read(fd, buf, len);
4200///     if (ret == len) break;
4201///     if (ret <= 0) {
4202///         if (ret < 0) {
4203///             if (errno == EINTR) continue;          // c:2933
4204///             if (fd != SHTTY)                       // c:2935
4205///                 zwarn("read failed: %e", errno);   // c:2936
4206///         }
4207///         return ret;
4208///     }
4209///     buf += ret; len -= ret;
4210/// }
4211/// ```
4212///
4213/// The previous Rust port omitted the `zwarn("read failed: %e", errno)`
4214/// emission at c:2935-2936. That's a real diagnostic divergence:
4215/// any non-SHTTY read failure in zsh emits a stderr message; the
4216/// Rust port silently propagated the io::Error to the caller, which
4217/// in most call sites was discarded (`let _ = read_loop(...)`).
4218/// Users debugging an interrupted file read (e.g. `< /broken/fd`)
4219/// would see no error message at all.
4220/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
4221pub fn read_loop(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
4222    // c:2923
4223    #[cfg(unix)]
4224    {
4225        let mut total = 0;
4226        while total < buf.len() {
4227            let n = unsafe {
4228                libc::read(
4229                    // c:2928
4230                    fd,
4231                    buf[total..].as_mut_ptr() as *mut libc::c_void,
4232                    buf.len() - total,
4233                )
4234            };
4235            if n <= 0 {
4236                // c:2931
4237                if n < 0 {
4238                    let e = io::Error::last_os_error();
4239                    if e.kind() == io::ErrorKind::Interrupted {
4240                        // c:2933
4241                        continue; // c:2934
4242                    }
4243                    // c:2935-2936 — `if (fd != SHTTY) zwarn("read failed: %e", errno);`
4244                    let shtty = SHTTY.load(Ordering::Relaxed);
4245                    if fd != shtty {
4246                        // c:2935
4247                        zwarn(
4248                            // c:2936
4249                            &format!("read failed: {}", e),
4250                        );
4251                    }
4252                    return Err(e);
4253                }
4254                break;
4255            }
4256            total += n as usize;
4257        }
4258        Ok(total)
4259    }
4260    #[cfg(not(unix))]
4261    {
4262        let _ = (fd, buf);
4263        Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
4264    }
4265}
4266
4267/// Port of `write_loop()` from `Src/utils.c:2949`.
4268///
4269/// Port of `ssize_t write_loop(int fd, const char *buf, size_t len)` from
4270/// `Src/utils.c:2949`.
4271///
4272/// C body (c:2949-2970):
4273/// ```c
4274/// while (1) {
4275///     ssize_t ret = write(fd, buf, len);
4276///     if (ret == len) break;
4277///     if (ret < 0) {
4278///         if (errno == EINTR) continue;
4279///         if (fd != SHTTY)                            // c:2960
4280///             zwarn("write failed: %e", errno);       // c:2961
4281///         return -1;
4282///     }
4283///     buf += ret; len -= ret;
4284/// }
4285/// ```
4286///
4287/// Same divergence as `read_loop`: previous Rust port omitted the
4288/// `zwarn("write failed: %e", errno)` emission for non-SHTTY fds.
4289/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
4290pub fn write_loop(fd: i32, buf: &[u8]) -> io::Result<usize> {
4291    // c:2949
4292    #[cfg(unix)]
4293    {
4294        let mut total = 0;
4295        while total < buf.len() {
4296            let n = unsafe {
4297                libc::write(
4298                    // c:2954
4299                    fd,
4300                    buf[total..].as_ptr() as *const libc::c_void,
4301                    buf.len() - total,
4302                )
4303            };
4304            if n <= 0 {
4305                if n < 0 {
4306                    let e = io::Error::last_os_error();
4307                    if e.kind() == io::ErrorKind::Interrupted {
4308                        // c:2958
4309                        continue; // c:2959
4310                    }
4311                    // c:2960-2961 — `if (fd != SHTTY) zwarn("write failed: %e", errno);`
4312                    let shtty = SHTTY.load(Ordering::Relaxed);
4313                    if fd != shtty {
4314                        // c:2960
4315                        zwarn(
4316                            // c:2961
4317                            &format!("write failed: {}", e),
4318                        );
4319                    }
4320                    return Err(e);
4321                }
4322                break;
4323            }
4324            total += n as usize;
4325        }
4326        Ok(total)
4327    }
4328    #[cfg(not(unix))]
4329    {
4330        let _ = (fd, buf);
4331        Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
4332    }
4333}
4334
4335/// Port of `read1char()` from `Src/utils.c:2972` — C decl `static int read1char(int echo)`.
4336///
4337/// Read a single character (from utils.c read1char).
4338///
4339///
4340/// C body (c:2972-2988):
4341/// ```c
4342/// char c;
4343/// int q = queue_signal_level();
4344/// dont_queue_signals();
4345/// while (read(SHTTY, &c, 1) != 1) {
4346///     if (errno != EINTR || errflag || retflag || breaks || contflag) {
4347///         restore_queue_signals(q);
4348///         return -1;
4349///     }
4350/// }
4351/// restore_queue_signals(q);
4352/// if (echo) write_loop(SHTTY, &c, 1);
4353/// return (unsigned char) c;
4354/// ```
4355///
4356/// Returns the byte read (0..=255) on success, `-1` on error. Three
4357/// divergences in the previous Rust port:
4358///   1. **Read from stdin** instead of SHTTY (the tty fd). zsh reads
4359///      single chars during `read -k`, `bindkey`, query prompts —
4360///      always against the controlling terminal, not the standard
4361///      input which may be a pipe.
4362///   2. **No `echo` parameter.** When `echo=1` C writes the byte
4363///      back to SHTTY so the user sees what they typed.
4364///   3. **No `errflag` / `retflag` / `breaks` / `contflag` early-exit.**
4365///      A SIGINT-driven errflag should abort the read; previous Rust
4366///      port had no break path.
4367/// WARNING: returns i32 not `Option<char>` to match C's signature
4368/// (`(unsigned char) c` for success, `-1` for error).
4369pub fn read1char(echo: i32) -> i32 {
4370    // c:2972
4371    #[cfg(unix)]
4372    {
4373        let shtty = SHTTY.load(Ordering::Relaxed);
4374        if shtty < 0 {
4375            return -1;
4376        }
4377        let mut c: u8 = 0;
4378        loop {
4379            let rc = unsafe { libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1) };
4380            if rc == 1 {
4381                // c:2978
4382                break;
4383            }
4384            // c:2979 — `if (errno != EINTR || errflag || retflag || breaks
4385            //              || contflag) return -1;`
4386            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
4387            if err != libc::EINTR {
4388                return -1;
4389            }
4390            // Check errflag interrupt flag (the most observable of the
4391            // four guards; retflag/breaks/contflag are control-flow
4392            // states that don't apply outside the exec engine).
4393            if errflag.load(Ordering::Relaxed) != 0 {
4394                return -1;
4395            }
4396        }
4397        // c:2985-2986 — `if (echo) write_loop(SHTTY, &c, 1);`
4398        if echo != 0 {
4399            // c:2985
4400            let _ = write_loop(shtty, &[c]); // c:2986
4401        }
4402        // c:2987 — `return (unsigned char) c;`
4403        c as i32
4404    }
4405    #[cfg(not(unix))]
4406    {
4407        let _ = echo;
4408        -1
4409    }
4410}
4411
4412/// Port of `noquery()` from `Src/utils.c:2992` — C decl `int noquery(int purge)`.
4413///
4414/// "If anything has been typed before the query, return without
4415/// asking. Optionally also purge the input queue." Returns the
4416/// number of bytes pending on `SHTTY` (via FIONREAD ioctl); when
4417/// `purge` is set, drains them before returning.
4418pub fn noquery(purge: bool) -> i32 {
4419    // c:2992
4420    let mut val: libc::c_int = 0; // c:2992
4421    #[cfg(unix)]
4422    {
4423        let shtty = SHTTY.load(Ordering::Relaxed); // c:2999
4424        if shtty == -1 {
4425            return 0;
4426        }
4427        unsafe {
4428            // c:2999
4429            libc::ioctl(shtty, libc::FIONREAD, &mut val as *mut libc::c_int);
4430        }
4431        if purge {
4432            // c:3000
4433            let mut c: u8 = 0;
4434            for _ in 0..val {
4435                // c:3001
4436                let _ = unsafe {
4437                    // c:3002
4438                    libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1)
4439                };
4440            }
4441        }
4442    }
4443    val // c:3009
4444}
4445
4446/// Port of `getquery()` from `Src/utils.c:3014`.
4447///
4448/// Port of `int getquery(char *valid_chars, int purge)` from
4449/// `Src/utils.c:3014`.
4450///
4451/// Read a single keystroke from the TTY in raw mode and return its
4452/// character code (as `int`, mirroring C). If `valid_chars` is `None`
4453/// (C `NULL`) any byte is accepted. If `valid_chars` is `Some(s)`, only
4454/// chars in `s` (plus `\n` which maps to `s[0]`) are accepted; other
4455/// input rings the bell and re-prompts. `Y`/`N` are pre-normalised to
4456/// `y`/`n` per c:3041-3045.
4457///
4458/// `purge != 0` triggers `noquery(purge)`'s queue-purge path — if input
4459/// is queued, returns `'n'` without reading.
4460pub fn getquery(valid_chars: Option<&str>, purge: i32) -> i32 {
4461    // c:3014
4462    // c:3016 — `int c, d, nl = 0;`
4463    let mut c: i32;
4464    let mut d: i32;
4465    let mut nl: i32 = 0;
4466    // c:3017 — `int isem = !strcmp(term, "emacs");`
4467    // `term` is the $TERM global declared in `Src/init.c`; its value is the
4468    // TERM parameter, read from paramtab (absent → empty string).
4469    let term: String = getsparam("TERM").unwrap_or_default();
4470    let isem: bool = term == "emacs";
4471    // c:3018 — `struct ttyinfo ti;`
4472    // Rust: termios is the canonical TTY-state type returned by gettyinfo.
4473    let mut ti: libc::termios; // c:3018
4474
4475    attachtty(mypgrp.load(Ordering::Relaxed)); // c:3020 attachtty(mypgrp)
4476
4477    // c:3022 — `gettyinfo(&ti);`
4478    ti = match gettyinfo() {
4479        Some(t) => t,
4480        None => return -1,
4481    };
4482    // c:3023-3030 — `ti.tio.c_lflag &= ~ECHO; if (!isem) {
4483    //                  ti.tio.c_lflag &= ~ICANON;
4484    //                  ti.tio.c_cc[VMIN] = 1;
4485    //                  ti.tio.c_cc[VTIME] = 0; }`
4486    #[cfg(unix)]
4487    {
4488        ti.c_lflag &= !(libc::ECHO); // c:3024
4489        if !isem {
4490            // c:3025
4491            ti.c_lflag &= !(libc::ICANON); // c:3026
4492            ti.c_cc[libc::VMIN] = 1; // c:3027
4493            ti.c_cc[libc::VTIME] = 0; // c:3028
4494        }
4495    }
4496    // c:3037 — `settyinfo(&ti);`
4497    settyinfo(&ti);
4498
4499    // c:3039 — `if (noquery(purge))`
4500    if noquery(purge != 0) != 0 {
4501        if !isem {
4502            // c:3040
4503            let saved = SHTTYINFO.lock().ok().and_then(|g| *g);
4504            if let Some(saved) = saved {
4505                // c:3041 settyinfo(&shttyinfo)
4506                settyinfo(&saved);
4507            }
4508        }
4509        let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"n\n"); // c:3042
4510        return b'n' as i32; // c:3043
4511    }
4512
4513    // c:3046-3061 — `while ((c = read1char(0)) >= 0) { ... }`
4514    c = -1;
4515    loop {
4516        let cc = read1char(0); // c:3046
4517        if cc < 0 {
4518            c = cc;
4519            break;
4520        }
4521        c = cc;
4522        if c == b'Y' as i32 {
4523            // c:3047
4524            c = b'y' as i32; // c:3048
4525        } else if c == b'N' as i32 {
4526            // c:3049
4527            c = b'n' as i32; // c:3050
4528        }
4529        if valid_chars.is_none() {
4530            // c:3051
4531            break; // c:3052
4532        }
4533        if c == b'\n' as i32 {
4534            // c:3053
4535            // c:3054 — `c = *valid_chars;`
4536            c = valid_chars.unwrap().bytes().next().unwrap_or(b'\n') as i32;
4537            nl = 1; // c:3055
4538            break; // c:3056
4539        }
4540        // c:3058 — `if (strchr(valid_chars, c))`
4541        if valid_chars.unwrap().bytes().any(|b| b as i32 == c) {
4542            nl = 1; // c:3059
4543            break; // c:3060
4544        }
4545        zbeep(); // c:3062
4546    }
4547    if c >= 0 {
4548        // c:3064
4549        // c:3065-3066 — `char buf = (char)c; write_loop(SHTTY, &buf, 1);`
4550        let buf = [c as u8]; // c:3065
4551        let _ = write_loop(SHTTY.load(Ordering::Relaxed), &buf); // c:3066
4552    }
4553    if nl != 0 {
4554        // c:3068
4555        let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n"); // c:3069
4556    }
4557
4558    if isem {
4559        // c:3071
4560        if c != b'\n' as i32 {
4561            // c:3072
4562            // c:3073 — `while ((d = read1char(1)) >= 0 && d != '\n');`
4563            loop {
4564                d = read1char(1);
4565                if d < 0 || d == b'\n' as i32 {
4566                    break;
4567                }
4568            }
4569        }
4570    } else if c != b'\n' as i32 && valid_chars.is_none() {
4571        // c:3075
4572        // c:3077-3094 — MULTIBYTE_SUPPORT branch: drain trailing bytes
4573        // of an incomplete multibyte sequence.
4574        if isset(MULTIBYTE) && c >= 0 {
4575            // c:3077
4576            // c:3083-3093 — `for (;;) { ret = mbrlen(&cc, 1, &mbs);
4577            //                          if (ret != MB_INCOMPLETE) break;
4578            //                          c = read1char(1); if (c < 0) break;
4579            //                          cc = (char)c; }`
4580            // Rust: model MB_INCOMPLETE detection by checking whether
4581            // the byte buffer so far forms a complete UTF-8 prefix.
4582            // `cc` carries the current byte under examination.
4583            let mut cc: u8 = c as u8; // c:3082
4584            let mut accum: Vec<u8> = vec![cc];
4585            loop {
4586                // mbrlen returns MB_INCOMPLETE when the partial buffer
4587                // doesn't yet form a complete UTF-8 character. Rust
4588                // equivalent: from_utf8 errors with error_len() == None
4589                // when more bytes are needed.
4590                let incomplete = match std::str::from_utf8(&accum) {
4591                    Ok(_) => false,
4592                    Err(e) => e.error_len().is_none(),
4593                };
4594                if !incomplete {
4595                    // c:3088
4596                    break;
4597                }
4598                let nc = read1char(1); // c:3089
4599                if nc < 0 {
4600                    // c:3090
4601                    break; // c:3091
4602                }
4603                cc = nc as u8; // c:3092
4604                accum.push(cc);
4605            }
4606            let _ = cc;
4607        }
4608        // c:3097 — `write_loop(SHTTY, "\n", 1);`
4609        let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n");
4610    }
4611
4612    // c:3101 — `settyinfo(&shttyinfo);` — restore saved TTY state.
4613    let saved = SHTTYINFO.lock().ok().and_then(|g| *g);
4614    if let Some(saved) = saved {
4615        settyinfo(&saved);
4616    }
4617
4618    c // c:3102 return c
4619}
4620
4621// `spscan` (Src/utils.c:3109) — canonical port lives below the
4622// thread_local block in this file. The pre-existing 3-arg
4623// `(name, candidates[], threshold) → Option<String>` shape was a
4624// drift fake (C is `void spscan(HashNode hn, scanflags)`); deleted
4625// because it had zero call sites and conflicted with the faithful
4626// port spckword needs.
4627
4628// spellcheck a word                                                       // c:3123
4629// fix s ; if hist is nonzero, fix the history list too                    // c:3124
4630
4631// File-static state shared with the (inlined) spscan callback. C uses
4632// raw file-statics at utils.c:3045-3050 (`best`, `d`, `guess`, `ic`,
4633// `spckpat`, `spnamepat`); Rust port mirrors them as thread_locals
4634// (per-evaluator per PORT_PLAN.md bucket-1) so concurrent worker
4635// threads each have their own correction state.
4636thread_local! {
4637    /// Port of `static int d;` from `Src/utils.c:3045`. Best dist seen.
4638    static SPCK_D: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
4639    /// Port of `static char *best;` from `Src/utils.c:3046`. Best-match.
4640    static SPCK_BEST: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4641    /// Port of `static char *guess;` from `Src/utils.c:3047`. Word to fix.
4642    static SPCK_GUESS: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4643    /// Port of `static Patprog spckpat;` from `Src/utils.c:3049`.
4644    static SPCK_PAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4645        = const { std::cell::RefCell::new(None) };
4646    /// Port of `static Patprog spnamepat;` from `Src/utils.c:3050`.
4647    static SPCK_NAMEPAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4648        = const { std::cell::RefCell::new(None) };
4649}
4650
4651/// Port of `spscan()` from `Src/utils.c:3109`.
4652///
4653/// Port of `static void spscan(HashNode hn, UNUSED(int scanflags))` from
4654/// `Src/utils.c:3109`. Inlined per-call below because hashtable.rs's
4655/// scan helpers don't model C's `scanhashtable(table, ..., scanfn, ...)`
4656/// callback shape — Rust call sites iterate the table directly. C
4657/// body:
4658/// ```c
4659/// int nd = spdist(hn->nam, guess, (int) strlen(guess) / 4 + 1);
4660/// if (nd < d && (!spckpat || !pattry(spckpat, hn->nam))) {
4661///     best = hn->nam;
4662///     d = nd;
4663/// }
4664/// ```
4665fn spscan(name: &str) {
4666    // c:3074
4667    let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4668    if guess.is_empty() {
4669        return;
4670    }
4671    let thresh = guess.len() / 4 + 1; // c:3076 `(int) strlen(guess) / 4 + 1`
4672    let nd = spdist(name, &guess, thresh) as i32; // c:3076
4673    let d = SPCK_D.with(|c| c.get());
4674    if nd < d {
4675        // c:3077
4676        // c:3078 — `if (!spckpat || !pattry(spckpat, hn->nam))`.
4677        let allow = SPCK_PAT.with(|p| {
4678            p.borrow()
4679                .as_ref()
4680                .map(|prog| !crate::ported::pattern::pattry(prog, name))
4681                .unwrap_or(true)
4682        });
4683        if allow {
4684            SPCK_BEST.with(|b| *b.borrow_mut() = Some(name.to_string())); // c:3079
4685            SPCK_D.with(|c| c.set(nd)); // c:3080
4686        }
4687    }
4688}
4689
4690/// Port of `spckword()` from `Src/utils.c:3128`.
4691///
4692/// Port of `void spckword(char **s, int hist, int cmd, int ask)` from
4693/// `Src/utils.c:3128`.
4694///
4695/// Spell-check `*s`. If a near-match is found in the appropriate
4696/// hashtable (params for `$x`, command tables for command position,
4697/// directory entries otherwise) AND the user accepts the correction
4698/// (`ask=0` auto-accepts), replace `*s` in place with the corrected
4699/// form and (if `hist!=0`) rewrite the history entry too.
4700///
4701/// Faithful 1:1 line-by-line port, including the interactive prompt
4702/// (c:3273-3287) that runs `SPROMPT` through `promptexpand` and reads
4703/// the answer with `getquery("nyae")`.
4704///
4705/// Caller updates: previous Rust signature `(word, candidates[], threshold)
4706/// → Option<String>` is gone — `lex.rs` builds candidate lists itself,
4707/// but C's spckword scans the canonical hashtables directly. The new
4708/// signature matches C exactly: takes `&mut String` for in-place fix.
4709pub fn spckword(s: &mut String, hist: i32, cmd: i32, ask: i32) {
4710    use crate::ported::hashtable::{
4711        aliastab_lock, cmdnamtab_lock, fillcmdnamtable, pathchecked, reswdtab_lock, shfunctab_lock,
4712    };
4713    use crate::ported::params::{getsparam, paramtab};
4714    use crate::ported::pattern::{patcompile, PAT_HEAPDUP};
4715    use crate::ported::zsh_h::{
4716        isset, Dash, Equals, Stringg as StringTok, Tilde, AUTOCD, HASHLISTALL,
4717    };
4718    // c:3130-3133 — locals.
4719    let _t: Option<String>;
4720    let mut ic: char = '\0'; // c:3131
4721    let mut preflen: usize = 0; // c:3132
4722                                // c:3133 — `autocd = cmd && isset(AUTOCD) && strcmp(*s, ".") && strcmp(*s, "..")`.
4723    let autocd = cmd != 0 && isset(AUTOCD) && s != "." && s != "..";
4724
4725    // c:3135-3136 — `if (!(*s)[0] || !(*s)[1]) return;`
4726    if s.len() < 2 {
4727        return;
4728    }
4729    // c:3137-3140 — HISTFLAG_NOEXEC or leading %/- skip.
4730    let bytes = s.as_bytes();
4731    let first = bytes[0] as char;
4732    let histdone = crate::ported::hist::histdone.load(std::sync::atomic::Ordering::Relaxed); // c:3137
4733    if (histdone & HISTFLAG_NOEXEC) != 0
4734        || (if cmd != 0 {
4735            first == '%' // c:3139 — leading % is a job
4736        } else {
4737            first == '-' || first == Dash // c:3139 — leading hyphen is an option
4738        })
4739    {
4740        return; // c:3140
4741    }
4742    // c:3141-3142 — `if (!strcmp(*s, "in")) return;`.
4743    if s == "in" {
4744        return; // c:3142
4745    }
4746    // c:3143-3155 — `cmd` branch: skip if it's already a known
4747    // function/builtin/cmdname/alias/reswd. Optional HASHLISTALL
4748    // re-fill of cmdnamtab on miss.
4749    if cmd != 0 {
4750        // c:3143
4751        let known = shfunctab_lock() // c:3144
4752            .read()
4753            .map(|t| t.get(s).is_some())
4754            .unwrap_or(false)
4755            || BUILTINS // c:3145
4756                .iter()
4757                .any(|b| b.node.nam == *s)
4758            || cmdnamtab_lock() // c:3146
4759                .read()
4760                .map(|t| t.get(s).is_some())
4761                .unwrap_or(false)
4762            || aliastab_lock() // c:3147
4763                .read()
4764                .map(|t| t.get(s).is_some())
4765                .unwrap_or(false)
4766            || reswdtab_lock() // c:3148
4767                .read()
4768                .map(|t| t.get(s).is_some())
4769                .unwrap_or(false);
4770        if known {
4771            return; // c:3149
4772        }
4773        // c:3150-3154 — HASHLISTALL: bulk-hash $PATH then retry.
4774        if isset(HASHLISTALL) {
4775            // c:3150
4776            let path: Vec<String> =
4777                getsparam("PATH") // c:3151
4778                    .map(|p| p.split(':').map(String::from).collect())
4779                    .unwrap_or_default();
4780            fillcmdnamtable(&path); // c:3151
4781            if cmdnamtab_lock() // c:3152
4782                .read()
4783                .map(|t| t.get(s).is_some())
4784                .unwrap_or(false)
4785            {
4786                return; // c:3153
4787            }
4788        }
4789    }
4790    // c:3156-3165 — Tilde/Equals/String prefix skip + itok/Dash
4791    // detok + early-return on any other tokenized char.
4792    let mut start = 0usize;
4793    let bytes = s.as_bytes(); // re-bind after potential string ops
4794    if !bytes.is_empty() {
4795        let c0 = bytes[0] as char;
4796        if c0 == Tilde || c0 == Equals || c0 == StringTok {
4797            // c:3157
4798            start = 1; // c:3158 t++
4799        }
4800    }
4801    // Scan from `start` for tokenized bytes.
4802    {
4803        let mut buf = s.clone().into_bytes();
4804        let mut i = start;
4805        let mut had_dash_only = true; // accumulator
4806        while i < buf.len() {
4807            let b = buf[i];
4808            if itok(b) {
4809                // c:3160
4810                if b as char == Dash {
4811                    // c:3161
4812                    buf[i] = b'-'; // c:3162
4813                } else {
4814                    return; // c:3164
4815                }
4816            } else {
4817                had_dash_only = had_dash_only && false;
4818            }
4819            i += 1;
4820        }
4821        let _ = had_dash_only;
4822        *s = String::from_utf8_lossy(&buf).into_owned();
4823    }
4824    // c:3166 — `best = NULL;`
4825    SPCK_BEST.with(|b| *b.borrow_mut() = None);
4826    SPCK_D.with(|c| c.set(100)); // initialised at each table-scan branch in C; mirror up-front.
4827                                 // c:3167-3169 — `for (t = *s; *t; t++) if (*t == '/') break;`
4828                                 // `t` is the position of the first slash (or end of string).
4829    let t_pos = s.find('/').unwrap_or(s.len()); // c:3167
4830                                                // c:3170-3171 — `if (**s == Tilde && !*t) return;`
4831    let bytes = s.as_bytes();
4832    if !bytes.is_empty() && (bytes[0] as char) == Tilde && t_pos == bytes.len() {
4833        // c:3170
4834        return; // c:3171
4835    }
4836
4837    // c:3173-3178 — compile CORRECT_IGNORE pattern if set.
4838    if let Some(ci) = getsparam("CORRECT_IGNORE") {
4839        // c:3173
4840        let prog = patcompile(
4841            &{
4842                let mut __pat_tok = (&ci).to_string();
4843                crate::ported::glob::tokenize(&mut __pat_tok);
4844                __pat_tok
4845            },
4846            PAT_HEAPDUP,
4847            None,
4848        ); // c:3176
4849        SPCK_PAT.with(|p| *p.borrow_mut() = prog);
4850    } else {
4851        SPCK_PAT.with(|p| *p.borrow_mut() = None); // c:3178
4852    }
4853    // c:3180-3185 — compile CORRECT_IGNORE_FILE pattern if set.
4854    if let Some(ci) = getsparam("CORRECT_IGNORE_FILE") {
4855        // c:3180
4856        let prog = patcompile(
4857            &{
4858                let mut __pat_tok = (&ci).to_string();
4859                crate::ported::glob::tokenize(&mut __pat_tok);
4860                __pat_tok
4861            },
4862            PAT_HEAPDUP,
4863            None,
4864        ); // c:3183
4865        SPCK_NAMEPAT.with(|p| *p.borrow_mut() = prog);
4866    } else {
4867        SPCK_NAMEPAT.with(|p| *p.borrow_mut() = None); // c:3185
4868    }
4869
4870    let bytes = s.as_bytes();
4871    let first = if bytes.is_empty() {
4872        '\0'
4873    } else {
4874        bytes[0] as char
4875    };
4876
4877    // c:3187-3193 — `**s == String && !*t`: $-prefixed name → scan paramtab.
4878    if first == StringTok && t_pos == bytes.len() {
4879        // c:3187
4880        // c:3188 — `guess = *s + 1;` strip leading $.
4881        let guess = s[1..].to_string();
4882        // c:3189-3190 — `if (itype_end(guess, INAMESPC, 1) == guess) return;`
4883        if itype_end(&guess, crate::ported::ztype_h::INAMESPC, true) == 0 {
4884            // c:3189
4885            return; // c:3190
4886        }
4887        ic = StringTok; // c:3191
4888        SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4889        SPCK_D.with(|c| c.set(100)); // c:3192
4890        if let Ok(t) = paramtab().read() {
4891            // c:3193 scanhashtable(paramtab, ..., spscan, ...)
4892            for k in t.keys() {
4893                spscan(k);
4894            }
4895        }
4896    // c:3194-3202 — `**s == Equals`: =cmd → hashcmd; then scan aliases+cmdnam.
4897    } else if first == Equals {
4898        // c:3194
4899        if t_pos != bytes.len() {
4900            // c:3195
4901            return; // c:3196
4902        }
4903        // c:3197-3198 — `if (hashcmd(guess = *s + 1, pathchecked)) return;`
4904        let guess = s[1..].to_string();
4905        let path: Vec<String> = getsparam("PATH")
4906            .map(|p| p.split(':').map(String::from).collect())
4907            .unwrap_or_default();
4908        let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4909        if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4910            return; // c:3198
4911        }
4912        SPCK_D.with(|c| c.set(100)); // c:3199
4913        ic = Equals; // c:3200
4914        SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4915        if let Ok(t) = aliastab_lock().read() {
4916            // c:3201
4917            for (k, _) in t.iter() {
4918                spscan(k);
4919            }
4920        }
4921        if let Ok(t) = cmdnamtab_lock().read() {
4922            // c:3202
4923            for (k, _) in t.iter() {
4924                spscan(k);
4925            }
4926        }
4927    // c:3203-3248 — default branch: filename / dir spell-check.
4928    } else {
4929        // c:3203
4930        let mut guess = s.clone(); // c:3204
4931                                   // c:3205-3218 — Tilde / String inline-expand prefix handling.
4932        if !guess.is_empty()
4933            && ((guess.as_bytes()[0] as char) == Tilde
4934                || (guess.as_bytes()[0] as char) == StringTok)
4935        {
4936            // c:3205
4937            ic = guess.as_bytes()[0] as char; // c:3207
4938            if t_pos + 1 >= s.len() {
4939                // c:3208 — `if (!*++t) return;`
4940                return; // c:3209
4941            }
4942            // c:3210-3214 — `noerrs=2; singsub(&guess); noerrs = ne;`
4943            let saved_noerrs = *crate::ported::utils::noerrs_lock().lock().unwrap();
4944            *crate::ported::utils::noerrs_lock().lock().unwrap() = 2; // c:3212
4945            guess = crate::ported::subst::singsub(&guess); // c:3213
4946            *crate::ported::utils::noerrs_lock().lock().unwrap() = saved_noerrs;
4947            if guess.is_empty() {
4948                return; // c:3216 `if (!guess) return;`
4949            }
4950            // c:3217 — `preflen = strlen(guess) - strlen(t);` t = original
4951            // s[t_pos..] (the post-slash remainder).
4952            let t_len = s.len() - t_pos;
4953            preflen = guess.len().saturating_sub(t_len);
4954        }
4955        // c:3219-3220 — `if (access(unmeta(guess), F_OK) == 0) return;`
4956        let cstr = match std::ffi::CString::new(unmeta(&guess).as_str()) {
4957            Ok(c) => c,
4958            Err(_) => return,
4959        };
4960        if unsafe { libc::access(cstr.as_ptr(), libc::F_OK) } == 0 {
4961            // c:3219
4962            return; // c:3220
4963        }
4964        // c:3221 — `best = spname(guess);`
4965        // The Rust spname has a signature-drift adaptation taking
4966        // `(name, dir)`; pass the parent dir extracted from guess.
4967        let path_obj = std::path::Path::new(&guess);
4968        let parent = path_obj
4969            .parent()
4970            .and_then(|p| p.to_str())
4971            .filter(|s| !s.is_empty())
4972            .unwrap_or(".");
4973        let basename = path_obj.file_name().and_then(|n| n.to_str()).unwrap_or("");
4974        let best = spname(basename, parent);
4975        SPCK_BEST.with(|b| *b.borrow_mut() = best);
4976        SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess.clone()));
4977        // c:3222-3247 — command-position default branch: hashcmd +
4978        // scan tables + autocd cdpath scan.
4979        if t_pos == s.len() && cmd != 0 {
4980            // c:3222
4981            // c:3223-3224 — hashcmd shortcut.
4982            let path: Vec<String> = getsparam("PATH")
4983                .map(|p| p.split(':').map(String::from).collect())
4984                .unwrap_or_default();
4985            let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4986            if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4987                return; // c:3224
4988            }
4989            SPCK_D.with(|c| c.set(100)); // c:3225
4990                                         // c:3226-3230 — scan reswd, alias, shfunc, builtin, cmdnam.
4991            if let Ok(t) = reswdtab_lock().read() {
4992                // c:3226
4993                for (k, _) in t.iter() {
4994                    spscan(k);
4995                }
4996            }
4997            if let Ok(t) = aliastab_lock().read() {
4998                // c:3227
4999                for (k, _) in t.iter() {
5000                    spscan(k);
5001                }
5002            }
5003            if let Ok(t) = shfunctab_lock().read() {
5004                // c:3228
5005                for (k, _) in t.iter() {
5006                    spscan(k);
5007                }
5008            }
5009            // c:3229 — builtintab scan: BUILTINS is a static array.
5010            for b in BUILTINS.iter() {
5011                spscan(&b.node.nam);
5012            }
5013            if let Ok(t) = cmdnamtab_lock().read() {
5014                // c:3230
5015                for (k, _) in t.iter() {
5016                    spscan(k);
5017                }
5018            }
5019            // c:3231-3247 — autocd $cdpath scan: for each cdpath
5020            // entry, find the closest filename match via mindist.
5021            // Strict `<` (not `<=` as in spscan) so a cdpath dir
5022            // wins only when STRICTLY better than the existing best
5023            // — preferring earlier cdpath dirs on ties.
5024            if autocd {
5025                // c:3232 — `if (cd_able_vars(unmeta(guess))) return;`
5026                let unmeta_guess = crate::ported::utils::unmeta(&guess);
5027                if crate::ported::builtin::cd_able_vars(&unmeta_guess).is_some() {
5028                    return; // c:3233
5029                }
5030                // c:3234 — `for (pp = cdpath; *pp; pp++)`. Read CDPATH.
5031                let cdpath: Vec<String> = crate::ported::params::paramtab()
5032                    .read()
5033                    .ok()
5034                    .and_then(|t| t.get("cdpath").and_then(|pm| pm.u_arr.clone()))
5035                    .unwrap_or_default();
5036                let mut cur_d = SPCK_D.with(|c| c.get());
5037                let mut cur_best = SPCK_BEST.with(|b| b.borrow().clone());
5038                for pp in cdpath.iter() {
5039                    // c:3235-3245 — `mindist(*pp, *s, bestcd, 1)`.
5040                    if let Some((bestcd, thisdist)) = mindist(pp, &s) {
5041                        // c:3239 — STRICT `<` (not `<=`) per C comment
5042                        // at c:3236-3239.
5043                        if (thisdist as i32) < cur_d {
5044                            cur_best = Some(bestcd);
5045                            cur_d = thisdist as i32;
5046                        }
5047                    }
5048                }
5049                SPCK_BEST.with(|b| *b.borrow_mut() = cur_best);
5050                SPCK_D.with(|c| c.set(cur_d));
5051            }
5052        }
5053    }
5054    // c:3250-3251 — `if (errflag) return;`
5055    // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
5056    // the C line cited above tests the WHOLE errflag, so masking here let an
5057    // interrupted shell keep going where zsh stops.
5058    if errflag.load(std::sync::atomic::Ordering::Relaxed) != 0 {
5059        return; // c:3251
5060    }
5061    // c:3252 — `if (best && strlen(best) > 1 && strcmp(best, guess))`.
5062    let best = SPCK_BEST.with(|b| b.borrow().clone());
5063    let mut guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
5064    let Some(mut best) = best else {
5065        return;
5066    };
5067    if best.len() <= 1 || best == guess {
5068        return;
5069    }
5070    // c:3253-3272 — assemble the prefixed/de-tokenized replacement.
5071    if ic != '\0' {
5072        // c:3254
5073        if preflen > 0 {
5074            // c:3256
5075            // c:3258-3259 — `if (strncmp(guess, best, preflen)) return;`
5076            if !best.starts_with(&guess[..preflen.min(guess.len())]) {
5077                return; // c:3259
5078            }
5079            // c:3261-3263 — `u = ...s[0..t-*s] + best[preflen..]`.
5080            let t_off = s.len() - (s.len() - t_pos);
5081            let mut u = String::with_capacity(t_off + best.len() - preflen + 1);
5082            u.push_str(&s[..t_off]);
5083            u.push_str(&best[preflen..]);
5084            best = u;
5085        } else {
5086            // c:3264 — `u = "\0" + best;` (prepend NUL placeholder).
5087            best = format!("\0{}", best);
5088        }
5089        // c:3269-3271 — `best = u; guess = *s; *guess = *best = ztokens[ic - Pound];`
5090        let pound = crate::ported::zsh_h::Pound as u8;
5091        let zt = crate::ported::lex::ztokens.as_bytes();
5092        let token_char = if (ic as u8) >= pound {
5093            let idx = (ic as u8 - pound) as usize;
5094            if idx < zt.len() {
5095                zt[idx] as char
5096            } else {
5097                ic
5098            }
5099        } else {
5100            ic
5101        };
5102        // Set first char of both `*s` (the original) and `best`.
5103        if !s.is_empty() {
5104            let mut sb = s.clone().into_bytes();
5105            sb[0] = token_char as u8;
5106            *s = String::from_utf8_lossy(&sb).into_owned();
5107        }
5108        if !best.is_empty() {
5109            let mut bb = best.into_bytes();
5110            bb[0] = token_char as u8;
5111            best = String::from_utf8_lossy(&bb).into_owned();
5112        }
5113        guess = s.clone(); // c:3270 `guess = *s;`
5114    }
5115    // c:3273-3289 — interactive prompt (`ask`) or auto-accept.
5116    let x: char;
5117    if ask != 0 {
5118        // c:3273
5119        if noquery(false) != 0 {
5120            // c:3274
5121            x = 'n'; // c:3275
5122        } else if *crate::ported::init::shout.lock().unwrap() != 0 {
5123            // c:3276 `else if (shout)`
5124            // c:3278 — `pptbuf = promptexpand(sprompt, 0, NULL, best, guess);`
5125            // rs/Rs reach the expansion through PROMPT_RS_STRINGS (see there).
5126            crate::ported::prompt::PROMPT_RS_STRINGS
5127                .with(|c| c.set((Some(best.clone()), Some(guess.clone()))));
5128            let sprompt = getsparam("SPROMPT").unwrap_or_default();
5129            let (pptbuf, _, _) = crate::ported::prompt::promptexpand(&sprompt, 0, None);
5130            let mut shout_buf: Vec<u8> = Vec::new();
5131            zputs(&pptbuf, &mut shout_buf); // c:3279
5132            crate::shout::write(&shout_buf);
5133            crate::shout::flush(); // c:3281 fflush(shout)
5134            zbeep(); // c:3282
5135            let q = getquery(Some("nyae"), 0); // c:3283
5136            x = if q < 0 { 'n' } else { q as u8 as char };
5137            if cmd != 0 && x == 'n' {
5138                // c:3284
5139                pathchecked.store(0, std::sync::atomic::Ordering::Relaxed); // c:3285 `pathchecked = path;`
5140            }
5141        } else {
5142            x = 'n'; // c:3287
5143        }
5144    } else {
5145        x = 'y'; // c:3289
5146    }
5147    // c:3290-3300 — apply chosen action.
5148    if x == 'y' {
5149        // c:3290
5150        *s = best.clone(); // c:3291 `*s = dupstring(best);`
5151        if hist != 0 {
5152            // c:3292
5153            crate::ported::hist::hwrep(&best); // c:3293
5154        }
5155    } else if x == 'a' {
5156        // c:3294
5157        crate::ported::hist::histdone
5158            .fetch_or(HISTFLAG_NOEXEC, std::sync::atomic::Ordering::Relaxed); // c:3295
5159    } else if x == 'e' {
5160        // c:3296
5161        crate::ported::hist::histdone.fetch_or(
5162            HISTFLAG_NOEXEC | crate::ported::zsh_h::HISTFLAG_RECALL,
5163            std::sync::atomic::Ordering::Relaxed,
5164        ); // c:3297
5165    }
5166    // c:3299-3300 — `if (ic) **s = ic;` — restore prefix sigil.
5167    if ic != '\0' && !s.is_empty() {
5168        let mut sb = s.clone().into_bytes();
5169        sb[0] = ic as u8;
5170        *s = String::from_utf8_lossy(&sb).into_owned();
5171    }
5172}
5173
5174/// Port of `ztrftimebuf()` from `Src/utils.c:3312` — C decl `ztrftimebuf(int *bufsizeptr, int decr)`.
5175///
5176/// ```c
5177/// static int ztrftimebuf(int *bufsizeptr, int decr) {
5178///     if (*bufsizeptr <= decr) return 1;
5179///     *bufsizeptr -= decr;
5180///     return 0;
5181/// }
5182/// ```
5183///
5184/// "Helper for ztrftime: try to fit decr more bytes (plus a NUL)
5185/// in the buffer, and a new string length to decrement from that.
5186/// Returns 0 if the new length fits, 1 otherwise."
5187///
5188/// Previous Rust port had wrong semantics — returned `needed.max(256)`
5189/// (a buffer-sizing helper) instead of the C "decrement-and-check"
5190/// semantics. New port matches C: takes &mut bufsize + decr,
5191/// returns i32 (0 = fit, 1 = doesn't fit).
5192pub fn ztrftimebuf(bufsizeptr: &mut i32, decr: i32) -> i32 {
5193    if *bufsizeptr <= decr {
5194        return 1;
5195    }
5196    *bufsizeptr -= decr;
5197    0
5198}
5199
5200/// Format time struct (from utils.c ztrftime)
5201/// Port of `ztrftime(char *buf, int bufsize, char *fmt, struct tm *tm, long nsec)` from `Src/utils.c:3337`.
5202/// WARNING: param names don't match C — Rust=(fmt, time) vs C=(buf, bufsize, fmt, tm, nsec)
5203// Rust idiom replacement: `SystemTime::duration_since(UNIX_EPOCH)`
5204// + libc::localtime covers the C tm-struct populate; the C source's
5205// 192-line body builds the fmt-walk by hand because C has no
5206// strftime extension support — Rust delegates to libc::strftime via
5207// the strftime crate equivalent inline.
5208/// Port of `ztrftime()` from `Src/utils.c:3337`.
5209///
5210/// `ztrftime` — see implementation.
5211///
5212/// `use_gmt` controls whether time fields are interpreted as UTC (true,
5213/// libc::gmtime) or local time (false, libc::localtime). C's ztrftime
5214/// takes a `struct tm *` produced by the caller via gmtime/localtime
5215/// (e.g. Src/Modules/stat.c:201 picks between them based on STF_GMT);
5216/// since the Rust signature takes `SystemTime` we hoist that choice into
5217/// this bool param.
5218pub fn ztrftime(fmt: &str, time: std::time::SystemTime, use_gmt: bool) -> String {
5219    // C takes `struct tm *` + nsec directly, so negative time_t
5220    // (pre-1970) flows through localtime untouched. SystemTime
5221    // pre-epoch surfaces as duration_since Err — recover the signed
5222    // seconds rather than collapsing to 0 (the old unwrap_or_default
5223    // pinned every pre-epoch timestamp to the epoch).
5224    let (secs, nsec): (i64, u64) = match time.duration_since(UNIX_EPOCH) {
5225        Ok(d) => (d.as_secs() as i64, d.subsec_nanos() as u64),
5226        Err(e) => {
5227            let d = e.duration();
5228            let s = -(d.as_secs() as i64);
5229            let n = d.subsec_nanos() as u64;
5230            // sub-second part rolls the seconds down one: -1.25s
5231            // before epoch is secs=-2, nsec=750ms in tm terms.
5232            if n > 0 {
5233                (s - 1, 1_000_000_000 - n)
5234            } else {
5235                (s, 0)
5236            }
5237        }
5238    };
5239
5240    #[cfg(unix)]
5241    unsafe {
5242        let tm = if use_gmt {
5243            libc::gmtime(&secs)
5244        } else {
5245            libc::localtime(&secs)
5246        };
5247        if tm.is_null() {
5248            return String::new();
5249        }
5250        let tm_ref = &*tm;
5251
5252        // c:3398-3406 — pre-pass: walk fmt and substitute zsh-specific
5253        // extensions BEFORE delegating to libc::strftime. The C source
5254        // handles these inline; Rust pre-rewrites them into literal
5255        // numbers so libc::strftime sees only standard specifiers.
5256        //   %K  = 24-hr clock, no leading zero (0-23)
5257        //   %L  = 12-hr clock, no leading zero (1-12)
5258        //   %f  = day of month, no leading zero (1-31)
5259        //   %.N = fractional seconds, N digits (0-9)
5260        let mut preprocessed = String::with_capacity(fmt.len());
5261        let bytes = fmt.as_bytes();
5262        let mut i = 0;
5263        while i < bytes.len() {
5264            if bytes[i] == b'%'
5265                && i + 1 < bytes.len()
5266                && bytes[i + 1..].iter().all(|c| b"OE^#_-0123456789".contains(c))
5267            {
5268                // c:3329-3337 `morefmt:` skips the modifier/width chars
5269                // (`while (*fmt && strchr("OE^#_-0123456789", *fmt))`); when
5270                // that runs off the end, `switch (*fmt++)` sees the NUL and
5271                // c:3368-3372 `case '\0': *buf++ = '%'; fmt--;` emits a bare
5272                // `%`, dropping the dangling modifiers (`a%E` -> `a%`).
5273                // `%%` so strftime(3) below renders that single `%`.
5274                preprocessed.push_str("%%");
5275                break;
5276            }
5277            if bytes[i] == b'%' && i + 1 < bytes.len() {
5278                // c:3374-3384 — parse optional `N.` prefix (digit count
5279                // for the `%.` fractional-seconds specifier).
5280                let mut j = i + 1;
5281                while j < bytes.len() && bytes[j].is_ascii_digit() {
5282                    j += 1;
5283                }
5284                let digs: u32 = if j > i + 1 {
5285                    std::str::from_utf8(&bytes[i + 1..j])
5286                        .unwrap_or("3")
5287                        .parse()
5288                        .unwrap_or(3)
5289                } else {
5290                    3
5291                };
5292                // c:3408 — `switch (*fmt++)` examines the specifier
5293                // char (after the digit prefix).
5294                if j < bytes.len() && bytes[j] == b'.' {
5295                    // c:3409-3430 — `case '.':` fractional seconds.
5296                    //   long fnsec = nsec;
5297                    //   if (digs < 0 || digs > 9) digs = 9;
5298                    //   if (digs < 9) {
5299                    //       int trunc; long max = 100000000;
5300                    //       for (trunc = 8 - digs; trunc; trunc--)
5301                    //           { max /= 10; fnsec /= 10; }
5302                    //       max -= 1;
5303                    //       fnsec = (fnsec + 5) / 10;
5304                    //       if (fnsec > max) fnsec = max;
5305                    //   }
5306                    //   sprintf(buf, "%0*ld", digs, fnsec);
5307                    //
5308                    // The `(fnsec + 5) / 10` is a ROUND-half-up, capped
5309                    // at all-nines so a carry can't overflow the field.
5310                    // The previous port plain-truncated, so 999_999 ns
5311                    // under `%3.` printed `000` where zsh prints `001`.
5312                    let mut fnsec: u64 = nsec;
5313                    // c:3412-3413 — digs default is 3 (c:3362); >9 clamps to 9.
5314                    let digs = if digs > 9 { 9 } else { digs };
5315                    if digs < 9 {
5316                        // c:3418-3422
5317                        let mut max: u64 = 100_000_000;
5318                        for _ in 0..(8 - digs) {
5319                            max /= 10;
5320                            fnsec /= 10;
5321                        }
5322                        max -= 1; // c:3423
5323                        fnsec = (fnsec + 5) / 10; // c:3424
5324                        if fnsec > max {
5325                            fnsec = max; // c:3426
5326                        }
5327                    }
5328                    // c:3428 — `sprintf(buf, "%0*ld", digs, fnsec)`.
5329                    preprocessed.push_str(&format!("{:0width$}", fnsec, width = digs as usize));
5330                    i = j + 1;
5331                    continue;
5332                }
5333                let next = bytes[i + 1];
5334                // c:3445-3460 — %K / %L / %f extensions.
5335                match next {
5336                    b'K' => {
5337                        preprocessed.push_str(&format!("{}", tm_ref.tm_hour));
5338                        i += 2;
5339                        continue;
5340                    }
5341                    b'L' => {
5342                        let mut h12 = tm_ref.tm_hour % 12;
5343                        if h12 == 0 {
5344                            h12 = 12;
5345                        }
5346                        preprocessed.push_str(&format!("{}", h12));
5347                        i += 2;
5348                        continue;
5349                    }
5350                    b'f' => {
5351                        preprocessed.push_str(&format!("{}", tm_ref.tm_mday));
5352                        i += 2;
5353                        continue;
5354                    }
5355                    b'N' => {
5356                        // c:3491-3495 — `%N`: nanoseconds, always 9 digits
5357                        // (`sprintf(buf, "%09ld", nsec)`).
5358                        preprocessed.push_str(&format!("{:09}", nsec));
5359                        i += 2;
5360                        continue;
5361                    }
5362                    _ => {}
5363                }
5364            }
5365            preprocessed.push(bytes[i] as char);
5366            i += 1;
5367        }
5368
5369        // c:3354-3359 — `if (*fmt == Meta) { int chr = fmt[1] ^ 32;
5370        // *buf++ = chr; fmt += 2; }`: C's format string is METAFIED, so an
5371        // embedded NUL arrives as `Meta` + 0x20 and is copied straight to
5372        // the output, never reaching `strftime(3)`. The Rust port carries
5373        // the format as un-metafied bytes, so a NUL is a real interior NUL
5374        // — `CString::new` rejected it and `unwrap_or_default()` silently
5375        // produced an EMPTY format, dropping the whole result
5376        // (`strftime $'%Y\0%m\0%d' …` printed nothing). Split on NUL,
5377        // run strftime(3) per segment, and re-join with NUL so the byte
5378        // passes through exactly as C's Meta arm does.
5379        let mut result = String::new();
5380        for (seg_idx, seg) in preprocessed.split('\0').enumerate() {
5381            if seg_idx > 0 {
5382                result.push('\0');
5383            }
5384            if seg.is_empty() {
5385                continue;
5386            }
5387            let mut buf = vec![0u8; 256];
5388            let c_fmt = match CString::new(seg) {
5389                Ok(c) => c,
5390                Err(_) => continue,
5391            };
5392            let len = libc::strftime(
5393                buf.as_mut_ptr() as *mut libc::c_char,
5394                buf.len(),
5395                c_fmt.as_ptr(),
5396                tm,
5397            );
5398            if len > 0 {
5399                buf.truncate(len);
5400                result.push_str(&String::from_utf8_lossy(&buf));
5401            }
5402        }
5403        result
5404    }
5405
5406    #[cfg(not(unix))]
5407    {
5408        let _ = (fmt, secs, nsec);
5409        String::new()
5410    }
5411}
5412
5413/// Port of `zjoin()` from `Src/utils.c:3622` — C decl `zjoin(char **arr, int delim, int heap)`.
5414///
5415/// Join array with delimiter (from utils.c zjoin)
5416/// Joins `arr` with `delim` between elements. When `delim` is itself a meta
5417/// byte (`imeta(delim)` — NUL, `Meta`, `Marker`, or a token in the
5418/// Pound..Nularg range; see `inittyptab` c:4195-4201), C writes the delimiter
5419/// in *metafied* form (`Meta` byte then `delim ^ 32`, c:3634-3637) rather than
5420/// the raw byte (c:3639). The earlier port emitted the raw char always, which
5421/// was byte-wrong for meta delimiters — notably `zjoin(array, '\0', …)` (NUL is
5422/// imeta), where C emits `0x83 0x20`, not a raw `0x00`.
5423///
5424/// WARNING: param names don't match C — Rust=(arr, delim) vs C=(arr, delim, heap).
5425/// The `heap` arg is dropped (Rust `String` owns its storage). As with
5426/// [`metafy`], the `Meta` byte cannot live in valid UTF-8, so the final
5427/// `String` boundary uses the same `from_utf8`/lossy fallback — byte-exact for
5428/// every ASCII (non-meta) delimiter, lossy only on meta delimiters. Callers
5429/// needing byte-exact meta-delimited output should join at the byte level.
5430pub fn zjoin(arr: &[String], delim: char) -> String {
5431    // c:3622
5432    // c:3629-3630 — empty array → "".
5433    if arr.is_empty() {
5434        return String::new();
5435    }
5436    let db = delim as u32;
5437    // c:3634 — `imeta(delim)`. Only byte-valued delimiters can be meta.
5438    let delim_meta = db < 0x100 && imeta_byte(db as u8);
5439    let mut out: Vec<u8> = Vec::new();
5440    for (i, s) in arr.iter().enumerate() {
5441        if i != 0 {
5442            // The separator that C writes after every element then chops off
5443            // the trailing one (c:3641) is equivalent to interposing it.
5444            if delim_meta {
5445                out.push(Meta); // c:3635
5446                out.push((db as u8) ^ 32); // c:3636
5447            } else if db < 0x100 {
5448                out.push(db as u8); // c:3639 — raw single byte
5449            } else {
5450                // Non-byte char delimiter (no C analogue): emit its UTF-8.
5451                let mut buf = [0u8; 4];
5452                out.extend_from_slice(delim.encode_utf8(&mut buf).as_bytes());
5453            }
5454        }
5455        out.extend_from_slice(s.as_bytes()); // c:3633 — strucpy(&ptr, *s)
5456    }
5457    String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
5458}
5459
5460/// Port of `colonsplit()` from `Src/utils.c:3650`.
5461///
5462/// Port of `char **colonsplit(char *s, int uniq)` from
5463/// `Src/utils.c:3650`. Splits `s` on `:`; when `uniq` is set,
5464/// duplicate segments are dropped (linear scan against the
5465/// already-emitted prefix).
5466/// ```c
5467/// char **
5468/// colonsplit(char *s, int uniq)
5469/// {
5470///     int ct;
5471///     char *t, **ret, **ptr, **p;
5472///     for (t = s, ct = 0; *t; t++)
5473///         if (*t == ':') ct++;
5474///     ptr = ret = zalloc(sizeof(char *) * (ct + 2));
5475///     t = s;
5476///     do {
5477///         s = t;
5478///         for (; *t && *t != ':'; t++);
5479///         if (uniq)
5480///             for (p = ret; p < ptr; p++)
5481///                 if (strlen(*p) == t - s && !strncmp(*p, s, t - s))
5482///                     goto cont;
5483///         *ptr = zalloc((t - s) + 1);
5484///         ztrncpy(*ptr++, s, t - s);
5485///       cont: ;
5486///     } while (*t++);
5487///     *ptr = NULL;
5488///     return ret;
5489/// }
5490/// ```
5491pub fn colonsplit(s: &str, uniq: bool) -> Vec<String> {
5492    // c:3650
5493    // c:3655-3657 — count colons.
5494    let ct = s.matches(':').count(); // c:3655
5495    let mut ret: Vec<String> = Vec::with_capacity(ct + 2); // c:3658 zalloc((ct+2)*sizeof(char *))
5496
5497    // c:3661-3673 — do-while loop walking segments.
5498    let bytes = s.as_bytes();
5499    let mut t: usize = 0;
5500    loop {
5501        let seg_start = t; // c:3662 s = t
5502                           // c:3664 — `for (; *t && *t != ':'; t++)`
5503        while t < bytes.len() && bytes[t] != b':' {
5504            t += 1;
5505        }
5506        let seg = &s[seg_start..t];
5507        // c:3665-3668 — uniq dedupe.
5508        if !uniq || !ret.iter().any(|p| p == seg) {
5509            // c:3665
5510            ret.push(seg.to_string()); // c:3670 zalloc + ztrncpy
5511        }
5512        // c:3673 — `while (*t++)` — break if at end, else step past colon.
5513        if t >= bytes.len() {
5514            break;
5515        } // c:3673
5516        t += 1; // c:3673 t++
5517    }
5518    // c:3674 — `*ptr = NULL;` — Rust Vec needs no sentinel.
5519    ret // c:3675
5520}
5521
5522/// Port of `skipwsep()` from `Src/utils.c:3680` — C decl `skipwsep(char **s)`.
5523///
5524/// Skip whitespace separators (`iwsep`-true bytes) at the start of
5525/// `s`. Returns `(remaining_str, count)` — `count` is the number of
5526/// bytes / chars skipped. C body:
5527///
5528/// ```c
5529/// while (*t && iwsep(*t == Meta ? t[1] ^ 32 : *t)) {
5530///     if (*t == Meta) t++;
5531///     t++;
5532///     i++;
5533/// }
5534/// ```
5535///
5536/// The C version honours Meta-encoded bytes (`Meta` followed by
5537/// `^32`-XOR'd byte). Rust port mirrors that byte-by-byte.
5538pub fn skipwsep(s: &str) -> (&str, usize) {
5539    let bytes = s.as_bytes();
5540    let mut i: usize = 0;
5541    let mut count: usize = 0;
5542    while i < bytes.len() {
5543        // Character-level Meta pair (see spacesplit's char_at).
5544        if bytes[i] == 0xc2 && i + 3 < bytes.len() && bytes[i + 1] == Meta {
5545            let Some(n) = s[i + 2..].chars().next() else { break };
5546            if !iwsep((n as u32 as u8) ^ 32) {
5547                break;
5548            }
5549            i += 2 + n.len_utf8();
5550            count += 1;
5551            continue;
5552        }
5553        let b = if bytes[i] == Meta && i + 1 < bytes.len() {
5554            bytes[i + 1] ^ 32
5555        } else {
5556            bytes[i]
5557        };
5558        if !iwsep(b) {
5559            break;
5560        }
5561        if bytes[i] == Meta {
5562            i += 1;
5563        }
5564        i += 1;
5565        count += 1;
5566    }
5567    (&s[i..], count)
5568}
5569
5570/// Port of `spacesplit()` from `Src/utils.c:3711`.
5571///
5572/// Port of `spacesplit(char *s, int allownull, int heap, int quote)` from
5573/// `Src/utils.c:3711`. Splits `s` on `$IFS` via the `ISEP`/`IWSEP` char
5574/// classes (TYPTAB, kept in sync with `$IFS` by `inittyptab`, which
5575/// re-runs on every IFS set — params.rs:8691, c:4795), NOT on hardcoded
5576/// whitespace. zsh's word-splitting rules: runs of IFS-WHITESPACE
5577/// collapse and the leading/trailing ones yield real `""` fields (which
5578/// callers elide); each IFS-NON-WHITESPACE char delimits a field, and
5579/// consecutive ones yield `nulstring` (`Nularg`, `Src/subst.c:36`
5580/// `{Nularg,'\0'}`) empty fields, which survive and are later stripped to
5581/// `""` by remnulargs.
5582///
5583/// WARNING: param names don't match C — Rust=(s, allownull, quote) vs
5584/// C=(s, allownull, heap, quote). The `heap` C-buffer param drops: Rust owns
5585/// its String storage. With `quote` (c:3722-3727, c:3741-3743) a backslash
5586/// before a separator keeps it inside its field and is stripped from the
5587/// private copy of `s` the split works on (`findsep`'s quote arm,
5588/// c:3795-3811). The previous port split only on hardcoded
5589/// `[' ','\t','\n']`, ignoring `$IFS` entirely (Bug #636).
5590pub fn spacesplit(s: &str, allownull: bool, quote: bool) -> Vec<String> {
5591    // c:3711
5592    if quote {
5593        // c:3722-3727 — "we will be stripping quoted separators by hacking
5594        // string, so make sure it's hackable": work on a private copy.
5595        let mut buf = s.to_string();
5596        let mut ret: Vec<String> = Vec::new();
5597        let mut si = 0usize;
5598        let mut t = si; // c:3729 — `t = s;`
5599        si += buf[si..].len() - skipwsep(&buf[si..]).0.len(); // c:3730 — `skipwsep(&s);`
5600        if si < buf.len() && itype_end(&buf[si..], ISEP as u32, true) != 0 {
5601            // c:3732-3733 — `*ptr++ = dup(allownull ? "" : nulstring);`
5602            ret.push(if allownull { String::new() } else { Nularg.to_string() });
5603        } else if !allownull && t != si {
5604            ret.push(String::new()); // c:3734-3735
5605        }
5606        while si < buf.len() {
5607            // c:3736
5608            let iend = itype_end(&buf[si..], ISEP as u32, true); // c:3737
5609            if iend != 0 {
5610                // c:3738-3740 — `s = iend; skipwsep(&s);`
5611                si += iend;
5612                si += buf[si..].len() - skipwsep(&buf[si..]).0.len();
5613            } else if buf.as_bytes()[si] == b'\\' {
5614                // c:3741-3743 — `else if (quote && *s == '\\') { s++; skipwsep(&s); }`
5615                si += 1;
5616                si += buf[si..].len() - skipwsep(&buf[si..]).0.len();
5617            }
5618            t = si; // c:3745 — `t = s;`
5619            findsep(&mut buf, &mut si, None, true); // c:3746 — `findsep(&s, NULL, quote);`
5620            if si > t || allownull {
5621                // c:3747-3750 — `ztrncpy(*ptr++, t, s - t);`
5622                ret.push(buf[t..si].to_string());
5623            } else {
5624                ret.push(Nularg.to_string()); // c:3752 — `dup(nulstring)`
5625            }
5626            t = si; // c:3753 — `t = s;`
5627            si += buf[si..].len() - skipwsep(&buf[si..]).0.len(); // c:3754
5628        }
5629        if !allownull && t != si {
5630            ret.push(String::new()); // c:3756-3757
5631        }
5632        return ret;
5633    }
5634    use crate::ported::ztype_h::zistype;
5635    let bytes = s.as_bytes();
5636    // Meta-aware decode of the logical char value + its byte length at a
5637    // byte position (mirrors skipwsep's `*x == Meta ? x[1] ^ 32 : *x`).
5638    //
5639    // zshrs metafies at the CHARACTER level: a raw byte B >= 0x80 is the
5640    // char U+0083 followed by the char `B ^ 32` (compile_zsh.rs
5641    // meta_encode_byte), i.e. the UTF-8 run `c2 83 <2 bytes>`. Decode that
5642    // run as the ONE byte C sees, or each of its UTF-8 bytes was classified
5643    // separately: `unsetopt multibyte; IFS=$'\xe9'` split `a\xe9b` into
5644    // four fields instead of two.
5645    let char_at = |i: usize| -> (u8, usize) {
5646        if bytes[i] == 0xc2 && i + 3 < bytes.len() && bytes[i + 1] == Meta {
5647            if let Some(n) = s[i + 2..].chars().next() {
5648                return ((n as u32 as u8) ^ 32, 2 + n.len_utf8());
5649            }
5650        }
5651        // c:Src/zsh.h Meta — `\u{83}` prefix; next byte XOR 0x20.
5652        if bytes[i] == Meta && i + 1 < bytes.len() {
5653            (bytes[i + 1] ^ 32, 2)
5654        } else {
5655            (bytes[i], 1)
5656        }
5657    };
5658    // skipwsep(&s) — advance past a run of IWSEP (IFS-whitespace). c:3730
5659    let skipwsep_at = |mut i: usize| -> usize {
5660        while i < bytes.len() {
5661            let (c, l) = char_at(i);
5662            if !iwsep(c) {
5663                break;
5664            }
5665            i += l;
5666        }
5667        i
5668    };
5669    // itype_end(s, ISEP, 1) — `once` advances past exactly ONE ISEP char
5670    // if present, else leaves the index unchanged (c:4462 `if (once) break`).
5671    // c:4412-4455 itype_end / c:3813-3814 findsep under MULTIBYTE —
5672    // `MB_METACHARLENCONV` + `WC_ZISTYPE(c, ISEP)`: a valid non-ASCII
5673    // character is ISEP when it is in `ifs_wide`. zshrs's Meta pair (an
5674    // invalid byte) stays on the byte path below, where it cannot match.
5675    let multibyte = isset(crate::ported::zsh_h::MULTIBYTE);
5676    let wide_sep_at = |i: usize| -> Option<usize> {
5677        if !multibyte || bytes[i] < 0xc0 || (bytes[i] == 0xc2 && bytes.get(i + 1) == Some(&Meta)) {
5678            return None;
5679        }
5680        let c = s[i..].chars().next()?;
5681        Some(if wcsitype(c, ISEP as u32) { c.len_utf8() } else { 0 })
5682    };
5683    let isep_one = |i: usize| -> usize {
5684        if i < bytes.len() {
5685            if let Some(l) = wide_sep_at(i) {
5686                return i + l;
5687            }
5688            let (c, l) = char_at(i);
5689            if zistype(c, ISEP as u32) {
5690                return i + l;
5691            }
5692        }
5693        i
5694    };
5695    // findsep(&s, NULL, 0) — advance to the next ISEP char. c:3784
5696    let findsep_at = |mut i: usize| -> usize {
5697        while i < bytes.len() {
5698            if let Some(l) = wide_sep_at(i) {
5699                if l > 0 {
5700                    break;
5701                }
5702                i += s[i..].chars().next().map_or(1, |c| c.len_utf8());
5703                continue;
5704            }
5705            let (c, l) = char_at(i);
5706            if zistype(c, ISEP as u32) {
5707                break;
5708            }
5709            i += l;
5710        }
5711        i
5712    };
5713    let nulstring = || Nularg.to_string(); // c:subst.c:36 nulstring = {Nularg,0}
5714
5715    let mut ret: Vec<String> = Vec::new();
5716    let mut si = 0usize;
5717    let mut t = si; // c:3729 — `t = s;`
5718    si = skipwsep_at(si); // c:3730 — `skipwsep(&s);`
5719                          // c:3732-3735 — leading-field handling.
5720    if si < bytes.len() && isep_one(si) != si {
5721        // c:3733 — at an IFS-non-whitespace sep after skipping whitespace.
5722        ret.push(if allownull {
5723            String::new()
5724        } else {
5725            nulstring()
5726        });
5727    } else if !allownull && t != si {
5728        // c:3734-3735 — leading IFS-whitespace was skipped → real "".
5729        ret.push(String::new());
5730    }
5731    // c:3736-3756 — main word loop.
5732    while si < bytes.len() {
5733        let iend = isep_one(si); // c:3737 itype_end(s, ISEP, 1)
5734        let consumed_sep = iend != si;
5735        if consumed_sep {
5736            // c:3738-3741 — consume the single non-ws sep + following WS.
5737            si = iend;
5738            si = skipwsep_at(si);
5739        }
5740        // !!! POSIX-FAITHFUL GATE (no C counterpart) !!!
5741        // zsh (and `emulate sh`) emits a trailing empty field when a
5742        // non-whitespace IFS separator is the last thing in the input:
5743        // `IFS=:; set -- $v` on `a:b:` → (a, b, "") = 3. dash/ksh/bash drop
5744        // that trailing empty → (a, b) = 2. A MIDDLE empty (`a::b`) keeps
5745        // `si < len` here and is unaffected. Scoped to `!allownull` (the
5746        // unquoted-split path `set -- $v` uses); the quoted `"${=v}"` /
5747        // `${(s::)}` allownull path keeps zsh semantics. `zshrs --sh/--ksh/
5748        // --dash` sets posix_faithful(); `--... --zsh` leaves it false.
5749        if consumed_sep && si >= bytes.len() && !allownull && crate::dash_mode::posix_faithful() {
5750            break;
5751        }
5752        t = si; // c:3746 — `t = s;`
5753        si = findsep_at(si); // c:3747 — `findsep(&s, NULL, quote);`
5754        if si > t || allownull {
5755            // c:3748-3751 — emit the word `t..si`.
5756            ret.push(s[t..si].to_string());
5757        } else {
5758            // c:3753 — empty field between non-ws seps → nulstring.
5759            ret.push(nulstring());
5760        }
5761        t = si; // c:3754 — `t = s;`
5762        si = skipwsep_at(si); // c:3755 — `skipwsep(&s);`
5763    }
5764    // c:3757 — trailing IFS-whitespace → real "".
5765    if !allownull && t != si {
5766        ret.push(String::new());
5767    }
5768    ret
5769}
5770
5771/// Port of `findsep()` from `Src/utils.c:3784` — C decl `findsep(char **s, char *sep, int quote)`.
5772///
5773/// c:3762 — "Find a separator.  Return 0 if already at separator, 1 if
5774/// separator found later, else -1."  `pos` is C's walking `*s`: on
5775/// return it points at the separator (or end-of-string) so the caller
5776/// reads the word as `s[entry_pos..pos]`.
5777///
5778/// `sep` (c:3772):
5779///   - `None`       → split on normal `$IFS` separators (ISEP chars).
5780///   - `Some(b"")`  → no real separator: advance past one character.
5781///   - `Some(seq)`  → look for the (possibly multi-byte) literal `seq`.
5782///
5783/// `quote` (c:3776) — a `\` before a separator suppresses it, and `\\`
5784/// collapses to `\`.  This only applies when `sep` is `None` and it
5785/// strips backslashes in place, so the buffer must be modifiable
5786/// (C demands "something modifiable"; Rust takes `&mut String`).
5787///
5788/// The standalone form has no in-tree caller yet — the hot split paths
5789/// (`splitstring`, `findword`, `wordcount`) inline this logic — but it
5790/// is kept as a faithful, callable name-parity anchor.
5791/// WARNING: param names match C semantics — `pos` is C's `*s`.
5792pub fn findsep(s: &mut String, pos: &mut usize, sep: Option<&[u8]>, quote: bool) -> i32 {
5793    // c:3784
5794    use crate::ported::zsh_h::{MB_METACHARINIT, MB_METACHARLEN, MB_METACHARLENCONV};
5795    use crate::ported::ztype_h::{isep, ISEP, WC_ZISTYPE};
5796
5797    MB_METACHARINIT(); // c:3792
5798    let is_isep = |c: Option<char>| c.map(|c| WC_ZISTYPE(c, ISEP as u32)).unwrap_or(false);
5799
5800    match sep {
5801        // c:3793-3824 — default separators (ISEP), with optional quoting.
5802        None => {
5803            let start = *pos;
5804            let mut t = *pos;
5805            while t < s.len() {
5806                let b = s.as_bytes()[t];
5807                if quote && b == b'\\' {
5808                    // c:3795 — `if (quote && *t == '\\')`
5809                    if t + 1 < s.len() && s.as_bytes()[t + 1] == b'\\' {
5810                        // c:3796-3799 — `\\` → `\`: drop one backslash,
5811                        // advance past the one we keep (ilen = 1).
5812                        chuck(s, t);
5813                        t += 1;
5814                        continue;
5815                    }
5816                    // c:3801 — measure the char *after* the backslash.
5817                    let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t + 1..]);
5818                    if is_isep(c) {
5819                        // c:3802-3804 — escaped separator: strip the
5820                        // backslash, then advance over the now-bare char.
5821                        chuck(s, t);
5822                        t += ilen.max(1);
5823                        continue;
5824                    }
5825                    // c:3806-3810 — backslash is a normal byte.
5826                    if isep(b) {
5827                        break; // (never taken: '\\' is not an ISEP)
5828                    }
5829                    t += 1; // ilen = 1
5830                } else {
5831                    // c:3814-3818 — ordinary character.
5832                    let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t..]);
5833                    if is_isep(c) {
5834                        break; // c:3817
5835                    }
5836                    t += ilen.max(1);
5837                }
5838            }
5839            let i = if t > start { 1 } else { 0 }; // c:3821 `i = (t > *s)`
5840            *pos = t; // c:3822
5841            i // c:3823
5842        }
5843        // c:3825-3834 — empty separator: advance past the first char.
5844        Some(seq) if seq.is_empty() => {
5845            if *pos < s.len() {
5846                *pos += MB_METACHARLEN(&s.as_bytes()[*pos..]); // c:3831
5847                1 // c:3832
5848            } else {
5849                -1 // c:3833
5850            }
5851        }
5852        // c:3835-3845 — explicit (possibly multi-byte) literal separator.
5853        Some(seq) => {
5854            let mut i = 0i32;
5855            while *pos < s.len() {
5856                // c:3840 — `for (t=sep, tt=*s; *t && *tt && *t==*tt; ...)`
5857                let bytes = s.as_bytes();
5858                let mut k = 0usize;
5859                while k < seq.len() && *pos + k < bytes.len() && seq[k] == bytes[*pos + k] {
5860                    k += 1;
5861                }
5862                if k == seq.len() {
5863                    return if i > 0 { 1 } else { 0 }; // c:3841 `return (i > 0)`
5864                }
5865                *pos += MB_METACHARLEN(&bytes[*pos..]); // c:3842
5866                i += 1;
5867            }
5868            -1 // c:3844
5869        }
5870    }
5871}
5872
5873/// Port of `findword()` from `Src/utils.c:3849` — C decl `findword(char **s, char *sep)`.
5874///
5875/// Find word at position (from utils.c findword)
5876pub fn findword<'a>(s: &'a str, sep: Option<&'a str>) -> Option<(&'a str, &'a str)> {
5877    let s = match sep {
5878        Some(_) => s,
5879        None => s.trim_start(),
5880    };
5881    if s.is_empty() {
5882        return None;
5883    }
5884    match sep {
5885        Some(sep) => {
5886            if let Some(pos) = s.find(sep) {
5887                Some((&s[..pos], &s[pos + sep.len()..]))
5888            } else {
5889                Some((s, ""))
5890            }
5891        }
5892        None => {
5893            let end = s.find(|c: char| c.is_ascii_whitespace()).unwrap_or(s.len());
5894            Some((&s[..end], &s[end..]))
5895        }
5896    }
5897}
5898
5899/// Port of `wordcount()` from `Src/utils.c:3879` — C decl `wordcount(char *s, char *sep, int mul)`.
5900///
5901/// Returns the number of words in `s` that would result from splitting
5902/// on `sep` (or `$IFS` when `sep` is None). `mul` controls how
5903/// consecutive empty fields are counted:
5904/// - `mul == 0`: don't count leading/trailing/consecutive empties.
5905/// - `mul > 0`: count consecutive empties (each `sep` boundary
5906///   produces one word, even if the surrounding text is empty).
5907/// - `mul < 0`: count empty trailing fields (final separator after
5908///   the last non-empty field counts as one extra empty word).
5909///
5910/// C body (paraphrased):
5911/// ```c
5912/// if (sep) {
5913///     r = 1;
5914///     sl = strlen(sep);
5915///     for (; (c = findsep(&s, sep, 0)) >= 0; s += sl)
5916///         if ((c || mul) && (sl || *(s + sl)))
5917///             r++;
5918/// } else {
5919///     /* IFS-based: walk skipwsep / itype_end(s, ISEP, 1) */
5920/// }
5921/// ```
5922///
5923/// This port walks the metafied byte stream directly to mirror C's
5924/// pointer arithmetic. The `sep`-based branch is exact; the IFS
5925/// branch uses [`iwsep`] for whitespace-separator detection (C's
5926/// `ISEP` char class collapsed to whitespace, which is the common
5927/// case for default `$IFS` = `" \t\n"`).
5928pub fn wordcount(s: &str, sep: Option<&str>, mul: i32) -> i32 {
5929    // c:3879
5930    let bytes = s.as_bytes();
5931    if let Some(sep) = sep {
5932        // C: r = 1; sl = strlen(sep); for (; findsep(&s,sep,0) >= 0; s+=sl)
5933        //        if ((c || mul) && (sl || *(s+sl))) r++;
5934        let sep_bytes = sep.as_bytes();
5935        let sl = sep_bytes.len();
5936        let mut r: i32 = 1;
5937        let mut pos = 0;
5938        while pos <= bytes.len() {
5939            let rest = &bytes[pos..];
5940            let c_offset = match sep_bytes.is_empty() {
5941                true => Some(0usize),
5942                false => rest.windows(sl).position(|w| w == sep_bytes),
5943            };
5944            let Some(c) = c_offset else { break };
5945            // C `c` is the chars before the separator; `(sl || *(s+sl))`
5946            // means: if sl is zero (empty sep), only count when there's a
5947            // following char. Otherwise (sl > 0), the second clause is true
5948            // when sep is non-empty AND there are bytes after sep.
5949            let after_off = pos + c + sl;
5950            let following_nonempty = after_off < bytes.len();
5951            let cond_b = sl != 0 || following_nonempty;
5952            if (c != 0 || mul != 0) && cond_b {
5953                r += 1;
5954            }
5955            if sl == 0 {
5956                // Avoid infinite loop on empty sep — mirrors C's findsep
5957                // which advances by 1 byte when sep is empty.
5958                pos += 1;
5959            } else {
5960                pos += c + sl;
5961            }
5962        }
5963        r
5964    } else {
5965        // c:3888-3905 — the `sep == NULL` (IFS) branch, ported statement by
5966        // statement. The previous rewrite inverted C's leading test — it
5967        // counted a leading WORD where C counts a leading SEPARATOR — and
5968        // used `iwsep` (IFS-whitespace) for a test C makes on `ISEP` (any
5969        // IFS char), so an all-separator string came out short:
5970        // `s='   '; ${(W)#s}` gave 2 against zsh's 4, and `'a b  c '` gave
5971        // 4 against 5.
5972        //
5973        // Meta-aware char reads mirror spacesplit's local closures
5974        // (c:Src/utils.c:3711): a Meta byte prefixes the escaped char.
5975        use crate::ported::ztype_h::zistype;
5976        let char_at = |i: usize| -> (u8, usize) {
5977            if bytes[i] == Meta && i + 1 < bytes.len() {
5978                (bytes[i + 1] ^ 32, 2)
5979            } else {
5980                (bytes[i], 1)
5981            }
5982        };
5983        // skipwsep(&s) — c:Src/utils.c:3730.
5984        let skipwsep_at = |mut i: usize| -> usize {
5985            while i < bytes.len() {
5986                let (c, l) = char_at(i);
5987                if !iwsep(c) {
5988                    break;
5989                }
5990                i += l;
5991            }
5992            i
5993        };
5994        // itype_end(s, ISEP, 1) — consumes exactly ONE ISEP char
5995        // (c:Src/utils.c:4462 `if (once) break`).
5996        let isep_one = |i: usize| -> usize {
5997            if i < bytes.len() {
5998                let (c, l) = char_at(i);
5999                if zistype(c, ISEP as u32) {
6000                    return i + l;
6001                }
6002            }
6003            i
6004        };
6005        // findsep(&s, NULL, 0) — advance to the next ISEP char (c:3784).
6006        let findsep_at = |mut i: usize| -> usize {
6007            while i < bytes.len() {
6008                let (c, l) = char_at(i);
6009                if zistype(c, ISEP as u32) {
6010                    break;
6011                }
6012                i += l;
6013            }
6014            i
6015        };
6016        let mut s_pos = 0usize; // c:3888 `char *t = s;`
6017        let mut t = s_pos; // c:3888
6018        let mut r: i32 = 0; // c:3889
6019        if mul <= 0 {
6020            // c:3890
6021            s_pos = skipwsep_at(s_pos); // c:3891
6022        }
6023        // c:3892-3894 — `if ((*s && itype_end(s, ISEP, 1) != s) ||
6024        //                    (mul < 0 && t != s)) r++;`
6025        if (s_pos < bytes.len() && isep_one(s_pos) != s_pos) || (mul < 0 && t != s_pos) {
6026            r += 1; // c:3894
6027        }
6028        // c:3895-3903 — `for (; *s; r++) { … }`.
6029        while s_pos < bytes.len() {
6030            let ie = isep_one(s_pos); // c:3896
6031            if ie != s_pos {
6032                // c:3897
6033                s_pos = ie; // c:3898
6034                if mul <= 0 {
6035                    // c:3899
6036                    s_pos = skipwsep_at(s_pos);
6037                }
6038            }
6039            s_pos = findsep_at(s_pos); // c:3901 `(void)findsep(&s, NULL, 0);`
6040            t = s_pos; // c:3902
6041            if mul <= 0 {
6042                // c:3903
6043                s_pos = skipwsep_at(s_pos);
6044            }
6045            r += 1; // c:3895 (loop increment)
6046        }
6047        if mul < 0 && t != s_pos {
6048            // c:3904
6049            r += 1; // c:3905
6050        }
6051        r
6052    }
6053}
6054
6055/// Join array with separator - port from zsh/Src/utils.c sepjoin() lines 3926-3958
6056///
6057/// If sep is None, uses first char of IFS (defaults to space).
6058/// Join an array with separator.
6059/// Port of `sepjoin(char **s, char *sep, int heap)` from Src/utils.c:3928.
6060/// WARNING: param names don't match C — Rust=(arr, sep) vs C=(s, sep, heap)
6061// Rust idiom replacement: `slice::join` covers the C `zalloc`+`strcpy`
6062// loop with running length pre-compute; the `heap` arg drops since
6063// String owns its own allocation.
6064/// Port of `sepjoin()` from `Src/utils.c:3928`.
6065///
6066/// `sepjoin` — see implementation.
6067pub fn sepjoin(arr: &[String], sep: Option<&str>) -> String {
6068    // c:3928
6069    // c:3934-3935 — if (!*s) return heap ? dupstring("") : ztrdup("");
6070    if arr.is_empty() {
6071        return String::new();
6072    }
6073    // c:3936-3945 — `if (!sep)` default-sep arm:
6074    //   if (ifs && *ifs != ' ') sep = dupstrpfx(ifs, MB_METACHARLEN(ifs));
6075    //   else sep = " ";
6076    // IFS SET-but-EMPTY takes the first branch (`*ifs` is '\0' != ' ')
6077    // and dupstrpfx("", …) yields an EMPTY separator — `"$*"` with
6078    // IFS="" concatenates. Only IFS UNSET (ifs == NULL → getsparam
6079    // None) or starting with a space falls back to " ".
6080    let ifs_storage: String;
6081    let sep_str: &str = match sep {
6082        Some(s) => s, // c:3936
6083        None => {
6084            ifs_storage = match getsparam("IFS") {
6085                // c:3938-3940 — set, first char not space (empty → "").
6086                Some(ifs) if !ifs.starts_with(' ') => ifs
6087                    .chars()
6088                    .next()
6089                    .map(|c| c.to_string())
6090                    .unwrap_or_default(),
6091                // c:3941-3944 — unset (NULL) or leading space → " ".
6092                _ => " ".to_string(),
6093            };
6094            &ifs_storage
6095        }
6096    };
6097    // c:3947-3956 — pre-compute total length, alloc, copy elements
6098    //               interleaving sep. Rust slice::join collapses all that
6099    //               into the one canonical call.
6100    arr.join(sep_str)
6101}
6102
6103/// Port of `sepsplit()` from `Src/utils.c:3962` — C decl `sepsplit(char *s, char *sep, int allownull, int heap)`.
6104///
6105/// Split string by separator - port from zsh/Src/utils.c sepsplit() lines 3961-3992
6106///
6107/// If sep is None, performs IFS-style word splitting (spacesplit).
6108/// Otherwise splits on the given separator string.
6109/// allownull: if true, allows empty strings in result
6110/// Split a string on `IFS` separators.
6111/// WARNING: param names don't match C — Rust=(s, sep, allownull) vs C=(s, sep, allownull, heap)
6112pub fn sepsplit(s: &str, sep: Option<&str>, allownull: bool) -> Vec<String> {
6113    // c:3962
6114    // Handle Nularg at start (zsh internal marker) - line 3968
6115    let s = if s.starts_with('\x00') && s.len() > 1 {
6116        &s[1..]
6117    } else {
6118        s
6119    };
6120
6121    match sep {
6122        None => spacesplit(s, allownull, false),
6123        Some("") => {
6124            // Empty separator: split into characters
6125            if allownull {
6126                s.chars().map(|c| c.to_string()).collect()
6127            } else {
6128                s.chars()
6129                    .map(|c| c.to_string())
6130                    .filter(|c| !c.is_empty())
6131                    .collect()
6132            }
6133        }
6134        Some(sep) => {
6135            let parts: Vec<String> = s.split(sep).map(|p| p.to_string()).collect();
6136            if allownull {
6137                parts
6138            } else {
6139                parts.into_iter().filter(|p| !p.is_empty()).collect()
6140            }
6141        }
6142    }
6143}
6144
6145/// Port of `getshfunc()` from `Src/utils.c:3998` — C decl `getshfunc(char *nam)`.
6146///
6147/// C body:
6148/// ```c
6149/// Shfunc getshfunc(char *nam) {
6150///     return (Shfunc) shfunctab->getnode(shfunctab, nam);
6151/// }
6152/// ```
6153///
6154/// Routes through the global `shfunctab` singleton in
6155/// hashtable.rs (hashtable::shfunctab_lock). Returns an owned
6156/// `shfunc` clone so callers can read `flags` (to detect
6157/// `PM_UNDEFINED` autoload stubs), `funcdef`, `filename`, and
6158/// `body` without holding the table lock. Owned clone vs C's
6159/// `*Shfunc` raw pointer trades one allocation for Rust
6160/// lifetime safety — `getshfunc` is a function-lookup site,
6161/// not per-statement, so the cost is irrelevant.
6162///
6163/// Returning `Option<String>` of just `body` (the old contract)
6164/// made every PM_UNDEFINED autoload stub invisible because
6165/// `body=None` collapsed via `and_then` to `None`, which callers
6166/// then read as "function doesn't exist."
6167pub fn getshfunc(nam: &str) -> Option<shfunc> {
6168    let tab = shfunctab_lock().read().expect("shfunctab poisoned");
6169    tab.get(nam).cloned()
6170}
6171
6172/// Port of `subst_string_by_func()` from `Src/utils.c:4017`.
6173///
6174/// Port of `char **subst_string_by_func(Shfunc func, char *arg1, char *orig)`
6175/// from Src/utils.c:4017.
6176///
6177/// Calls the named shell function with `[func, arg1?, orig]` as
6178/// positional args under `sfcontext = SFC_SUBST` and returns the
6179/// `$reply` array on success. Routes through `callhookfunc` (the
6180/// static-linked equivalent of `doshfunc`), then reads `$reply`
6181/// from the env-var fallback because the global `paramtab` is not
6182/// yet a singleton in the Rust port (params::getaparam takes a
6183/// `&ParamTable` arg).
6184pub fn subst_string_by_func(
6185    func_name: &str,
6186    arg1: Option<&str>,
6187    orig: &str,
6188) -> Option<Vec<String>> // c:4017
6189{
6190    let osc = SFCONTEXT.load(Ordering::Relaxed); // c:4019
6191    let osm = STOPMSG.load(Ordering::Relaxed);
6192    let old_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
6193    let mut args: Vec<String> = Vec::with_capacity(3); // c:4020-4026
6194    args.push(func_name.to_string()); // c:4023
6195    if let Some(a) = arg1 {
6196        // c:4024
6197        args.push(a.to_string()); // c:4025
6198    }
6199    args.push(orig.to_string()); // c:4026
6200    SFCONTEXT // c:4027
6201        .store(SFC_SUBST, Ordering::Relaxed);
6202    INCOMPFUNC.store(0, Ordering::Relaxed); // c:4028
6203
6204    // c:Src/exec.c:1429 — `oldlineno = lineno;` / c:1696 — `lineno = oldlineno;`
6205    // C runs the hook body through `execlist`, which saves the caller's
6206    // `lineno` on entry and restores it on exit, so a shell function
6207    // invoked from inside an expansion leaves the CALLER's line number
6208    // untouched. zshrs drives `$LINENO` from a per-pipe
6209    // BUILTIN_SET_LINENO op: the statement that triggered this expansion
6210    // already emitted its own SET_LINENO, the hook body's ops then
6211    // overwrite `$LINENO`, and nothing puts it back — so `PS4='%N:%i> '`
6212    // rendered the hook's last line for the triggering statement
6213    // (`+fn:14>` where zsh prints `+fn:7>`, D01prompt.ztst "Recursive
6214    // use of prompts"). Same save/restore shape as the `$(...)` site in
6215    // vm_helper.rs:4733/5090.
6216    let saved_lineno = getsparam("LINENO");
6217
6218    // c:4030 — `if (doshfunc(func, l, 1))`. Direct doshfunc call
6219    // mirrors C. body_runner routes through the host body-only entry
6220    // (matching the same shfunc that `callhookfunc` would resolve).
6221    let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
6222        .read()
6223        .ok()
6224        .and_then(|t| t.get(func_name).cloned());
6225    // !!! WARNING: RUST-ONLY CARRIER SCOPING — NO C COUNTERPART !!!
6226    // In C the `${~spec}` GLOB_SUBST switch is a paramsubst LOCAL
6227    // (c:Src/subst.c:1671, c:2596), so a hook run from inside that expansion
6228    // (`~[name]` -> zsh_directory_name) sees the user's option and cannot
6229    // touch the local. zshrs carries the switch through the global option
6230    // table plus subst::TILDE_GLOBSUBST_CARRIER, so the hook body's command
6231    // boundary consumed the carrier, and under LOCAL_OPTIONS doshfunc's
6232    // restore then put the forced ON back with nothing left to undo it:
6233    // GLOB_SUBST stayed on for the rest of the caller, and compsys `_expand`
6234    // filename-generated a word it meant to quote (Y01completion #10).
6235    // Hand the hook the user's value and re-apply the forced one afterwards.
6236    let tilde_user = crate::ported::subst::TILDE_GLOBSUBST_CARRIER.with(|c| c.take());
6237    let forced_globsubst = crate::ported::zsh_h::isset(crate::ported::zsh_h::GLOBSUBST);
6238    if let Some(user) = tilde_user {
6239        crate::ported::options::opt_state_set("globsubst", user);
6240    }
6241    let rc = if let Some(mut shf) = shf_clone {
6242        let name_for_body = func_name.to_string();
6243        let body_args = args.clone();
6244        let body_runner = move || -> i32 {
6245            crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
6246        };
6247        crate::ported::exec::doshfunc(&mut shf, args.clone(), true, body_runner)
6248    } else {
6249        callhookfunc(func_name, Some(&args), 0, std::ptr::null_mut())
6250    };
6251    if let Some(user) = tilde_user {
6252        crate::ported::options::opt_state_set("globsubst", forced_globsubst);
6253        crate::ported::subst::TILDE_GLOBSUBST_CARRIER.with(|c| c.set(Some(user)));
6254    }
6255    // c:4033 — `ret = getaparam("reply")` against paramtab. `reply`
6256    // is a shell-local PM_ARRAY entry, never exported to env.
6257    let ret: Option<Vec<String>> = if rc != 0 {
6258        None // c:4031
6259    } else {
6260        getaparam("reply") // c:4033
6261    };
6262
6263    // c:1696 — `lineno = oldlineno;` (see the save above). `LINENO`
6264    // carries PM_READONLY (`integer-readonly-special`), so the restore
6265    // writes `u_val` directly instead of going through setsparam — the
6266    // same bypass BUILTIN_SET_LINENO uses (fusevm_bridge.rs:7205).
6267    if let Some(ln) = saved_lineno {
6268        let n: crate::ported::zsh_h::zlong = ln.parse().unwrap_or(0);
6269        if let Ok(mut tab) = crate::ported::params::paramtab().write() {
6270            if let Some(pm) = tab.get_mut("LINENO") {
6271                // c:Src/utils.c:121 `zlong lineno` — the value lives in the C
6272                // GLOBAL, reached through LINENO's GSU. A `typeset -h +g LINENO`
6273                // local shadow has no PM_SPECIAL and no GSU, so C's `lineno = N`
6274                // never touches it; skip the paramtab mirror for the same reason.
6275                if (pm.node.flags & crate::ported::zsh_h::PM_SPECIAL as i32) != 0 {
6276                    pm.u_val = n;
6277                    pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
6278                }
6279            }
6280        }
6281        set_lineno(n as i32);
6282        crate::ported::lex::set_lineno(n as u64);
6283    }
6284    SFCONTEXT.store(osc, Ordering::Relaxed); // c:4035
6285    STOPMSG.store(osm, Ordering::Relaxed); // c:4036
6286    INCOMPFUNC.store(old_incompfunc, Ordering::Relaxed); // c:4037
6287    ret // c:4038
6288}
6289
6290/// Port of `subst_string_by_hook()` from `Src/utils.c:4049`.
6291///
6292/// Port of `char **subst_string_by_hook(char *name, char *arg1, char *orig)`
6293/// from Src/utils.c:4049.
6294///
6295/// Looks up `name` as a shell function and calls
6296/// `subst_string_by_func` on it. If that returns no result, walks
6297/// `${name}_hook` as an array of function names, trying each in
6298/// order until one yields a `$reply`.
6299pub fn subst_string_by_hook(name: &str, arg1: Option<&str>, orig: &str) -> Option<Vec<String>> // c:4049
6300{
6301    let mut ret: Option<Vec<String>> = None;
6302    if getshfunc(name).is_some() {
6303        // c:4054
6304        ret = subst_string_by_func(name, arg1, orig); // c:4055
6305    }
6306    if ret.is_none() {
6307        // c:4058
6308        let arrnam = format!("{}_hook", name); // c:4061-4063
6309                                               // c:4065 — `arr = getaparam(arrnam)`. The previous Rust port
6310                                               // read `env::var(arrnam)` and NUL-split — wrong: the hook
6311                                               // array is a shell-local PM_ARRAY in paramtab, not env.
6312        if let Some(arr) = getaparam(&arrnam) {
6313            // c:4065
6314            for f in arr.iter() {
6315                // c:4068
6316                if f.is_empty() {
6317                    continue;
6318                }
6319                if getshfunc(f).is_some() {
6320                    // c:4069
6321                    ret = subst_string_by_func(f, arg1, orig); // c:4070
6322                    if ret.is_some() {
6323                        // c:4071
6324                        break; // c:4072
6325                    }
6326                }
6327            }
6328        }
6329    }
6330    ret // c:4094
6331}
6332
6333/// Port of `mkarray()` from `Src/utils.c:4083` — C decl `mkarray(char *s)`.
6334///
6335/// Make single-element array (from utils.c mkarray)
6336pub fn mkarray(s: Option<&str>) -> Vec<String> {
6337    match s {
6338        Some(val) => vec![val.to_string()],
6339        None => Vec::new(),
6340    }
6341}
6342
6343/// Port of `hmkarray()` from `Src/utils.c:4094` — C decl `hmkarray(char *s)`.
6344///
6345/// Make single-element array on heap (from utils.c hmkarray)
6346pub fn hmkarray(s: &str) -> Vec<String> {
6347    if s.is_empty() {
6348        Vec::new()
6349    } else {
6350        vec![s.to_string()]
6351    }
6352}
6353
6354/// Port of `zbeep()` from `Src/utils.c:4105` — C decl `void zbeep(void)`.
6355///
6356/// Honours `$ZBEEP` (a key-string sequence) when set and the BEEP
6357/// option when unset; emits the BEL char (\007) to SHTTY by
6358/// default. The Rust port writes via `write_loop` to mirror C's
6359/// raw-write semantics.
6360pub fn zbeep() {
6361    // c:4105
6362    queue_signals(); // c:4105
6363    if let Ok(zbeep) = std::env::var("ZBEEP") {
6364        // c:4109
6365        let (decoded, _) = getkeystring(&zbeep); // c:4111
6366        #[cfg(unix)]
6367        {
6368            let shtty = SHTTY.load(Ordering::Relaxed);
6369            if shtty != -1 {
6370                let _ = write_loop(shtty, decoded.as_bytes()); // c:4112
6371            } else {
6372                eprint!("{}", decoded);
6373            }
6374        }
6375        #[cfg(not(unix))]
6376        eprint!("{}", decoded);
6377    } else if isset(BEEP) {
6378        // c:4113
6379        #[cfg(unix)]
6380        {
6381            let shtty = SHTTY.load(Ordering::Relaxed);
6382            if shtty != -1 {
6383                let _ = write_loop(shtty, b"\x07"); // c:4114
6384            } else {
6385                eprint!("\x07");
6386            }
6387        }
6388        #[cfg(not(unix))]
6389        eprint!("\x07");
6390    }
6391    unqueue_signals(); // c:4115
6392}
6393
6394/// Port of `freearray()` from `Src/utils.c:4120` — C decl `freearray(char **s)`.
6395///
6396/// Free array (no-op in Rust, provided for API compat)
6397pub fn freearray(s: Vec<String>) {
6398    // c:4124 — DPUTS(!s, "freearray() with zero argument")
6399    // Rust takes Vec<String> by value (never null), so the C !s
6400    // check maps to no condition that can fire in Rust. Document
6401    // the gap.
6402    let _ = &s; // c:4124 (no-op; Vec is never NULL in Rust)
6403                // Rust Drop handles this
6404}
6405
6406/// Port of `equalsplit()` from `Src/utils.c:4133` — C decl `equalsplit(char *s, char **t)`.
6407///
6408/// Split on '=' returning (name, value) (from utils.c equalsplit)
6409/// WARNING: param names don't match C — Rust=(s) vs C=(s, t)
6410pub fn equalsplit(s: &str) -> Option<(String, String)> {
6411    let eq = s.find('=')?;
6412    Some((s[..eq].to_string(), s[eq + 1..].to_string()))
6413}
6414
6415// initialize the ztypes table                                              // c:4151
6416/// Port of `inittyptab()` from `Src/utils.c:4155`. Initialise the
6417/// `typtab[256]` lookup table that backs the `idigit`/`ialnum`/etc.
6418/// predicates in `ztype_h`.
6419///
6420/// C body (c:4155-4250) does:
6421///   1. Zero the table.
6422///   2. Mark 0..=31 and 128..=159 + 127 as ICNTRL.
6423///   3. Mark '0'..='9' as IDIGIT|IALNUM|IWORD|IIDENT|IUSER.
6424///   4. Mark 'a'..='z' and 'A'..='Z' as IALPHA|IALNUM|IIDENT|IUSER|IWORD.
6425///   5. Mark '_' as IIDENT|IUSER.
6426///   6. Mark '-', '.', Dash as IUSER.
6427///   7. Mark ' '/'\t' as IBLANK|INBLANK; '\n' as INBLANK.
6428///   8. Mark '\0', Meta, Marker as IMETA.
6429///   9. Mark Pound..LAST_NORMAL_TOK as ITOK|IMETA.
6430///   10. Mark Snull..Nularg as ITOK|IMETA|INULL.
6431///   11. Walk $IFS adding ISEP and IWSEP for blanks.
6432///
6433/// This first-pass port covers steps 1-7. Steps 8-11 require Meta /
6434/// Marker / Pound / Snull / Nularg constants from zsh_h/zsh.h and
6435/// the `ifs` global; the remaining Meta/IFS marks are skipped until
6436/// those land. Idempotent — safe to call multiple times.
6437pub fn inittyptab() {
6438    // utils.c:4155
6439
6440    // c:4160-4164 — `if (!(typtab_flags & ZTF_INIT))` one-off init:
6441    //
6442    //     typtab_flags = ZTF_INIT;
6443    //     if (interact && isset(SHINSTDIN))
6444    //         typtab_flags |= ZTF_INTERACT;
6445    //
6446    // The `ZTF_INTERACT` half was missing, and it is load-bearing: it is the
6447    // ONLY writer of that bit, c:4257 reads it to decide `ZTF_BANGCHAR`, and
6448    // c:4291 reads THAT to decide whether `bangchar` is `ISPECIAL`. Without
6449    // it the chain never starts, so `!` was never a special character in an
6450    // interactive shell and `${(q)v}` on `v='a!b'` published `a!b` where zsh
6451    // publishes `a\!b`.
6452    //
6453    // C latches this ONCE, on the first call, and never revisits it — later
6454    // calls keep whatever the first decided. Preserved exactly: the whole
6455    // block is inside the `ZTF_INIT` guard.
6456    {
6457        let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
6458        if (flags & ZTF_INIT) == 0 {
6459            let mut init = ZTF_INIT; // c:4161
6460            if interact() && isset(crate::ported::zsh_h::SHINSTDIN) {
6461                init |= ZTF_INTERACT; // c:4163
6462            }
6463            TYPTAB_FLAGS.store(init, Ordering::Relaxed);
6464        }
6465    }
6466
6467    // Local scratch, flushed to the atomic table at fn end — C mutates
6468    // typtab[] in place unlocked; the atomic store-per-slot flush keeps
6469    // readers lock-free (see TYPTAB doc in ztype_h.rs).
6470    let mut t = [0u32; 256]; // c:4168 memset(typtab, 0, sizeof(typtab))
6471                             // c:4169-4170 — control chars 0..32 and 128..160.
6472    for c in 0..32u32 {
6473        t[c as usize] = ICNTRL as u32;
6474        t[(c + 128) as usize] = ICNTRL as u32;
6475    }
6476    // c:4171 — `typtab[127] = ICNTRL;`
6477    t[127] = ICNTRL as u32;
6478    // c:4172-4173 — '0'..='9'.
6479    for c in (b'0' as usize)..=(b'9' as usize) {
6480        t[c] = (IDIGIT | IALNUM | IWORD | IIDENT | IUSER) as u32;
6481    }
6482    // c:4174-4175 — 'a'..='z' and matching 'A'..='Z'.
6483    for c in (b'a' as usize)..=(b'z' as usize) {
6484        let upper = c - (b'a' as usize) + (b'A' as usize);
6485        let bits = (IALPHA | IALNUM | IIDENT | IUSER | IWORD) as u32;
6486        t[c] = bits;
6487        t[upper] = bits;
6488    }
6489    // c:4190 — `typtab['_'] = IIDENT | IUSER;`
6490    t[b'_' as usize] = (IIDENT | IUSER) as u32;
6491    // c:4191 — `typtab['-'] = typtab['.'] = typtab[(unsigned char) Dash] = IUSER;`.
6492    t[b'-' as usize] = IUSER as u32;
6493    t[b'.' as usize] = IUSER as u32;
6494    // c:4191 — `Dash` token marker (0x9b per zsh.h:182, "Only in patterns").
6495    // Marking it IUSER lets pattern-side $-named-character paths
6496    // accept it as a user-name byte. Previously omitted in the port.
6497    t[crate::token_char::token_byte(crate::ported::zsh_h::Dash).unwrap() as usize] = IUSER as u32;
6498    // c:4192-4194 — blanks.
6499    t[b' ' as usize] |= (IBLANK | INBLANK) as u32;
6500    t[b'\t' as usize] |= (IBLANK | INBLANK) as u32;
6501    t[b'\n' as usize] |= INBLANK as u32;
6502
6503    // c:4195 — `typtab['\0'] |= IMETA;`. Previously omitted in the
6504    // Rust port — '\0' had only ICNTRL (set by the 0..32 loop at
6505    // c:4169) and was missing IMETA. C `imeta('\0')` is true (the
6506    // NUL byte must be Meta-encoded as `Meta + ('\0' ^ 32)`); the
6507    // Rust `imeta()` typtab predicate returned false, so any code
6508    // routing through `imeta(c)` (e.g. `input::shingetline`)
6509    // failed to Meta-encode NUL bytes from stdin, corrupting the
6510    // SHIN buffer when piped binary data hit a `\0`.
6511    t[0] |= IMETA as u32; // c:4195
6512                          // c:4196-4197 — Meta + Marker marked IMETA.
6513    {
6514        t[Meta as usize] |= IMETA as u32;
6515        t[crate::token_char::token_byte(Marker).unwrap() as usize] |= IMETA as u32;
6516    }
6517
6518    // c:4198-4199 — `for (t0 = (int) (unsigned char) Pound;
6519    //                     t0 <= (int) (unsigned char) LAST_NORMAL_TOK; t0++)
6520    //                    typtab[t0] |= ITOK | IMETA;`
6521    // Marks all char-rewrite token markers (Pound, Stringg, Hat,
6522    // Star, ...). Without this, `itok(Stringg)` returns false and
6523    // `is_valid_assignment_target("$NAME")` wrongly accepts the
6524    // leading `$` as part of an identifier prefix → `$=cmd` lexes
6525    // as ENVSTRING instead of STRING.
6526    {
6527        let lo = crate::token_char::token_byte(Pound).unwrap() as usize;
6528        let hi = crate::token_char::token_byte(LAST_NORMAL_TOK).unwrap() as usize;
6529        for t0 in lo..=hi {
6530            t[t0] |= (ITOK | IMETA) as u32;
6531        }
6532    }
6533
6534    // c:4200-4201 — `for (t0 = (int) (unsigned char) Snull;
6535    //                     t0 <= (int) (unsigned char) Nularg; t0++)
6536    //                    typtab[t0] |= ITOK | IMETA | INULL;`
6537    {
6538        let lo = crate::token_char::token_byte(Snull).unwrap() as usize;
6539        let hi = crate::token_char::token_byte(Nularg).unwrap() as usize;
6540        for t0 in lo..=hi {
6541            t[t0] |= (ITOK | IMETA | INULL) as u32;
6542        }
6543    }
6544
6545    // c:4202-4231 — IFS walk. Sets ISEP on every IFS char and IWSEP
6546    // on the blank (inblank) subset. Reads the current `ifs` global
6547    // (defaulting to DEFAULT_IFS), demetafies `Meta+X` pairs, and
6548    // skips a doubled blank (`s[1]==c`) so the IWSEP bit doesn't
6549    // mark "blank repeated → no-skip" IFS chars. Mirrors C exactly.
6550    {
6551        // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; *s; s++)`.
6552        // The default-IFS fallback is keyed on the `ifs` POINTER being
6553        // NULL (i.e. IFS unset), NOT on it pointing at an empty string.
6554        // `IFS=''` leaves a non-NULL empty `ifs`, the loop body never
6555        // runs, and typtab ends up with ZERO ISEP/IWSEP bits — which is
6556        // exactly how an empty IFS disables all word splitting
6557        // (`IFS=''; v='a b  c'; print -rl -- ${=v}` is one word). The
6558        // port used to coerce "" → DEFAULT_IFS at both the paramtab read
6559        // and the fallback, so an empty IFS still split on whitespace.
6560        // c:4207 / c:4216 read the global `ifs`, which `ifssetfn` keeps (the
6561        // paramtab node's getfn, `ifsgetfn`, returns the same global). `None`
6562        // is C's `ifs == NULL` after an unset (c:Src/params.c:4748): the node
6563        // is not yet PM_UNSET when `stdunsetfn` rebuilds the table, so it must
6564        // not be consulted.
6565        let pt_ifs: Option<String> = crate::ported::params::ifs_lock()
6566            .lock()
6567            .map(|g| g.clone())
6568            .unwrap_or_default();
6569        // A SET IFS (possibly "") is walked verbatim; c:4216
6570        // `ifs ? ifs : CURRENT_DEFAULT_IFS`. The default is taken in its
6571        // parameter-value (unmetafied) spelling, the one `ifs_lock` starts with
6572        // and c:4210's reset below stores; `DEFAULT_IFS` is C's metafied
6573        // spelling (c:Src/zsh.h:149), which the MULTIBYTE check rejects.
6574        let src: String = pt_ifs.unwrap_or_else(|| " \t\n\0".to_string());
6575        // c:4205-4213 — under MULTIBYTE, `set_widearray(ifs)`; an IFS that
6576        // does not convert (`!ifs_wide.chars`) warns and is reset to the
6577        // default, which is what the ISEP walk below then reads.
6578        let multibyte = isset(crate::ported::zsh_h::MULTIBYTE);
6579        let src = if multibyte && !src.is_empty() && set_widearray(&src).is_empty() {
6580            zwarn("IFS has an invalid character; resetting IFS to default"); // c:4208
6581            // c:4210 `ztrdup(CURRENT_DEFAULT_IFS)` — as the parameter value,
6582            // i.e. unmetafied (`DEFAULT_IFS` is the metafied spelling, c:149).
6583            let dflt = " \t\n\0".to_string();
6584            *crate::ported::params::ifs_lock().lock().expect("ifs poisoned") = Some(dflt.clone());
6585            if let Ok(mut tab) = crate::ported::params::paramtab().write() {
6586                if let Some(pm) = tab.get_mut("IFS") {
6587                    pm.u_str = Some(dflt.clone());
6588                }
6589            }
6590            dflt
6591        } else {
6592            src
6593        };
6594        // c:4207/4212 — `set_widearray(ifs, &ifs_wide)`.
6595        if multibyte {
6596            if let Ok(mut w) = IFS_WIDE.write() {
6597                *w = set_widearray(&src);
6598            }
6599        }
6600        let bytes = src.as_bytes();
6601        let mut i = 0;
6602        while i < bytes.len() {
6603            // c:4217 — `int c = (unsigned char) (*s == Meta ? *++s ^ 32 : *s)`.
6604            // zshrs's character-level Meta pair (U+0083 + char `B ^ 32`, the
6605            // UTF-8 run `c2 83 xx xx`) is ONE logical byte, as spacesplit
6606            // decodes it.
6607            let c = if bytes[i] == 0xc2 && i + 3 < bytes.len() && bytes[i + 1] == Meta {
6608                let n = src[i + 2..].chars().next().map_or(0, |n| n as u32 as u8);
6609                i += 1 + src[i + 2..].chars().next().map_or(1, |n| n.len_utf8());
6610                n ^ 32
6611            } else if bytes[i] == Meta && i + 1 < bytes.len() {
6612                i += 1;
6613                bytes[i] ^ 32
6614            } else {
6615                bytes[i]
6616            };
6617            // c:4218-4223 — MULTIBYTE non-ASCII skip. Bytes >= 0x80
6618            // (after demetafy) are not classified by typtab — they
6619            // reach wcsitype via WC_ZISTYPE instead.
6620            if multibyte && c >= 0x80 {
6621                i += 1;
6622                continue;
6623            }
6624            let cu = c as usize;
6625            // c:4224-4229 — `if (inblank(c))` — for default-IFS
6626            // chars space/tab/newline, mark IWSEP unless the next
6627            // byte repeats the same char.
6628            let is_inblank = (t[cu] & (INBLANK as u32)) != 0;
6629            if is_inblank {
6630                if i + 1 < bytes.len() && bytes[i + 1] == c {
6631                    i += 1; // c:4226 — skip the dup
6632                } else {
6633                    t[cu] |= IWSEP as u32; // c:4228
6634                }
6635            }
6636            // c:4230 — `typtab[c] |= ISEP;`
6637            t[cu] |= ISEP as u32;
6638            i += 1;
6639        }
6640    }
6641
6642    // c:4232-4252 — wordchars walk. ORs IWORD onto every byte in
6643    // `$WORDCHARS` (or DEFAULT_WORDCHARS when unset). Used by every
6644    // word-class lookup in pattern matching, `${var:#word}`, etc.
6645    // Drops to ASCII-only under MULTIBYTE_SUPPORT (the non-ASCII path
6646    // routes through wordchars_wide).
6647    {
6648        // Same fallback as IFS above: paramtab first, then wordchars_lock
6649        // so wordcharssetfn updates that bypass paramtab still reach
6650        // the typtab rebuild.
6651        // c:4237 — `for (s = wordchars ? wordchars : DEFAULT_WORDCHARS;
6652        // *s; s++)`. C tests the POINTER, so `WORDCHARS=''` is a
6653        // legitimately EMPTY word-character set and only an UNSET
6654        // `wordchars` falls back to the default. Treating "" as "unset"
6655        // pinned `/` as a word character forever, so
6656        // `WORDCHARS="" [[ / = [[:WORD:]] ]]` wrongly matched.
6657        let wc: Option<String> = crate::ported::params::paramtab()
6658            .read()
6659            .ok()
6660            .and_then(|t| {
6661                t.get("WORDCHARS")
6662                    .map(|pm| crate::ported::params::wordcharsgetfn(pm))
6663            })
6664            .or_else(|| {
6665                crate::ported::params::wordchars_lock()
6666                    .lock()
6667                    .ok()
6668                    .map(|g| g.clone())
6669            });
6670        let src: String = wc.unwrap_or_else(|| DEFAULT_WORDCHARS.to_string());
6671        let bytes = src.as_bytes();
6672        let mut i = 0;
6673        while i < bytes.len() {
6674            // c:4238 — Meta+X demetafy.
6675            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
6676                i += 1;
6677                bytes[i] ^ 32
6678            } else {
6679                bytes[i]
6680            };
6681            // c:4239-4249 — MULTIBYTE non-ASCII skip.
6682            if c < 0x80 {
6683                t[c as usize] |= IWORD as u32; // c:4251
6684            }
6685            i += 1;
6686        }
6687    }
6688
6689    // c:4253-4254 — SPECCHARS walk. ORs ISPECIAL onto every member
6690    // of the hardcoded SPECCHARS string. Drives glob-special and
6691    // quote-special detection.
6692    {
6693        for &b in SPECCHARS.as_bytes() {
6694            t[b as usize] |= ISPECIAL as u32; // c:4254
6695        }
6696    }
6697
6698    // c:4255-4256 — comma special only when ZTF_SP_COMMA was set
6699    // via `makecommaspecial(1)`. KSH_GLOB / extended-glob path.
6700    {
6701        let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
6702        if (flags & ZTF_SP_COMMA) != 0 {
6703            // c:4255
6704            t[b',' as usize] |= ISPECIAL as u32; // c:4256
6705        }
6706    }
6707
6708    // c:4257-4261 — bangchar special when BANGHIST + interact +
6709    // bangchar != 0. Sets ZTF_BANGCHAR flag bit then marks the
6710    // bangchar byte ISPECIAL.
6711    {
6712        let bangchar2 = bangchar.load(Ordering::SeqCst) as usize;
6713        let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
6714        let interact_flag = (flags & ZTF_INTERACT) != 0;
6715        let banghist = isset(BANGHIST);
6716        if banghist && bangchar2 != 0 && bangchar2 < 256 && interact_flag {
6717            // c:4257
6718            TYPTAB_FLAGS.fetch_or(ZTF_BANGCHAR, Ordering::Relaxed); // c:4258
6719            t[bangchar2] |= ISPECIAL as u32; // c:4259
6720        } else {
6721            TYPTAB_FLAGS.fetch_and(!ZTF_BANGCHAR, Ordering::Relaxed); // c:4261
6722        }
6723    }
6724
6725    // c:4262-4263 — PATCHARS walk. ORs IPATTERN onto every member.
6726    // Used by pattern compilation to detect glob metachars.
6727    {
6728        for &b in PATCHARS.as_bytes() {
6729            t[b as usize] |= IPATTERN as u32; // c:4263
6730        }
6731    }
6732
6733    // Flush the scratch to the lock-free table (see header note).
6734    for (i, v) in t.iter().enumerate() {
6735        TYPTAB[i].store(*v, Ordering::Relaxed);
6736    }
6737}
6738
6739/// Port of `makecommaspecial()` from `Src/utils.c:4270` — C decl `void makecommaspecial(int yesno)`.
6740///
6741/// Toggles `ZTF_SP_COMMA` and the `ISPECIAL` bit on `,` in the
6742/// global typtab — used by glob/extended-glob to flag `,`
6743/// (KSH_GLOB) as a metacharacter.
6744pub fn makecommaspecial(yesno: bool) {
6745    // c:4270
6746    if yesno {
6747        // c:4272
6748        TYPTAB_FLAGS.fetch_or(ZTF_SP_COMMA, Ordering::Relaxed); // c:4273
6749        TYPTAB[b',' as usize].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4274
6750    } else {
6751        TYPTAB_FLAGS.fetch_and(!ZTF_SP_COMMA, Ordering::Relaxed); // c:4276
6752        TYPTAB[b',' as usize].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4277
6753    }
6754}
6755
6756/// Port of `makebangspecial()` from `Src/utils.c:4283` — C decl `void makebangspecial(int yesno)`.
6757///
6758/// Toggles `ISPECIAL` on the current `bangchar`. When `yesno==0`
6759/// always clears; when nonzero, sets only if `ZTF_BANGCHAR` was
6760/// stored by `inittyptab` (i.e. BANGHIST is on).
6761pub fn makebangspecial(yesno: bool) {
6762    // c:4283
6763    let bc = bangchar.load(Ordering::SeqCst) as usize;
6764    if bc == 0 || bc >= 256 {
6765        return;
6766    }
6767    let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
6768    if !yesno {
6769        // c:4289
6770        TYPTAB[bc].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4290
6771    } else if (flags & ZTF_BANGCHAR) != 0 {
6772        // c:4291
6773        TYPTAB[bc].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4292
6774    }
6775}
6776
6777/// Port of `wcsiblank()` from `Src/utils.c:4302` — C decl `wcsiblank(wint_t wc)`.
6778///
6779/// ```c
6780/// mod_export int wcsiblank(wint_t wc) {
6781///     if (iswspace(wc) && wc != L'\n')
6782///         return 1;
6783///     return 0;
6784/// }
6785/// ```
6786///
6787/// "wide-character version of the iblank() macro" — true for any
6788/// whitespace EXCEPT newline. The previous Rust port included
6789/// newline (since `c.is_whitespace()` returns true for '\n') —
6790/// wrong for callers that use this to find token boundaries.
6791pub fn wcsiblank(wc: char) -> bool {
6792    wc.is_whitespace() && wc != '\n'
6793}
6794
6795/// Port of `wcsitype()` from `Src/utils.c:4321` — C decl `int wcsitype(wchar_t c, int itype)`.
6796///
6797/// "zistype macro extended to support wide characters. Works for
6798/// IIDENT, IWORD, IALNUM, ISEP."
6799///
6800/// The Rust port checks whether `c` falls in the typtab class
6801/// represented by `itype`. ASCII chars consult the global TYPTAB
6802/// (the same one C's `zistype()` macro indexes); non-ASCII chars
6803/// route through Unicode predicates that mirror the C `iswalnum`
6804/// fallback at line 4346.
6805pub fn wcsitype(c: char, itype: u32) -> bool {
6806    // c:4321
6807    if !isset(MULTIBYTE) {
6808        // c:4327
6809        if (c as u32) < 256 {
6810            return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
6811        }
6812        return false;
6813    }
6814    if (c as u32) < 128 {
6815        // c:4343
6816        return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
6817    }
6818    let cls = itype as u16;
6819    if cls == IIDENT {
6820        // c:4347
6821        if isset(POSIXIDENTIFIERS) {
6822            return false; // c:4348
6823        }
6824        return c.is_alphanumeric(); // c:4350
6825    }
6826    if cls == IWORD {
6827        // c:4352
6828        if c.is_alphanumeric() {
6829            return true;
6830        } // c:4353
6831          // C: IS_COMBINING(c) — no Rust crate-free combining-mark
6832          // predicate. zero-width chars (combining, zero-width-joiner,
6833          // etc.) are treated as word per c:4362.
6834        if unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0 {
6835            // c:4362
6836            return true;
6837        }
6838        // c:4364 — `wmemchr(wordchars_wide.chars, c, …)`. Reads from
6839        // the canonical `wordchars` global (writable by
6840        // `wordcharssetfn` at `Src/params.c:5143`). Previously routed
6841        // through `std::env::var("WORDCHARS")` which is the libc
6842        // process environment — never reflects runtime `WORDCHARS=:`
6843        // assignments inside the shell.
6844        let w = crate::ported::params::paramtab()
6845            .read()
6846            .ok()
6847            .and_then(|t| {
6848                t.get("WORDCHARS")
6849                    .map(|pm| crate::ported::params::wordcharsgetfn(pm))
6850            })
6851            .unwrap_or_default();
6852        return w.chars().any(|x| x == c);
6853    }
6854    if cls == ISEP {
6855        // c:4366
6856        // c:4367 — `return !!wmemchr(ifs_wide.chars, c, ifs_wide.len);`
6857        return IFS_WIDE.read().map(|w| w.contains(&c)).unwrap_or(false);
6858    }
6859    let _ = IALNUM;
6860    c.is_alphanumeric() // c:4370
6861}
6862
6863/// Port of `itype_end()` from `Src/utils.c:4395` — C decl `char *itype_end(const char *ptr, int itype, int once)`.
6864///
6865/// Check if a character type at end of string (from utils.c itype_end)
6866///
6867/// Walks `ptr` byte-by-byte advancing past every char whose
6868/// type-bits include `itype`. Returns the byte offset where the
6869/// walk stops (C returns the advanced pointer; the byte offset is
6870/// the Rust equivalent — `ptr + return_value` reproduces the C
6871/// pointer).
6872///
6873/// C body branches (ported line-for-line):
6874///
6875/// 1. **INAMESPC special-case** (c:4399-4413). If itype == INAMESPC
6876///    and we're not POSIXIDENTIFIERS-strict outside ksh emulation,
6877///    recurse on `ptr+(*ptr=='.')` with IIDENT to allow dotted
6878///    ksh93 namespace names. The recursion result determines
6879///    whether to advance past the dot or fall through.
6880///
6881/// 2. **MULTIBYTE branch** (c:4416-4470, gated `isset(MULTIBYTE)
6882///    && (itype != IIDENT || !isset(POSIXIDENTIFIERS))`). Walks
6883///    multibyte chars via mb_metacharlenconv (multibyte len +
6884///    wchar conversion). Per-codepoint test:
6885///      - itok byte (raw command line): test the byte via zistype
6886///      - Meta-prefixed pair: extract real byte via XOR, test via
6887///        zistype
6888///      - ASCII (< 0x80): test via zistype
6889///      - Non-ASCII codepoint: route through wcsitype (which
6890///        already handles IWORD/ISEP via WORDCHARS/IFS paramtab
6891///        lookup matching c:4364/c:4367)
6892///
6893/// 3. **Non-multibyte branch** (c:4474-4480). Simple Meta-byte +
6894///    zistype byte loop.
6895///
6896/// `once = true` stops after the first matching char (used for
6897/// "is this a valid first char" checks per c:1576 etc).
6898pub fn itype_end(s: &str, mut itype: u32, once: bool) -> usize {
6899    use crate::ported::zsh_h::{isset, Meta, EMULATE_KSH, EMULATION, MULTIBYTE, POSIXIDENTIFIERS};
6900    use crate::ported::ztype_h::{itok, zistype, IIDENT, INAMESPC};
6901
6902    // c:4399-4413 — INAMESPC handling with ksh93 namespace dot.
6903    let mut start = 0usize;
6904    if itype == INAMESPC {
6905        itype = IIDENT as u32;
6906        if !isset(POSIXIDENTIFIERS) || EMULATION(EMULATE_KSH) {
6907            // c:4403 — `t = itype_end(ptr + (*ptr == '.'), itype, 0);`
6908            let first_is_dot = s.as_bytes().first().copied() == Some(b'.');
6909            let recurse_start = if first_is_dot { 1 } else { 0 };
6910            let t = recurse_start + itype_end(&s[recurse_start..], itype, false);
6911            // c:4404-4410 — `if (t > ptr + (*ptr == '.')) {
6912            //                  if (*t == '.') ptr = t + 1;  /* Fall through */
6913            //                  else if (!once) return t;
6914            //              }`
6915            if t > recurse_start {
6916                let t_byte = s.as_bytes().get(t).copied();
6917                if t_byte == Some(b'.') {
6918                    start = t + 1; // c:4407
6919                } else if !once {
6920                    return t; // c:4409
6921                }
6922            }
6923        }
6924    }
6925
6926    let bytes = s.as_bytes();
6927    let mb = isset(MULTIBYTE) && (itype != IIDENT as u32 || !isset(POSIXIDENTIFIERS));
6928
6929    if mb {
6930        // c:4416-4470 — multibyte walk. mb_charinit() in C is a
6931        // no-op stateless reset; not needed in Rust since str::chars
6932        // is already stateless UTF-8.
6933        let mut i = start;
6934        while i < bytes.len() {
6935            let b = bytes[i];
6936            // c:4422-4425 — itok: raw token byte, test via zistype,
6937            // advance 1.
6938            if itok(b) {
6939                if !zistype(b, itype) {
6940                    break;
6941                }
6942                i += 1;
6943            } else if b == Meta {
6944                // c:4438-4440 (WEOF arm) + non-MB Meta fallback:
6945                // Meta-prefixed pair = single unescaped char via XOR.
6946                let nxt = bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20;
6947                if (nxt as u32) > 127 || !zistype(nxt, itype) {
6948                    break;
6949                }
6950                i += 2;
6951            } else if b < 0x80 {
6952                // c:4441-4443 (len == 1 && isascii) — ASCII: direct
6953                // zistype on the byte.
6954                if !zistype(b, itype) {
6955                    break;
6956                }
6957                i += 1;
6958            } else {
6959                // c:4446-4467 — non-ASCII codepoint. Decode the UTF-8
6960                // sequence starting at `i` and route through wcsitype
6961                // (which already handles IWORD via WORDCHARS paramtab
6962                // c:4364, ISEP via IFS paramtab c:4367, default via
6963                // iswalnum/is_alphanumeric c:4370).
6964                let tail = match std::str::from_utf8(&bytes[i..]) {
6965                    Ok(t) => t,
6966                    Err(_) => break, // invalid UTF-8 — stop walk
6967                };
6968                let ch = match tail.chars().next() {
6969                    Some(c) => c,
6970                    None => break,
6971                };
6972                if !wcsitype(ch, itype) {
6973                    break;
6974                }
6975                i += ch.len_utf8();
6976            }
6977            if once {
6978                break;
6979            }
6980        }
6981        i
6982    } else {
6983        // c:4474-4480 — non-multibyte arm. Simple byte loop with
6984        // Meta-prefix collapse.
6985        let mut i = start;
6986        while i < bytes.len() {
6987            let b = bytes[i];
6988            let chr = if b == Meta {
6989                bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20
6990            } else {
6991                b
6992            };
6993            if !zistype(chr, itype) {
6994                break;
6995            }
6996            i += if b == Meta { 2 } else { 1 };
6997            if once {
6998                break;
6999            }
7000        }
7001        i
7002    }
7003}
7004
7005/// Port of `arrdup()` from `Src/utils.c:4493` — C decl `arrdup(char **s)`.
7006///
7007/// Duplicate array (from utils.c arrdup)
7008pub fn arrdup(s: &[String]) -> Vec<String> {
7009    s.to_vec()
7010}
7011
7012/// Port of `arrdup_max()` from `Src/utils.c:4508` — C decl `arrdup_max(char **s, unsigned max)`.
7013///
7014/// Duplicate array with max elements (from utils.c arrdup_max)
7015pub fn arrdup_max(s: &[String], max: usize) -> Vec<String> {
7016    s.iter().take(max).cloned().collect()
7017}
7018
7019/// Port of `zarrdup()` from `Src/utils.c:4532` — C decl `zarrdup(char **s)`.
7020///
7021/// Duplicate array with zsh allocation (from utils.c zarrdup)
7022pub fn zarrdup(s: &[String]) -> Vec<String> {
7023    s.to_vec()
7024}
7025
7026/// Port of `wcs_zarrdup()` from `Src/utils.c:4547` — C decl `wcs_zarrdup(wchar_t **s)`.
7027///
7028/// Duplicate array of wide strings (from utils.c wcs_zarrdup) - same as zarrdup in Rust
7029pub fn wcs_zarrdup(s: &[String]) -> Vec<String> {
7030    s.to_vec()
7031}
7032
7033/// Port of `spname()` from `Src/utils.c:4562` — C decl `spname(char *oldname)`.
7034///
7035/// Spelling correction: find closest match (from utils.c spname)
7036/// WARNING: param names don't match C — Rust=(name, dir) vs C=(oldname)
7037pub fn spname(name: &str, dir: &str) -> Option<String> {
7038    let entries = match fs::read_dir(dir) {
7039        Ok(e) => e,
7040        Err(_) => return None,
7041    };
7042
7043    let mut best = None;
7044    let mut best_dist = 4; // threshold
7045
7046    for entry in entries.flatten() {
7047        if let Some(entry_name) = entry.file_name().to_str() {
7048            let dist = spdist(name, entry_name, best_dist);
7049            if dist < best_dist {
7050                best_dist = dist;
7051                best = Some(entry_name.to_string());
7052            }
7053        }
7054    }
7055    best
7056}
7057
7058/// Port of `mindist()` from `Src/utils.c:4624` — C decl `mindist(char *dir, char *mindistguess, char *mindistbest, int wantdir)`.
7059///
7060/// Spelling correction with full path (from utils.c mindist)
7061/// WARNING: param names don't match C — Rust=(dir, name) vs C=(dir, mindistguess, mindistbest, wantdir)
7062pub fn mindist(dir: &str, name: &str) -> Option<(String, usize)> {
7063    let entries = match fs::read_dir(dir) {
7064        Ok(e) => e,
7065        Err(_) => return None,
7066    };
7067
7068    let mut best = None;
7069    let mut best_dist = 4;
7070
7071    for entry in entries.flatten() {
7072        if let Some(entry_name) = entry.file_name().to_str() {
7073            let dist = spdist(name, entry_name, best_dist);
7074            if dist < best_dist {
7075                best_dist = dist;
7076                best = Some(entry_name.to_string());
7077            }
7078        }
7079    }
7080    best.map(|name| (name, best_dist))
7081}
7082
7083// spellcheck a word                                                        // c:3123
7084// fix s ; if hist is nonzero, fix the history list too                     // c:3124
7085/// Port of `spdist()` from `Src/utils.c:4675`.
7086///
7087/// Compute edit distance between two strings (for spelling correction)
7088/// Direct port of `int spdist(char *s, char *t, int thresh)` from
7089/// `Src/utils.c:4675-4750`. Drives the CORRECT-option typo prompt:
7090/// returns 0 for identical, 1 for case-only mistakes, 2 for one
7091/// transposition / missing letter / QWERTY-adjacent mistype, 200 for
7092/// anything farther.
7093///
7094/// **This is NOT a Levenshtein DP — that was the previous Rust impl
7095/// dressed as a port.** The C source uses a keyboard-adjacency model
7096/// (qwertykeymap / dvorakkeymap arrays + tulower equality) which is
7097/// the actual behaviour `setopt CORRECT` depends on. Restored
7098/// faithfully.
7099pub fn spdist(s: &str, t: &str, thresh: usize) -> usize {
7100    // c:4679-4690 — qwerty keymap (rows of 14 chars each: numeric row,
7101    // QWERTY top row, home row, bottom row, then shift versions).
7102    // Embedded `\n` / `\t` mark off-grid cells.
7103    const QWERTYKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
7104\t1234567890-=\t\
7105\tqwertyuiop[]\t\
7106\tasdfghjkl;'\n\t\
7107\tzxcvbnm,./\t\t\t\
7108\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
7109\t!@#$%^&*()_+\t\
7110\tQWERTYUIOP{}\t\
7111\tASDFGHJKL:\"\n\t\
7112\tZXCVBNM<>?\n\n\t\
7113\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
7114    // c:4691-4702 — dvorak keymap, same shape.
7115    const DVORAKKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
7116\t1234567890[]\t\
7117\t',.pyfgcrl/=\t\
7118\taoeuidhtns-\n\t\
7119\t;qjkxbmwvz\t\t\t\
7120\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
7121\t!@#$%^&*(){}\t\
7122\t\"<>PYFGCRL?+\t\
7123\tAOEUIDHTNS_\n\t\
7124\t:QJKXBMWVZ\n\n\t\
7125\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
7126
7127    // c:4703-4707 — `keymap = isset(DVORAK) ? dvorakkeymap : qwertykeymap;`
7128    let keymap = if isset(DVORAK) {
7129        DVORAKKEYMAP.as_bytes()
7130    } else {
7131        QWERTYKEYMAP.as_bytes()
7132    };
7133
7134    let s_b = s.as_bytes();
7135    let t_b = t.as_bytes();
7136
7137    // c:4709-4710 — `if (!strcmp(s, t)) return 0;`
7138    if s == t {
7139        return 0;
7140    }
7141
7142    // c:4712-4714 — `for (p, q; *p && tulower(*p) == tulower(*q); p++, q++);
7143    //                  if (!*p && !*q) return 1;` — case-only mismatch.
7144    let mut p = 0usize;
7145    let mut q = 0usize;
7146    while p < s_b.len() && q < t_b.len() && tulower(s_b[p] as char) == tulower(t_b[q] as char) {
7147        p += 1;
7148        q += 1;
7149    }
7150    if p == s_b.len() && q == t_b.len() {
7151        return 1;
7152    }
7153
7154    // c:4715-4716 — `if (!thresh) return 200;`
7155    if thresh == 0 {
7156        return 200;
7157    }
7158
7159    // c:4717-4727 — first walk: detect transposition / missing letter at
7160    // first divergence.
7161    p = 0;
7162    q = 0;
7163    while p < s_b.len() && q < t_b.len() {
7164        if s_b[p] == t_b[q] {
7165            // c:4718-4719 — match: skip (don't count `aa` as transposed).
7166            p += 1;
7167            q += 1;
7168            continue;
7169        }
7170        // c:4720-4721 — `if (p[1] == q[0] && q[1] == p[0]) return
7171        //                  spdist(p+2, q+2, thresh-1) + 1;` transposition.
7172        if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p + 1] == t_b[q] && t_b[q + 1] == s_b[p] {
7173            // SAFETY: bytes are ASCII for the keymap test below; from_utf8
7174            // is fine on a non-ASCII tail here because spdist takes &str.
7175            let s_tail = std::str::from_utf8(&s_b[p + 2..]).unwrap_or("");
7176            let t_tail = std::str::from_utf8(&t_b[q + 2..]).unwrap_or("");
7177            return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
7178        }
7179        // c:4722-4723 — `if (p[1] == q[0]) return spdist(p+1, q, thresh-1) + 2;`
7180        if p + 1 < s_b.len() && s_b[p + 1] == t_b[q] {
7181            let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
7182            let t_tail = std::str::from_utf8(&t_b[q..]).unwrap_or("");
7183            return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
7184        }
7185        // c:4724-4725 — `if (p[0] == q[1]) return spdist(p, q+1, thresh-1) + 2;`
7186        if q + 1 < t_b.len() && s_b[p] == t_b[q + 1] {
7187            let s_tail = std::str::from_utf8(&s_b[p..]).unwrap_or("");
7188            let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
7189            return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
7190        }
7191        // c:4726-4727 — `if (*p != *q) break;`
7192        break;
7193    }
7194    // c:4728-4729 — `if ((!*p && strlen(q) == 1) || (!*q && strlen(p) == 1))
7195    //                  return 2;` — single trailing-char insertion.
7196    if (p == s_b.len() && (t_b.len() - q) == 1) || (q == t_b.len() && (s_b.len() - p) == 1) {
7197        return 2;
7198    }
7199    // c:4730-4748 — second walk: keyboard-adjacency mistype detection.
7200    p = 0;
7201    q = 0;
7202    while p < s_b.len() && q < t_b.len() {
7203        if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p] != t_b[q] && s_b[p + 1] == t_b[q + 1] {
7204            // c:4737-4738 — `if (!(z = strchr(keymap, p[0])) || *z == '\n' ||
7205            //                       *z == '\t') return spdist(p+1, q+1,
7206            //                       thresh-1) + 1;`
7207            let pos = keymap.iter().position(|&b| b == s_b[p]);
7208            let z_ok = match pos {
7209                Some(i) => keymap[i] != b'\n' && keymap[i] != b'\t',
7210                None => false,
7211            };
7212            if !z_ok {
7213                let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
7214                let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
7215                return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
7216            }
7217            // c:4739 — `t0 = z - keymap;`
7218            let t0 = pos.unwrap() as isize;
7219            // c:4740-4744 — eight adjacency offsets (-15,-14,-13,-1,+1,
7220            //                +13,+14,+15) → keyboard neighbours.
7221            let offsets: [isize; 8] = [-15, -14, -13, -1, 1, 13, 14, 15];
7222            let adjacent = offsets.iter().any(|&off| {
7223                let idx = t0 + off;
7224                if idx >= 0 && (idx as usize) < keymap.len() {
7225                    keymap[idx as usize] == t_b[q]
7226                } else {
7227                    false
7228                }
7229            });
7230            if adjacent {
7231                // c:4745 — `return spdist(p+1, q+1, thresh-1) + 2;`
7232                let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
7233                let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
7234                return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
7235            }
7236            // c:4746 — `return 200;`
7237            return 200;
7238        } else if p < s_b.len() && q < t_b.len() && s_b[p] != t_b[q] {
7239            // c:4747-4748 — `else if (*p != *q) break;`
7240            break;
7241        }
7242        p += 1;
7243        q += 1;
7244    }
7245    // c:4749 — `return 200;`
7246    200
7247}
7248
7249/// Set terminal to cbreak mode (from utils.c setcbreak)
7250#[cfg(unix)]
7251/// Port of `setcbreak` from `Src/utils.c:4756`.
7252pub fn setcbreak() -> bool {
7253    // c:4760 — `ti = shttyinfo;`
7254    let saved = SHTTYINFO.lock().ok().and_then(|g| *g);
7255    if let Some(mut ti) = saved {
7256        ti.c_lflag &= !libc::ICANON; // c:4762
7257        ti.c_cc[libc::VMIN] = 1; // c:4763
7258        ti.c_cc[libc::VTIME] = 0; // c:4764
7259        settyinfo(&ti) // c:4768
7260    } else {
7261        false
7262    }
7263}
7264
7265#[cfg(not(unix))]
7266/// Port of `setcbreak` from `Src/utils.c:4756`.
7267pub fn setcbreak() -> bool {
7268    false
7269}
7270
7271/// Port of `attachtty()` from `Src/utils.c:4775` — C decl `void attachtty(pid_t pgrp)`.
7272/// Hands the controlling terminal to `pgrp`. Gated by `jobbing &&
7273/// interact`; falls back to `mypgrp` and disables MONITOR on permanent
7274/// failure (matching the C source's recursion + `opts[MONITOR]=0` path).
7275#[cfg(unix)]
7276pub fn attachtty(pgrp: i32) {
7277    // c:4775
7278
7279    if !(jobbing() && interact()) {
7280        return; // c:4779
7281    }
7282    let shtty = SHTTY.load(Ordering::Relaxed); // c:4781
7283    if shtty == -1 {
7284        return;
7285    }
7286    let ep = ATTACHTTY_EP.load(Ordering::Relaxed);
7287    let rc = unsafe { libc::tcsetpgrp(shtty, pgrp) }; // c:4781
7288    if rc == -1 && ep == 0 {
7289        // c:4781
7290        let mypgrp_val = *crate::ported::jobs::MYPGRP // c:4792
7291            .get_or_init(|| Mutex::new(0))
7292            .lock()
7293            .unwrap();
7294        if pgrp != mypgrp_val && unsafe { libc::kill(-pgrp, 0) } == -1 {
7295            attachtty(mypgrp_val); // c:4793
7296        } else {
7297            let errno_val = io::Error::last_os_error().raw_os_error().unwrap_or(0);
7298            if errno_val != libc::ENOTTY {
7299                // c:4795
7300                zwarn(&format!(
7301                    "can't set tty pgrp: {}", // c:4797
7302                    io::Error::from_raw_os_error(errno_val)
7303                ));
7304                let _ = io::stderr().flush(); // c:4798
7305            }
7306            opt_state_set("monitor", false); // c:4815 opts[MONITOR]=0
7307            ATTACHTTY_EP.store(1, Ordering::Relaxed); // c:4815
7308        }
7309    } else if rc != -1 {
7310        // c:4815
7311        *crate::ported::jobs::LAST_ATTACHED_PGRP // c:4815
7312            .get_or_init(|| Mutex::new(0))
7313            .lock()
7314            .unwrap() = pgrp;
7315    }
7316}
7317
7318/// Port of `gettygrp()` from `Src/utils.c:4815` — C decl `pid_t gettygrp(void)`.
7319#[cfg(unix)]
7320pub fn gettygrp() -> i32 {
7321    // c:4815
7322    let shtty = SHTTY.load(Ordering::Relaxed);
7323    if shtty == -1 {
7324        // c:4819
7325        return -1; // c:4820
7326    }
7327    unsafe { libc::tcgetpgrp(shtty) } // c:4823
7328}
7329
7330/// Port of `metafy()` from `Src/utils.c:4856`.
7331///
7332/// Convert raw bytes (possibly containing NUL / 0x83-0x9b) to
7333/// zsh's metafied form: each `imeta(b)` byte becomes `Meta` (0x83)
7334/// followed by `b ^ 32`.
7335///
7336/// NOTE ON THE RETURN TYPE: C hands back a `char *`, and a metafied byte
7337/// string is routinely NOT valid UTF-8 — metafication escapes every byte
7338/// in `{0x00} ∪ [0x83, 0xa2]` (c:4195-4201), a range that sits inside the
7339/// UTF-8 lead/continuation ranges, so `日` (`e6 97 a5`) metafies to
7340/// `e6 83 b7 a5`. This `String`-returning form therefore falls back to
7341/// `from_utf8_lossy` on exactly those inputs. A caller that needs the
7342/// bytes C would see (`matchcmp` at `Src/Zle/compcore.c:3194`, which
7343/// collates the metafied form) must run the c:4880 loop over `u8`
7344/// directly rather than through here.
7345///
7346/// Port of `metafy(char *buf, int len, int heap)` from Src/utils.c:4856. The C source takes a
7347/// `heap` mode controlling whether the result is `zalloc`'d /
7348/// `zhalloc`'d / written into a static buffer / appended to the
7349/// existing buffer; in Rust we always return an owned `String`
7350/// since allocation strategy is uniform. The byte-level transform
7351/// is identical: walk the input, count metafy hits, allocate
7352/// `len + meta` bytes, expand each `Meta+X` pair in reverse.
7353/// Rust idiom replacement: forward byte-walk + Vec::push covers the
7354/// C two-pass (count + alloc + reverse-expand) approach; Vec grows
7355/// on demand so the pre-count is unnecessary in Rust.
7356/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len, heap)
7357pub fn metafy(buf: &str) -> String {
7358    // c:4856
7359    let bytes = buf.as_bytes();
7360    let mut out = Vec::with_capacity(bytes.len());
7361    for &b in bytes {
7362        // C: `#define imeta(c) ((c) >= Meta)` from Src/zsh.h —
7363        // every byte >= 0x83 needs escaping. The previous
7364        // narrow-range check `(0x83..=0x9b)` was a bug: bytes
7365        // 0x9c..=0xff (e.g. UTF-8 continuation bytes, high-Latin
7366        // characters) escaped C's imeta() but not the Rust
7367        // version, which then fed un-escaped bytes downstream
7368        // and corrupted Meta-aware loops.
7369        if imeta_byte(b) {
7370            out.push(Meta);
7371            out.push(b ^ 32);
7372        } else {
7373            out.push(b);
7374        }
7375    }
7376    // metafied bytes are in [0..=0x7f]∪{0x83}∪[expanded ^ 32 range];
7377    // String::from_utf8 may fail on the high bytes — fall back to
7378    // lossy. The lossy fallback inserts U+FFFD for invalid
7379    // sequences, which is fine for String-level callers (lexer /
7380    // pattern compile / display) but breaks byte-round-trip with
7381    // `unmetafy`.
7382    //
7383    // Callers that need the bytes back unchanged go through
7384    // `mb_metacharlenconv`, which hands back each unit in a recoverable form
7385    // instead of falling back to lossy.
7386    String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
7387}
7388
7389/// Port of `ztrdup_metafy()` from `Src/utils.c:4929` — C decl `ztrdup_metafy(const char *s)`.
7390///
7391/// ```c
7392/// mod_export char *
7393/// ztrdup_metafy(const char *s)
7394/// {
7395///     if (!s) return NULL;
7396///     return metafy((char *)s, -1, META_DUP);
7397/// }
7398/// ```
7399pub fn ztrdup_metafy(s: &str) -> String {
7400    metafy(s)
7401}
7402
7403/// Port of `unmetafy()` from `Src/utils.c:4954` — C decl `unmetafy(char *s, int *len)`.
7404///
7405/// Take a metafied byte buffer in `s` and convert it in place to
7406/// its literal representation. C signature:
7407///
7408/// ```c
7409/// char *unmetafy(char *s, int *len);
7410/// ```
7411///
7412/// The Rust port mutates `s` in place and returns the resulting
7413/// length (mirroring C's `*len` out-parameter). C control flow:
7414///
7415/// ```c
7416/// for (p = s; *p && *p != Meta; p++);     // skip prefix with no Meta
7417/// for (t = p; (*t = *p++);)                 // walk the rest
7418///     if (*t++ == Meta && *p)
7419///         t[-1] = *p++ ^ 32;                // un-escape: XOR with 32
7420/// ```
7421///
7422/// Same algorithm here, byte-indexed against the Vec rather than
7423/// pointer-walked.
7424/// WARNING: param names don't match C — Rust=(s) vs C=(s, len)
7425pub fn unmetafy(s: &mut Vec<u8>) -> usize {
7426    // c:4954
7427    // c:4958 `for (p = s; *p && *p != Meta; p++);` — when the string holds no
7428    // Meta byte at all, C's second loop copies each byte onto itself and the
7429    // call is just that scan. Ask the question with `<[u8]>::contains`, which
7430    // resolves to libcore's precompiled `memchr`, and skip both loops: the
7431    // index-by-index walk below is monomorphised into this crate and compiled
7432    // UNOPTIMISED in the debug build, where it measured ~24% of a `pr <TAB>`
7433    // (one `unmetafy` per history entry while paging the history in).
7434    // Returning `s.len()` is exactly what the two loops below produce on this
7435    // input — `p` runs to the end, the copy loop never executes, and
7436    // `truncate(t)` is a no-op.
7437    if !s.contains(&Meta) {
7438        return s.len();
7439    }
7440    // First loop: find the first `Meta` byte. Everything before it
7441    // stays as-is, so we don't need to copy.
7442    let mut p: usize = 0;
7443    while p < s.len() && s[p] != Meta {
7444        p += 1;
7445    }
7446    // Second loop: walk from `p` onward, copying each byte into the
7447    // `t` slot (which trails `p` by one position per Meta-escape
7448    // we collapse).
7449    let mut t: usize = p;
7450    while p < s.len() {
7451        let b = s[p];
7452        s[t] = b;
7453        p += 1;
7454        if b == Meta && p < s.len() {
7455            // C: t[-1] = *p++ ^ 32; — overwrite the just-written
7456            // Meta with the un-escaped byte.
7457            s[t] = s[p] ^ 32;
7458            p += 1;
7459        }
7460        t += 1;
7461    }
7462    s.truncate(t);
7463    t
7464}
7465
7466/// Port of `metalen()` from `Src/utils.c:4972` — C decl `int metalen(const char *s, int len)`.
7467/// ```c
7468/// int mlen = len;
7469/// while (len--) {
7470///     if (*s++ == Meta) { mlen++; s++; }
7471/// }
7472/// return mlen;
7473/// ```
7474/// Doc: "Return the character length of a metafied substring, given
7475/// the unmetafied substring length." So **input `len` is the
7476/// UNMETAFIED char count, output is the METAFIED byte count.**
7477///
7478/// Previously the Rust port had INVERTED semantics: looped while
7479/// `i < len` byte-walk, returned char count. That's the reverse
7480/// operation. Pin the correct C contract.
7481pub fn metalen(s: &str, len: usize) -> usize {
7482    // c:4972
7483    let bytes = s.as_bytes();
7484    let mut mlen = len; // c:4974
7485    let mut remaining = len;
7486    let mut i = 0;
7487    while remaining > 0 && i < bytes.len() {
7488        // c:4976 `while (len--)`
7489        if bytes[i] == Meta {
7490            // c:4977
7491            mlen += 1; // c:4978
7492            i += 2; // c:4979 s++ (already advanced past Meta)
7493        } else {
7494            i += 1;
7495        }
7496        remaining -= 1;
7497    }
7498    mlen
7499}
7500
7501/// Port of `unmeta(const char *file_name)` from `Src/utils.c:4994`.
7502///
7503/// Convert a zsh internal (metafied) string to a system-call-safe
7504/// form (e.g. for passing to `open(2)`).
7505///
7506/// C body shape (c:4994-5010):
7507/// ```c
7508/// meta = 0;
7509/// for (t = file_name; *t; t++)
7510///     if (*t == Meta) { meta = 1; break; }
7511/// if (!meta) return (char *) file_name;        // no-copy fast path
7512/// for (t = file_name, p = fn; *t; p++)
7513///     if ((*p = *t++) == Meta && *t)
7514///         *p = *t++ ^ 32;
7515/// ```
7516// Rust idiom replacement: byte-scan fast path + `unmetafy` covers
7517// the C in-place decode + alloc-on-copy dance; String owns its own
7518// allocation so no `heap` flag needed.
7519/// Port of `unmeta()` from `Src/utils.c:4994`.
7520///
7521/// `unmeta` — see implementation.
7522pub fn unmeta(s: &str) -> String {
7523    // c:4994
7524    // c:4995-4996 — Meta scan; no-copy fast path. A `&str` carries Meta as
7525    // the CHAR U+0083 (the char-level encoding `unmetafy_str` decodes), never
7526    // as a lone 0x83 byte: in valid UTF-8 that byte is only ever a
7527    // continuation byte (`ホ` is E3 83 9B). Scanning bytes decoded those
7528    // continuations as Meta pairs and turned every such character into
7529    // U+FFFD before a write or syscall (`zpty -w z ホ`, zsh/files `rm ホ`).
7530    if !s.contains(Meta as char) {
7531        return s.to_string();
7532    }
7533    // c:4999-5001 — `if ((*p = *t++) == Meta && *t) *p = *t++ ^ 32;`
7534    String::from_utf8_lossy(&unmetafy_str(s)).into_owned()
7535}
7536
7537/// Port of `unmeta_one()` from `Src/utils.c:5058` — C decl `convchar_t unmeta_one(const char *in, int *sz)`.
7538/// Non-MULTIBYTE branch (c:5077-5083):
7539/// ```c
7540/// if (in[0] == Meta) { *sz = 2; wc = (unsigned char)(in[1] ^ 32); }
7541/// else               { *sz = 1; wc = (unsigned char) in[0]; }
7542/// ```
7543/// Returns `(decoded_char, bytes_consumed)`. The c:5070 NULL/empty
7544/// guard returns `(0, 0)`. Previously used hardcoded `0x83` for the
7545/// Meta byte — now routes through the canonical `Meta` constant
7546/// (defined as `'\u{83}'` at zsh.h:144).
7547/// WARNING: param names don't match C — Rust=(s) vs C=(in, sz)
7548pub fn unmeta_one(s: &str) -> (char, usize) {
7549    // c:5058
7550    let bytes = s.as_bytes();
7551    // c:5070 — `if (!in || !*in) return 0;`
7552    if bytes.is_empty() {
7553        return ('\0', 0);
7554    }
7555    // c:5077 — `if (in[0] == Meta)`.
7556    if bytes[0] == Meta && bytes.len() > 1 {
7557        // c:5078-5079 — `*sz = 2; wc = (unsigned char)(in[1] ^ 32);`.
7558        ((bytes[1] ^ 32) as char, 2)
7559    } else {
7560        // c:5081-5082 — `*sz = 1; wc = (unsigned char) in[0];`.
7561        (bytes[0] as char, 1)
7562    }
7563}
7564
7565/// Port of `ztrcmp()` from `Src/utils.c:5106` — C decl `ztrcmp(char const *s1, char const *s2)`.
7566///
7567/// Byte-walking compare with lazy Meta resolution. C body skips
7568/// matching bytes wholesale, then on the first differing byte
7569/// un-meta-fies just that byte and compares. Faster than
7570/// `unmeta(s1).cmp(unmeta(s2))` because:
7571///   1. No allocation up front,
7572///   2. Only the first differing position pays the un-meta cost.
7573///
7574/// ```c
7575/// while(*s1 && *s1 == *s2) { s1++; s2++; }
7576/// if (!(c1 = *s1)) c1 = -1;
7577/// else if (c1 == Meta) c1 = *++s1 ^ 32;
7578/// if (!(c2 = *s2)) c2 = -1;
7579/// else if (c2 == Meta) c2 = *++s2 ^ 32;
7580/// return c1 - c2;
7581/// ```
7582pub fn ztrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
7583    // c:5106
7584    let b1 = s1.as_bytes();
7585    let b2 = s2.as_bytes();
7586    let mut i1 = 0;
7587    let mut i2 = 0;
7588    // Skip the matching prefix.
7589    while i1 < b1.len() && i2 < b2.len() && b1[i1] == b2[i2] {
7590        i1 += 1;
7591        i2 += 1;
7592    }
7593    // Resolve c1: -1 for end-of-string, else the next byte
7594    // (un-meta-fied if it's a Meta marker).
7595    let c1: i32 = if i1 >= b1.len() {
7596        -1
7597    } else if b1[i1] == Meta && i1 + 1 < b1.len() {
7598        (b1[i1 + 1] ^ 32) as i32
7599    } else {
7600        b1[i1] as i32
7601    };
7602    let c2: i32 = if i2 >= b2.len() {
7603        -1
7604    } else if b2[i2] == Meta && i2 + 1 < b2.len() {
7605        (b2[i2 + 1] ^ 32) as i32
7606    } else {
7607        b2[i2] as i32
7608    };
7609    c1.cmp(&c2)
7610}
7611
7612// pastebuf() DELETED — was a misplaced fn. The real `pastebuf()`
7613// lives in `Src/Zle/zle_misc.c:558` (a ZLE clipboard helper that
7614// pastes a Cutbuffer into the line-edit buffer). It has nothing to
7615// do with metafication, despite this file's prior body which
7616// reimplemented half of `metafy`. The real metafy lives below at
7617// `pub fn metafy()` (port of utils.c:4856).
7618
7619/// Port of `ztrlen()` from `Src/utils.c:5136`.
7620///
7621/// Unmetafied string length (from utils.c ztrlen lines 5135-5152)
7622pub fn ztrlen(s: &str) -> usize {
7623    // c:5136
7624    let mut len = 0;
7625    let chars: Vec<char> = s.chars().collect();
7626    let mut i = 0;
7627    while i < chars.len() {
7628        len += 1;
7629        if chars[i] as u32 == Meta as u32 && i + 1 < chars.len() {
7630            i += 2;
7631        } else {
7632            i += 1;
7633        }
7634    }
7635    len
7636}
7637
7638/// Port of `ztrlenend()` from `Src/utils.c:5162` — C decl `ztrlenend(char const *s, char const *eptr)`.
7639///
7640/// ```c
7641/// for (l = 0; s < eptr; l++) {
7642///     if (*s++ == Meta) s++;  // skip past Meta-escaped pair
7643/// }
7644/// return l;
7645/// ```
7646///
7647/// Count the unmetafied character length from `s` up to `end`
7648/// bytes. Each Meta-escaped pair counts as 1 character.
7649/// Previous Rust port called `chars().count()` which counts UTF-8
7650/// codepoints, not byte-walked Meta-pairs — wrong semantics.
7651pub fn ztrlenend(s: &str, eptr: usize) -> usize {
7652    let bytes = s.as_bytes();
7653    let cap = eptr.min(bytes.len());
7654    let mut l = 0;
7655    let mut i = 0;
7656    while i < cap {
7657        if bytes[i] == Meta {
7658            // Meta sentinel + escaped byte = 1 visible char.
7659            i += 2;
7660        } else {
7661            i += 1;
7662        }
7663        l += 1;
7664    }
7665    l
7666}
7667
7668/// Port of `ztrsub()` from `Src/utils.c:5187` — C decl `int ztrsub(char const *t, char const *s)`.
7669/// ```c
7670/// int l = t - s;
7671/// while (s != t) {
7672///     if (*s++ == Meta) { s++; l--; }
7673/// }
7674/// return l;
7675/// ```
7676/// "Subtract two pointers in a metafied string." `s` is the start
7677/// pointer, `t` is the end pointer; both point into the SAME buffer.
7678/// Returns the count of unmetafied chars in `[s, t)` — same as
7679/// `ztrlen` of the substring but expressed via pointer arithmetic.
7680///
7681/// Rust API: takes the full buffer and a byte-offset pair
7682/// `[start..end)` since Rust can't replicate raw-pointer subtraction
7683/// across distinct `&str` arguments without UB risk. The semantics
7684/// stay faithful: count of unmetafied chars between two offsets.
7685/// Previous Rust port took two unrelated `&str` and computed
7686/// `ztrlen(&t[..t.len()-s.len()])` — a fundamentally different
7687/// operation that worked only when `s` was the suffix of `t`.
7688/// WARNING: param names don't match C — Rust=(buf, start, end) vs C=(t, s)
7689pub fn ztrsub(buf: &str, start: usize, end: usize) -> usize {
7690    // c:5187
7691    let bytes = buf.as_bytes();
7692    let end = end.min(bytes.len());
7693    let start = start.min(end);
7694    let mut l = (end - start) as isize; // c:5189
7695    let mut i = start;
7696    while i < end {
7697        if bytes[i] == Meta {
7698            // c:5192
7699            i += 2; // c:5198
7700            l -= 1; // c:5199
7701        } else {
7702            i += 1;
7703        }
7704    }
7705    l.max(0) as usize
7706}
7707
7708/// Port of `zreaddir()` from `Src/utils.c:5217`.
7709///
7710/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
7711/// `Src/utils.c:5217`.
7712///
7713/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
7714/// `Src/utils.c:5217-5240`. Pulls the next entry from a DIR* the
7715/// caller owns; returns the entry's bare name (metafied String) or
7716/// `None` at EOF. The C source's `ignoredots` flag (c:5232) skips
7717/// `.` and `..` when set; callers (`spnamepat` at c:4648) that
7718/// want dot entries pass 0.
7719///
7720/// Caller pattern matches C exactly:
7721///   let mut dir = fs::read_dir(path)?;
7722///   while let Some(name) = zreaddir(&mut dir, 1) { ... }
7723///   // dir auto-closed on drop (= closedir)
7724pub fn zreaddir(dir: &mut fs::ReadDir, ignoredots: i32) -> Option<String> {
7725    // c:5217
7726    for entry in dir.by_ref() {
7727        // c:5221 readdir loop
7728        let Ok(e) = entry else { continue };
7729        let Ok(name) = e.file_name().into_string() else {
7730            continue;
7731        };
7732        // c:5232 — `if (ignoredots && de->d_name[0] == '.' &&
7733        //              (!de->d_name[1] || (de->d_name[1] == '.' && !de->d_name[2])))`.
7734        if ignoredots != 0 && (name == "." || name == "..") {
7735            continue; // c:5234
7736        }
7737        return Some(name); // c:5238 return de->d_name
7738    }
7739    None // c:5240 — fell off the end
7740}
7741
7742/// Port of `zputs()` from `Src/utils.c:5265` — C decl `int zputs(char const *s, FILE *stream)`.
7743///
7744/// ```c
7745/// while (*s) {
7746///     if (*s == Meta)     c = *++s ^ 32;
7747///     else if (itok(*s)) { s++; continue; }
7748///     else                c = *s;
7749///     s++;
7750///     if (fputc(c, stream) < 0) return EOF;
7751/// }
7752/// return 0;
7753/// ```
7754///
7755/// Writes `s` to `stream` with Meta+X pair decoding and ITOK-char
7756/// skipping. Returns `0` on success, `-1` on write error (mirroring
7757/// C's `EOF` sentinel for the int return).
7758///
7759/// The walk is over CHARS, not bytes. C can scan bytes at c:5267
7760/// because zsh metafies: a raw byte >= 0x80 is stored escaped
7761/// (`Meta`, then `byte ^ 32`), so a bare `0x84..=0xa1` in a metafied
7762/// string is always a real token. zshrs keeps UTF-8 `str` and stores
7763/// tokens as PUA CHARS (crate::token_char) — the representation
7764/// `lex::untokenize` and `has_token` (utils.rs:3089) both test with
7765/// `c as u32`. Scanning bytes here mistook a UTF-8 CONTINUATION byte
7766/// for a token and DROPPED it: `functions f` printed `print "a<E2><80>b"`
7767/// for a body containing `—` (U+2014 = E2 80 94, 0x94 = `Inang`), and
7768/// the same for `→` (E2 86 92) and `“` (E2 80 9C). This is the
7769/// `has_token` defect (fixed in 1e549c1dec) at a second site.
7770pub fn zputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
7771    // c:5265
7772    let mut it = s.chars(); // c:5265 *s walk
7773    while let Some(ch) = it.next() {
7774        // c:5267 while (*s)
7775        if ch as u32 == Meta as u32 {
7776            // c:5269 if (*s == Meta)
7777            // c:5270 — `c = *++s ^ 32;`. zshrs metafies at the CHARACTER
7778            // level (U+0083 then `char::from(byte ^ 32)`, see
7779            // mb_niceformat's note at utils.rs:7574), so the escaped
7780            // byte is recovered as a single raw byte exactly as C's
7781            // `fputc` writes it.
7782            let Some(n) = it.next() else {
7783                continue; // c:5267 — lone trailing Meta ends the walk
7784            };
7785            let c = (n as u32 as u8) ^ 32; // c:5270
7786                                           // c:5276 s++ — `it.next()` already advanced past both chars.
7787            if stream.write_all(&[c]).is_err() {
7788                // c:5277 fputc(c, stream)
7789                return -1; // c:5278 return EOF
7790            }
7791        } else if crate::token_char::itok_char(ch) {
7792            // c:5271 else if (itok(*s))
7793            // c:5272 — `s++; continue;` (skip token char)
7794            continue;
7795        } else {
7796            // c:5274 else c = *s — a plain char goes out in its own
7797            // UTF-8 encoding; C's byte-at-a-time `fputc` emits the same
7798            // bytes because its `s` is already the encoded form.
7799            let mut buf = [0u8; 4];
7800            // c:5276 s++
7801            if stream.write_all(ch.encode_utf8(&mut buf).as_bytes()).is_err() {
7802                // c:5277 fputc(c, stream)
7803                return -1; // c:5278 return EOF
7804            }
7805        }
7806    }
7807    0 // c:5280 return 0
7808}
7809
7810/// Port of `nicedup()` from `Src/utils.c:5530`.
7811///
7812/// Port of `nicedup(char const *s, int heap)` from `Src/utils.c:5289`
7813/// (single-byte build) and `Src/utils.c:5530` (multibyte build).
7814///
7815/// C bodies:
7816/// ```c
7817/// /* Src/utils.c:5289 — !MULTIBYTE_SUPPORT */
7818/// char *retstr;
7819/// (void)sb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
7820/// return retstr;
7821///
7822/// /* Src/utils.c:5530 — MULTIBYTE_SUPPORT */
7823/// char *retstr;
7824/// (void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
7825/// return retstr;
7826/// ```
7827///
7828/// zshrs targets MULTIBYTE_SUPPORT (matches every modern zsh
7829/// build) — route through `mb_niceformat`. Previous Rust port
7830/// went through `sb_niceformat`, which is the !MULTIBYTE path —
7831/// strips wide-char handling and corrupts non-ASCII via byte-mask
7832/// `nicechar(c & 0xff)`.
7833///
7834/// C signature faithful: `(s, heap) -> String`. `heap` selects between
7835/// arena (`NICEFLAG_HEAP`) and persistent (default `ztrdup`) allocation
7836/// in C; Rust has a single allocator so both paths produce the same
7837/// owned `String`, but the `heap` parameter is preserved per Rule S1.
7838pub fn nicedup(s: &str, heap: i32) -> String {
7839    // c:5530
7840    // c:5532 — `char *retstr;`
7841    let retstr: Option<String>;
7842    // c:5534 — `(void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);`
7843    let mut slot: Option<String> = None;
7844    let _ = mb_niceformat(
7845        s,
7846        None,
7847        Some(&mut slot),
7848        if heap != 0 { NICEFLAG_HEAP } else { 0 },
7849    );
7850    retstr = slot;
7851    // c:5536 — `return retstr;`
7852    retstr.unwrap_or_default()
7853}
7854
7855/// Port of `nicedupstring()` from `Src/utils.c:5301` — C decl `nicedupstring(char const *s)`.
7856///
7857/// Nice-format and duplicate string.
7858/// C body: `return nicedup(s, 1);` — heap-arena allocation form.
7859pub fn nicedupstring(s: &str) -> String {
7860    // c:5301
7861    nicedup(s, 1) // c:5303
7862}
7863
7864/// Port of `nicezputs()` from `Src/utils.c:5313` — C decl `nicezputs(char const *s, FILE *stream)`.
7865///
7866/// Nicely format a string
7867///
7868/// Under `MULTIBYTE_SUPPORT` (the daily-driver path), C defines
7869/// `nicezputs(str, outs)` as a macro at zsh.h:3274:
7870///   `(void)mb_niceformat((str), (outs), NULL, 0)`.
7871/// Without MULTIBYTE (c:5313): `sb_niceformat(s, stream, NULL, 0)`.
7872///
7873/// The previous Rust impl was `s.chars().map(nicechar).collect()` —
7874/// wrong on every front:
7875///   1. No unmetafy step (Meta-byte pairs surfaced as `\M-…` mangled).
7876///   2. `nicechar` is byte-based (`c & 0xff`), corrupting non-ASCII
7877///      multibyte codepoints into spurious `\M-X` escapes.
7878///   3. No `itok` ZSH-token-byte handling.
7879///
7880/// Route through `mb_niceformat` to match the C macro under
7881/// MULTIBYTE — which itself unmetafies and uses `wcs_nicechar` for
7882/// proper wide-char handling.
7883pub fn nicezputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
7884    // c:5313 (non-MULTIBYTE) / zsh.h:3274 (MULTIBYTE macro:
7885    //   `(void)mb_niceformat((str), (outs), NULL, 0)`).
7886    let _ = mb_niceformat(s, Some(stream), None, 0);
7887    0 // c:5316 return 0
7888}
7889
7890/// Port of `niceztrlen()` from `Src/utils.c:5324` — C decl `niceztrlen(char const *s)`.
7891///
7892/// Returns the length (in bytes) of the visible representation of
7893/// the metafied string `s`. C body walks each char via `nicechar`
7894/// and accumulates `strlen(nicechar(c))`; the Rust port mirrors
7895/// this via [`sb_niceformat`] which is the same render-then-measure
7896/// path.
7897pub fn niceztrlen(s: &str) -> usize {
7898    // c:5343-5348 — under MULTIBYTE_SUPPORT (which zshrs targets) `niceztrlen`
7899    // IS `mb_niceformat`, so the unit it walks is whatever the current locale's
7900    // `mbrtowc` returns: a whole character under a UTF-8 locale (U+2010 `‐`,
7901    // 3 bytes, counts 1), a single byte under a C/POSIX one (the same `‐`
7902    // counts 1+5+5 = 11, because its two continuation bytes are non-printable
7903    // and take `\M-^@`-style five-column representations). `mb_niceformat`
7904    // carries that locale branch; see its body.
7905    //
7906    // The port previously called `sb_niceformat`, the single-byte (`#else`,
7907    // c:5320-5341) version, which counts every byte through `nicechar` and so
7908    // charges 4+5+5 = 14 for that `‐` in EITHER locale — over-measuring in a
7909    // UTF-8 locale and still not matching zsh in a C one.
7910    //
7911    // A string of printable ASCII (0x20-0x7e) is the identity under every
7912    // step of `mb_niceformat` with `flags == 0`: `untokenize` and `unmetafy`
7913    // only touch bytes >= 0x83, `mbrtowc` yields one character per byte in
7914    // either locale, and `wcs_nicechar_sel` gives each a width of 1
7915    // (c:644-704). Its width is its length. `calclist` measures every match
7916    // with this (c:1600/1615), twice per `$compstate[list_lines]` read, and
7917    // the general walk allocated three copies of each string to arrive at
7918    // that number: about 4.5k of 14k samples on `arch <TAB>`'s 47058 matches.
7919    if s.bytes().all(|b| (0x20..0x7f).contains(&b)) {
7920        return s.len();
7921    }
7922    mb_niceformat(s, None, None, 0)
7923}
7924
7925/// Port of `mb_niceformat()` from `Src/utils.c:5366`.
7926///
7927/// Multibyte-aware nice-format of a string.
7928/// Port of `mb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from Src/utils.c:5366. Walks the
7929/// (un-metafied) string char-by-char; for each control byte or
7930/// invalid sequence emits a `^X`/`\\xNN` representation, otherwise
7931/// passes the char through. The C source threads an
7932/// `mbstate_t` through `mbrtowc()` and falls back to single-byte
7933/// `\M-` notation on `MB_INVALID`.
7934///
7935/// `mbrtowc` is locale-driven, so the port is too: under a UTF-8
7936/// codeset it decodes one UTF-8 scalar per iteration (Rust's own
7937/// UTF-8 validation standing in for the `mbstate_t` machine, with
7938/// the invalid-byte arm reached through `from_utf8`'s error), and
7939/// under a single-byte codeset (`MB_CUR_MAX == 1`, the C/POSIX
7940/// locale) it takes one byte per iteration whose value IS the wide
7941/// character. The width this function returns differs between the
7942/// two for any non-ASCII input — see the `mb_single_byte` block.
7943/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream, outstrp, flags)
7944/// `mb_niceformat` — see implementation.
7945pub fn mb_niceformat(
7946    s: &str,
7947    mut stream: Option<&mut dyn std::io::Write>,
7948    outstrp: Option<&mut Option<String>>,
7949    flags: i32,
7950) -> usize {
7951    // c:5366
7952    let mut l: usize = 0; // c:5368 size_t l = 0
7953    let mut newl: usize; // c:5368 size_t newl
7954                         // c:5369 — `int umlen, outalloc, outleft, eol = 0;`. outalloc/outleft
7955                         // model C's buffer-growth math; Rust String auto-grows so the realloc
7956                         // loop at c:5430-5440 collapses to push_str. umlen and eol carry.
7957    let mut umlen: usize; // c:5369
7958    let mut eol: bool = false; // c:5369 int eol = 0
7959                               // c:5370 — `wchar_t c;` (carried inside loop in Rust)
7960                               // c:5371 — `char *ums, *ptr, *fmt, *outstr, *outptr;`
7961    let mut ums: Vec<u8>; // c:5371 char *ums
7962    let mut ptr: usize; // c:5371 char *ptr
7963    let mut fmt: String; // c:5371 char *fmt
7964    let mut outstr: Option<String>; // c:5371 char *outstr
7965                                    // c:5372 — `mbstate_t mbs;` (Rust UTF-8 has no shift state; tracked
7966                                    // by the `valid_up_to` / `error_len` logic below).
7967
7968    // c:5374-5380 — `if (outstrp) outptr = outstr = zalloc(5*strlen(s));`
7969    if outstrp.is_some() {
7970        // c:5374
7971        outstr = Some(String::with_capacity(5 * s.len())); // c:5376
7972    } else {
7973        // c:5377
7974        outstr = None; // c:5379 outstr = NULL
7975    }
7976
7977    // c:5382 — `ums = ztrdup(s);`
7978    // c:5383-5387 — comment-only block carried verbatim:
7979    /*
7980     * is this necessary at this point? niceztrlen does this
7981     * but it's used in lots of places.  however, one day this may
7982     * be, too.
7983     */                                                                      // c:5383-5387
7984    // c:5388 — `untokenize(ums);` — Rust port uses the char-based
7985    // `lex::untokenize` (only fires on codepoints in the 0x84..=0xa1
7986    // ITOK range, never on UTF-8 continuation bytes); the byte-level
7987    // shape of C's untokenize is unsafe on raw UTF-8 input because a
7988    // continuation byte like 0x97 (part of `字` = 0xE5 0xAD 0x97) sits
7989    // inside the ITOK range and would be misclassified as a token.
7990    // c:5389 — `ptr = unmetafy(ums, &umlen);` — `unmeta` is the safe
7991    // UTF-8 wrapper that runs unmetafy only when Meta bytes are
7992    // present and otherwise no-ops.
7993    //
7994    // `unmetafy_str` (not `unmeta`) is what produces the raw byte buffer C
7995    // expects here. zshrs metafies at the CHARACTER level — an undecodable byte
7996    // is stored as U+0083 followed by `char::from(byte ^ 32)` — and U+0083
7997    // itself encodes as the two UTF-8 bytes C2 83. `unmeta` scans for a 0x83
7998    // BYTE, so it finds the trailing byte of that pair and mangles the string:
7999    // `$'\M-a'` came back out as `\M-^C\M-A` instead of `\M-a`.
8000    //
8001    // The variant matters: C's `untokenize` (c:Src/exec.c:2077) MAPS every
8002    // token char back to its source character through `ztokens`
8003    // (c:Src/lex.c:38) — Dnull -> `"`, Snull -> `'`, String -> `$`. Its
8004    // `lex::untokenize` namesake here is the SUBSTITUTION-stream variant,
8005    // which DELETES the quote nulls instead, so `%s` rendered a still-
8006    // tokenized word with its quoting silently erased: zsh prints
8007    // `condition expected: "$glob"` where zshrs printed the invisible
8008    // Dnull/Qstring bytes around a bare `glob`. The two `sb_*` twins below
8009    // already inline the ztokens mapping (c:5871 / c:5943); use the ported
8010    // form of the same walk here.
8011    let detok = crate::ported::lex::untokenize_ztokens(s);
8012    ums = unmetafy_str(&detok);
8013    umlen = ums.len(); // c:5389 *umlen
8014    ptr = 0; // c:5389 ptr starts at 0 in ums
8015
8016    // c:5393 — `mbrtowc` is LOCALE-driven: with `MB_CUR_MAX == 1` (the
8017    // C/POSIX locale) it consumes exactly one byte per call and hands
8018    // back that byte's value as the wide character, so a UTF-8 sequence
8019    // sitting in the string is measured as its separate bytes — a
8020    // printable one (`\xe2` = U+00E2) costs its own column, a
8021    // non-printable one (`\x80`, `\x90`) costs the five columns of the
8022    // `\M-^@` representation `wcs_nicechar_sel` returns for it. zsh
8023    // therefore charges 1+5+5 = 11 columns for `‐` (U+2010) under
8024    // `LC_ALL=C`, not the 1 column it charges under a UTF-8 locale, and
8025    // every `ZMB_nicewidth` consumer (described-list truncation in
8026    // computil.c's `cd_get`, `cd_calc`'s `premaxw`, `calclist`'s column
8027    // widths) budgets against that number.
8028    //
8029    // The port decoded UTF-8 unconditionally, so under the C locale it
8030    // measured 1 where zsh measured 11 and let descriptions run past
8031    // zsh's cut column. Branch on the active locale's CODESET, the same
8032    // way the `${#…}` length arm does (subst.rs, Bug #378), and take one
8033    // byte at a time when it is not UTF-8. Inlined rather than factored
8034    // out because src/ported/ forbids Rust-original helper fns.
8035    //
8036    // Platform note: an 8-bit locale's `mbrtowc` accepts all 256 byte
8037    // values; the ASCII C locale rejects bytes >= 0x80 (glibc, and zshrs on
8038    // every platform via `crate::c_locale` — macOS libc would accept them),
8039    // so such a byte takes C's `MB_INVALID` arm and costs `nicechar_sel`'s
8040    // `\M-b` (4) rather than 1, and the numbers above become 4+5+5 = 14.
8041    //
8042    // Scope: the branch applies to the WIDTH-ONLY shape — C names it
8043    // `ZMB_nicewidth(s)` = `mb_niceformat(s, NULL, NULL, 0)`
8044    // (Src/Zle/zle.h:57), reached through `niceztrlen` — AND to the STREAM
8045    // shape, which is how `printmatch` (compresult.c:2260) writes a match's
8046    // display string to the terminal. The stream shape was excluded because C
8047    // emits the RAW byte for a printable high byte (`wcrtomb` in a
8048    // single-byte locale writes one byte, and `zputs` un-metafies
8049    // `wcs_nicechar_sel`'s buffer on the way out) while a Rust
8050    // `String`/`write_all` of that scalar writes its two UTF-8 bytes; the
8051    // emission below now converts back to bytes, so the objection is gone.
8052    // Until then `gifdiff -` printed `don’t` where zsh prints
8053    // `don<e2>\M-^@\M-^Yt`, and the same for every description carrying a
8054    // curly apostrophe, an en dash or a typographic minus.
8055    //
8056    // The `outstrp` shape (`nicedup`, `quotedzputs`) takes the branch too. C's
8057    // result there is METAFIED: `wcs_nicechar_sel` copies `wcrtomb`'s byte into
8058    // its buffer through `imeta` (c:690-697), and the printers write it with
8059    // `zputs`, which un-metafies. The append below stores such a byte the way
8060    // zshrs metafies any lone byte (U+0083, byte ^ 32), so the String carries it
8061    // and `zputs` emits the ONE byte. Excluding this shape left
8062    // `LC_ALL=C which ヌ` printing the UTF-8 name raw where zsh prints
8063    // `$'\M-c\M-\C-C\M-\C-L'` (E02xtrace.ztst; 0xe3 is MB_INVALID in
8064    // the ASCII C locale).
8065    let mb_single_byte = unsafe {
8066            // c:5583 — `mbrtowc(&wc, &inchar, 1, mbsp)` is LOCALE-driven: with
8067            // MB_CUR_MAX == 1 it consumes one byte and returns that byte as
8068            // the wide character, so `\303\255` is TWO characters under
8069            // LC_ALL=C, not `í`. Rust's UTF-8 decoder has no such notion, so
8070            // the locale has to be asked directly. Inlined rather than
8071            // factored out: src/ported/ is a port and build.rs rejects any fn
8072            // with no C counterpart (its stated remedy #1 is to inline).
8073            //
8074            // Rust never calls `setlocale` on its own, so run it once from the
8075            // environment before asking `nl_langinfo` — otherwise the startup
8076            // default ("C") would shadow the process's LC_CTYPE.
8077            let _ = *MB_LOCALE_READY;
8078            let cs_ptr = libc::nl_langinfo(libc::CODESET);
8079            if cs_ptr.is_null() {
8080                false
8081            } else {
8082                let cs = std::ffi::CStr::from_ptr(cs_ptr).to_string_lossy();
8083                !(cs.eq_ignore_ascii_case("UTF-8") || cs.eq_ignore_ascii_case("utf8"))
8084            }
8085        };
8086
8087    // c:5391 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
8088    while umlen > 0 {
8089        // c:5392
8090        // c:5393 — `cnt = eol ? MB_INVALID : mbrtowc(&c, ptr, umlen, &mbs);`
8091        // Rust equivalent: try to decode one UTF-8 scalar from ums[ptr..],
8092        // honoring `eol` (force the invalid arm when set).
8093        let cnt: usize;
8094        let decoded_c: Option<char>;
8095        if eol {
8096            // c:5396 — MB_INVALID arm via `eol = 1`.
8097            decoded_c = None;
8098            cnt = 1;
8099        } else if mb_single_byte {
8100            // c:5393 — single-byte locale: `mbrtowc` returns 1 and sets
8101            // `c` to the byte value itself, except that the ASCII C locale
8102            // rejects a byte above 0x7f (`crate::c_locale`), which then takes
8103            // the c:5397 MB_INVALID arm and prints as `\M-c`.
8104            let mut wc: libc::wchar_t = 0;
8105            let mut mbs: MbStateBuf = MBSTATE_ZERO;
8106            let r = unsafe {
8107                mbrtowc(
8108                    &mut wc,
8109                    ums[ptr..].as_ptr() as *const libc::c_char,
8110                    1,
8111                    &mut mbs as *mut MbStateBuf as *mut libc::c_void,
8112                )
8113            };
8114            decoded_c = if r == MB_INVALID { None } else { Some(char::from(ums[ptr])) };
8115            cnt = 1;
8116        } else {
8117            // c:5393 — `mbrtowc` decodes at most ONE character and looks at no
8118            // more than the bytes that character occupies; it never validates
8119            // the rest of the buffer. Handing the whole tail to `from_utf8`
8120            // validated every remaining byte on every iteration, making the
8121            // walk quadratic in the string length. A UTF-8 scalar is at most
8122            // four bytes, so a four-byte window sees exactly what `mbrtowc`
8123            // sees: a sequence that runs past it cannot be valid UTF-8, and a
8124            // sequence truncated by end-of-input is still reported incomplete
8125            // because the window is then shorter than four bytes.
8126            let remaining = &ums[ptr..ptr + umlen.min(4)];
8127            match std::str::from_utf8(remaining) {
8128                Ok(s_slice) => {
8129                    // Valid UTF-8 throughout remaining; consume one char.
8130                    if let Some(ch) = s_slice.chars().next() {
8131                        decoded_c = Some(ch);
8132                        cnt = ch.len_utf8();
8133                    } else {
8134                        // Empty? Shouldn't happen given umlen > 0, but
8135                        // mirror MB_INVALID.
8136                        decoded_c = None;
8137                        cnt = 1;
8138                    }
8139                }
8140                Err(e) => {
8141                    let valid_up_to = e.valid_up_to();
8142                    if valid_up_to > 0 {
8143                        // Decode the first valid char then continue.
8144                        let valid_slice =
8145                            unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
8146                        let ch = valid_slice.chars().next().unwrap();
8147                        decoded_c = Some(ch);
8148                        cnt = ch.len_utf8();
8149                    } else if e.error_len().is_none() {
8150                        // Incomplete sequence at end of input — MB_INCOMPLETE.
8151                        // c:5394 — `case MB_INCOMPLETE: eol = 1; FALL THROUGH`
8152                        eol = true;
8153                        decoded_c = None; // MB_INVALID
8154                        cnt = 1;
8155                    } else {
8156                        // Definite invalid byte — MB_INVALID.
8157                        decoded_c = None;
8158                        cnt = 1;
8159                    }
8160                }
8161            }
8162        }
8163
8164        // c:5396-5403 / c:5404-5424 switch dispatch on cnt/decoded_c.
8165        let cnt_used: usize;
8166        match decoded_c {
8167            None => {
8168                // c:5397 — `case MB_INVALID:` (or fall-through from
8169                // MB_INCOMPLETE at c:5394).
8170                // c:5400 — `fmt = nicechar_sel(*ptr, flags & NICEFLAG_QUOTE);`
8171                fmt = nicechar_sel(ums[ptr] as char, (flags & NICEFLAG_QUOTE) != 0);
8172                newl = fmt.len(); // c:5401 newl = strlen(fmt)
8173                cnt_used = 1; // c:5402 cnt = 1
8174                              // c:5403 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless)
8175            }
8176            Some(c) if c == '\0' => {
8177                // c:5406 — `case 0:` — '\0' decodes to 0; consume 1 byte
8178                // and fall through to default.
8179                cnt_used = 1; // c:5409
8180                              // c:5411-5421 default arm (FALL THROUGH from case 0)
8181                if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
8182                    // c:5413
8183                    fmt = "\\'".to_string(); // c:5414
8184                    newl = 2; // c:5415
8185                } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
8186                    // c:5417
8187                    fmt = "\\\\".to_string(); // c:5418
8188                    newl = 2; // c:5419
8189                } else {
8190                    // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
8191                    //                                 flags & NICEFLAG_QUOTE);`
8192                    let mut width: usize = 0;
8193                    fmt =
8194                        wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
8195                    newl = width;
8196                }
8197            }
8198            Some(c) => {
8199                // c:5411 — `default:` arm (cnt > 0, valid char).
8200                cnt_used = cnt;
8201                if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
8202                    // c:5413
8203                    fmt = "\\'".to_string(); // c:5414
8204                    newl = 2; // c:5415
8205                } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
8206                    // c:5417
8207                    fmt = "\\\\".to_string(); // c:5418
8208                    newl = 2; // c:5419
8209                } else {
8210                    // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
8211                    //                                 flags & NICEFLAG_QUOTE);`
8212                    let mut width: usize = 0;
8213                    fmt =
8214                        wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
8215                    newl = width;
8216                }
8217            }
8218        }
8219
8220        umlen -= cnt_used; // c:5427 umlen -= cnt
8221        ptr += cnt_used; // c:5428 ptr += cnt
8222        l += newl; // c:5429 l += newl
8223
8224        if let Some(ref mut w) = stream {
8225            // c:5431 if (stream)
8226            // c:5432 — `zputs(fmt, stream);`. In a single-byte locale
8227            // `wcs_nicechar_sel`'s printable arm is C's `wcrtomb`, which
8228            // writes exactly ONE byte; this port holds that byte as a `char`
8229            // in U+0080..U+00FF whose UTF-8 form is two bytes, so it has to be
8230            // narrowed back on the way out. Escapes (`\M-^@`) are ASCII and
8231            // pass through either way.
8232            if mb_single_byte {
8233                let mut out: Vec<u8> = Vec::with_capacity(fmt.len());
8234                let mut b4 = [0u8; 4];
8235                for ch in fmt.chars() {
8236                    if (ch as u32) < 0x100 {
8237                        out.push(ch as u8);
8238                    } else {
8239                        out.extend_from_slice(ch.encode_utf8(&mut b4).as_bytes());
8240                    }
8241                }
8242                let _ = w.write_all(&out);
8243            } else {
8244                let _ = w.write_all(fmt.as_bytes());
8245            }
8246        }
8247        if let Some(ref mut buf) = outstr {
8248            // c:5433 if (outstr)
8249            // c:5434-5446 — append fmt to outstr, growing on demand. Rust
8250            // String auto-grows; the realloc loop collapses to push_str.
8251            if mb_single_byte {
8252                // c:690-697 — the single byte `wcrtomb` produced is stored
8253                // metafied; in zshrs's char-level form that is U+0083 followed
8254                // by `byte ^ 32` for every byte >= 0x80 (see the note above).
8255                for ch in fmt.chars() {
8256                    let cu = ch as u32;
8257                    if (0x80..0x100).contains(&cu) {
8258                        buf.push(char::from(Meta));
8259                        buf.push(char::from((cu as u8) ^ 32));
8260                    } else {
8261                        buf.push(ch);
8262                    }
8263                }
8264            } else {
8265                buf.push_str(&fmt); // c:5446 memcpy(outptr, fmt, outlen)
8266            }
8267        }
8268        let _ = fmt;
8269    }
8270
8271    // c:5451 — `free(ums);` (Rust drop at scope exit)
8272    drop(ums);
8273    if let Some(slot) = outstrp {
8274        // c:5452 if (outstrp)
8275        // c:5453 — `*outptr = '\0';` (no-op for Rust String)
8276        // c:5455-5460 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping. Rust has
8277        // a single allocator so all three paths produce identical owned
8278        // String contents; transfer ownership into caller's slot.
8279        *slot = outstr.take();
8280    }
8281
8282    l // c:5462 return l
8283}
8284
8285/// Port of `is_mb_niceformat()` from `Src/utils.c:5474` — C decl `is_mb_niceformat(const char *s)`.
8286///
8287/// Predicate: would any character in `s` need representation by
8288/// `mb_niceformat` / `nicedup`? C body:
8289/// ```c
8290/// ums = ztrdup(s);
8291/// untokenize(ums);
8292/// ptr = unmetafy(ums, &umlen);
8293/// while (umlen > 0) {
8294///     cnt = mbrtowc(&c, ptr, umlen, &mbs);
8295///     switch (cnt) {
8296///     case MB_INCOMPLETE: case MB_INVALID:
8297///         if (is_nicechar(*ptr)) { ret = 1; break; }
8298///         cnt = 1;
8299///         memset(&mbs, 0, sizeof mbs);
8300///         break;
8301///     case 0: cnt = 1;  /* FALLTHROUGH */
8302///     default:
8303///         if (is_wcs_nicechar(c)) ret = 1;
8304///         break;
8305///     }
8306///     if (ret) break;
8307///     umlen -= cnt; ptr += cnt;
8308/// }
8309/// ```
8310///
8311/// Rust port: unmetafy in place via [`unmetafy`], then walk the
8312/// resulting bytes. For valid UTF-8 sequences, check
8313/// `is_wcs_nicechar(scalar)`; for invalid bytes, check
8314/// `is_nicechar(byte)`. Either path bailing positive returns true.
8315pub fn is_mb_niceformat(s: &str) -> i32 {
8316    // c:5474
8317    // c:5476 — `int umlen, ret = 0;`
8318    let umlen: usize; // c:5476
8319    let mut ret: i32 = 0; // c:5476
8320                          // c:5477 — `char *ums, *ptr;` (eptr modelled by bytes.len())
8321    let mut ums: Vec<u8>; // c:5477
8322    let mut ptr: usize; // c:5477
8323
8324    // c:5481-5483 — `ums = ztrdup(s); untokenize(ums); ptr =
8325    //                unmetafy(ums, &umlen);`. Use BYTE-level `unmetafy_str`,
8326    // not char-level `unmeta`: `unmeta` decodes a metafied eight-bit byte
8327    // (Meta 0x83 + 0xC1 for `$'\M-a'`) into the valid Rust char U+00E1
8328    // (`á`), which the UTF-8 scan below reads as a PRINTABLE Latin-1 letter
8329    // → is_mb_niceformat=0 → `(q+)`/quotedzputs left the raw byte unquoted
8330    // (`\341`) where zsh emits `$'\M-a'`. `unmetafy_str` preserves the raw
8331    // 0xE1 byte (C's `unmetafy`), so the MB_INVALID arm's is_nicechar(0xE1)
8332    // fires (high-bit → nice) exactly as C does.
8333    // c:5481 `untokenize` is exec.c:2077 — the ztokens-MAPPING walk, not the
8334    // substitution-stream variant that deletes quote nulls. Must agree with
8335    // `mb_niceformat` above or the "does this need $'…'" answer is computed
8336    // over a different string than the one that gets formatted.
8337    let detok = crate::ported::lex::untokenize_ztokens(s);
8338    ums = unmetafy_str(&detok);
8339    umlen = ums.len(); // c:5483 *umlen
8340    ptr = 0; // c:5483 ptr starts at 0
8341
8342    // c:5488 — `mbrtowc` is LOCALE-driven, exactly as in `mb_niceformat`
8343    // above: under a single-byte codeset (`LC_ALL=C`) every byte is one wide
8344    // character whose value is the byte. Decoding UTF-8 regardless answered
8345    // "no reformat needed" for `ヌ` (E3 83 8C) under LC_ALL=C, so `which`
8346    // and `${(q+)…}` printed it bare where zsh sees the non-printable 0x83
8347    // and quotes the whole name. Same inlined CODESET probe as there.
8348    let mb_single_byte = unsafe {
8349        let _ = *MB_LOCALE_READY;
8350        let cs_ptr = libc::nl_langinfo(libc::CODESET);
8351        if cs_ptr.is_null() {
8352            false
8353        } else {
8354            let cs = std::ffi::CStr::from_ptr(cs_ptr).to_string_lossy();
8355            !(cs.eq_ignore_ascii_case("UTF-8") || cs.eq_ignore_ascii_case("utf8"))
8356        }
8357    };
8358    if mb_single_byte {
8359        // c:5486-5516 — one byte per `mbrtowc`; `case 0` (NUL) falls through
8360        // to the default arm, so every converted byte reaches
8361        // `is_wcs_nicechar`. A byte the ASCII C locale rejects
8362        // (`crate::c_locale`) takes the c:5494 MB_INVALID arm: `is_nicechar`.
8363        while ret == 0 && ptr < umlen {
8364            let mut wc: libc::wchar_t = 0;
8365            let mut mbs: MbStateBuf = MBSTATE_ZERO;
8366            let r = unsafe {
8367                mbrtowc(
8368                    &mut wc,
8369                    ums[ptr..].as_ptr() as *const libc::c_char,
8370                    1,
8371                    &mut mbs as *mut MbStateBuf as *mut libc::c_void,
8372                )
8373            };
8374            let nice = if r == MB_INVALID {
8375                is_nicechar(char::from(ums[ptr])) // c:5495
8376            } else {
8377                is_wcs_nicechar(char::from(ums[ptr])) // c:5508
8378            };
8379            if nice {
8380                ret = 1; // c:5509
8381            }
8382            ptr += 1; // c:5515
8383        }
8384    }
8385    // c:5485 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
8386    while ret == 0 && ptr < ums.len() {
8387        // c:5486 while (umlen > 0)
8388        let remaining = &ums[ptr..];
8389        match std::str::from_utf8(remaining) {
8390            Ok(s_slice) => {
8391                // c:5503-5511 — `default: if (is_wcs_nicechar(c)) ret = 1;`
8392                for ch in s_slice.chars() {
8393                    if is_wcs_nicechar(ch) {
8394                        // c:5508
8395                        ret = 1; // c:5509
8396                        break;
8397                    }
8398                }
8399                break;
8400            }
8401            Err(e) => {
8402                let valid_up_to = e.valid_up_to();
8403                if valid_up_to > 0 {
8404                    let valid = unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
8405                    for ch in valid.chars() {
8406                        if is_wcs_nicechar(ch) {
8407                            // c:5508
8408                            ret = 1; // c:5509
8409                            break;
8410                        }
8411                    }
8412                    if ret != 0 {
8413                        break;
8414                    }
8415                    ptr += valid_up_to;
8416                    continue;
8417                }
8418                // c:5493-5498 — `case MB_INVALID: if (is_nicechar(*ptr))
8419                //                ret = 1; break;`
8420                if is_nicechar(remaining[0] as char) {
8421                    // c:5494
8422                    ret = 1; // c:5495
8423                    break;
8424                }
8425                ptr += 1;
8426            }
8427        }
8428    }
8429
8430    drop(ums); // c:5519 free(ums)
8431    ret // c:5523 return ret
8432}
8433
8434/// Multibyte metachar length with conversion (from utils.c mb_metacharlenconv_r)
8435/// Port of `mb_metacharlenconv_r(const char *s, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5548`.
8436/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, wcp, mbsp)
8437// Rust idiom replacement: Rust strings are UTF-8 with valid scalar
8438// values, so `chars().next()` covers the mbrtowc state machine; no
8439// mbstate_t threading required.
8440/// Port of `mb_metacharlenconv_r()` from `Src/utils.c:5548`.
8441///
8442/// `mb_metacharlenconv_r` — see implementation.
8443pub fn mb_metacharlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
8444    if let Some(c) = s[pos..].chars().next() {
8445        (c.len_utf8(), Some(c))
8446    } else {
8447        (0, None)
8448    }
8449}
8450
8451/// Port of `mb_metacharlenconv()` from `Src/utils.c:5611`.
8452///
8453/// Multibyte-aware metafied-string char advance.
8454/// Port of `mb_metacharlenconv(const char *s, wint_t *wcp)` from Src/utils.c:5611. Returns
8455/// `(bytes_consumed, scalar_char)` for the next character of `s`.
8456///
8457/// `s` is the DEMETAFIED byte stream, not the port's `String` form. C
8458/// takes a metafied `char *` and un-metafies inline as it walks (c:5616
8459/// `*s == Meta ? s[1] ^ 32 : *s`, c:5562-5563 in the `_r` guts); the port
8460/// hoists that one step to the caller, which reaches this function via
8461/// `unmetafy_str`. The hoist is forced, not stylistic: with the MULTIBYTE
8462/// option off a unit is a single BYTE, so a caller must be able to cut
8463/// mid-character — and a Rust `String` cannot be sliced there. Working on
8464/// the byte stream is also what makes a `Meta`+byte pair and a literal
8465/// high byte the same input, exactly as they are to C. A demetafied
8466/// stream contains no `Meta` bytes to re-interpret, so a literal 0x83 in
8467/// a value (`$'\x83'`) stays one byte here instead of being mistaken for
8468/// an escape prefix.
8469///
8470/// c:5613 is the guard that makes byte mode work: with MULTIBYTE unset —
8471/// or for any 7-bit byte — the answer is one byte, no decoding. Without
8472/// it every caller silently kept counting characters in a mode where zsh
8473/// counts bytes.
8474///
8475/// The third return value has no counterpart in C's signature: it is the
8476/// unit itself, back in the port's `String` form. C needs no such thing —
8477/// its caller just slices the `char *` it already holds at the returned
8478/// length. A Rust caller cannot, because in byte mode the unit can be half
8479/// a character, so the re-encode has to happen where the boundary is known.
8480/// Returning it here is what keeps that one rule in one place: every
8481/// subscript, slice, split and quote walk re-encodes identically, and
8482/// concatenating any run of units reproduces exactly the bytes C would have
8483/// copied.
8484/// WARNING: param names don't match C — Rust=(s) vs C=(s, wcp)
8485pub fn mb_metacharlenconv(s: &[u8]) -> (usize, Option<char>, String) {
8486    if s.is_empty() {
8487        // c:5594-5595 — nothing left to decode.
8488        return (0, None, String::new());
8489    }
8490    // The unit in the port's `String` form: well-formed text verbatim,
8491    // anything else escaped as `Meta` + `byte ^ 32` — the representation
8492    // `unmetafy_str` decodes. C's own metafication (c:4869 `metafy`) escapes
8493    // the `imeta` set for the same reason; Rust's `String` additionally
8494    // cannot hold a byte that is not part of a valid sequence, so the escape
8495    // covers those too.
8496    let encode = |chunk: &[u8]| -> String {
8497        match std::str::from_utf8(chunk) {
8498            Ok(v) => v.to_string(),
8499            Err(_) => {
8500                let mut v = String::with_capacity(2 * chunk.len());
8501                for &b in chunk {
8502                    if b < 0x80 {
8503                        v.push(b as char);
8504                    } else {
8505                        v.push(char::from(Meta));
8506                        v.push(char::from(b ^ 32));
8507                    }
8508                }
8509                v
8510            }
8511        }
8512    };
8513    // c:5613 — `if (!isset(MULTIBYTE) || (unsigned char) *s <= 0x7f)`:
8514    // treat as a single byte. The option is read from the live slot with
8515    // its declared default (on, c:Src/options.c:197) rather than through
8516    // `isset()`, which maps a never-written slot to false and would invert
8517    // the default in any context that skips init's `emulate()`.
8518    // `mb_single_byte` above is the SAME single-byte outcome reached the
8519    // other way: the option is on, but the locale's `mbrtowc` is a single-byte
8520    // codec, so c:5583 `mbrtowc(&wc, &inchar, 1, mbsp)` returns 1 with `wc` =
8521    // the byte. C needs no such test because it calls the real `mbrtowc`; the
8522    // Rust UTF-8 decoder below is locale-blind, so it has to ask.
8523    // Rust never calls `setlocale` on its own, so run it once from the
8524    // environment; otherwise `mbrtowc` below would still be the startup
8525    // "C" codec and shadow the process's LC_CTYPE.
8526    let _ = *MB_LOCALE_READY;
8527    // c:5613 — `if (!isset(MULTIBYTE) || (unsigned char) *s <= 0x7f)`:
8528    // treat as a single byte. The option is read from the live slot with
8529    // its declared default (on, c:Src/options.c:197) rather than through
8530    // `isset()`, which maps a never-written slot to false and would invert
8531    // the default in any context that skips init's `emulate()`.
8532    //
8533    // There is deliberately NO codeset test here, because C has none: it
8534    // calls `mbrtowc` and lets the locale decide. A `CODESET != "UTF-8"`
8535    // short-circuit used to send EVERY non-UTF-8 locale down the
8536    // single-byte path, but `zh_CN.GB2312` and `ja_JP.eucJP` are multibyte
8537    // (`MB_CUR_MAX` 2 and 3), so `${s[1]}` yielded one byte where zsh
8538    // yields the whole two-byte character. Single-byte locales still take
8539    // the single-byte path — `mbrtowc` itself consumes exactly one byte
8540    // under `LC_ALL=C` and returns that byte as the wide character.
8541    if !crate::ported::options::opt_state_get("multibyte").unwrap_or(true) || s[0] <= 0x7f {
8542        // c:5616 — the byte itself is the scalar.
8543        return (1, Some(s[0] as char), encode(&s[..1]));
8544    }
8545    // c:5635 → mb_metacharlenconv_r: feed bytes to mbrtowc until one
8546    // character completes (c:5577-5585).
8547    let mut mbs: MbStateBuf = MBSTATE_ZERO;
8548    let mut n: usize = 0;
8549    while n < s.len() {
8550        let inchar = s[n];
8551        n += 1;
8552        let mut wc: libc::wchar_t = 0;
8553        let ret = unsafe {
8554            mbrtowc(
8555                &mut wc,
8556                &inchar as *const u8 as *const libc::c_char,
8557                1,
8558                &mut mbs as *mut MbStateBuf as *mut libc::c_void,
8559            )
8560        };
8561        if ret == MB_INVALID {
8562            // c:5579-5580 — break out and fall through to the single-byte
8563            // answer below.
8564            break;
8565        }
8566        if ret == MB_INCOMPLETE {
8567            // c:5581-5582 — keep feeding bytes.
8568            continue;
8569        }
8570        // c:5583-5585 — one complete character of `n` bytes.
8571        return (n, char::from_u32(wc as u32), encode(&s[..n]));
8572    }
8573    // c:5592-5593 — no valid multibyte sequence: treat as a single byte.
8574    (1, None, encode(&s[..1]))
8575}
8576
8577/// Port of `mb_metastrlenend()` from `Src/utils.c:5655` — C decl `mb_metastrlenend(char *ptr, int width, char *eptr)`.
8578///
8579/// Multibyte metastring length to end (from utils.c mb_metastrlenend)
8580///
8581/// Counts characters (`width == false`) or display columns
8582/// (`width == true`) in a metafied string. C walks the metafied
8583/// byte stream, un-metafies inline (`if (*ptr == Meta) inchar =
8584/// *++ptr ^ 32`, c:5672), and feeds one byte at a time to
8585/// `mbrtowc` to assemble each multibyte character (c:5691); a
8586/// complete character counts 1 (or its `WCWIDTH`), an invalid byte
8587/// counts as a single character (c:5712-5714).
8588///
8589/// Rust-port note: zshrs stores both metafied byte-pairs (`Meta` +
8590/// `byte ^ 32`, produced by `getkeystring` for `$'\xNN'` escapes)
8591/// AND real multibyte `char`s (the literal `$'é'` path) in one
8592/// `String`, whereas C only ever holds metafied bytes. So the port
8593/// first reduces the slice to its raw byte stream via `unmetafy_str`
8594/// (metafied pair → its byte; real `char` → its UTF-8 bytes — the
8595/// same bytes C's inline demetafy would yield), then runs C's
8596/// `mbrtowc` character-assembly loop over those bytes. Rust's UTF-8
8597/// validation is the `mbrtowc` analogue: the shortest valid prefix
8598/// is one character; a byte that begins no valid sequence counts as
8599/// one (c:5712).
8600pub fn mb_metastrlenend(ptr: &str, width: i32, eptr: usize) -> usize {
8601    // c:5672 — un-metafy the (optionally end-bounded) slice to raw bytes.
8602    let bytes = unmetafy_str(&ptr[..eptr.min(ptr.len())]);
8603    // c:5662-5663 — `if (!isset(MULTIBYTE) || MB_CUR_MAX == 1) return
8604    // eptr ? (int)(eptr - ptr) : ztrlen(ptr);`. With the MULTIBYTE
8605    // option unset every length op counts BYTES, and `ztrlen` counts a
8606    // `Meta`+byte pair as the ONE byte it encodes — which is exactly
8607    // the length of the demetafied stream computed above.
8608    //
8609    // This guard belongs HERE, not at the call sites. It used to live
8610    // only in the `${#v}` length arm (subst.rs), so `${#v}` counted
8611    // bytes under `unsetopt multibyte` while every other caller of this
8612    // function — dopadding's (l)/(r)/(m) widths above all — kept
8613    // counting characters.
8614    //
8615    // The option is read from the live slot with the DECLARED DEFAULT
8616    // (on, c:Src/options.c:197) rather than through `isset()`, which
8617    // maps a never-written slot to false and would invert the
8618    // default-on semantics in any context that skips init's
8619    // `emulate()` — unit tests most of all.
8620    // The decode is `mbrtowc`, not Rust's UTF-8 validator, because C's is
8621    // LOCALE-driven (c:5689). `zh_CN.GB2312` and `ja_JP.eucJP` are multibyte
8622    // codesets that are not UTF-8: zsh counts `\xc4\xe3\xba\xc3\xca\xc0` as
8623    // 3 characters there, and a UTF-8-only scan counted 6. `setlocale` has
8624    // to have run first or `mbrtowc` would still be the startup "C" codec —
8625    // and the `MB_CUR_MAX` test below reads the same locale, so the
8626    // initialisation has to happen BEFORE the guard, not after it.
8627    let _ = *MB_LOCALE_READY;
8628    // c:5662-5663 — the guard is `!isset(MULTIBYTE) || MB_CUR_MAX == 1`.
8629    // The `MB_CUR_MAX == 1` half was missing, and it is the half that
8630    // makes a SINGLE-BYTE locale report BYTES for a WIDTH request too:
8631    // C returns `ztrlen` here and never looks at `width`, so under
8632    // `LC_ALL=C` every byte counts 1 whatever glyph it belongs to.
8633    // Without it the port ran the `mbrtowc` loop, which under that
8634    // locale decodes each byte to its own wide character and then
8635    // charges C1 bytes (0x80-0x9f) zero columns via the width table:
8636    // `${(m)#日本語}` was 6 against zsh's 9 (the three C1 bytes 0x97,
8637    // 0x9c, 0x9e dropped), `${(m)#aé日}` 5 against 6, and the same
8638    // count reached `dopadding`, so `${(ml:12::x:)日本語}` padded with
8639    // six `x` where zsh pads with three. The character COUNT was
8640    // already right because that path adds 1 per decoded byte.
8641    // `MB_CUR_MAX` is a C macro over a per-platform accessor, so it is
8642    // read inline here exactly as C writes it — there is no C function
8643    // to port it as, and src/ported/ takes no Rust-original helpers.
8644    if !crate::ported::options::opt_state_get("multibyte").unwrap_or(true)
8645        || (unsafe { mb_cur_max_raw() } as usize) == 1
8646    {
8647        return bytes.len();
8648    }
8649    let mut num: usize = 0; // c:5658 size_t ret / counter
8650    let mut num_in_char: usize = 0; // c:5660
8651    let mut complete = true; // c:5660
8652    let mut i: usize = 0;
8653    let mut laststart: usize = 0; // c:5657
8654                                  // c:5670 — memset(&mb_shiftstate, 0, sizeof(mb_shiftstate)).
8655    let mut mbs: MbStateBuf = MBSTATE_ZERO;
8656    while i < bytes.len() {
8657        // c:5672-5676 — the stream is already demetafied above, so C's
8658        // inline `*ptr == Meta ? *++ptr ^ 32` step has nothing left to undo.
8659        let inchar = bytes[i];
8660        i += 1;
8661
8662        // c:5678-5687 — 7-bit US-ASCII subset: one character, skip mbrtowc.
8663        if complete && inchar <= 0x7f {
8664            num += 1;
8665            laststart = i;
8666            num_in_char = 0;
8667            continue;
8668        }
8669
8670        // c:5689 — `ret = mbrtowc(&wc, &inchar, 1, &mb_shiftstate);`.
8671        let mut wc: libc::wchar_t = 0;
8672        let ret = unsafe {
8673            mbrtowc(
8674                &mut wc,
8675                &inchar as *const u8 as *const libc::c_char,
8676                1,
8677                &mut mbs as *mut MbStateBuf as *mut libc::c_void,
8678            )
8679        };
8680
8681        if ret == MB_INCOMPLETE {
8682            // c:5691-5708 — trailing octets of a character still being
8683            // assembled; counted once at c:5736 if it never completes.
8684            num_in_char += 1;
8685            complete = false;
8686        } else {
8687            if ret == MB_INVALID {
8688                // c:5710-5714 — reset the shift state and treat the byte
8689                // that started the run as ONE character. This arm runs
8690                // BEFORE the width test, so an invalid byte counts 1 even
8691                // when a WIDTH was asked for: `${(m)#}` over six invalid
8692                // bytes is 6 in zsh, and taking `WCWIDTH` of an
8693                // undecodable byte (0, via `unwrap_or`) made it 0 here.
8694                mbs = MBSTATE_ZERO;
8695                i = laststart + 1;
8696                num += 1;
8697            } else if width != 0 {
8698                // c:5715-5726 — WCWIDTH(wc); C turns "not printable" (-1)
8699                // into 0 and adds nothing. WCWIDTH is u9_wcwidth in a
8700                // --enable-unicode9 build (Src/zsh.h:3302).
8701                let wcw = char::from_u32(wc as u32).map_or(-1, crate::ported::compat::u9_wcwidth);
8702                if wcw > 0 {
8703                    let wcw = wcw as usize;
8704                    // c:5722-5725 — `width == 1` adds the glyph's COLUMNS,
8705                    // any larger value adds ONE per printable character.
8706                    // C takes `width` as an `int` straight from the `(m)`
8707                    // flag's `multi_width` counter (c:2376), which counts
8708                    // the flag's repetitions: `(m)` is 1, `(mm)` is 2. A
8709                    // `bool` here collapsed the two, so `${(mm)#日本語}`
8710                    // answered 6 (the `(m)` column total) against zsh's 3.
8711                    if width == 1 {
8712                        num += wcw;
8713                    } else {
8714                        num += 1;
8715                    }
8716                }
8717            } else {
8718                // c:5727-5728 — one character.
8719                num += 1;
8720            }
8721            laststart = i;
8722            num_in_char = 0;
8723            complete = true;
8724        }
8725    }
8726    // c:5735-5736 — "If incomplete, treat remainder as trailing single
8727    // character".
8728    num + usize::from(num_in_char != 0)
8729}
8730
8731/// Port of `mb_charlenconv_r()` from `Src/utils.c:5747` — C decl `mb_charlenconv_r(const char *s, int slen, wint_t *wcp, mbstate_t *mbsp)`.
8732///
8733/// Multibyte char length with conversion (from utils.c mb_charlenconv_r)
8734/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp, mbsp)
8735pub fn mb_charlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
8736    mb_metacharlenconv_r(s, pos)
8737}
8738
8739/// Port of `mb_charlenconv()` from `Src/utils.c:5793` — C decl `mb_charlenconv(const char *s, int slen, wint_t *wcp)`.
8740///
8741/// Multibyte char length (from utils.c mb_charlenconv)
8742/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp)
8743pub fn mb_charlenconv(s: &str, pos: usize) -> usize {
8744    s[pos..].chars().next().map(|c| c.len_utf8()).unwrap_or(0)
8745}
8746
8747/// Port of `metacharlenconv()` from `Src/utils.c:5811` — C decl `int metacharlenconv(const char *x, int *c)`.
8748/// ```c
8749/// if (*x == Meta) {
8750///     if (c) *c = x[1] ^ 32;
8751///     return 2;
8752/// }
8753/// if (c) *c = (char)*x;
8754/// return 1;
8755/// ```
8756/// Single-byte metafied char advance — Meta+X is 2 bytes (decode
8757/// via XOR 32), plain byte is 1 byte. Previously used hardcoded
8758/// `0x83`; now routes through the canonical `Meta` const for
8759/// maintainability parity.
8760/// WARNING: param names don't match C — Rust=(s) vs C=(x, c)
8761pub fn metacharlenconv(s: &str) -> (usize, Option<char>) {
8762    // c:5811
8763    let bytes = s.as_bytes();
8764    if bytes.is_empty() {
8765        return (0, None);
8766    }
8767    if bytes[0] == Meta && bytes.len() >= 2 {
8768        // c:5818
8769        let raw = bytes[1] as u32 ^ 32; // c:5820
8770        return (2, char::from_u32(raw)); // c:5821
8771    }
8772    (1, Some(bytes[0] as char)) // c:5823-5825
8773}
8774
8775/// Port of `charlenconv()` from `Src/utils.c:5832`.
8776///
8777/// Plain (non-metafy) char advance.
8778/// Port of `charlenconv(const char *x, int len, int *c)` from Src/utils.c:5832 — the
8779/// non-MULTIBYTE_SUPPORT branch. Single-byte read with
8780/// `len`-bound check; matches the C source's `if (!len)` early
8781/// exit.
8782/// WARNING: param names don't match C — Rust=(s, len) vs C=(x, len, c)
8783pub fn charlenconv(s: &str, len: usize) -> (usize, Option<char>) {
8784    if len == 0 {
8785        return (0, None);
8786    }
8787    let bytes = s.as_bytes();
8788    if bytes.is_empty() {
8789        return (0, None);
8790    }
8791    (1, Some(bytes[0] as char))
8792}
8793
8794/// Port of `sb_niceformat()` from `Src/utils.c:5851` — C decl `size_t sb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)`.
8795/// ```c
8796/// ums = ztrdup(s); untokenize(ums); ptr = unmetafy(ums, &umlen);
8797/// while (ptr < eptr) {
8798///     int c = (unsigned char) *ptr;
8799///     if      (c == '\''  && (flags & NICEFLAG_QUOTE)) fmt = "\\'";
8800///     else if (c == '\\'  && (flags & NICEFLAG_QUOTE)) fmt = "\\\\";
8801///     else                                              fmt = nicechar_sel(c, ...);
8802/// }
8803/// ```
8804/// Single-byte nice format. **Unmetafies the input first** (c:5872),
8805/// then calls `nicechar_sel` for EVERY byte (not just controls).
8806///
8807/// Param mapping (Rule S1, faithful to C):
8808/// - `s: &str`            ← C `const char *s`
8809/// - `stream: Option<&mut dyn Write>` ← C `FILE *stream` (`None` ≡ `NULL`)
8810/// - `outstrp: Option<&mut Option<String>>` ← C `char **outstrp`
8811///   (outer `None` ≡ `NULL`; inner `Option<String>` is the storage slot
8812///   the caller passes as `&local`, mirroring `char *retstr;
8813///   sb_niceformat(s, NULL, &retstr, ...);`).
8814/// - returns `usize` ← C `size_t l`.
8815pub fn sb_niceformat(
8816    s: &str,
8817    mut stream: Option<&mut dyn std::io::Write>,
8818    outstrp: Option<&mut Option<String>>,
8819    flags: i32,
8820) -> usize {
8821    // c:5851
8822    let mut l: usize = 0; // c:5853 size_t l = 0
8823    let mut newl: usize; // c:5853 size_t newl
8824                         // c:5854 — `int umlen, outalloc, outleft;`. outalloc/outleft model
8825                         // C's buffer-growth math; Rust String auto-grows so the realloc
8826                         // loop at c:5897-5906 collapses to push_str. umlen carries through.
8827    let umlen: usize; // c:5854
8828                      // c:5855 — `char *ums, *ptr, *eptr, *fmt, *outstr, *outptr;`
8829    let mut ums: Vec<u8>; // c:5855 char *ums
8830    let mut ptr: usize; // c:5855 char *ptr
8831    let eptr: usize; // c:5855 char *eptr
8832    let mut fmt: String; // c:5855 char *fmt
8833    let mut outstr: Option<String>; // c:5855 char *outstr
8834
8835    // c:5857-5863 — `if (outstrp) outptr = outstr = zalloc(2*strlen(s));`
8836    if outstrp.is_some() {
8837        // c:5857
8838        outstr = Some(String::with_capacity(2 * s.len())); // c:5859
8839    } else {
8840        // c:5860
8841        outstr = None; // c:5862 outstr = NULL
8842    }
8843
8844    // c:5865 — `ums = ztrdup(s);`
8845    ums = s.as_bytes().to_vec();
8846    // c:5866-5870 — comment-only block carried verbatim:
8847    /*
8848     * is this necessary at this point? niceztrlen does this
8849     * but it's used in lots of places.  however, one day this may
8850     * be, too.
8851     */                                                                      // c:5866-5870
8852    // c:5871 — `untokenize(ums);`, over the bytes (see untokenize_bytes).
8853    ums = crate::token_char::untokenize_bytes(s.as_bytes());
8854    // c:5872 — `ptr = unmetafy(ums, &umlen);`
8855    umlen = unmetafy(&mut ums);
8856    ums.truncate(umlen);
8857    eptr = umlen; // c:5873 eptr = ptr + umlen
8858    ptr = 0; // c:5872 ptr starts at 0 in ums
8859
8860    while ptr < eptr {
8861        // c:5875
8862        let c: i32 = ums[ptr] as i32; // c:5876 int c = (unsigned char) *ptr
8863        if c == b'\'' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
8864            // c:5877
8865            fmt = "\\'".to_string(); // c:5878
8866            newl = 2; // c:5879
8867        } else if c == b'\\' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
8868            // c:5881
8869            fmt = "\\\\".to_string(); // c:5882
8870            newl = 2; // c:5883
8871        } else {
8872            // c:5885
8873            fmt = nicechar_sel(c as u8 as char, (flags & NICEFLAG_QUOTE) != 0); // c:5886
8874            newl = 1; // c:5887
8875        }
8876
8877        ptr += 1; // c:5890 ++ptr
8878        l += newl; // c:5891 l += newl
8879
8880        if let Some(ref mut w) = stream {
8881            // c:5893 if (stream)
8882            // c:5894 — `zputs(fmt, stream);`
8883            let _ = w.write_all(fmt.as_bytes());
8884        }
8885        if let Some(ref mut buf) = outstr {
8886            // c:5895 if (outstr)
8887            // c:5896-5912 — append fmt to outstr, growing on demand. Rust
8888            // String auto-grows; the realloc loop at c:5897-5906 collapses
8889            // to push_str (memcpy + outptr/outleft bookkeeping unneeded).
8890            buf.push_str(&fmt); // c:5907 memcpy(outptr, fmt, outlen)
8891        }
8892        // `fmt` is consumed at next iter assignment (C reuses pointer).
8893        let _ = fmt;
8894    }
8895
8896    // c:5915 — `free(ums);` (Rust drop at scope exit)
8897    drop(ums);
8898    if let Some(slot) = outstrp {
8899        // c:5916 if (outstrp)
8900        // c:5917 — `*outptr = '\0';` (no-op for String — push_str leaves
8901        // no embedded NUL, and Rust String is length-prefixed).
8902        // c:5919-5925 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping: in C this
8903        // selects between ztrdup (perm) / dupstring (heap arena) / direct
8904        // ownership-transfer (NODUP). Rust has a single allocator so all
8905        // three paths produce identical owned String contents; transfer
8906        // ownership of `outstr` into the caller's slot.
8907        *slot = outstr.take();
8908    }
8909
8910    l // c:5928 return l
8911}
8912
8913/// Port of `is_sb_niceformat()` from `Src/utils.c:5937` — C decl `int is_sb_niceformat(const char *s)`.
8914///
8915/// Predicate: would `sb_niceformat` change the input? Walks each byte
8916/// after unmetafy, returns `1` if any byte is "nice" per `is_nicechar`,
8917/// else `0` (C `int` return faithful to Rule S1).
8918pub fn is_sb_niceformat(s: &str) -> i32 {
8919    // c:5937
8920    // c:5939 — `int umlen, ret = 0;`
8921    let umlen: usize; // c:5939
8922    let mut ret: i32 = 0; // c:5939
8923                          // c:5940 — `char *ums, *ptr, *eptr;`
8924    let mut ums: Vec<u8>; // c:5940 char *ums
8925    let mut ptr: usize; // c:5940 char *ptr
8926    let eptr: usize; // c:5940 char *eptr
8927
8928    // c:5942-5943 — `ums = ztrdup(s); untokenize(ums);`
8929    ums = crate::token_char::untokenize_bytes(s.as_bytes());
8930    umlen = unmetafy(&mut ums); // c:5944 ptr = unmetafy(ums, &umlen)
8931    ums.truncate(umlen);
8932    eptr = umlen; // c:5945 eptr = ptr + umlen
8933    ptr = 0; // c:5944 ptr starts at 0 in ums
8934
8935    while ptr < eptr {
8936        // c:5947
8937        if is_nicechar(ums[ptr] as char) {
8938            // c:5948 is_nicechar(*ptr)
8939            ret = 1; // c:5949
8940            break; // c:5950
8941        }
8942        ptr += 1; // c:5952 ++ptr
8943    }
8944
8945    drop(ums); // c:5955 free(ums)
8946    ret // c:5957 return ret
8947}
8948
8949/// Port of `zexpandtabs()` from `Src/utils.c:5975`.
8950///
8951/// Tab expansion — direct port of `zexpandtabs(const char *s, int len, int width, int startpos, FILE *fout, int all)` in zsh/Src/utils.c:5975.
8952/// Writes `s` into `out` with TAB characters expanded to spaces against
8953/// a tabstop of `width`. `startpos` carries the cumulative emitted
8954/// column from previous calls (used by `print -X` which preserves
8955/// alignment across args). When `all_tabs` is false, only leading TABs
8956/// (those at the start of a line) are expanded; embedded TABs are
8957/// emitted verbatim and `startpos` is advanced by one tabstop. When
8958/// `all_tabs` is true, every TAB expands. Returns the new `startpos`.
8959pub(crate) fn zexpandtabs(
8960    s: &str,
8961    width: i32,
8962    startpos: i32,
8963    all_tabs: bool,
8964    out: &mut String,
8965) -> i32 {
8966    let mut startpos = startpos;
8967    let mut at_start = true;
8968    for c in s.chars() {
8969        if c == '\t' {
8970            if all_tabs || at_start {
8971                if width <= 0 || startpos % width == 0 {
8972                    out.push(' ');
8973                    startpos += 1;
8974                }
8975                if width > 0 {
8976                    while startpos % width != 0 {
8977                        out.push(' ');
8978                        startpos += 1;
8979                    }
8980                }
8981            } else {
8982                let rem = startpos % width;
8983                startpos += width - rem;
8984                out.push('\t');
8985            }
8986            continue;
8987        } else if c == '\n' || c == '\r' {
8988            out.push(c);
8989            startpos = 0;
8990            at_start = true;
8991            continue;
8992        }
8993        at_start = false;
8994        out.push(c);
8995        startpos += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as i32;
8996    }
8997    startpos
8998}
8999
9000/// Port of `hasspecial()` from `Src/utils.c:6072` — C decl `int hasspecial(char const *s)`.
9001/// ```c
9002/// while (*s) {
9003///     if (ispecial(*s == Meta ? *++s ^ 32 : *s)) return 1;
9004///     s++;
9005/// }
9006/// return 0;
9007/// ```
9008/// Predicate: does `s` contain any byte that needs shell-quoting?
9009/// Routes through the canonical typtab-driven `ztype_h::ispecial`
9010/// which respects the `ZTF_SP_COMMA` (set by `makecommaspecial`)
9011/// and `ZTF_BANGCHAR` (BANGHIST + interact) flags. Previously used
9012/// a hardcoded char list — diverged from C for `,` (KSH_GLOB
9013/// extended-glob) and for the dynamic bangchar (`!` by default but
9014/// user-rewritable via `$HISTCHARS`).
9015pub fn hasspecial(s: &str) -> bool {
9016    // c:6072
9017    let bytes = s.as_bytes();
9018    let mut i = 0;
9019    while i < bytes.len() {
9020        let c = if bytes[i] == Meta && i + 1 < bytes.len() {
9021            // c:6075 — `*s == Meta ? *++s ^ 32 : *s`.
9022            let v = bytes[i + 1] ^ 32;
9023            i += 2;
9024            v
9025        } else {
9026            let v = bytes[i];
9027            i += 1;
9028            v
9029        };
9030        if crate::ported::ztype_h::ispecial(c) {
9031            // c:6075
9032            return true;
9033        }
9034    }
9035    false
9036}
9037
9038/// Port of `addunprintable()` from `Src/utils.c:6083`.
9039///
9040/// Port of `static char *addunprintable(char *v, const char *u, const char *uend)`
9041/// from `Src/utils.c:6083-6124`. Renders unprintable bytes using
9042/// **shell-compatible C-string escapes**:
9043///   - `\0` (NUL, with `\000` form if next byte is octal-digit)
9044///   - `\a` (0x07 BEL), `\b` (BS), `\f` (FF), `\n` (LF),
9045///     `\r` (CR), `\t` (TAB), `\v` (VT)
9046///   - `\nnn` 3-digit octal fallback for any other control byte
9047///
9048/// Previously the Rust port emitted ZLE-style caret notation (`^X`,
9049/// `\u{:04x}`) — completely different convention. C semantics target
9050/// `printf %q` / `$'...'` reuse where the output must round-trip
9051/// through `printf %b` to reconstruct the original byte. Caret
9052/// notation is what ZLE *displays* but not what `addunprintable`
9053/// emits.
9054/// WARNING: param names don't match C — Rust=(c) vs C=(v, u, uend)
9055pub fn addunprintable(c: char) -> String {
9056    // c:6082
9057    let b = c as u32 & 0xff;
9058    match b as u8 {
9059        // c:6097-6103 — `\0`. C peeks next byte and uses `\000` form
9060        // when followed by an octal digit. Rust port works on a single
9061        // char so emits just `\0`; callers needing the disambiguation
9062        // form can pass the lookahead byte separately.
9063        0x00 => "\\0".to_string(),
9064        0x07 => "\\a".to_string(), // c:6106
9065        0x08 => "\\b".to_string(), // c:6107
9066        0x0c => "\\f".to_string(), // c:6108
9067        0x0a => "\\n".to_string(), // c:6109
9068        0x0d => "\\r".to_string(), // c:6110
9069        0x09 => "\\t".to_string(), // c:6111
9070        0x0b => "\\v".to_string(), // c:6112
9071        // c:6114-6119 — `\nnn` 3-digit octal default.
9072        _ => format!("\\{:o}{:o}{:o}", (b >> 6) & 7, (b >> 3) & 7, b & 7),
9073    }
9074}
9075
9076/// One element of a metafied string, as C's `MB_METACHARLENCONV` loop sees it:
9077/// either a decoded character, or a raw byte that is not one.
9078///
9079/// A byte that cannot appear in a Rust `String` (an invalid-UTF-8 byte such as
9080/// the 0xE1 produced by `$'\M-a'`) is stored metafied — `Meta` (0x83) followed
9081/// by `byte ^ 32` — so a plain `.chars()` walk sees two innocuous characters
9082/// where the shell means one raw byte, and passes them straight through. C hits
9083/// exactly this case as `WEOF` out of `MB_METACHARLENCONV` (c:6210) and routes
9084/// it to `addunprintable()`.
9085#[derive(Clone, Copy, PartialEq)]
9086enum MetaChar {
9087    Ch(char),
9088    Raw(u8),
9089}
9090
9091/// Port of `quotestring()` from `Src/utils.c:6141`.
9092///
9093/// Quote a string according to the specified type
9094/// Port from zsh/Src/utils.c quotestring() (lines 6141-6452)
9095/// Quote a string per the requested bslashquote style.
9096/// Port of `quotestring(const char *s, int instring)` from Src/utils.c — used by `print
9097/// -%q`, `${(q)var}`, completion-output escaping, history
9098/// re-emission.
9099/// WARNING: param names don't match C — Rust=(s, quote_type) vs C=(s, instring)
9100pub fn quotestring(s: &str, quote_type: i32) -> String {
9101    // c:6207-6210 — C walks the string with MB_METACHARINIT/MB_METACHARLENCONV,
9102    // which yields one decoded character at a time, or WEOF for a byte that does
9103    // not decode. These three closures are that walk, kept inline exactly as C
9104    // keeps it inline in each arm below.
9105    //
9106    // `meta_chars` is the decoder: a byte that cannot live in a Rust `String`
9107    // (an invalid-UTF-8 byte such as the 0xE1 from `$'\M-a'`) is stored metafied
9108    // — Meta (0x83) then `byte ^ 32` — so a naive `.chars()` walk sees two
9109    // innocuous characters where the shell means one raw byte and passes them
9110    // straight through. That is C's WEOF case, and it must reach addunprintable.
9111    //
9112    // The walk is `mb_metacharlenconv` (c:Src/utils.c:5611) over the DEMETAFIED
9113    // byte stream, which is the input C's macro actually sees. The port used to
9114    // decode each `Meta`+byte pair to its own `Raw` unit and stop there, so a
9115    // pair of pairs that spells ONE character never got assembled: `${(q)v}` for
9116    // `v=$'\xce\xb1'` emitted `$'\316'$'\261'` where zsh emits the two bytes of
9117    // `α` verbatim (`ce b1`), because C's mbrtowc joins them into a printable
9118    // wide character at c:6422 and copies the unit whole (c:6431-6433).
9119    // c:Src/utils.c:5613 — `if (!isset(MULTIBYTE) || …)`. C reads the option
9120    // through `isset(X)` = `opts[X]`, a single array load. The port went via
9121    // `opt_state_get("multibyte")`, which allocates a lowercased copy of the
9122    // name in `optlookup` and then does two String-keyed HashMap lookups
9123    // under an RwLock — on EVERY quotestring call. `quotestring` runs once
9124    // per completion match (`multiquote` → `quotestring`, compcore.c:1073),
9125    // so a 46765-match `compadd -k functions` paid that name lookup 46765
9126    // times. Same precedent as options.rs:768-770, which replaced
9127    // `isset(optlookup("shoptionletters"))` with the optno constant for
9128    // exactly this reason.
9129    let mb = isset(crate::ported::zsh_h::MULTIBYTE);
9130    // c:5583 — `mbrtowc` is locale-driven; in a single-byte locale it consumes
9131    // ONE byte and hands back that byte's value. Rust's UTF-8 decoder is not,
9132    // so read the locale once per call (an `nl_langinfo` load, no allocation)
9133    // instead of per byte. This runs once per completion match
9134    // (`multiquote` -> `quotestring`, c:Src/Zle/compcore.c:1073), which is why
9135    // it is hoisted out of the walk below.
9136    let mb_sb = mb
9137        && unsafe {
9138            // c:5583 — `mbrtowc(&wc, &inchar, 1, mbsp)` is LOCALE-driven: with
9139            // MB_CUR_MAX == 1 it consumes one byte and returns that byte as
9140            // the wide character, so `\303\255` is TWO characters under
9141            // LC_ALL=C, not `í`. Rust's UTF-8 decoder has no such notion, so
9142            // the locale has to be asked directly. Inlined rather than
9143            // factored out: src/ported/ is a port and build.rs rejects any fn
9144            // with no C counterpart (its stated remedy #1 is to inline).
9145            //
9146            // Rust never calls `setlocale` on its own, so run it once from the
9147            // environment before asking `nl_langinfo` — otherwise the startup
9148            // default ("C") would shadow the process's LC_CTYPE.
9149            let _ = *MB_LOCALE_READY;
9150            let cs_ptr = libc::nl_langinfo(libc::CODESET);
9151            if cs_ptr.is_null() {
9152                false
9153            } else {
9154                let cs = std::ffi::CStr::from_ptr(cs_ptr).to_string_lossy();
9155                !(cs.eq_ignore_ascii_case("UTF-8") || cs.eq_ignore_ascii_case("utf8"))
9156            }
9157        };
9158    let meta_chars = |s: &str| -> Vec<MetaChar> {
9159        // c:5672 — the byte stream is what mbrtowc consumes; a `Meta`+byte pair
9160        // and a literal high byte are the same input to the step below.
9161        let bytes = unmetafy_str(s);
9162        let mut out = Vec::with_capacity(bytes.len());
9163        let mut i = 0usize;
9164        while i < bytes.len() {
9165            let b = bytes[i];
9166            // c:5613 — `if (!isset(MULTIBYTE) || (unsigned char) *s <= 0x7f)`:
9167            // one byte, and `*wcp` is that byte widened. With the option off
9168            // that is EVERY byte, which is why `unsetopt multibyte;
9169            // ${(q)$'\xe6\x97\xa5'}` comes out as `e6` `$'\227'` `a5` in zsh —
9170            // 0xe6 and 0xa5 are printable Latin-1, 0x97 is a C1 control.
9171            // `mb_sb` reaches the same place through the LOCALE rather than
9172            // the option: c:5635 hands the bytes to `mb_metacharlenconv_r`,
9173            // whose `mbrtowc` (c:5583) in a single-byte locale returns 1 with
9174            // `wc` = the byte. `Raw(b)` is the right unit for it — c:6431-6433
9175            // copies the ONE source byte through, which `push_metachar` does
9176            // and `MetaChar::Ch(char::from(b))` would not (that emits the two
9177            // UTF-8 bytes of U+00`b`). The printability test below then decides
9178            // raw vs `$'\NNN'`, exactly as `WC_ISPRINT(cc)` does.
9179            if !mb || mb_sb || b <= 0x7f {
9180                out.push(if b <= 0x7f {
9181                    MetaChar::Ch(b as char)
9182                } else {
9183                    MetaChar::Raw(b)
9184                });
9185                i += 1;
9186                continue;
9187            }
9188            // c:5635 → mb_metacharlenconv_r: shortest valid multibyte prefix.
9189            let hi = (i + 4).min(bytes.len());
9190            let mut n = 0usize;
9191            for end in (i + 1)..=hi {
9192                if let Some(c) = std::str::from_utf8(&bytes[i..end])
9193                    .ok()
9194                    .and_then(|v| v.chars().next())
9195                {
9196                    out.push(MetaChar::Ch(c));
9197                    n = end - i;
9198                    break;
9199                }
9200            }
9201            if n == 0 {
9202                // c:5592-5593 — no valid sequence: WEOF, one byte.
9203                out.push(MetaChar::Raw(b));
9204                n = 1;
9205            }
9206            i += n;
9207        }
9208        out
9209    };
9210    // c:6212 / c:6424 — `cc != WEOF && WC_ISPRINT(cc)`.
9211    let mc_printable = |mc: MetaChar| -> bool {
9212        match mc {
9213            MetaChar::Ch(c) => crate::ported::compat::u9_iswprint(c),
9214            // With MULTIBYTE on an undecodable byte IS the WEOF case, so the
9215            // `cc != WEOF` conjunct short-circuits. With it off there is no
9216            // WEOF at all (c:5615-5617 always sets `*wcp` to the byte), so the
9217            // test is `WC_ISPRINT` of that Latin-1 scalar. In a single-byte
9218            // locale c:5583 `mbrtowc` decides: the ASCII C locale rejects a
9219            // byte above 0x7f (`crate::c_locale`) — WEOF, not printable.
9220            MetaChar::Raw(b) => {
9221                let converts = || {
9222                    let mut wc: libc::wchar_t = 0;
9223                    let mut mbs: MbStateBuf = MBSTATE_ZERO;
9224                    let r = unsafe {
9225                        mbrtowc(
9226                            &mut wc,
9227                            &b as *const u8 as *const libc::c_char,
9228                            1,
9229                            &mut mbs as *mut MbStateBuf as *mut libc::c_void,
9230                        )
9231                    };
9232                    r != MB_INVALID
9233                };
9234                (!mb || (mb_sb && converts()))
9235                    && crate::ported::compat::u9_iswprint(char::from(b))
9236            }
9237        }
9238    };
9239    // c:6431-6433 — `while (u < uend) { if (*u == Meta) *v++ = *u++; *v++ = *u++; }`
9240    // copies a printable unit through STILL METAFIED. A raw byte therefore has
9241    // to go back out as `Meta` + `byte ^ 32`; pushing `b as char` would emit the
9242    // UTF-8 encoding of U+00`b` instead of the one byte the shell holds.
9243    let push_metachar = |out: &mut String, mc: MetaChar| match mc {
9244        MetaChar::Ch(c) => out.push(c),
9245        MetaChar::Raw(b) => {
9246            out.push(char::from(Meta));
9247            out.push(char::from(b ^ 32));
9248        }
9249    };
9250    // c:6082-6124 addunprintable — emit a non-printable element BYTE by byte, so
9251    // a non-printable multibyte character becomes one escape per UTF-8 byte.
9252    // `\0` widens to `\000` when an octal digit follows so the escape cannot
9253    // swallow it (c:6097-6103). There is deliberately no `\e` case: C has none,
9254    // so ESC comes out as the octal `\033`.
9255    let push_unprintable = |out: &mut String, mc: MetaChar, next: Option<MetaChar>| {
9256        let mut buf = [0u8; 4];
9257        let bytes: &[u8] = match mc {
9258            MetaChar::Ch(c) => c.encode_utf8(&mut buf).as_bytes(),
9259            MetaChar::Raw(b) => {
9260                buf[0] = b;
9261                &buf[0..1]
9262            }
9263        };
9264        for i in 0..bytes.len() {
9265            let b = bytes[i];
9266            if b == 0 {
9267                out.push_str("\\0");
9268                let follows = if i + 1 < bytes.len() {
9269                    Some(bytes[i + 1])
9270                } else {
9271                    next.map(|n| match n {
9272                        MetaChar::Ch(c) => {
9273                            let mut nb = [0u8; 4];
9274                            c.encode_utf8(&mut nb).as_bytes()[0]
9275                        }
9276                        MetaChar::Raw(rb) => rb,
9277                    })
9278                };
9279                if matches!(follows, Some(b'0'..=b'7')) {
9280                    out.push_str("00");
9281                }
9282            } else {
9283                out.push_str(&addunprintable(b as char));
9284            }
9285        }
9286    };
9287
9288    // c:6141
9289    if s.is_empty() {
9290        return if quote_type == QT_NONE {
9291            String::new()
9292        } else if quote_type == QT_BACKSLASH {
9293            // c:6194 `if (!*s && shownull)` — shownull is 0 for plain
9294            // QT_BACKSLASH, so an empty string produces NO quotes.
9295            String::new()
9296        } else if quote_type == QT_BACKSLASH_SHOWNULL {
9297            // c:6165 sets shownull=1, so empty → '' (c:6194 adds the pair).
9298            "''".to_string()
9299        } else if quote_type == QT_SINGLE_OPTIONAL {
9300            // c:6187 sets `quotesub = shownull = 1`, so empty → '' (c:6194).
9301            "''".to_string()
9302        } else if quote_type == QT_SINGLE || quote_type == QT_DOUBLE || quote_type == QT_DOLLARS {
9303            // c:6194 — `if (!*s && shownull)`. `shownull` is set ONLY by
9304            // QT_BACKSLASH_SHOWNULL (c:6165) and QT_SINGLE_OPTIONAL (c:6187);
9305            // it is 0 for QT_SINGLE / QT_DOUBLE / QT_DOLLARS, which take the
9306            // `default:` arm at c:6190. Those styles return the quoted BODY
9307            // only — c:6131-6134: "Most quote styles other than backslash
9308            // assume the quotes are to be added outside quotestring()" — so an
9309            // empty input yields an empty body, and the CALLER supplies the
9310            // `''` / `""` / `$''` pair (subst.c:4083-4090, text.c:1086-1092).
9311            String::new()
9312        } else {
9313            String::new()
9314        };
9315    }
9316
9317    if quote_type == QT_NONE {
9318        s.to_string()
9319    } else if quote_type == QT_BACKSLASH_PATTERN {
9320        // Only bslashquote pattern characters (lines 6242-6247)
9321        let mut result = String::with_capacity(s.len() * 2);
9322        for c in s.chars() {
9323            if matches!(
9324                c,
9325                '*' | '?' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '#' | '^' | '~'
9326            ) {
9327                result.push('\\');
9328            }
9329            result.push(c);
9330        }
9331        result
9332    } else if quote_type == QT_BACKSLASH
9333        || quote_type == QT_BACKSLASH_SHOWNULL
9334        || quote_type == QT_SINGLE
9335        || quote_type == QT_SINGLE_OPTIONAL
9336        || quote_type == QT_DOUBLE
9337    {
9338        // c:Src/utils.c:6248-6443 — the `else` arm of quotestring's three-way
9339        // split, shared by EVERY style except QT_DOLLARS (c:6203) and
9340        // QT_BACKSLASH_PATTERN (c:6242). C runs ONE loop here and branches on
9341        // `instring` inside it. This port used to hold four separate loops —
9342        // one per style — and the three non-BACKSLASH ones had each decayed
9343        // into a plain per-character walk missing the c:6262-6299 verbatim-copy
9344        // preludes, the c:6272-6280 `$'…'`-in-double-quotes arm, the
9345        // c:6392-6416 token pass-through and the c:6396-6412 Inparmath group.
9346        // They are one loop again, exactly as C has it, so a fix to a prelude
9347        // cannot land in one style and miss the other three.
9348        //
9349        // Per-style behaviour lives entirely in the c:6307-6313 subcondition
9350        // and the two branches at c:6314 / c:6364:
9351        //   QT_BACKSLASH        every ispecial char takes a `\` (c:6307); `\n`
9352        //                       upgrades to `$'\n'` (c:6366-6371) and a
9353        //                       non-printable char to `$'…'` (c:6435-6442).
9354        //   QT_SINGLE           only `'` (c:6313), rewritten `'\''`, or `''`
9355        //                       under RCQUOTES (c:6372-6379).
9356        //   QT_DOUBLE           only `$`, `` ` ``, `"`, `\` (c:6311-6312), plus
9357        //                       the history character under BANGHIST
9358        //                       (c:6309-6310).
9359        //   QT_SINGLE_OPTIONAL  every ispecial char (c:6308), but it opens
9360        //                       `'…'` spans only where they are needed
9361        //                       (c:6314-6363).
9362        //
9363        // c:6166 — QT_BACKSLASH_SHOWNULL sets `shownull` and FALLS THROUGH to
9364        // QT_BACKSLASH, so from here the two are one style; `shownull` only
9365        // changes the empty-input case, handled at c:6194 above.
9366        let instring = if quote_type == QT_BACKSLASH_SHOWNULL {
9367            QT_BACKSLASH
9368        } else {
9369            quote_type
9370        };
9371        let mut result = String::with_capacity(s.len() * 2);
9372        // c:6304-6305 lookbehind `u[-1]`. The verbatim arms below advance `u`
9373        // several bytes at a time, so they carry it forward themselves.
9374        let mut prev: char = '\0'; // would-be u[-1]
9375        // c:6149-6156 — `quotesub` drives QT_SINGLE_OPTIONAL and nothing else:
9376        // 0 = mechanism off, 1 = pending (no `'` emitted yet; one belongs at
9377        // `quotestart`), 2 = a `'…'` span is open and gets closed at c:6445.
9378        let mut quotesub: u8 = if instring == QT_SINGLE_OPTIONAL { 1 } else { 0 }; // c:6187
9379        let mut quotestart: usize = 0; // c:6197 — byte index into `result`
9380                                   // c:6392 — `if (itok(*u) || instring != QT_BACKSLASH)`: a parser TOKEN
9381                                   // byte "needs to be passed straight through" — never backslashed and
9382                                   // never printability-tested. That half of the test was missing from
9383                                   // this arm, and `meta_chars` cannot express it: it demetafies, and a
9384                                   // token is NOT a metafied pair (this port stores C's token bytes as
9385                                   // PUA chars, crate::token_char), so `unmetafy_str` re-encodes one as its
9386                                   // UTF-8 bytes. `Inbrace` therefore fell through to the c:6435
9387                                   // not-printable arm and `quotename(Inbrace)` produced `$'\302\217'`
9388                                   // where C produces the raw byte that `untokenize` then maps back to
9389                                   // `{` — which is what the c:1931-2218 brace tail in zle_tricky.c
9390                                   // stores in `brbeg` (`ls /usr/{b<TAB>`).
9391                                   // Split the input at token chars, hand only the runs BETWEEN them to
9392                                   // `meta_chars`, and remember which units were tokens. A `Meta` byte is
9393                                   // deliberately NOT a split point (C gives it IMETA but not ITOK,
9394                                   // utils.c:4196-4201), and its partner byte is skipped so a metafied
9395                                   // pair whose second byte lands in the token range stays intact.
9396        let (mcs, tokmask): (Vec<MetaChar>, Vec<bool>) = {
9397            let mut v: Vec<MetaChar> = Vec::with_capacity(s.len());
9398            let mut m: Vec<bool> = Vec::with_capacity(s.len());
9399            let mut run = String::new();
9400            let mut prev_meta = false;
9401            for c in s.chars() {
9402                let cu = c as u32;
9403                if !prev_meta && crate::token_char::itok_char(c) {
9404                    if !run.is_empty() {
9405                        let seg = meta_chars(&run);
9406                        m.resize(m.len() + seg.len(), false);
9407                        v.extend(seg);
9408                        run.clear();
9409                    }
9410                    v.push(MetaChar::Raw(cu as u8));
9411                    m.push(true);
9412                } else {
9413                    prev_meta = !prev_meta && c == char::from(Meta);
9414                    run.push(c);
9415                }
9416            }
9417            if !run.is_empty() {
9418                let seg = meta_chars(&run);
9419                m.resize(m.len() + seg.len(), false);
9420                v.extend(seg);
9421            }
9422            (v, m)
9423        };
9424        // c:6262-6299 — before ANY quoting decision C copies three
9425        // constructs through verbatim. Each `*u` C tests there is one BYTE,
9426        // which is one element of `mcs` here. C can compare bytes directly
9427        // because a metafied string never holds a bare token byte as data;
9428        // `mcs` is DEMETAFIED, so a data byte in the token range does reach
9429        // this loop as `Raw` — every test is therefore gated on `tokmask`,
9430        // which marks the elements that really were tokens in `s`.
9431        let cval = |k: usize| -> u32 {
9432            match mcs[k] {
9433                // A token element holds its C byte; hand back its token char.
9434                MetaChar::Raw(b) if tokmask[k] => crate::token_char::token_char_from_byte(b)
9435                    .map_or(b as u32, |t| t as u32),
9436                MetaChar::Raw(b) => b as u32,
9437                MetaChar::Ch(c) => c as u32,
9438            }
9439        };
9440        let is_tok = |k: usize, t: char| -> bool { tokmask[k] && cval(k) == t as u32 };
9441        // The mirror of `is_tok`: element `k` is the literal character `ch` as
9442        // DATA. Needed because c:6272 compares against `'$'` and `'\''`, which
9443        // are ordinary bytes, not tokens.
9444        let is_ch = |k: usize, ch: char| -> bool { !tokmask[k] && cval(k) == ch as u32 };
9445        // c:6431-6433 / c:6414 — copy one element out unchanged.
9446        let verbatim = |out: &mut String, k: usize| {
9447            if tokmask[k] {
9448                out.push(char::from_u32(cval(k)).unwrap_or('\0'));
9449            } else {
9450                push_metachar(out, mcs[k]);
9451            }
9452        };
9453        let bangchar_v = crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
9454        let mut i = 0usize;
9455        while i < mcs.len() {
9456            // c:6261 — set by the c:6382-6389 arm, consumed at c:6394 or
9457            // c:6428. It is deliberately DROPPED by the c:6435 not-printable
9458            // arm, which emits `$'…'` instead of a backslash.
9459            let mut dobackslash = false;
9460            // c:6272-6280 has NO `continue`: it copies the sigil, advances,
9461            // and falls through to the c:6392 pass-through with the `'` as the
9462            // current element — so it must skip the c:6301 gate, not the tail.
9463            let mut skip_special = false;
9464            // c:6281-6299 — `$(…)`, `$[…]`, `${…}` (and the `Qstring` forms
9465            // they take inside double quotes) are already syntactically
9466            // quoted, so the whole group goes through untouched. Without this
9467            // the `(` of `echo "${(` reached the c:6301 ispecial arm and came
9468            // out `${\(`, so `$PREFIX` was `${\(` where zsh publishes `${(`
9469            // and `_brace_parameter` never saw a parameter-flag word.
9470            // (Computed up front only because Rust cannot take a block as an
9471            // `else if let` scrutinee; it is side-effect free, and the chain
9472            // below still consults it in C's order.)
9473            let opener = if (is_tok(i, Stringg) || is_tok(i, Qstring)) && i + 1 < mcs.len() {
9474                if is_tok(i + 1, Inpar) {
9475                    Some((Inpar, Outpar))
9476                } else if is_tok(i + 1, Inbrack) {
9477                    Some((Inbrack, Outbrack))
9478                } else if is_tok(i + 1, Inbrace) {
9479                    Some((Inbrace, Outbrace))
9480                } else {
9481                    None
9482                }
9483            } else {
9484                None
9485            };
9486            // c:6262-6271 — a backtick group is copied whole, and C emits the
9487            // closing tick even when the input ran out before one appeared.
9488            if is_tok(i, Tick) || is_tok(i, Qtick) {
9489                let tick = if is_tok(i, Tick) { Tick } else { Qtick };
9490                result.push(tick);
9491                i += 1;
9492                while i < mcs.len() && !is_tok(i, tick) {
9493                    verbatim(&mut result, i);
9494                    prev = char::from_u32(cval(i)).unwrap_or('\0');
9495                    i += 1;
9496                }
9497                result.push(tick); // c:6268
9498                prev = tick;
9499                if i < mcs.len() {
9500                    i += 1; // c:6269-6270
9501                }
9502                continue;
9503            } else if instring == QT_DOUBLE
9504                && i + 1 < mcs.len()
9505                && (is_tok(i, Qstring) || is_ch(i, '$'))
9506                && is_ch(i + 1, '\'')
9507            {
9508                // c:6272-6280 — `$'…'` needs no quoting inside a double-quoted
9509                // string, so the sigil goes through bare. C copies only the
9510                // `$`/`Qstring` here; the `'` is handled by the c:6392
9511                // pass-through on the next statement, which is why there is no
9512                // `continue` and why the c:6301 gate has to be skipped.
9513                verbatim(&mut result, i); // c:6280
9514                prev = char::from_u32(cval(i)).unwrap_or('\0');
9515                i += 1;
9516                skip_special = true;
9517            } else if let Some((open, close)) = opener {
9518                // c:6283-6286 — `beg` is the `String`/`Qstring` byte, and it is
9519                // what raises the nesting level, not the bracket itself.
9520                let beg = if is_tok(i, Stringg) { Stringg } else { Qstring };
9521                let mut level = 0i32;
9522                result.push(beg); // c:6288
9523                result.push(open); // c:6289
9524                i += 2;
9525                // c:6290-6296
9526                prev = open;
9527                while i < mcs.len() && (!is_tok(i, close) || level != 0) {
9528                    if is_tok(i, beg) {
9529                        level += 1;
9530                    } else if is_tok(i, close) {
9531                        level -= 1;
9532                    }
9533                    verbatim(&mut result, i);
9534                    prev = char::from_u32(cval(i)).unwrap_or('\0');
9535                    i += 1;
9536                }
9537                // c:6297-6298 — the closer, only when the input actually had one.
9538                if i < mcs.len() {
9539                    verbatim(&mut result, i);
9540                    prev = char::from_u32(cval(i)).unwrap_or('\0');
9541                    i += 1;
9542                }
9543                continue;
9544            }
9545            let mc = mcs[i];
9546            let c = match mc {
9547                MetaChar::Ch(c) => c,
9548                // A raw byte is never `\n` and never ispecial — it is the
9549                // not-printable case below. Stand in with its byte value so the
9550                // `=`/`~` lookbehind still advances correctly.
9551                MetaChar::Raw(b) => b as char,
9552            };
9553            if !skip_special {
9554                // c:6301 — `ispecial(*u)`. A token is excluded because C's
9555                // typtab carries ISPECIAL only for SPECCHARS (zsh.h:228), the
9556                // comma (c:4256) and the history character (c:4259) — all
9557                // ASCII — so `ispecial(Inbrace)` is false in C too.
9558                let is_sp = matches!(mc, MetaChar::Ch(_)) && !tokmask[i] && ispecial(c);
9559                // c:6302-6306 gate for `=`/`~` — mid-word they are left bare
9560                // unless the char is at position 0 (c:6303), MAGICEQUALSUBST is
9561                // on and the previous byte is `=` or `:` (c:6304-6305), or it
9562                // is `~` under EXTENDEDGLOB (c:6306).
9563                let eq_tilde_gate = if c == '=' || c == '~' {
9564                    i == 0 // c:6303 u == s
9565                        || (isset(MAGICEQUALSUBST) && (prev == '=' || prev == ':')) // c:6304-6305
9566                        || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
9567                } else {
9568                    true // c:6302 *u != '=' && *u != '~'
9569                };
9570                // c:6307-6313 — the per-style subcondition.
9571                let style_gate = instring == QT_BACKSLASH // c:6307
9572                    || instring == QT_SINGLE_OPTIONAL // c:6308
9573                    || (isset(BANGHIST)
9574                        && bangchar_v != 0
9575                        && c as u32 == bangchar_v as u32
9576                        && instring != QT_SINGLE) // c:6309-6310
9577                    || (instring == QT_DOUBLE && matches!(c, '$' | '`' | '"' | '\\')) // c:6311-6312
9578                    || (instring == QT_SINGLE && c == '\''); // c:6313
9579                if is_sp && eq_tilde_gate && style_gate {
9580                    if instring == QT_SINGLE_OPTIONAL {
9581                        // c:6314-6363 — minimum quoting. `quotestart` is where
9582                        // an opening `'` would be back-filled, so a run of
9583                        // ordinary characters ends up INSIDE the span rather
9584                        // than each special char getting its own `'…'`.
9585                        if quotesub == 1 {
9586                            if c == '\'' {
9587                                // c:6319-6323 — a bare `'` is cheaper as `\'`
9588                                // than as an opened-and-closed span, so no
9589                                // span starts here and quotesub stays 1.
9590                                result.push('\\');
9591                            } else {
9592                                // c:6328-6337 — time to quote: insert the
9593                                // opening `'` at `quotestart`, shifting
9594                                // everything after it right (C's addq loop).
9595                                result.insert(quotestart, '\''); // c:6335
9596                                quotesub = 2; // c:6337
9597                            }
9598                            verbatim(&mut result, i); // c:6339
9599                            prev = c;
9600                            i += 1;
9601                            quotestart = result.len(); // c:6343
9602                        } else if c == '\'' {
9603                            if unset(RCQUOTES) {
9604                                // c:6346-6348 — close the span, then `\'`.
9605                                result.push('\''); // c:6346
9606                                result.push('\\'); // c:6347
9607                                result.push('\''); // c:6348
9608                                quotesub = 1; // c:6350
9609                                quotestart = result.len(); // c:6351
9610                            } else {
9611                                // c:6353-6355 — inside a span, `''` IS a
9612                                // literal `'`, so the span never breaks.
9613                                result.push('\''); // c:6354
9614                                result.push('\''); // c:6355
9615                            }
9616                            prev = c;
9617                            i += 1; // c:6358
9618                        } else {
9619                            // c:6360-6361 — already quoting, just add.
9620                            verbatim(&mut result, i);
9621                            prev = c;
9622                            i += 1;
9623                        }
9624                        continue; // c:6363
9625                    } else if c == '\n' || (instring == QT_SINGLE && c == '\'') {
9626                        // c:6364-6381
9627                        if c == '\n' {
9628                            // c:6366-6371 — a newline cannot be backslashed,
9629                            // so it is upgraded to a `$'\n'` island.
9630                            result.push_str("$'\\n'");
9631                        } else if unset(RCQUOTES) {
9632                            // c:6372-6377 — `'` → `'\''`.
9633                            result.push('\''); // c:6373
9634                            if c == '\'' {
9635                                result.push('\\'); // c:6374-6375
9636                            }
9637                            result.push(c); // c:6376
9638                            result.push('\''); // c:6377
9639                        } else {
9640                            // c:6378-6379 — RCQUOTES: `''` is a literal `'`.
9641                            result.push('\'');
9642                            result.push('\'');
9643                        }
9644                        prev = c;
9645                        i += 1; // c:6380
9646                        continue; // c:6381
9647                    } else {
9648                        // c:6382-6388 — a backslash is owed, but it is not
9649                        // emitted yet: if the character turns out not to be
9650                        // printable the c:6435 arm upgrades it to `$'…'` and
9651                        // drops the backslash entirely.
9652                        dobackslash = true;
9653                    }
9654                }
9655            }
9656            // c:6392 — `if (itok(*u) || instring != QT_BACKSLASH)`. Everything
9657            // that is not a plain QT_BACKSLASH character is passed straight
9658            // through here; only QT_BACKSLASH reaches the printability test.
9659            if tokmask[i] || instring != QT_BACKSLASH {
9660                if dobackslash {
9661                    result.push('\\'); // c:6394-6395
9662                }
9663                if is_tok(i, Inparmath) {
9664                    // c:6396-6412 — `$((…))` is already syntactically quoted,
9665                    // so the whole math group is copied out with no further
9666                    // quoting. C counts nesting on Inparmath/Outparmath and
9667                    // stops at the matching close or at end of string.
9668                    let mut inmath = 1i32; // c:6401
9669                    verbatim(&mut result, i); // c:6402
9670                    prev = char::from_u32(cval(i)).unwrap_or('\0');
9671                    i += 1;
9672                    loop {
9673                        // c:6404-6407 — C reads `uc = *u` and breaks AFTER
9674                        // copying it; a NUL there is the end of the string,
9675                        // which is `i == mcs.len()` here.
9676                        if i >= mcs.len() {
9677                            break;
9678                        }
9679                        let was_out = is_tok(i, Outparmath);
9680                        let was_in = is_tok(i, Inparmath);
9681                        verbatim(&mut result, i); // c:6405
9682                        prev = char::from_u32(cval(i)).unwrap_or('\0');
9683                        i += 1;
9684                        if was_out {
9685                            // c:6408-6409
9686                            inmath -= 1;
9687                            if inmath == 0 {
9688                                break;
9689                            }
9690                        } else if was_in {
9691                            inmath += 1; // c:6410-6411
9692                        }
9693                    }
9694                } else {
9695                    verbatim(&mut result, i); // c:6413-6414
9696                    prev = c;
9697                    i += 1;
9698                }
9699                continue; // c:6415
9700            }
9701            // c:6418-6427 — QT_BACKSLASH only: is the element printable in the
9702            // current character set? (`uend = u + MB_METACHARLENCONV(u, &cc)`
9703            // then `cc != WEOF && WC_ISPRINT(cc)`; `meta_chars` already did the
9704            // decode, so the test is `mc_printable`.)
9705            if mc_printable(mc) {
9706                // c:6428-6433 — owed backslash, then the unit copied through
9707                // still metafied.
9708                if dobackslash {
9709                    result.push('\\'); // c:6428-6429
9710                }
9711                push_metachar(&mut result, mc); // c:6430-6433
9712            } else {
9713                // c:6435-6442 — not printable: one `$'…'` island per element,
9714                // built by addunprintable. The owed backslash is dropped.
9715                result.push_str("$'"); // c:6437-6438
9716                push_unprintable(&mut result, mc, mcs.get(i + 1).copied()); // c:6439
9717                result.push('\''); // c:6440
9718            }
9719            prev = c;
9720            i += 1;
9721        }
9722        // c:6445-6446 — QT_SINGLE_OPTIONAL left a span open.
9723        if quotesub == 2 {
9724            result.push('\'');
9725        }
9726        result
9727    } else if quote_type == QT_DOLLARS {
9728        // c:6203-6241 — `$'…'` quoting. Each element is either printable, in
9729        // which case it is emitted verbatim (backslashing `\`, `'`, and — under
9730        // BANGHIST — the history character), or it is NOT, in which case C hands
9731        // its raw bytes to addunprintable(): a named escape (`\t`, `\n`, `\a`, …)
9732        // or a 3-digit octal one.
9733        //
9734        // Two things this must NOT do, both of which the previous port did:
9735        //   * pass an undecodable byte through raw. `$'\M-a'` is the byte 0xE1,
9736        //     which is not valid UTF-8 and is stored metafied; walking `.chars()`
9737        //     re-emitted it verbatim instead of `\341`, so the output could not
9738        //     be re-parsed.
9739        //   * emit `\e` for ESC. C's addunprintable has no `\e` case — ESC comes
9740        //     out as the octal `\033`.
9741        //
9742        // c:6131-6134 — the `$'` … `'` wrapper is added by the CALLER
9743        // (subst.c:4085-4090 writes the quote pair and then `val[0] = '$'`),
9744        // so this arm returns the BODY only.
9745        let mut result = String::with_capacity(s.len() + 4);
9746        // c:6204-6209 — "The only way to get Nularg here is when it is
9747        // placeholding for the empty string": `if (inull(*u)) u++;`. Without
9748        // the skip, `${(qqqq):-""}` quoted the Nularg itself (`$'¡'` under LANG=C).
9749        let s = match s.chars().next() {
9750            Some(c) if crate::token_char::token_byte(c).is_some_and(crate::ported::ztype_h::inull) => {
9751                &s[c.len_utf8()..]
9752            }
9753            _ => s,
9754        };
9755        let mcs = meta_chars(s);
9756        let bang = crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
9757        for i in 0..mcs.len() {
9758            let mc = mcs[i];
9759            if mc_printable(mc) {
9760                // c:6214-6224. The escape test looks at the DECODED character
9761                // (`cc`), so a raw byte — which only reaches here with MULTIBYTE
9762                // off — can never be `\`, `'` or the history bang, all ASCII.
9763                if let MetaChar::Ch(c) = mc {
9764                    if c == '\\'
9765                        || c == '\''
9766                        || (isset(BANGHIST) && bang != 0 && c as u32 == bang as u32)
9767                    {
9768                        result.push('\\');
9769                    }
9770                }
9771                // c:6224-6225 — `while (u < uend) *v++ = *u++;`
9772                push_metachar(&mut result, mc);
9773            } else {
9774                // c:6226-6229
9775                push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
9776            }
9777        }
9778        result
9779    } else if quote_type == QT_BACKTICK {
9780        // c:6199 — C's DPUTS rejects QT_BACKTICK as a `quotestring` mode; the
9781        // only caller that asks for it wants the backticks escaped.
9782        s.replace('`', "\\`")
9783    } else {
9784        // Unknown quote_type — treat as no-op to match C's `default:` arm.
9785        s.to_string()
9786    }
9787}
9788
9789// ===========================================================
9790// xtrace helpers moved from src/ported/vm_helper.
9791// printprompt4 is a direct port of utils.c:1718-1735; quotedzputs
9792// is its argument-formatter companion (zsh formats `set -x` lines
9793// via the same utils.c path).
9794// ===========================================================
9795
9796/// Port of `quotedzputs()` from `Src/utils.c:6464` — C decl `quotedzputs(char const *s, FILE *stream)`.
9797///
9798/// Quote a string for re-readable output (`set -x`, `typeset -p`,
9799/// `set` listing, etc.). zsh's algorithm under MULTIBYTE_SUPPORT
9800/// (c:6464-6543):
9801///   1. empty input → `''`                                     (c:6470-6475)
9802///   2. needs nice-format (controls / non-printables) → emit
9803///      `$'<mb_niceformat output>'`                            (c:6478-6492)
9804///   3. no SPECCHARS member → return string unchanged          (c:6511-6517)
9805///   4. otherwise wrap in `'…'` (Bourne) or RCQUOTES form,
9806///      with embedded `'` rewritten as `'\''` / `''`           (c:6533-6587)
9807///
9808/// Previous Rust port omitted step 2 (the `$'…'` branch).
9809/// Result: strings containing control bytes (`\n`, `\t`, escape
9810/// sequences) were single-quoted instead of `$'…'`-quoted,
9811/// breaking round-trip through `typeset -p` / `set` / `set -x`
9812/// because POSIX single-quotes are *strong* — embedded `\n` would
9813/// be re-fed as a literal newline rather than the C-escape.
9814///
9815/// The C signature is `char *quotedzputs(char const *s, FILE *stream)`
9816/// — when `stream` is non-NULL it writes there and returns NULL,
9817/// otherwise it returns the quoted string. Rust's variant covers
9818/// only the `stream==NULL` form (the `set -x` callers all want the
9819/// string back, not direct stdout writing). The `stream==Some` form
9820/// is fputs/fputc-direct in C; Rust callers that need writer-based
9821/// output should compose `print!`/`write!` with this fn's return.
9822/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream)
9823pub(crate) fn quotedzputs(s: &str) -> String {
9824    // c:6464
9825    // c:6469-6475 — `if (!*s)` empty string emits `''` literal.
9826    if s.is_empty() {
9827        return "''".to_string(); // c:6472
9828    }
9829    // c:6477-6508 — `is_mb_niceformat(s)` / `is_sb_niceformat(s)` arm:
9830    // if the string contains nice-formatted chars (controls,
9831    // non-printables), wrap in `$'…'` using sb/mb_niceformat with
9832    // NICEFLAG_QUOTE so embedded `'`/`\` get backslash-escaped.
9833    if is_mb_niceformat(s) != 0 {
9834        // c:6478-6492 (MULTIBYTE_SUPPORT branch): use mb_niceformat
9835        //   with NICEFLAG_QUOTE so multi-byte chars round-trip through
9836        //   wcs_nicechar (raw UTF-8 for printable wides, `\u`/`\U` for
9837        //   large codepoints) AND `'`/`\\` get backslash-escaped.
9838        // Under !MULTIBYTE_SUPPORT (c:6494-6508) C would use
9839        //   sb_niceformat instead. Static-link path: MULTIBYTE is
9840        //   always available in Rust.
9841        // c:6485-6492 — `mb_niceformat(s, NULL, &substr,
9842        //                              NICEFLAG_QUOTE|NICEFLAG_NODUP);`
9843        let mut substr: Option<String> = None;
9844        let _ = mb_niceformat(s, None, Some(&mut substr), NICEFLAG_QUOTE | NICEFLAG_NODUP);
9845        return format!("$'{}'", substr.unwrap_or_default()); // c:6488
9846    }
9847    // c:6511-6518 — `if (!hasspecial(s)) return dupstring(s);`.
9848    if !hasspecial(s) {
9849        // c:6511
9850        return s.to_string(); // c:6516
9851    }
9852    // c:6520-6529 — outstr buffer alloc (zhalloc). Rust uses growable
9853    // String; the C `l = strlen(s) + 2 + (per-' overhead)` size hint
9854    // is just allocation tuning and doesn't affect output.
9855    let mut out = String::with_capacity(s.len() + 2);
9856    let bytes = s.as_bytes();
9857    let csh_junkie = isset(CSHJUNKIEQUOTES); // c:6554 / c:6612
9858
9859    if isset(RCQUOTES) {
9860        // c:6533 — RCQUOTES: wrap entire string in `'…'`; each
9861        // embedded `'` becomes `''` (the rc-style doubled quote).
9862        out.push('\''); // c:6539
9863                        // c:6540-6547 — C walks METAFIED bytes; the Rust String is
9864                        // UTF-8, so the faithful transposition walks CHARS (a byte
9865                        // walk Latin-1-casts every multibyte char: em-dash E2 80 94
9866                        // became "â" + a token byte that downstream passes ate).
9867                        // Token chars (Dash U+e19B, Meta U+0083) are codepoints here.
9868        let chars_v: Vec<char> = s.chars().collect();
9869        let mut i = 0;
9870        while i < chars_v.len() {
9871            // c:6565-6569 — a decoded byte with imeta(c) goes back out as
9872            // Meta + c^32, i.e. the pair it came in as.
9873            let meta_pair = chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len();
9874            let c = if chars_v[i] == Dash {
9875                // c:6541 — `if (*s == Dash) c = '-';`
9876                i += 1;
9877                '-'
9878            } else if meta_pair {
9879                // c:6543 — `else if (*s == Meta) c = *++s ^ 32;`
9880                let n = chars_v[i + 1] as u32;
9881                i += 2;
9882                char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
9883            } else {
9884                // c:6546 — `else c = *s;`
9885                let dec = chars_v[i];
9886                i += 1;
9887                dec
9888            };
9889            if c == '\'' {
9890                // c:6548-6553 — `if (c == '\'') *ptr++ = '\'';` (the
9891                // rc-quote doubling).
9892                out.push('\''); // c:6553
9893            } else if c == '\n' && csh_junkie {
9894                // c:6554-6560 — `if (c == '\n' && isset(CSHJUNKIEQUOTES))
9895                //                 *ptr++ = '\\';`
9896                out.push('\\'); // c:6559
9897            }
9898            // c:6561-6570 — emit c (metafy on imeta).
9899            if meta_pair {
9900                out.push(char::from(Meta)); // c:6566
9901                out.push(chars_v[i - 1]); // c:6567 — c ^ 32
9902            } else {
9903                out.push(c); // c:6569
9904            }
9905        }
9906        out.push('\''); // c:6576
9907    } else {
9908        // c:6578-6637 — Bourne-style quoting, "avoiding empty quoted
9909        // strings". Tracks `inquote` so that `it's` becomes
9910        // `'it'\''s'` (no empty `''` runs).
9911        let mut inquote = false; // c:6466 (initialised at top of C fn)
9912                                 // c:6579-6586 — char-wise decode, same transposition as the
9913                                 // RCQUOTES arm above (C's byte walk is over metafied bytes).
9914        let chars_v: Vec<char> = s.chars().collect();
9915        let mut i = 0;
9916        while i < chars_v.len() {
9917            // c:6623-6627 — imeta(c) re-emits the Meta pair, as above.
9918            let meta_pair = chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len();
9919            let c = if chars_v[i] == Dash {
9920                i += 1;
9921                '-' // c:6581
9922            } else if meta_pair {
9923                let n = chars_v[i + 1] as u32; // c:6583
9924                i += 2;
9925                char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
9926            } else {
9927                let dec = chars_v[i]; // c:6585
9928                i += 1;
9929                dec
9930            };
9931            if c == '\'' {
9932                // c:6587-6602 — `'` closes any open inquote then emits
9933                // `\'` outside the quotes.
9934                if inquote {
9935                    out.push('\''); // c:6593
9936                    inquote = false; // c:6594
9937                }
9938                out.push('\\'); // c:6600
9939                out.push('\''); // c:6601
9940            } else {
9941                // c:6603-6629 — other chars open a quote run if not
9942                // already open, optionally backslash-escape `\n` under
9943                // CSHJUNKIEQUOTES, then emit the byte.
9944                if !inquote {
9945                    out.push('\''); // c:6609
9946                    inquote = true; // c:6610
9947                }
9948                if c == '\n' && csh_junkie {
9949                    out.push('\\'); // c:6617
9950                }
9951                if meta_pair {
9952                    out.push(char::from(Meta)); // c:6624
9953                    out.push(chars_v[i - 1]); // c:6625 — c ^ 32
9954                } else {
9955                    out.push(c); // c:6627
9956                }
9957            }
9958        }
9959        if inquote {
9960            out.push('\''); // c:6636
9961        }
9962    }
9963    // c:6639-6640 — `if (!stream) *ptr++ = '\0';` — Rust String already
9964    // NUL-terminated implicitly; no-op.
9965    out // c:6642
9966}
9967
9968/// Port of `dquotedztrdup()` from `Src/utils.c:6649` — C decl `char *dquotedztrdup(char const *s)`.
9969/// Two arms (selected by `isset(CSHJUNKIEQUOTES)` at c:6655):
9970///
9971/// **CSHJUNKIEQUOTES path** (c:6656-6686): the csh-junk-quote style
9972/// where only the non-special sections are wrapped in `"..."` and
9973/// special chars (`"`, `$`, `` ` ``) appear OUTSIDE the quotes with
9974/// backslash escape. `\n` inside the quotes gets an extra `\` so it
9975/// round-trips through history.
9976///
9977/// **Default path** (c:6687-6719): wraps the whole string in `"..."`.
9978/// `\` is doubled to `\\`. `"`, `$`, `` ` `` get backslash-escaped.
9979/// A trailing `\` gets an extra `\` appended (the `pending` quirk).
9980///
9981/// Previously the Rust port only implemented the default arm; the
9982/// CSHJUNKIEQUOTES path is now ported faithfully.
9983pub fn dquotedztrdup(s: &str) -> String {
9984    // c:6648
9985    let mut out = String::with_capacity(s.len() * 4 + 2);
9986    let bytes = s.as_bytes();
9987    // c:6655 — `if (isset(CSHJUNKIEQUOTES))`.
9988    if isset(CSHJUNKIEQUOTES) {
9989        let mut inquote = false;
9990        let mut i = 0;
9991        while i < bytes.len() {
9992            // c:6661-6662 — Meta byte decode.
9993            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
9994                i += 2;
9995                (bytes[i - 1] ^ 32) as char
9996            } else {
9997                i += 1;
9998                bytes[i - 1] as char
9999            };
10000            match c {
10001                // c:6664-6673 — `"` / `$` / `` ` `` — close quote
10002                // (if open), then `\<c>`.
10003                '"' | '$' | '`' => {
10004                    if inquote {
10005                        out.push('"');
10006                        inquote = false;
10007                    }
10008                    out.push('\\');
10009                    out.push(c);
10010                }
10011                // c:6674-6682 — default arm: open quote if needed,
10012                // backslash-escape newline, emit char.
10013                _ => {
10014                    if !inquote {
10015                        out.push('"');
10016                        inquote = true;
10017                    }
10018                    if c == '\n' {
10019                        out.push('\\');
10020                    }
10021                    out.push(c);
10022                }
10023            }
10024        }
10025        // c:6685-6686 — close trailing quote.
10026        if inquote {
10027            out.push('"');
10028        }
10029    } else {
10030        // c:6687-6718 — default (non-CSH) arm.
10031        out.push('"');
10032        let mut pending = false;
10033        let mut i = 0;
10034        while i < bytes.len() {
10035            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
10036                i += 2;
10037                (bytes[i - 1] ^ 32) as char
10038            } else {
10039                i += 1;
10040                bytes[i - 1] as char
10041            };
10042            match c {
10043                '\\' => {
10044                    if pending {
10045                        out.push('\\');
10046                    }
10047                    out.push('\\');
10048                    pending = true;
10049                }
10050                '"' | '$' | '`' => {
10051                    if pending {
10052                        out.push('\\');
10053                    }
10054                    out.push('\\');
10055                    out.push(c);
10056                    pending = false;
10057                }
10058                other => {
10059                    out.push(other);
10060                    pending = false;
10061                }
10062            }
10063        }
10064        if pending {
10065            out.push('\\');
10066        }
10067        out.push('"');
10068    }
10069    // c:6720 — `ret = metafy(buf, p - buf, META_DUP);` re-metafy result.
10070    metafy(&out)
10071}
10072
10073/// Port of `dquotedzputs()` from `Src/utils.c:6729`.
10074///
10075/// Port of `dquotedzputs(char const *s, FILE *stream)` from
10076/// Src/utils.c:6729. C body (4 lines):
10077///     `char *d = dquotedztrdup(s);
10078///      int ret = zputs(d, stream);
10079///      zsfree(d);
10080///      return ret;`
10081/// Rust returns the quoted string directly (callers compose via
10082/// `format!` rather than streaming through a FILE*), so the zputs
10083/// call drops.
10084pub fn dquotedzputs(s: &str) -> String {
10085    // c:6729
10086    dquotedztrdup(s) // c:6731
10087}
10088
10089/// Port of `ucs4toutf8()` from `Src/utils.c:6743` — C decl `ucs4toutf8(char *dest, unsigned int wval)`.
10090///
10091/// Convert UCS-4 to UTF-8 (from utils.c ucs4toutf8)
10092///
10093/// C accepts 0..=0x7FFFFFFF (legacy UCS-4 / 6-byte UTF-8) — wider
10094/// than Unicode's 0..=0x10FFFF — to match `wctomb(3)` on Linux
10095/// with UTF-8 locale. The Rust `char::from_u32` path would reject
10096/// codepoints above 0x10FFFF (and surrogates), so this port mirrors
10097/// the C bit-pattern dispatch directly.
10098///
10099/// C body (c:6743-6779):
10100/// ```c
10101/// if (wval < 0x80) len = 1;            // ASCII
10102/// else if (wval < 0x800) len = 2;
10103/// else if (wval < 0x10000) len = 3;
10104/// else if (wval < 0x200000) len = 4;
10105/// else if (wval < 0x4000000) len = 5;
10106/// else if (wval < 0x80000000) len = 6;
10107/// else { zerr("character not in range"); return -1; }
10108///
10109/// switch (len) { /* falls through except to the last case */
10110/// case 6: dest[5] = (wval & 0x3f) | 0x80; wval >>= 6;
10111/// case 5: dest[4] = (wval & 0x3f) | 0x80; wval >>= 6;
10112/// case 4: dest[3] = (wval & 0x3f) | 0x80; wval >>= 6;
10113/// case 3: dest[2] = (wval & 0x3f) | 0x80; wval >>= 6;
10114/// case 2: dest[1] = (wval & 0x3f) | 0x80; wval >>= 6;
10115///     *dest = wval | ((0xfc << (6 - len)) & 0xfc);
10116///     break;
10117/// case 1: *dest = wval;
10118/// }
10119/// return len;
10120/// ```
10121///
10122/// Returns the encoded bytes (1..=6 long) on success; out of range it
10123/// raises C's `zerr("character not in range")` (c:6763) and returns None.
10124/// WARNING: param names don't match C — Rust=(wval) vs C=(dest, wval)
10125pub fn ucs4toutf8(wval: u32) -> Option<Vec<u8>> {
10126    // c:6743
10127    let len: usize = if wval < 0x80 {
10128        1
10129    }
10130    // c:6750
10131    else if wval < 0x800 {
10132        2
10133    }
10134    // c:6752
10135    else if wval < 0x10000 {
10136        3
10137    }
10138    // c:6754
10139    else if wval < 0x200000 {
10140        4
10141    }
10142    // c:6756
10143    else if wval < 0x4000000 {
10144        5
10145    }
10146    // c:6758
10147    else if wval < 0x80000000 {
10148        6
10149    }
10150    // c:6760
10151    else {
10152        // c:6762
10153        zerr("character not in range"); // c:6763
10154        return None; // c:6764
10155    };
10156    let mut buf = [0u8; 6];
10157    let mut w = wval;
10158    // c:6767-6776 — fall-through switch building trailing bytes first.
10159    match len {
10160        1 => {
10161            buf[0] = w as u8;
10162        } // c:6775
10163        n => {
10164            // Trailing (len-1) bytes: each is `(w & 0x3f) | 0x80`,
10165            // shifting w right 6 between them (c:6768-6772).
10166            for i in (1..n).rev() {
10167                buf[i] = ((w & 0x3f) as u8) | 0x80;
10168                w >>= 6;
10169            }
10170            // Leading byte: `w | ((0xfc << (6 - len)) & 0xfc)` (c:6773).
10171            buf[0] = (w as u8) | (((0xfcu32 << (6 - n)) & 0xfc) as u8);
10172        }
10173    }
10174    // c:6777 — the bytes go to `dest` as built. A surrogate's 3-byte
10175    // form is not UTF-8 a `String` can hold, so they stay bytes here.
10176    Some(buf[..len].to_vec())
10177}
10178
10179/// Port of `ucs4tomb()` from `Src/utils.c:6788` — C decl `ucs4tomb(unsigned int wval, char *buf)`.
10180///
10181/// Encode a UCS-4 codepoint into the buffer `buf` using the current
10182/// locale's multibyte encoding. Returns the number of bytes written,
10183/// or -1 on conversion failure (after `zerr("character not in range")`).
10184///
10185/// C picks one of two bodies at compile time on `__STDC_ISO_10646__`
10186/// (c:6790): `wctomb(3)` where it is defined, else `nl_langinfo(CODESET)`
10187/// → `ucs4toutf8` for "UTF-8" and `iconv(3)` from "UCS-4BE" for any other
10188/// codeset. glibc defines the macro; macOS libc does NOT (checked with
10189/// `cc` on Darwin 25), so the zsh macOS builds run the iconv body, which
10190/// rejects every value above 0x7f in the C locale ("US-ASCII"). macOS
10191/// `wctomb` instead accepts 0x80-0xff as raw bytes there, so taking the
10192/// wctomb body everywhere let `LC_ALL=C; : ${(#X):-0x80}` succeed.
10193pub fn ucs4tomb(wval: u32, buf: &mut [u8]) -> i32 {
10194    // c:6790-6795 — `#if … && defined(__STDC_ISO_10646__)`.
10195    #[cfg(any(target_os = "linux", target_os = "android"))]
10196    {
10197        // wchar_t is i32 on the Linux targets; declare the POSIX prototype
10198        // directly (libc crate does not expose wctomb on all platforms).
10199        extern "C" {
10200            fn wctomb(s: *mut libc::c_char, wc: libc::wchar_t) -> libc::c_int;
10201        }
10202        let mut local = [0 as libc::c_char; 16];
10203        let count = unsafe { wctomb(local.as_mut_ptr(), wval as libc::wchar_t) }; // c:6791
10204        if count < 0 || count as usize > buf.len() {
10205            zerr("character not in range"); // c:6793
10206            return -1;
10207        }
10208        for i in 0..count as usize {
10209            buf[i] = local[i] as u8;
10210        }
10211        count // c:6794
10212    }
10213    // c:6795-6863 — the `!__STDC_ISO_10646__` body.
10214    #[cfg(not(any(target_os = "linux", target_os = "android")))]
10215    {
10216        #[link(name = "iconv")]
10217        extern "C" {
10218            fn iconv_open(tocode: *const libc::c_char, fromcode: *const libc::c_char) -> *mut libc::c_void;
10219            fn iconv(
10220                cd: *mut libc::c_void,
10221                inbuf: *mut *mut libc::c_char,
10222                inbytesleft: *mut libc::size_t,
10223                outbuf: *mut *mut libc::c_char,
10224                outbytesleft: *mut libc::size_t,
10225            ) -> libc::size_t;
10226            fn iconv_close(cd: *mut libc::c_void) -> libc::c_int;
10227        }
10228        // c:6797 — `nl_langinfo(CODESET)`.
10229        let codeset: String = unsafe {
10230            let p = libc::nl_langinfo(libc::CODESET);
10231            if p.is_null() {
10232                String::new()
10233            } else {
10234                std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
10235            }
10236        };
10237        if codeset == "UTF-8" {
10238            // c:6798 — `return ucs4toutf8(buf, wval);`
10239            let Some(enc) = ucs4toutf8(wval) else {
10240                return -1;
10241            };
10242            let b = enc.as_slice();
10243            if b.len() > buf.len() {
10244                zerr("character not in range");
10245                return -1;
10246            }
10247            buf[..b.len()].copy_from_slice(b);
10248            return b.len() as i32;
10249        }
10250        // c:6823-6826 — ICONV_FROM_LIBICONV: an empty codeset is US-ASCII.
10251        let codeset = if codeset.is_empty() { "US-ASCII".to_string() } else { codeset };
10252        let to = std::ffi::CString::new(codeset.clone()).unwrap_or_default();
10253        let from = std::ffi::CString::new("UCS-4BE").unwrap_or_default();
10254        let mut cd = unsafe { iconv_open(to.as_ptr(), from.as_ptr()) }; // c:6827
10255        if cd as isize == -1 && codeset == "646" {
10256            // c:6829-6832 — Solaris' "646" spelling of US-ASCII.
10257            let ascii = std::ffi::CString::new("US-ASCII").unwrap_or_default();
10258            cd = unsafe { iconv_open(ascii.as_ptr(), from.as_ptr()) };
10259        }
10260        if cd as isize == -1 {
10261            zerr("cannot do charset conversion (iconv failed)"); // c:6835
10262            return -1;
10263        }
10264        // c:6839-6843 — store value in big endian form.
10265        let mut inbuf: [libc::c_char; 4] = [0; 4];
10266        let mut w = wval;
10267        for i in (0..4).rev() {
10268            inbuf[i] = (w & 0xff) as u8 as libc::c_char;
10269            w >>= 8;
10270        }
10271        let mut local = [0 as libc::c_char; 6];
10272        let mut inptr = inbuf.as_mut_ptr();
10273        let mut outptr = local.as_mut_ptr();
10274        let mut inbytes: libc::size_t = 4;
10275        let mut outbytes: libc::size_t = 6;
10276        let count = unsafe { iconv(cd, &mut inptr, &mut inbytes, &mut outptr, &mut outbytes) }; // c:6844
10277        unsafe { iconv_close(cd) }; // c:6845
10278        if count != 0 {
10279            // c:6846-6851 — -1 is an error; a positive count is the number of
10280            // non-reversible conversions, also "out-of-range".
10281            zerr("character not in range");
10282            return -1;
10283        }
10284        let n = 6 - outbytes as usize; // c:6853 `buf - bsave`
10285        if n > buf.len() {
10286            zerr("character not in range");
10287            return -1;
10288        }
10289        for i in 0..n {
10290            buf[i] = local[i] as u8;
10291        }
10292        n as i32
10293    }
10294}
10295
10296/// Port of `getkeystring()` from `Src/utils.c:6915` — C decl `getkeystring(char *s, int *len, int how, int *misc)`.
10297///
10298/// Parse getkeystring escape sequences (from utils.c getkeystring)
10299/// Handles \n \t \r \e \a \b \f \v \\ \' \" \xNN \uNNNN \UNNNNNNNN \0NNN
10300/// WARNING: param names don't match C — Rust=(s) vs C=(s, len, how, misc)
10301pub fn getkeystring(s: &str) -> (String, usize) {
10302    // c:6915
10303    let mut result = String::new();
10304    let mut chars = s.chars().peekable();
10305    let mut consumed = 0;
10306
10307    while let Some(c) = chars.next() {
10308        consumed += c.len_utf8();
10309        if c != '\\' {
10310            result.push(c);
10311            continue;
10312        }
10313        match chars.next() {
10314            Some('n') => {
10315                result.push('\n');
10316                consumed += 1;
10317            }
10318            Some('t') => {
10319                result.push('\t');
10320                consumed += 1;
10321            }
10322            Some('r') => {
10323                result.push('\r');
10324                consumed += 1;
10325            }
10326            Some('e') | Some('E') => {
10327                result.push('\x1b');
10328                consumed += 1;
10329            }
10330            Some('a') => {
10331                result.push('\x07');
10332                consumed += 1;
10333            }
10334            Some('b') => {
10335                result.push('\x08');
10336                consumed += 1;
10337            }
10338            Some('f') => {
10339                result.push('\x0c');
10340                consumed += 1;
10341            }
10342            Some('v') => {
10343                result.push('\x0b');
10344                consumed += 1;
10345            }
10346            Some('\\') => {
10347                result.push('\\');
10348                consumed += 1;
10349            }
10350            Some('\'') => {
10351                result.push('\'');
10352                consumed += 1;
10353            }
10354            Some('"') => {
10355                result.push('"');
10356                consumed += 1;
10357            }
10358            Some('x') => {
10359                consumed += 1;
10360                let mut hex = String::new();
10361                for _ in 0..2 {
10362                    if let Some(&c) = chars.peek() {
10363                        if c.is_ascii_hexdigit() {
10364                            hex.push(chars.next().unwrap());
10365                            consumed += 1;
10366                        } else {
10367                            break;
10368                        }
10369                    }
10370                }
10371                // c:Src/utils.c:7169-7170 — `\x` ALWAYS runs `*t++ =
10372                // zstrtol(s+1, &s, 16)`, even with no hex digit. zstrtol
10373                // over a non-hex byte parses zero digits and returns 0, so
10374                // a NUL byte is emitted (the offending char is processed
10375                // next). `$'\xg'` → NUL + 'g'; `$'\x'` → NUL. Previously
10376                // `from_str_radix("")` errored and nothing was pushed.
10377                let val: u8 = if hex.is_empty() {
10378                    0 // c:7170 zstrtol on empty reads as 0
10379                } else {
10380                    u8::from_str_radix(&hex, 16).unwrap_or(0)
10381                };
10382                // c:Src/utils.c — `\xNN` is one raw BYTE; metafied
10383                // per c:7289-7294 (vm_helper::meta_encode_byte) so
10384                // the String stays valid UTF-8. Bug #127.
10385                {
10386                    // c:Src/utils.c metafy byte-encode step:
10387                    // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
10388                    let b_ = val;
10389                    if b_ < 0x80 {
10390                        result.push(b_ as char);
10391                    } else {
10392                        result.push('\u{83}');
10393                        result.push(char::from(b_ ^ 32));
10394                    }
10395                }
10396            }
10397            Some('u') => {
10398                consumed += 1;
10399                let mut hex = String::new();
10400                for _ in 0..4 {
10401                    if let Some(&c) = chars.peek() {
10402                        if c.is_ascii_hexdigit() {
10403                            hex.push(chars.next().unwrap());
10404                            consumed += 1;
10405                        } else {
10406                            break;
10407                        }
10408                    }
10409                }
10410                if let Ok(val) = u32::from_str_radix(&hex, 16) {
10411                    if let Some(c) = char::from_u32(val) {
10412                        result.push(c);
10413                    }
10414                }
10415            }
10416            Some('U') => {
10417                consumed += 1;
10418                let mut hex = String::new();
10419                for _ in 0..8 {
10420                    if let Some(&c) = chars.peek() {
10421                        if c.is_ascii_hexdigit() {
10422                            hex.push(chars.next().unwrap());
10423                            consumed += 1;
10424                        } else {
10425                            break;
10426                        }
10427                    }
10428                }
10429                if let Ok(val) = u32::from_str_radix(&hex, 16) {
10430                    if let Some(c) = char::from_u32(val) {
10431                        result.push(c);
10432                    }
10433                }
10434            }
10435            Some(c @ '0'..='7') => {
10436                consumed += 1;
10437                let mut oct = String::new();
10438                oct.push(c);
10439                for _ in 0..2 {
10440                    if let Some(&c) = chars.peek() {
10441                        if ('0'..='7').contains(&c) {
10442                            oct.push(chars.next().unwrap());
10443                            consumed += 1;
10444                        } else {
10445                            break;
10446                        }
10447                    }
10448                }
10449                if let Ok(val) = u8::from_str_radix(&oct, 8) {
10450                    // c:Src/utils.c — octal escape is one raw BYTE;
10451                    // metafied per c:7289-7294. Bug #127.
10452                    {
10453                        // c:Src/utils.c metafy byte-encode step:
10454                        // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
10455                        let b_ = val;
10456                        if b_ < 0x80 {
10457                            result.push(b_ as char);
10458                        } else {
10459                            result.push('\u{83}');
10460                            result.push(char::from(b_ ^ 32));
10461                        }
10462                    }
10463                }
10464            }
10465            Some('c') => {
10466                consumed += 1;
10467                // \cX = control character
10468                if let Some(c) = chars.next() {
10469                    consumed += 1;
10470                    result.push((c as u8 & 0x1f) as char);
10471                }
10472            }
10473            Some(mod_letter @ ('C' | 'M')) => {
10474                // c:Src/utils.c:7029-7052 + c:7265-7275 — `\C` /
10475                // `\M` set control / meta flags; optional `-`
10476                // separator; then read the next char (possibly
10477                // through additional `\C`/`\M` chains) and apply
10478                // the mask: control → `& 0x9f` (or `\C-?` → 0x7f),
10479                // meta → `| 0x80`. Bug #113 in docs/BUGS.md: the
10480                // previous Rust port matched these in the default
10481                // arm and emitted literal `\C` / `\M`. This is the
10482                // second getkeystring (utils.rs); the in-tree
10483                // `getkeystring_dollar_quote` (lex.rs) also needs
10484                // the fix and gets it in the same commit.
10485                consumed += 1;
10486                let mut control = mod_letter == 'C';
10487                let mut meta = mod_letter == 'M';
10488                // Consume optional `-` separator + chained `\C`/`\M`.
10489                loop {
10490                    if chars.peek() == Some(&'-') {
10491                        chars.next();
10492                        consumed += 1;
10493                        continue;
10494                    }
10495                    // chained `\C` / `\M`?
10496                    let mut iter_clone = chars.clone();
10497                    if iter_clone.next() == Some('\\') {
10498                        if let Some(nx) = iter_clone.next() {
10499                            if nx == 'C' || nx == 'M' {
10500                                chars.next(); // consume '\'
10501                                chars.next(); // consume C/M
10502                                consumed += 2;
10503                                if nx == 'C' {
10504                                    control = true;
10505                                } else {
10506                                    meta = true;
10507                                }
10508                                continue;
10509                            }
10510                        }
10511                    }
10512                    break;
10513                }
10514                // Read one base character (allowing nested simple
10515                // escapes for common cases).
10516                let base: Option<char> = if chars.peek() == Some(&'\\') {
10517                    chars.next();
10518                    consumed += 1;
10519                    match chars.next() {
10520                        Some('n') => {
10521                            consumed += 1;
10522                            Some('\n')
10523                        }
10524                        Some('t') => {
10525                            consumed += 1;
10526                            Some('\t')
10527                        }
10528                        Some('r') => {
10529                            consumed += 1;
10530                            Some('\r')
10531                        }
10532                        Some('a') => {
10533                            consumed += 1;
10534                            Some('\x07')
10535                        }
10536                        Some('b') => {
10537                            consumed += 1;
10538                            Some('\x08')
10539                        }
10540                        Some('e') | Some('E') => {
10541                            consumed += 1;
10542                            Some('\x1b')
10543                        }
10544                        Some('f') => {
10545                            consumed += 1;
10546                            Some('\x0c')
10547                        }
10548                        Some('v') => {
10549                            consumed += 1;
10550                            Some('\x0b')
10551                        }
10552                        Some('\\') => {
10553                            consumed += 1;
10554                            Some('\\')
10555                        }
10556                        Some('\'') => {
10557                            consumed += 1;
10558                            Some('\'')
10559                        }
10560                        Some('"') => {
10561                            consumed += 1;
10562                            Some('"')
10563                        }
10564                        Some(other) => {
10565                            consumed += 1;
10566                            Some(other)
10567                        }
10568                        None => None,
10569                    }
10570                } else {
10571                    chars.next().inspect(|c| {
10572                        consumed += c.len_utf8();
10573                    })
10574                };
10575                if let Some(ch) = base {
10576                    let mut byte = ch as u32;
10577                    if control {
10578                        if byte == '?' as u32 {
10579                            byte = 0x7f;
10580                        } else {
10581                            byte &= 0x9f;
10582                        }
10583                    }
10584                    if meta {
10585                        byte |= 0x80;
10586                    }
10587                    // c:Src/utils.c:7265-7275 — masked result is one
10588                    // raw BYTE (`$'\M-i'` = 0xe9), metafied per
10589                    // c:7289-7294. Multibyte base chars (> 0xff)
10590                    // keep the codepoint form. Bug #127.
10591                    if byte <= 0xff {
10592                        {
10593                            // c:Src/utils.c metafy byte-encode step:
10594                            // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
10595                            let b_ = byte as u8;
10596                            if b_ < 0x80 {
10597                                result.push(b_ as char);
10598                            } else {
10599                                result.push('\u{83}');
10600                                result.push(char::from(b_ ^ 32));
10601                            }
10602                        }
10603                    } else if let Some(c) = char::from_u32(byte) {
10604                        result.push(c);
10605                    }
10606                }
10607            }
10608            Some(c)
10609                if isset(BANGHIST)
10610                    && crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst)
10611                        != 0
10612                    && c as u32
10613                        == crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst)
10614                            as u32 =>
10615            {
10616                // The reverse of quotestring's BANGHIST escape (c:Src/utils.c:6230
10617                // — `\!` is emitted for the history char): unquoting `$'a\!b'`
10618                // must strip the backslash so `${(Q)${(qqqq)v}}` round-trips.
10619                // In zsh this happens because `(Q)` re-lexes the string and
10620                // history expansion consumes the `\<bangchar>`; here the decoder
10621                // consumes it directly.
10622                consumed += 1;
10623                result.push(c);
10624            }
10625            Some(c) => {
10626                consumed += 1;
10627                result.push('\\');
10628                result.push(c);
10629            }
10630            None => {
10631                result.push('\\');
10632            }
10633        }
10634    }
10635    (result, consumed)
10636}
10637
10638/// Check if s is a prefix of t (from utils.c strpfx)
10639// Return non-zero if s is a prefix of t.                                  // c:7345
10640/// Port of `strpfx()` from `Src/utils.c:7334`.
10641///
10642/// `strpfx` — see implementation.
10643pub fn strpfx(s: &str, t: &str) -> bool {
10644    t.starts_with(s)
10645}
10646
10647/// Check if s is a suffix of t (from utils.c strsfx)
10648// Return non-zero if s is a suffix of t.                                  // c:7345
10649/// Port of `strsfx()` from `Src/utils.c:7345`.
10650///
10651/// `strsfx` — see implementation.
10652pub fn strsfx(s: &str, t: &str) -> bool {
10653    t.ends_with(s)
10654}
10655
10656/// Port of `upchdir()` from `Src/utils.c:7356` — C decl `upchdir(int n)`.
10657///
10658/// Go up n directories (from utils.c upchdir)
10659pub fn upchdir(n: usize) -> io::Result<()> {
10660    let mut path = String::new();
10661    for i in 0..n {
10662        if i > 0 {
10663            path.push('/');
10664        }
10665        path.push_str("..");
10666    }
10667    std::env::set_current_dir(&path)?;
10668    Ok(())
10669}
10670
10671/// Port of `struct dirsav` from `Src/zsh.h:1159`.
10672///
10673/// ```c
10674/// struct dirsav {
10675///     int dirfd, level;
10676///     char *dirname;
10677///     dev_t dev;
10678///     ino_t ino;
10679/// };
10680/// ```
10681///
10682/// The previous Rust port omitted `dev` and `ino` which the
10683/// `restoredir` integrity check (utils.c:7592) reads. Adding them
10684/// so callers can verify the saved-and-restored cwd matches the
10685/// captured device + inode.
10686// `struct dirsav` lives in `dirsav` per Rule C
10687// (its C definition is `Src/zsh.h:1159`, not utils.c). The previous
10688// Rust port had a `pub struct DirSav` PascalCase duplicate of the
10689// canonical lowercase struct; deleted in favour of routing through
10690// `zsh_h::dirsav` directly.
10691
10692/// Port of `init_dirsav()` from `Src/utils.c:7381` — C decl `init_dirsav(Dirsav d)`. Initialize a
10693/// `dirsav` struct to its empty/default state. C body memset's the
10694/// fields to 0 (dirfd to -1).
10695///
10696/// C signature: `void init_dirsav(Dirsav d)` where
10697/// `Dirsav = struct dirsav *`. Rust port returns the initialised
10698/// struct since callers always pair-with a fresh allocation.
10699/// WARNING: param names don't match C — Rust=() vs C=(path)
10700/// C body (3 lines):
10701///   `d->ino = d->dev = 0; d->dirname = NULL; d->dirfd = d->level = -1;`
10702///
10703/// `dirname` stays `None` exactly as C leaves it: the field is filled
10704/// later, by `lchdir`/`zgetdir`, only on the paths that need a name to
10705/// get back. An earlier Rust port prefilled it with `current_dir()`,
10706/// which put a `getcwd` (plus an `open` on macOS) in front of every
10707/// `scanner()` call in `glob.rs:444` — a syscall pair C never issues.
10708/// The two consumers both cope with `None`: `restoredir` (c:7565) falls
10709/// through to `fchdir(dirfd)` or `upchdir(level)`, and `lchdir` (c:7442-7448)
10710/// fills `d->level` for the soft path before any restore can happen.
10711pub fn init_dirsav() -> dirsav {
10712    // c:7381
10713    dirsav {
10714        ino: 0,
10715        dev: 0,        // c:7383 `d->ino = d->dev = 0;`
10716        dirname: None, // c:7384 `d->dirname = NULL;`
10717        dirfd: -1,
10718        level: -1, // c:7385 `d->dirfd = d->level = -1;`
10719    }
10720}
10721
10722/// Port of `lchdir()` from `Src/utils.c:7400` — C decl `lchdir(char const *path, struct dirsav *d, int hard)`.
10723///
10724/// c:7388 — "Change directory, without following symlinks."  With `hard`
10725/// set, descends `path` one component at a time: `lstat`s each component
10726/// (so a symlink is never followed), `chdir`s into it, then re-`lstat`s
10727/// `.` and compares dev/ino against the component it just stat'd. A
10728/// symlink swapped under us between the `lstat` and the `chdir` is caught
10729/// by the mismatch and the saved directory is restored via `restoredir`.
10730/// `d` (when non-NULL) is filled so the caller can later restore the cwd.
10731///
10732/// Returns 0 on success, -1 on a normal descent failure, -2 if the cwd
10733/// could not even be restored afterwards.
10734///
10735/// zshrs targets macOS/Linux, so the live C arms are HAVE_LSTAT +
10736/// HAVE_FCHDIR; the no-lstat / no-fchdir fallbacks (which never compile
10737/// on our platforms) are not ported. C restores `errno = err` before each
10738/// non-zero return so the caller can read the break-reason; the same
10739/// restore is done here with the per-platform errno setter.
10740/// WARNING: param names match C — (path, d, hard).
10741#[cfg(unix)]
10742pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
10743    // c:7400
10744    use std::ffi::CString;
10745
10746    let mut ds = init_dirsav(); // c:7405 — local fallback `struct dirsav ds`
10747    let used_local = d.is_none(); // tracks C's `d == &ds`
10748                                  // c:7415-7418 — `if (!d) { init_dirsav(&ds); d = &ds; }`
10749    let d: &mut dirsav = d.unwrap_or(&mut ds);
10750
10751    let bytes = path.as_bytes();
10752    let mut level: i32;
10753
10754    // c:7419-7424 (HAVE_LSTAT arm) —
10755    //   `if ((*path == '/' || !hard) && (d != &ds || hard))`
10756    if (bytes.first() == Some(&b'/') || hard == 0) && (!used_local || hard != 0) {
10757        level = -1; // c:7425
10758    } else {
10759        level = 0; // c:7431
10760                   // c:7432-7435 — record dev/ino of `.` if not already captured.
10761        if d.dev == 0 && d.ino == 0 {
10762            if let Ok(meta) = fs::metadata(".") {
10763                d.dev = meta.dev(); // c:7433
10764                d.ino = meta.ino(); // c:7434
10765            }
10766        }
10767    }
10768
10769    // c:7439-7451 — soft (`!hard`) path: count components, set d->level,
10770    // and delegate to zchdir.
10771    if hard == 0 {
10772        if !used_local {
10773            // c:7443-7447 — count '/'-separated components into `level`.
10774            let mut p = 0usize;
10775            while p < bytes.len() {
10776                while p < bytes.len() && bytes[p] != b'/' {
10777                    p += 1;
10778                }
10779                while p < bytes.len() && bytes[p] == b'/' {
10780                    p += 1;
10781                }
10782                level += 1;
10783            }
10784            d.level = level; // c:7448
10785        }
10786        return zchdir(path); // c:7450
10787    }
10788
10789    // c:7453+ — hard path (HAVE_LSTAT + HAVE_FCHDIR).
10790    let mut close_dir = false; // c:7412
10791                               // c:7455-7460 — save the starting dir via an fd if we don't have one.
10792    if d.dirfd < 0 {
10793        close_dir = true; // c:7456
10794        let dot = CString::new(".").unwrap();
10795        d.dirfd = unsafe { libc::open(dot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:7457
10796        if d.dirfd < 0
10797            && zgetdir(Some(&mut *d)).is_some()
10798            && d.dirname
10799                .as_deref()
10800                .map(|s| !s.starts_with('/'))
10801                .unwrap_or(false)
10802        {
10803            // c:7458-7459 — cwd is relative; fall back to opening "..".
10804            let dotdot = CString::new("..").unwrap();
10805            d.dirfd = unsafe { libc::open(dotdot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
10806        }
10807    }
10808
10809    // c:7462-7464 — absolute path: start the descent from root.
10810    if bytes.first() == Some(&b'/') {
10811        let root = CString::new("/").unwrap();
10812        if unsafe { libc::chdir(root.as_ptr()) } < 0 {
10813            zwarn(&format!(
10814                "failed to chdir(/): {}",
10815                io::Error::last_os_error()
10816            )); // c:7464
10817        }
10818    }
10819
10820    let dot = CString::new(".").unwrap();
10821    let mut pos = 0usize; // index into `path`, replaces C `path`/`pptr`
10822    let mut err: i32 = 0; // c:7408
10823    loop {
10824        // c:7466-7467 — skip leading slashes.
10825        while pos < bytes.len() && bytes[pos] == b'/' {
10826            pos += 1;
10827        }
10828        // c:7468-7480 — end of path reached: success.
10829        if pos >= bytes.len() {
10830            if !used_local {
10831                d.level = level; // c:7472
10832            }
10833            // c:7474-7477 — close the saved-dir fd if we opened it.
10834            if d.dirfd >= 0 && close_dir {
10835                unsafe { libc::close(d.dirfd) };
10836                d.dirfd = -1;
10837            }
10838            return 0; // c:7479
10839        }
10840        // c:7481 — find the end of this path component.
10841        let start = pos;
10842        let mut end = start + 1;
10843        while end < bytes.len() && bytes[end] != b'/' {
10844            end += 1;
10845        }
10846        // c:7482-7485 — component too long.
10847        if end - start > libc::PATH_MAX as usize {
10848            err = libc::ENAMETOOLONG;
10849            break;
10850        }
10851        // c:7486-7488 — copy the component into `buf`.
10852        let comp = &bytes[start..end];
10853        pos = end;
10854        let cbuf = match CString::new(comp) {
10855            Ok(c) => c,
10856            Err(_) => {
10857                err = libc::ENOENT; // embedded NUL — lstat would fail anyway.
10858                break;
10859            }
10860        };
10861        // c:7489-7492 — lstat the component (does NOT follow a symlink).
10862        let mut st1: libc::stat = unsafe { std::mem::zeroed() };
10863        if unsafe { libc::lstat(cbuf.as_ptr(), &mut st1) } != 0 {
10864            err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
10865            break;
10866        }
10867        // c:7493-7496 — must be a real directory, not a symlink/other.
10868        if st1.st_mode as u32 & libc::S_IFMT as u32 != libc::S_IFDIR as u32 {
10869            err = libc::ENOTDIR;
10870            break;
10871        }
10872        // c:7497-7500 — chdir into it.
10873        if unsafe { libc::chdir(cbuf.as_ptr()) } != 0 {
10874            err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
10875            break;
10876        }
10877        if level >= 0 {
10878            level += 1; // c:7501-7502
10879        }
10880        // c:7503-7510 — re-lstat `.` and verify the dir we landed in is
10881        // the same dev/ino we lstat'd, catching a symlink swap race.
10882        let mut st2: libc::stat = unsafe { std::mem::zeroed() };
10883        if unsafe { libc::lstat(dot.as_ptr(), &mut st2) } != 0 {
10884            err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
10885            break;
10886        }
10887        if st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino {
10888            err = libc::ENOTDIR; // c:7508
10889            break;
10890        }
10891    }
10892
10893    // c:7512-7548 — descent failed; restore the saved directory.
10894    if restoredir(&mut *d) != 0 {
10895        let restoreerr = io::Error::last_os_error(); // c:7513
10896                                                     // c:7519-7532 — restore failed too; force cwd to $HOME then "/".
10897        let mut reached = false;
10898        for i in 0..2 {
10899            // c:7521-7527 — destination: $HOME on pass 0, "/" on pass 1.
10900            let cdest = if i != 0 {
10901                "/".to_string()
10902            } else {
10903                match getsparam("HOME") {
10904                    Some(h) => h,     // c:7526
10905                    None => continue, // c:7525 `if (!home) continue;`
10906                }
10907            };
10908            setsparam("PWD", &cdest); // c:7528-7529 `zsfree(pwd); pwd = ztrdup(cdest);`
10909            if let Ok(cd) = CString::new(cdest.as_bytes()) {
10910                if unsafe { libc::chdir(cd.as_ptr()) } == 0 {
10911                    // c:7530
10912                    reached = true;
10913                    break;
10914                }
10915            }
10916        }
10917        if !reached {
10918            // c:7533-7534 — couldn't even reach "/".
10919            zerr(&format!(
10920                "lost current directory, failed to cd to /: {}",
10921                io::Error::last_os_error()
10922            ));
10923        } else {
10924            // c:7535-7537
10925            let pwd = getsparam("PWD").unwrap_or_default();
10926            zerr(&format!(
10927                "lost current directory: {}: changed to `{}'",
10928                restoreerr, pwd
10929            ));
10930        }
10931        // c:7540-7545 — close the saved-dir fd if we opened it.
10932        if d.dirfd >= 0 && close_dir {
10933            unsafe { libc::close(d.dirfd) };
10934            d.dirfd = -1;
10935        }
10936        #[cfg(target_os = "macos")]
10937        unsafe {
10938            *libc::__error() = err; // c:7546
10939        }
10940        #[cfg(target_os = "linux")]
10941        unsafe {
10942            *libc::__errno_location() = err; // c:7546
10943        }
10944        return -2; // c:7547
10945    }
10946    // c:7549-7558 — descent failed but the saved directory was restored.
10947    if d.dirfd >= 0 && close_dir {
10948        unsafe { libc::close(d.dirfd) };
10949        d.dirfd = -1;
10950    }
10951    #[cfg(target_os = "macos")]
10952    unsafe {
10953        *libc::__error() = err; // c:7557
10954    }
10955    #[cfg(target_os = "linux")]
10956    unsafe {
10957        *libc::__errno_location() = err;
10958    }
10959    -1 // c:7558
10960}
10961
10962/// Port of `lchdir()` from `Src/utils.c:7400`.
10963///
10964/// Non-unix build: lchdir's symlink-safe descent is built on POSIX
10965/// `lstat`/`fchdir`/`chdir`, which have no Windows equivalent here.
10966#[cfg(not(unix))]
10967pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
10968    let _ = (path, d, hard);
10969    -1
10970}
10971
10972/// Port of `restoredir()` from `Src/utils.c:7565` — C decl `restoredir(struct dirsav *d)`.
10973///
10974/// ```c
10975/// int restoredir(struct dirsav *d) {
10976///     if (d->dirname && *d->dirname == '/')
10977///         return chdir(d->dirname);
10978///     if (d->dirfd >= 0) {
10979///         if (!fchdir(d->dirfd)) {
10980///             if (!d->dirname) return 0;
10981///             else if (chdir(d->dirname)) {
10982///                 close(d->dirfd); d->dirfd = -1; err = -2;
10983///             }
10984///         } else {
10985///             close(d->dirfd); d->dirfd = err = -1;
10986///         }
10987///     } else if (d->level > 0)
10988///         err = upchdir(d->level);
10989///     else if (d->level < 0) err = -1;
10990///     // dev/ino integrity check ...
10991/// }
10992/// ```
10993///
10994/// Restore the cwd captured in `d`. Absolute `dirname` short-
10995/// circuits to `chdir`. Otherwise tries `fchdir(dirfd)` (when
10996/// supported) then falls through to `upchdir(level)` for the
10997/// nested-fn-exit case. Returns 0 on success, non-zero on failure
10998/// (matching C's int return).
10999///
11000/// Signature change: previous Rust port took `saved: &str` and
11001/// returned `bool` — different shape from C, missed the dirfd /
11002/// level / dev / ino fields entirely.
11003pub fn restoredir(d: &mut dirsav) -> i32 {
11004    // C: if (d->dirname && *d->dirname == '/') return chdir(d->dirname);
11005    if let Some(name) = d.dirname.as_ref() {
11006        if name.starts_with('/') {
11007            return match std::env::set_current_dir(name) {
11008                Ok(_) => 0,
11009                Err(_) => -1,
11010            };
11011        }
11012    }
11013    let mut err: i32 = 0;
11014    // C: HAVE_FCHDIR path — try fchdir(dirfd) first.
11015    #[cfg(unix)]
11016    if d.dirfd >= 0 {
11017        let rc = unsafe { libc::fchdir(d.dirfd) };
11018        if rc == 0 {
11019            if d.dirname.is_none() {
11020                return 0;
11021            }
11022            let name = d.dirname.as_ref().unwrap();
11023            if std::env::set_current_dir(name).is_err() {
11024                unsafe { libc::close(d.dirfd) };
11025                d.dirfd = -1;
11026                err = -2;
11027            }
11028        } else {
11029            unsafe { libc::close(d.dirfd) };
11030            d.dirfd = -1;
11031            err = -1;
11032        }
11033    } else if d.level > 0 {
11034        // C: err = upchdir(d->level);
11035        // The result is the caller's break-reason (glob.c:696 reports
11036        // "current directory lost during glob" on it), so it must not be
11037        // dropped — this is the branch every `init_dirsav`-then-`lchdir`
11038        // soft restore lands in now that `dirname` starts out NULL.
11039        if upchdir(d.level as usize).is_err() {
11040            err = -1;
11041        }
11042    } else if d.level < 0 {
11043        err = -1;
11044    }
11045    // C: dev/ino integrity check after the chdir/fchdir — unconditional
11046    // in C (c:7591-7595), it can only escalate an existing failure to -2.
11047    if d.dev != 0 || d.ino != 0 {
11048        if let Ok(meta) = fs::metadata(".") {
11049            if meta.ino() != d.ino || meta.dev() != d.dev {
11050                err = -2; // c:7594
11051            }
11052        } else {
11053            err = -2; // c:7592 `stat(".", &sbuf) < 0`
11054        }
11055    }
11056    err
11057}
11058
11059/// Port of `privasserted()` from `Src/utils.c:7608`.
11060///
11061/// "Check whether the shell is running with privileges in effect.
11062/// This is the case if EITHER the euid is zero, OR (if the system
11063/// supports POSIX.1e (POSIX.6) capability sets) the process'
11064/// Effective or Inheritable capability sets are non-empty."
11065///
11066/// ```c
11067/// if (!geteuid()) return 1;
11068/// #ifdef HAVE_CAP_GET_PROC
11069///     cap_t caps = cap_get_proc();
11070///     if (caps) {
11071///         cap_flag_value_t val;
11072///         for (cap_value_t cap = 0;
11073///              !cap_get_flag(caps, cap, CAP_EFFECTIVE, &val); cap++)
11074///             if (val && cap != CAP_WAKE_ALARM) {
11075///                 cap_free(caps);
11076///                 return 1;
11077///             }
11078///     }
11079///     cap_free(caps);
11080/// #endif
11081///     return 0;
11082/// ```
11083///
11084/// The previous Rust port checked `getuid() != geteuid()` which is
11085/// the SUID-binary detection, not the "running with privileges"
11086/// check. The capability-set inspection requires libcap (gated
11087/// behind the `libcap` feature in `crate::ported::modules::cap`);
11088/// without it, only the euid==0 path is exercised — same as the
11089/// C `#else` arm when HAVE_CAP_GET_PROC isn't defined.
11090pub fn privasserted() -> bool {
11091    #[cfg(unix)]
11092    {
11093        if unsafe { libc::geteuid() } == 0 {
11094            return true;
11095        }
11096    }
11097    // POSIX.1e capabilities check (HAVE_CAP_GET_PROC) — only
11098    // active on Linux when zshrs is built with `--features
11099    // libcap`. The cap module's `cap_get_proc` returns Ok(text)
11100    // when the process has any capability set; we treat any
11101    // non-default-empty result as "privileges asserted".
11102    #[cfg(all(target_os = "linux", feature = "libcap"))]
11103    {
11104        // Pending: walk the capability set with cap_get_flag and
11105        // skip CAP_WAKE_ALARM as the C source does. The cap.rs
11106        // port doesn't yet expose the flag-iteration FFI; until
11107        // it does, conservative-true on any non-empty cap text.
11108        if let Ok(text) = crate::ported::modules::cap::cap_get_proc() {
11109            // Empty / default-empty cap text = no privileges.
11110            if !text.is_empty() && text != "=" {
11111                return true;
11112            }
11113        }
11114    }
11115    false
11116}
11117
11118/// Port of `mode_to_octal()` from `Src/utils.c:7634` — C decl `mode_to_octal(mode_t mode)`.
11119///
11120/// Convert a `mode_t` into the equivalent canonical octal value
11121/// by testing each `S_I*` flag explicitly and OR-ing the matching
11122/// octal bit. This is NOT just `mode & 07777` — on systems where
11123/// the libc `S_IRUSR`/etc. constants don't match the canonical
11124/// values (e.g. when zsh runs against a non-POSIX libc),
11125/// `mode & 07777` returns the libc representation. The C version
11126/// translates to canonical bits explicitly so callers get a stable
11127/// portable result.
11128///
11129/// ```c
11130/// int mode_to_octal(mode_t mode)
11131/// {
11132///     int m = 0;
11133///     if (mode & S_ISUID) m |= 04000;
11134///     ... (12 bit-by-bit mappings)
11135///     return m;
11136/// }
11137/// ```
11138pub fn mode_to_octal(mode: u32) -> i32 {
11139    // c:7634
11140    #[cfg(not(unix))]
11141    {
11142        // No POSIX permission bits on non-Unix; fall back to canonical
11143        // octal-bit layout via the same mask values C uses on POSIX.
11144        let mut o: i32 = 0;
11145        if mode & 0o4000 != 0 {
11146            o |= 0o4000;
11147        } // c:7638-7639
11148        if mode & 0o2000 != 0 {
11149            o |= 0o2000;
11150        } // c:7640-7641
11151        if mode & 0o1000 != 0 {
11152            o |= 0o1000;
11153        } // c:7642-7643
11154        if mode & 0o0400 != 0 {
11155            o |= 0o0400;
11156        } // c:7644-7645
11157        if mode & 0o0200 != 0 {
11158            o |= 0o0200;
11159        } // c:7646-7647
11160        if mode & 0o0100 != 0 {
11161            o |= 0o0100;
11162        } // c:7648-7649
11163        if mode & 0o0040 != 0 {
11164            o |= 0o0040;
11165        } // c:7650-7651
11166        if mode & 0o0020 != 0 {
11167            o |= 0o0020;
11168        } // c:7652-7653
11169        if mode & 0o0010 != 0 {
11170            o |= 0o0010;
11171        } // c:7654-7655
11172        if mode & 0o0004 != 0 {
11173            o |= 0o0004;
11174        } // c:7656-7657
11175        if mode & 0o0002 != 0 {
11176            o |= 0o0002;
11177        } // c:7658-7659
11178        if mode & 0o0001 != 0 {
11179            o |= 0o0001;
11180        } // c:7660-7661
11181        return o; // c:7662
11182    }
11183    #[cfg(unix)]
11184    {
11185        // c:7636 — int m = 0;
11186        let mut m: i32 = 0;
11187        // c:7638-7661 — 12 bit-by-bit mappings from libc S_I* → canonical octal.
11188        if mode & S_ISUID as u32 != 0 {
11189            m |= 0o4000;
11190        } // c:7638-7639
11191        if mode & S_ISGID as u32 != 0 {
11192            m |= 0o2000;
11193        } // c:7640-7641
11194        if mode & S_ISVTX as u32 != 0 {
11195            m |= 0o1000;
11196        } // c:7642-7643
11197        if mode & S_IRUSR as u32 != 0 {
11198            m |= 0o0400;
11199        } // c:7644-7645
11200        if mode & S_IWUSR as u32 != 0 {
11201            m |= 0o0200;
11202        } // c:7646-7647
11203        if mode & S_IXUSR as u32 != 0 {
11204            m |= 0o0100;
11205        } // c:7648-7649
11206        if mode & S_IRGRP as u32 != 0 {
11207            m |= 0o0040;
11208        } // c:7650-7651
11209        if mode & S_IWGRP as u32 != 0 {
11210            m |= 0o0020;
11211        } // c:7652-7653
11212        if mode & S_IXGRP as u32 != 0 {
11213            m |= 0o0010;
11214        } // c:7654-7655
11215        if mode & S_IROTH as u32 != 0 {
11216            m |= 0o0004;
11217        } // c:7656-7657
11218        if mode & S_IWOTH as u32 != 0 {
11219            m |= 0o0002;
11220        } // c:7658-7659
11221        if mode & S_IXOTH as u32 != 0 {
11222            m |= 0o0001;
11223        } // c:7660-7661
11224        m // c:7662
11225    }
11226}
11227
11228/// Port of `mailstat()` from `Src/utils.c:7685` — C decl `mailstat(char *path, struct stat *st)`.
11229///
11230/// C signature: `int mailstat(char *path, struct stat *st)`.
11231/// Writes maildir aggregate stats into `*st` (or the native stat for
11232/// non-directory paths) and returns the underlying `stat(2)` return
11233/// (0 on success, -1 on error — matches C's `i = stat(path, st);
11234/// return i;`).
11235///
11236/// When `path` is a maildir directory (containing `cur/`, `tmp/`,
11237/// `new/` subdirs), walks `new/` + `cur/` and aggregates into `*st`:
11238/// nlink=1, S_IFDIR→S_IFREG, size=Σ message bytes,
11239/// blocks=Σ messages, atime=newest, mtime=newest. When the path is
11240/// a plain file, leaves the native `stat(2)` result in `*st`.
11241pub fn mailstat(path: &str, st: &mut libc::stat) -> i32 {
11242    // c:7685
11243    let c_path = match CString::new(path) {
11244        Ok(c) => c,
11245        Err(_) => return -1,
11246    };
11247    // C: if ((i = stat(path, st)) != 0 || !S_ISDIR(st->st_mode)) return i;
11248    let i = unsafe { libc::stat(c_path.as_ptr(), st as *mut _) }; // c:7693
11249    if i != 0 || (st.st_mode & libc::S_IFMT) != libc::S_IFDIR {
11250        // c:7693
11251        return i; // c:7693
11252    }
11253    // C 7700-7706: nlink=1, S_IFDIR → S_IFREG, zero size/blocks.
11254    st.st_nlink = 1; // c:7701
11255    st.st_mode &= !libc::S_IFDIR; // c:7702
11256    st.st_mode |= libc::S_IFREG; // c:7703
11257    st.st_size = 0; // c:7704
11258    st.st_blocks = 0; // c:7705
11259                      // C 7707-7712: stat(path/cur). If absent or not a dir, return 0
11260                      // with the partial out (just the IFREG-coerced root).
11261    let cur_path = match CString::new(format!("{}/cur", path)) {
11262        Ok(c) => c,
11263        Err(_) => return 0,
11264    };
11265    let mut sub: libc::stat = unsafe { std::mem::zeroed() };
11266    if unsafe { libc::stat(cur_path.as_ptr(), &mut sub) } != 0               // c:7708
11267        || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
11268    // c:7708
11269    {
11270        return 0; // c:7710
11271    }
11272    st.st_atime = sub.st_atime; // c:7712
11273                                // C 7715-7722: stat(path/tmp).
11274    let tmp_path = match CString::new(format!("{}/tmp", path)) {
11275        Ok(c) => c,
11276        Err(_) => return 0,
11277    };
11278    if unsafe { libc::stat(tmp_path.as_ptr(), &mut sub) } != 0               // c:7716
11279        || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
11280    // c:7716
11281    {
11282        return 0; // c:7718
11283    }
11284    st.st_mtime = sub.st_mtime; // c:7720
11285                                // C 7724-7730: stat(path/new). C overwrites mtime with new/'s mtime.
11286    let new_path = match CString::new(format!("{}/new", path)) {
11287        Ok(c) => c,
11288        Err(_) => return 0,
11289    };
11290    if unsafe { libc::stat(new_path.as_ptr(), &mut sub) } != 0               // c:7724
11291        || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
11292    // c:7724
11293    {
11294        return 0; // c:7726
11295    }
11296    st.st_mtime = sub.st_mtime; // c:7728
11297                                // C 7749-7778: walk new/ and cur/, sum size + blocks, track newest
11298                                // atime / mtime.
11299    let mut atime: libc::time_t = 0; // c:7748
11300    let mut mtime: libc::time_t = 0; // c:7748
11301    for sub_name in ["new", "cur"] {
11302        let dir = format!("{}/{}", path, sub_name);
11303        let entries = match fs::read_dir(&dir) {
11304            Ok(e) => e,
11305            Err(_) => return 0,
11306        };
11307        for entry in entries.flatten() {
11308            let name = entry.file_name();
11309            let name_bytes = name.as_encoded_bytes();
11310            // C: if (fn->d_name[0] == '.') continue;
11311            if name_bytes.first() == Some(&b'.') {
11312                // c:7758
11313                continue;
11314            }
11315            let entry_path = match CString::new(entry.path().to_string_lossy().as_bytes()) {
11316                Ok(c) => c,
11317                Err(_) => continue,
11318            };
11319            let mut entry_st: libc::stat = unsafe { std::mem::zeroed() };
11320            if unsafe { libc::stat(entry_path.as_ptr(), &mut entry_st) } != 0 {
11321                // c:7762
11322                continue;
11323            }
11324            st.st_size += entry_st.st_size; // c:7766
11325            st.st_blocks += 1; // c:7767
11326                               // C: if (atime != mtime && atime > atime_max) atime_max = atime;
11327            if entry_st.st_atime != entry_st.st_mtime && entry_st.st_atime > atime {
11328                // c:7769
11329                atime = entry_st.st_atime;
11330            }
11331            if entry_st.st_mtime > mtime {
11332                // c:7771
11333                mtime = entry_st.st_mtime;
11334            }
11335        }
11336    }
11337    // C 7783-7784: if (atime) st_ret.st_atime = atime;
11338    if atime != 0 {
11339        // c:7783
11340        st.st_atime = atime;
11341    }
11342    if mtime != 0 {
11343        // c:7785
11344        st.st_mtime = mtime;
11345    }
11346    0 // c:7787
11347}
11348
11349/// Script name for error messages
11350pub static mut SCRIPT_NAME: Option<String> = None;
11351/// Script filename
11352pub static mut SCRIPT_FILENAME: Option<String> = None;
11353
11354/// Print a fatal error to stderr.
11355/// Port of `zerr(VA_ALIST1(const char *fmt))` from Src/utils.c:173. C source sets `errflag`
11356/// after emitting `<prefix>: <msg>\n` so the running script aborts
11357/// at the next safe point. The Rust port currently just prints —
11358// =====================================================================
11359// Module-static state for the zerr/zwarn/zerrnam/zwarnnam family —
11360// port of the file-statics in Src/init.c + Src/exec.c that
11361// `zwarning()` (utils.c:142) reads to build the error prefix.
11362// =====================================================================
11363
11364// name of script being sourced                                             // c:33
11365/// Port of `char *scriptname` from `Src/init.c`. Set when `source`
11366/// is reading a script; cleared on return. Used by `zwarning()`
11367/// (utils.c:147) as the diagnostic prefix.
11368///
11369/// Storage is [`crate::thread_shell_state::ThreadMutex`], not a plain
11370/// `Mutex`: C's `scriptname` says where the ONE thread of execution is,
11371/// and `doshfunc` overwrites it with the callee's name on every function
11372/// entry (c:Src/exec.c:5903). An `async_precmd` hook running on a pool
11373/// worker therefore stamped its own name over the shell thread's, and a
11374/// diagnostic the user caused at the prompt came out prefixed with the
11375/// hook's name. Per-thread is C's model — background work there is a
11376/// forked child with its own copy.
11377static SCRIPTNAME: crate::thread_shell_state::ThreadMutex<Option<String>> =
11378    crate::thread_shell_state::ThreadMutex::new(None, || None);
11379
11380/// Port of `char *argzero` from `Src/init.c`. The shell's argv[0].
11381/// Used by `zwarning()` (utils.c:147) as the fallback diagnostic
11382/// prefix when scriptname is unset.
11383static ARGZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
11384
11385/// Port of `char *scriptfilename` from `Src/init.c` (listed in
11386/// `zsh.export:356`). The FILE that the current code was PARSED
11387/// from. Distinct from `scriptname` (which doshfunc overrides on
11388/// function entry at c:5903). `scriptfilename` stays at the outer
11389/// file across function calls; PS4's `%x` reads it at
11390/// `Src/prompt.c:937`.
11391///
11392/// Set at init.c:479 (`-c` mode → `scriptname = scriptfilename
11393/// = "zsh"`), init.c:1367 (`source` enters the named file),
11394/// init.c:1558+1592+1667 (save/install/restore around `.` /
11395/// `source` bin_dot dispatch).
11396///
11397/// Per-thread for the same reason as [`SCRIPTNAME`]: a hook function that
11398/// `source`s a file on a worker would otherwise move the file PS4's `%x`
11399/// and the `funcstack` frames name on the shell thread.
11400static SCRIPTFILENAME: crate::thread_shell_state::ThreadMutex<Option<String>> =
11401    crate::thread_shell_state::ThreadMutex::new(None, || None);
11402
11403/// Port of `char *posixzero` from `Src/params.c:76`. The original
11404/// argv[0] preserved unchanged by later mutations. Used by
11405/// `argzerogetfn` for `$0` under `isset(POSIXARGZERO)`.
11406static POSIXZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
11407
11408// error flag: bits from enum errflag_bits                                 // c:124
11409/// Port of `int errflag` from `Src/init.c`. Tracks whether an
11410/// error has been raised (`ERRFLAG_ERROR = 1`) or break/return
11411/// is in flight (`ERRFLAG_INT = 2`).
11412///
11413/// Direct global access: `errflag.load(Ordering::Relaxed)` reads
11414/// the current value, `errflag.fetch_or(ERRFLAG_ERROR, …)` matches
11415/// C's `errflag |= ERRFLAG_ERROR`, `errflag.store(0, …)` matches
11416/// C's `errflag = 0`.
11417///
11418/// Storage is [`crate::errflag_cell::ErrflagCell`], not a bare
11419/// `AtomicI32`: C's `errflag` is a per-PROCESS `int` and C's background
11420/// work is a forked child, so a child's errors never reach the parent's
11421/// line editor. zshrs runs that work on threads, so the flag is
11422/// process-global on the shell thread and thread-local everywhere else —
11423/// which is what `fork()` gave C. See that module for the ^G-abort
11424/// corruption this prevents.
11425#[allow(non_upper_case_globals)]
11426pub static errflag: crate::errflag_cell::ErrflagCell = crate::errflag_cell::ErrflagCell::new();
11427
11428/// Port of `int noerrs` from `Src/init.c`. Counter — when `> 0`,
11429/// suppresses error printing. `noerrs >= 2` also suppresses the
11430/// `errflag` set inside `zerr`/`zerrnam`.
11431static NOERRS: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
11432
11433/// Port of `int locallevel` from `Src/init.c`. Function-call depth
11434/// (0 = top-level, 1+ = inside a fn). `zwarning()` checks this in
11435/// the script-prefix path (utils.c:150).
11436// `LOCALLEVEL` removed — see `locallevel_lock` removal comment
11437// below. Canonical static lives in `LOCALLEVEL`
11438// (port of params.c:54).
11439
11440/// Port of `int lineno` from `Src/init.c`. Current line number;
11441/// `zerrmsg()` includes it in the diagnostic when locallevel > 0
11442/// or shinstdin is unset (utils.c:301).
11443static LINENO: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
11444
11445/// Port of the `isset(SHINSTDIN)` check from utils.c:150. C reads
11446/// the SHINSTDIN option directly; the Rust port caches it here so
11447/// callers don't pull in the whole option-table for every error.
11448static SHINSTDIN_OPT: std::sync::OnceLock<Mutex<bool>> = std::sync::OnceLock::new();
11449
11450/// `ERRFLAG_ERROR` from `Src/zsh.h`. Set on `zerr`/`zerrnam` to
11451/// signal a fatal error has occurred.
11452pub const ERRFLAG_ERROR: i32 = 1;
11453
11454/// Port of `mod_export int incompfunc` from `Src/utils.c:46`.
11455/// Set non-zero while a `comp*` builtin is dispatching from inside
11456/// a user-defined completion function — guards `comparguments` /
11457/// `compset` / `compadd` / `compdescribe` / `comptags` / `compvalues`
11458/// / `compfiles` / `compgroups` / `compquote` against being called
11459/// outside the `compfunc` shfunc context (each builtin checks
11460/// `INCOMPFUNC` early and emits "can only be called from completion
11461/// function" when zero).
11462pub static INCOMPFUNC: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:46
11463
11464/// Port of `mod_export int resetneeded` from `Src/utils.c:1821`.
11465/// Set when the editor needs a redraw — incremented by widgets +
11466/// signal handlers (e.g. SIGWINCH); the next prompt loop tick clears
11467/// it after running zrefresh.
11468pub static RESETNEEDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1821
11469
11470/// Port of `mod_export int winchanged` from `Src/utils.c:1827`.
11471/// Set by the SIGWINCH handler — the next refresh re-queries the
11472/// terminal size and re-renders, then clears the flag.
11473pub static WINCHANGED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1827
11474
11475/// Port of C's `GETKEY_*` flag enumeration from `Src/zsh.h:3143`.
11476/// Passed to `getkeystring_with` to alter escape interpretation:
11477///   - GETKEY_OCTAL_ESC: `\NNN` octal escapes are interpreted (1<<0)
11478///   - GETKEY_EMACS: unknown `\<char>` drops the backslash (1<<1)
11479///   - GETKEY_BACKSLASH_C: `\c` truncates the result (1<<3)
11480pub const GETKEY_OCTAL_ESC: u32 = 1 << 0; // c:zsh.h:3143
11481/// `GETKEY_EMACS` constant.
11482pub const GETKEY_EMACS: u32 = 1 << 1; // c:zsh.h:3150
11483/// `GETKEY_CTRL` constant.
11484pub const GETKEY_CTRL: u32 = 1 << 2; // c:zsh.h:3152
11485/// `GETKEY_BACKSLASH_C` constant.
11486pub const GETKEY_BACKSLASH_C: u32 = 1 << 3; // c:zsh.h:3154
11487
11488/// `GETKEYS_PRINT = GETKEY_OCTAL_ESC | GETKEY_BACKSLASH_C |
11489/// GETKEY_EMACS` per Src/zsh.h:3185 — the flag set `bin_print` uses
11490/// when interpreting escapes (with EMACS: unknown `\<c>` → `<c>`).
11491pub const GETKEYS_PRINT: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_BACKSLASH_C; // c:zsh.h:3185
11492
11493/// `GETKEYS_ECHO = GETKEY_BACKSLASH_C` per Src/zsh.h:3178 — the flag
11494/// set `echo` uses (and `print -e`). NOT GETKEY_EMACS: unknown
11495/// `\<c>` is preserved as literal `\<c>`. NOT GETKEY_OCTAL_ESC:
11496/// `\NNN` is NOT interpreted as octal. Only `\a \b \e \E \f \n \r
11497/// \t \v \\` (plus `\c` truncation) are handled.
11498///
11499/// Per builtin.c:4754-4760 — `bin_print` selects this for BIN_ECHO
11500/// or when `-e` is set; otherwise GETKEYS_PRINT.
11501pub const GETKEYS_ECHO: u32 = GETKEY_BACKSLASH_C; // c:zsh.h:3178
11502
11503/// `GETKEYS_BINDKEY = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL`
11504/// per Src/zsh.h:3187 — the flag set `print -b` uses. Like GETKEYS_PRINT
11505/// it has EMACS (so `\C-`/`\M-` backslash escapes are processed and
11506/// unknown `\<c>` collapses to `<c>`) but adds GETKEY_CTRL (enabling the
11507/// `^X` caret notation) and drops GETKEY_BACKSLASH_C (no `\c` truncation).
11508pub const GETKEYS_BINDKEY: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL; // c:zsh.h:3187
11509
11510/// Static `int ep` from Src/utils.c:4775 — sticky flag suppressing the
11511/// `can't set tty pgrp` warning after the first failure.
11512static ATTACHTTY_EP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:4775
11513
11514// =====================================================================
11515// !!! WARNING: RUST-ONLY HELPERS — NO DIRECT C COUNTERPART !!!
11516// =====================================================================
11517//
11518// These three ported (`fdtable_lock`, `fdtable_get`, `fdtable_set`) DO
11519// NOT EXIST as functions in `Src/utils.c`. The C source declares
11520// `fdtable` as a bare `unsigned char *` global (Src/utils.c:~63) and
11521// every call site reads/writes it as a direct array index:
11522//
11523//     if (fdtable[fd] != FDT_UNUSED) ...
11524//     fdtable[fd] = FDT_EXTERNAL;
11525//
11526// Rust can't hand out raw mutable indexes through a `Mutex<Vec<u8>>`
11527// safely without a borrow scope, so the Rust port wraps the same
11528// slot access in three `pub fn`s. Each call site to these helpers
11529// corresponds 1:1 to a `fdtable[fd] = X` or `fdtable[fd] != X`
11530// statement in the C source — they are NOT new policy, they only
11531// adapt the storage shape from `unsigned char *` to a growable
11532// `Mutex<Vec<i32>>`.
11533//
11534// Also adapts `growfdtable` (Src/utils.c:1965) by lazily growing the
11535// Vec inside `fdtable_set` instead of a separate `growfdtable`
11536// call — the C source calls `growfdtable(fd)` immediately before
11537// every `fdtable[fd] = X` write anyway.
11538//
11539// !!! Do NOT use these for any state that the C source does not
11540// already store in `fdtable[]`. Adding a new "fd kind" here is a
11541// scope expansion, not a port. !!!
11542// =====================================================================
11543
11544static FDTABLE: std::sync::OnceLock<Mutex<Vec<i32>>> = std::sync::OnceLock::new();
11545
11546/// Port of `int max_zsh_fd` global from `Src/exec.c:201`.
11547/// "The highest fd we know zsh is using" — bumped by `fdtable_set`
11548/// callers, shrunk by `zclose` when trailing slots fall to UNUSED.
11549pub static MAX_ZSH_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
11550
11551/// Port of `int fdtable_flocks` global from `Src/exec.c:204`.
11552/// Count of `FDT_FLOCK`-tagged fds; consulted by `closem()` to
11553/// decide whether to skip the flock-fd sweep.
11554pub static FDTABLE_FLOCKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
11555
11556/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11557/// standalone function.
11558/// C assigns the `char *scriptname` global (`Src/utils.c:36`) directly, e.g.
11559/// `scriptname = ...` in `bin_dot`. Rust needs a setter over the `OnceLock<
11560/// Mutex<Option<String>>>` that holds it.
11561/// Setter for `scriptname`. Called from `bin_dot` / `source`
11562/// when entering a script.
11563pub fn set_scriptname(name: Option<String>) {
11564    *scriptname_lock().lock().unwrap() = name;
11565}
11566
11567/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11568/// standalone function.
11569/// Reader for the same storage; C dereferences the `char *scriptname`
11570/// global at `Src/utils.c:36` in place.
11571/// Read `scriptname` — direct mirror of the C file-scope
11572/// `char *scriptname;` at `Src/utils.c:36`. Exposed for the
11573/// prompt expander (`%N`), `bin_dot`, and ZLE source tracking.
11574pub fn scriptname_get() -> Option<String> {
11575    scriptname_lock().lock().unwrap().clone()
11576}
11577
11578/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11579/// standalone function.
11580/// Setter for the storage mirroring C's `char *scriptfilename` global at
11581/// `Src/utils.c:41`, which C assigns directly.
11582/// Setter for `scriptfilename` — direct mirror of the C
11583/// `scriptfilename` global at `Src/init.c` (zsh.export:356).
11584/// Called from `bin_dot` at init.c:1592, restored at c:1667.
11585pub fn set_scriptfilename(name: Option<String>) {
11586    *scriptfilename_lock().lock().unwrap() = name;
11587}
11588
11589/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11590/// standalone function.
11591/// Reader for the same storage; C dereferences `scriptfilename`
11592/// (`Src/utils.c:41`) in place, e.g. PS4's `%x` in `Src/prompt.c`.
11593/// Read `scriptfilename` — direct mirror of the C global.
11594/// Used by PS4's `%x` (`Src/prompt.c:937`) and the function-call
11595/// frame at `Src/exec.c:1613` (`fstack.filename = scriptfilename`).
11596pub fn scriptfilename_get() -> Option<String> {
11597    scriptfilename_lock().lock().unwrap().clone()
11598}
11599
11600/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11601/// standalone function.
11602/// Reader for the `int locallevel` global declared at `Src/params.c:54`;
11603/// C reads the global directly.
11604/// Read `locallevel` — function nesting depth. Routes through the
11605/// canonical `LOCALLEVEL` static (port of
11606/// `Src/params.c:54`). The prior Rust port stored a duplicate
11607/// `Mutex<i32>` here that split state from params.rs; both copies
11608/// were never kept in sync.
11609pub fn locallevel() -> i32 {
11610    LOCALLEVEL.load(Ordering::Relaxed)
11611}
11612
11613/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11614/// standalone function.
11615/// C writes `locallevel++` inline in `startparamscope` (`Src/params.c`);
11616/// the global is declared at `Src/params.c:54`.
11617/// Bump `locallevel` (called by `startparamscope`).
11618pub fn inc_locallevel() {
11619    // c:Src/params.c:5837 `locallevel++` assumes one thread of shell
11620    // execution. An `async_precmd` batch runs hook functions on a pool worker
11621    // against this same counter and the same paramtab, so a scope opened here
11622    // while it runs gets its level-stamped params deleted by the worker's
11623    // `endparamscope` (c:5904-5907) — a widget's `$BUFFER` among them. Let the
11624    // batch finish (or withdraw it) before the shell thread opens a scope.
11625    crate::async_precmd::quiesce();
11626    LOCALLEVEL.fetch_add(1, Ordering::Relaxed);
11627}
11628
11629/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11630/// standalone function.
11631/// C writes `locallevel--` inline in `endparamscope` (`Src/params.c`);
11632/// the global is declared at `Src/params.c:54`.
11633/// Decrement `locallevel` (called by `endparamscope`).
11634pub fn dec_locallevel() {
11635    LOCALLEVEL.fetch_sub(1, Ordering::Relaxed);
11636}
11637
11638/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11639/// standalone function.
11640/// Setter for the `char *argzero` global declared at `Src/params.c:72`,
11641/// which C assigns directly from `parseargs`.
11642/// Setter for `argzero`. Called once at shell init from `parseargs`.
11643pub fn set_argzero(name: Option<String>) {
11644    // c:271 (init.c) — `argv0 = argzero = posixzero = *argv++;`. At
11645    // shell init both argzero and posixzero share the same source.
11646    // posixzero is preserved unchanged after later mutations (exec -a,
11647    // function frames) — argzero is what gets rewritten. If posixzero
11648    // hasn't been independently set yet, mirror argzero so the
11649    // POSIXARGZERO branch in `argzerogetfn` returns something useful.
11650    let mut posix_lock = posixzero_lock().lock().unwrap();
11651    if posix_lock.is_none() {
11652        *posix_lock = name.clone();
11653    }
11654    drop(posix_lock);
11655    *argzero_lock().lock().unwrap() = name;
11656}
11657
11658/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11659/// standalone function.
11660/// Reader for the same `char *argzero` global (`Src/params.c:72`);
11661/// C's `argzerogetfn` returns the global itself.
11662/// Read `argzero`. Used by `argzerogetfn` for `$0`.
11663pub fn argzero() -> Option<String> {
11664    argzero_lock().lock().unwrap().clone()
11665}
11666
11667/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11668/// standalone function.
11669/// Setter for the `char *posixzero` global declared at `Src/params.c:76`;
11670/// C assigns it directly in the init path.
11671/// Port of `char *posixzero` from `Src/params.c:76`. The original
11672/// `argv[0]` preserved from shell startup, unchanged by later `exec -a`
11673/// or function-call rewrites. C's `argzerogetfn` (params.c:4958)
11674/// returns this instead of `argzero` when `isset(POSIXARGZERO)`.
11675///
11676/// Set once at init via `set_posixzero` (called from the init path
11677/// in C at init.c:271/297/321). `set_argzero` mirrors the value here
11678/// on first call so the POSIXARGZERO branch has something to read
11679/// even if the init path hasn't run yet.
11680pub fn set_posixzero(name: Option<String>) {
11681    *posixzero_lock().lock().unwrap() = name;
11682}
11683
11684/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11685/// standalone function.
11686/// Reader for the same `char *posixzero` global (`Src/params.c:76`),
11687/// which C's `argzerogetfn` returns at `Src/params.c:4957`.
11688/// Read `posixzero`. C: `posixzero` (params.c:76 global). Used by
11689/// `argzerogetfn` when `isset(POSIXARGZERO)`.
11690pub fn posixzero() -> Option<String> {
11691    posixzero_lock().lock().unwrap().clone()
11692}
11693
11694/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11695/// standalone function.
11696/// Writer for the `int noerrs` global declared at `Src/exec.c:117`;
11697/// C does `noerrs++` / `noerrs--` on the global in place.
11698/// Setter for `noerrs`. Increment to suppress error output;
11699/// decrement to restore.
11700pub fn set_noerrs(v: i32) {
11701    *noerrs_lock().lock().unwrap() = v;
11702}
11703
11704/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11705/// standalone function.
11706/// Writer for the `int locallevel` global declared at `Src/params.c:54`.
11707/// Setter for `locallevel`. Called by function-call entry/exit.
11708pub fn set_locallevel(v: i32) {
11709    LOCALLEVEL.store(v, Ordering::Relaxed);
11710}
11711
11712/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11713/// standalone function.
11714/// Writer for the `zlong lineno` global declared at `Src/params.c:123`;
11715/// C assigns the global directly from the lexer/parser.
11716/// Setter for `lineno`. Called by the parser as it advances.
11717pub fn set_lineno(v: i32) {
11718    *lineno_lock().lock().unwrap() = v;
11719}
11720
11721/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11722/// standalone function.
11723/// Rust-only cache of the SHINSTDIN option bit (option table entry at
11724/// `Src/options.c:248`) so error emission does not have to reach into the
11725/// option table. C tests `isset(SHINSTDIN)` against `opts[]` directly.
11726/// Setter for the cached SHINSTDIN flag. Called by the option
11727/// machinery whenever `setopt shinstdin` / `unsetopt shinstdin`
11728/// fires.
11729pub fn set_shinstdin(v: bool) {
11730    *shinstdin_lock().lock().unwrap() = v;
11731}
11732
11733/// Check if string is a valid identifier
11734// `isident` DELETED — fake duplicate (cited `c:params.c:1288` from
11735// utils.rs, location mismatch). Canonical port is
11736// `isident` at `params.rs:2056`, which
11737// correctly handles namespace prefix (`ns.var`) per the real C
11738// body — the utils.rs copy dropped that arm. Callers now route
11739// through `isident` directly.
11740
11741// QuoteType enum + impl deleted — the canonical quote-type values are
11742// the bare `QT_*: i32` constants at `zsh_h.rs:175` (port of anonymous
11743// `enum { QT_NONE, QT_BACKSLASH, … }` from `Src/zsh.h:253-298`).
11744// `quotestring()` takes `quote_type: i32` matching C's signature.
11745
11746/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11747/// standalone function.
11748/// C has no such function: the `(q)`-count to `QT_*` mapping is done inline
11749/// in `paramsubst`'s flag loop at `Src/subst.c:2236`, where repeated `q`
11750/// flags run `quotemod++, quotetype++` (c:2252) over the `QT_*` enum.
11751/// Map a `(q)` flag count to a `QT_*` value.
11752/// Port of the q-flag dispatch in `Src/subst.c` `paramsubst()` —
11753/// `(q)`=`QT_BACKSLASH`, `(qq)`=`QT_SINGLE`, `(qqq)`=`QT_DOUBLE`,
11754/// `(qqqq+)`=`QT_DOLLARS`.
11755pub fn qflag_quotetype(count: u32) -> i32 {
11756    match count {
11757        0 => QT_NONE,
11758        1 => QT_BACKSLASH,
11759        2 => QT_SINGLE,
11760        3 => QT_DOUBLE,
11761        _ => QT_DOLLARS,
11762    }
11763}
11764
11765/// Port of `ispecial()` from `Src/ztype.h:59`.
11766///
11767/// Port of `ispecial()` macro from `Src/ztype.h:59` — `zistype(X,
11768/// ISPECIAL)`. C populates the ISPECIAL typtab bit at
11769/// `Src/utils.c:4253-4262`:
11770///   - Every byte in `SPECCHARS` (`Src/zsh.h:228` =
11771///     `"#$^*()=|{}[]\`<>?~;&\\n\\t \\\\\\'\\""`)
11772///   - `,` when `typtab_flags & ZTF_SP_COMMA` (set by
11773///     `makecommaspecial(1)` per c:4271)
11774///   - `bangchar` (default `!`) when `BANGHIST` is set AND
11775///     `ZTF_INTERACT` is set (interactive shell)
11776///
11777/// Reads the LIVE table, which is the whole point of the macro: the
11778/// set is not a constant. `inittyptab` recomputes it from options on
11779/// every relevant change, and two members exist only at runtime —
11780/// `,` under `makecommaspecial` and `bangchar` under
11781/// BANGHIST+interactive.
11782///
11783/// This used to enumerate `SPECCHARS` as a literal `matches!`, which
11784/// made both runtime members unreachable: `quotestring` is the only
11785/// caller, so `${(q)v}` on `v='a!b'` published `a!b` where an
11786/// interactive zsh publishes `a\!b`, and `makecommaspecial` could not
11787/// affect quoting at all. The bit was being computed correctly all
11788/// along (utils.c:4257-4259, and `makebangspecial` at c:4283) — it
11789/// just had no reader.
11790///
11791/// C indexes `typtab` by `unsigned char`, so anything outside 0..=255
11792/// has no entry; return false rather than truncating a `char` into a
11793/// byte that would alias an unrelated slot.
11794fn ispecial(c: char) -> bool {
11795    // c:59 (Src/ztype.h) — `zistype(X, ISPECIAL)`.
11796    let cp = c as u32;
11797    if cp > 0xff {
11798        return false;
11799    }
11800    crate::ported::ztype_h::ispecial(cp as u8)
11801}
11802
11803/// Port of `convbase()` from `Src/params.c:5632`.
11804///
11805/// Convert integer to string with specified base — re-export of
11806/// the canonical port at `convbase_param`.
11807///
11808/// The previous Rust port at this file was a SECOND, divergent
11809/// implementation of `convbase`:
11810///   - Used LOWERCASE digit chars (`"0123456789abc..."`) for
11811///     bases 11..36, while C uses UPPERCASE (`(dig - 10) + 'A'`
11812///     at Src/params.c:5621).
11813///   - Skipped CBASES + OCTALZEROES prefix handling at c:5598-5604
11814///     (`0x`/`0` prefixes when option-gated).
11815///   - Returned wrong format for bases not handled by the explicit
11816///     match arms.
11817///
11818/// The canonical port lives at `params.rs::convbase_ptr` (c:5588
11819/// faithful body) + `params.rs::convbase` (c:5634 wrapper). Route
11820/// through there so utils.rs / params.rs agree byte-for-byte and
11821/// no divergent duplicate stays alive.
11822pub fn convbase(val: i64, base: u32) -> String {
11823    // c:5634 (Src/params.c)
11824    convbase_param(val, base)
11825}
11826
11827// `checkglobqual` DELETED — fake bracket-depth scanner cited
11828// `c:glob.c:1158` from utils.rs. Canonical port is
11829// `crate::ported::glob::checkglobqual(str, sl, nobareglob, sp)` at
11830// `glob.rs:813`, which matches the real C signature
11831// `(char *str, int sl, int nobareglob, char **sp)`. The utils.rs
11832// fake reduced the signature to a single bool and discarded the
11833// `sp` glob-qualifier-start out-pointer that callers need. Zero
11834// callers used the utils.rs version.
11835
11836// `dupstrpfx` DELETED — fake duplicate cited `c:string.c:161` from
11837// utils.rs (wrong location). Canonical port is
11838// `dupstrpfx` at `string.rs:161`. Zero
11839// callers used the utils.rs version.
11840
11841/// Port of `statuidprint()` from `Src/Modules/stat.c:132`.
11842/// !!! DIVERGENT DUPLICATE !!! The faithful port of that C body (with the
11843/// `STF_RAW` / `STF_STRING` flag arms and the no-`getpwuid` fallback) is
11844/// `statuidprint(uid, flags) -> String` at `src/ported/modules/stat.rs:112`.
11845/// This copy implements only the `getpwuid(uid) -> pw_name` arm (c:142-144)
11846/// and has no caller; it should be deleted in favour of the stat.rs port.
11847///
11848/// Get username from UID (from utils.c getpwuid handling)
11849pub fn statuidprint(uid: u32) -> Option<String> {
11850    #[cfg(unix)]
11851    {
11852        let pwd = unsafe { libc::getpwuid(uid) };
11853        if pwd.is_null() {
11854            return None;
11855        }
11856        let name = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_name) };
11857        name.to_str().ok().map(|s| s.to_string())
11858    }
11859    #[cfg(not(unix))]
11860    {
11861        let _ = uid;
11862        None
11863    }
11864}
11865
11866// `ztrdup` / `dyncat` / `tricat` / `bicat` DELETED — these were
11867// fake duplicates of the canonical ports in `src/ported/string.rs`
11868// (`Src/string.c:62`, `:131`, `:98`, `:145`). The utils.rs copies
11869// admitted they were not in utils.c via `c:string.c:NNN`
11870// annotations; PORT.md Rule 1 disallows the duplicate location.
11871// Callers now route through `crate::ported::string::{ztrdup,
11872// dyncat, tricat, bicat}` directly.
11873
11874// `iwsep` / `iwsep_byte` DELETED — these were fake hardcoded
11875// `c==' '||c=='\t'||c=='\n'` checks claiming to port `iwsep` from
11876// "Src/zsh.h", but the real macro lives at `Src/ztype.h:61`
11877// (`#define iwsep(X) zistype(X, IWSEP)`) and consults the `typtab[]`
11878// lookup — which mutates when IFS is reassigned. The canonical port
11879// is `iwsep(u8) -> bool` at `ztype_h.rs:133`.
11880// Internal utils.rs callers (skipwsep / spacesplit / findword) now
11881// route through it directly.
11882
11883// `imeta(c: char)` DELETED — fake `char`-arg wrapper cited
11884// `c:ztype.h:60` from utils.rs (wrong location, wrong signature).
11885// Canonical port is `crate::ported::ztype_h::imeta(u8) -> bool` at
11886// `ztype_h.rs:130`. C `imeta(X)` takes a byte; wrapping with `char`
11887// invented an `> 0xff` early-out that C doesn't have. Migrated 1
11888// caller (`zle/computil.rs:6194`).
11889
11890/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11891/// standalone function.
11892/// C calls the libc `gethostname(2)` directly at `Src/params.c:874`
11893/// (`gethostname(hostnam, 256);`). The zsh function of this name
11894/// (`Src/compat.c:64`) is the `#ifndef HAVE_GETHOSTNAME` fallback built on
11895/// `uname()`, which this does NOT implement — this wraps libc's real one.
11896/// Get hostname
11897pub fn gethostname() -> String {
11898    #[cfg(unix)]
11899    {
11900        let mut buf = vec![0u8; 256];
11901        unsafe {
11902            if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) == 0 {
11903                let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
11904                return String::from_utf8_lossy(&buf[..len]).to_string();
11905            }
11906        }
11907    }
11908    // gethostname(2) failure fallback. C consults `$HOST` /
11909    // `cached_hostname`. Read paramtab; fall back to "localhost".
11910    getsparam("HOST")
11911        .or_else(|| getsparam("HOSTNAME"))
11912        .unwrap_or_else(|| "localhost".to_string())
11913}
11914
11915/// Get current working directory — re-export of the canonical
11916/// port at `compat::zgetcwd` (Src/compat.c:559).
11917// `zgetcwd` DELETED — fake `Option<String>` wrapper around the
11918// canonical `compat::zgetcwd() -> String` port (`Src/compat.c:559`).
11919// C `zgetcwd` never returns NULL (falls back to `"."` per the
11920// `dupstring(".")` arm), so the `Option` was caller-API churn, not
11921// a port. Callers route through `crate::ported::compat::zgetcwd()`
11922// directly.
11923
11924// `zchdir` duplicate deleted — canonical port at compat.rs:253
11925// matches C's `int zchdir(char *dir)` signature (returns i32, not
11926// bool). Zero callers used the utils.rs bool variant.
11927
11928// `realpath` / `mkdir` / `symlink` / `readlink` / `getenv` DELETED —
11929// five Rust-only convenience wrappers around std::fs / std::env /
11930// std::os::unix::fs with ZERO callers across the tree. Each was a
11931// thin std-lib re-export with no zsh C counterpart:
11932//   - realpath  → std::fs::canonicalize (libc realpath(3) — not in
11933//                 utils.c; the zsh source uses chrealpath at hist.c:1971)
11934//   - mkdir     → std::fs::create_dir (libc mkdir(2) — zsh's analogue
11935//                 is Modules/files.c::bin_mkdir, not a utils.c helper)
11936//   - symlink   → std::os::unix::fs::symlink (libc symlink(2))
11937//   - readlink  → std::fs::read_link (libc readlink(2))
11938//   - getenv    → std::env::var (libc getenv(3) — zsh's analogue is
11939//                 zgetenv in compat.c)
11940// PORT.md Rule 0/A: names must exist in upstream zsh C source as a
11941// `<name>(` function. None of these qualify; with zero callers they
11942// were dead code by definition. Callers needing the same semantics
11943// route through the canonical port (chrealpath / bin_mkdir / etc.)
11944// or std::fs directly.
11945
11946/// Per-prompt callback registry.
11947/// Port of the static `prepromptfns` LinkList in Src/utils.c:1313.
11948/// Holds the bare-fn pointers `addprepromptfn`/`delprepromptfn`
11949/// register and `preprompt()` walks.
11950static PREPROMPT_FNS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
11951
11952/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11953/// standalone function.
11954/// C has no `setenv` of its own — `Src/params.c`'s `zputenv` maintains the
11955/// `environ` array with metafied names. This is a plain
11956/// `std::env::set_var` wrapper for Rust-side callers, not that port.
11957/// Set environment variable
11958pub fn setenv(name: &str, value: &str) {
11959    std::env::set_var(name, value);
11960}
11961
11962/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11963/// standalone function.
11964/// Counterpart of `setenv` above; C's environment removal goes through
11965/// `Src/params.c`'s `zputenv`/`delenv`, not a function of this shape.
11966/// Unset environment variable
11967pub fn unsetenv(name: &str) {
11968    std::env::remove_var(name);
11969}
11970
11971/// Time-ordered timed-function registry.
11972/// Port of the `timedfns` LinkList Src/utils.c:1365 keeps for the
11973/// `sched` builtin. Sorted ascending by `when` (epoch seconds);
11974/// `addtimedfn` does an insertion sort (matches the C source's
11975/// `for (;;)` walk at lines 1394-1411).
11976pub static TIMED_FNS: Mutex<Vec<(i64, fn())>> = Mutex::new(Vec::new()); // c:1365 timedfns (mod_export in C)
11977
11978/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11979/// standalone function.
11980/// Wrapper over libc `getuid(2)`, which C calls directly at its use sites.
11981/// zsh has no `getuid` function of its own.
11982/// Get current user ID
11983pub fn getuid() -> u32 {
11984    #[cfg(unix)]
11985    unsafe {
11986        libc::getuid()
11987    }
11988    #[cfg(not(unix))]
11989    0
11990}
11991
11992/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11993/// standalone function.
11994/// Wrapper over libc `geteuid(2)`, which C calls directly at its use sites.
11995/// Get effective user ID
11996pub fn geteuid() -> u32 {
11997    #[cfg(unix)]
11998    unsafe {
11999        libc::geteuid()
12000    }
12001    #[cfg(not(unix))]
12002    0
12003}
12004
12005/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12006/// standalone function.
12007/// Wrapper over libc `getgid(2)`, which C calls directly at its use sites.
12008/// Get current group ID
12009pub fn getgid() -> u32 {
12010    #[cfg(unix)]
12011    unsafe {
12012        libc::getgid()
12013    }
12014    #[cfg(not(unix))]
12015    0
12016}
12017
12018/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12019/// standalone function.
12020/// Wrapper over libc `getegid(2)`, which C calls directly at its use sites.
12021/// Get effective group ID
12022pub fn getegid() -> u32 {
12023    #[cfg(unix)]
12024    unsafe {
12025        libc::getegid()
12026    }
12027    #[cfg(not(unix))]
12028    0
12029}
12030
12031/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12032/// standalone function.
12033/// Wrapper over libc `getpid(2)`, which C calls directly at its use sites.
12034/// Get process ID
12035pub fn getpid() -> i32 {
12036    std::process::id() as i32
12037}
12038
12039/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12040/// standalone function.
12041/// Wrapper over libc `getppid(2)`, which C calls directly at its use sites.
12042/// Get parent process ID
12043pub fn getppid() -> i32 {
12044    #[cfg(unix)]
12045    unsafe {
12046        libc::getppid()
12047    }
12048    #[cfg(not(unix))]
12049    0
12050}
12051
12052/// Port of `getkeystring()` from `Src/utils.c:6915`.
12053///
12054/// `getkeystring` with the C `how` parameter (Src/utils.c:6915).
12055/// The plain `getkeystring(s)` shim above defaults `how=0` for
12056/// non-EMACS callers (zbeep, dollar-quote — they keep unknown
12057/// `\<char>` as literal `\<char>`). Pass `GETKEYS_PRINT` to get
12058/// the print/echo behavior (drop backslash on unknown).
12059/// `getkeystring` with the C `how` parameter AND the
12060/// `GETKEY_UPDATE_OFFSET` cursor-offset out-param (Src/utils.c:6915).
12061///
12062/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from
12063/// `Src/utils.c:6915`, specifically its `GETKEY_UPDATE_OFFSET` path.
12064/// When `how` sets `GETKEY_UPDATE_OFFSET` and `misc` is `Some`, `*misc`
12065/// is the C `*misc` completion offset — a byte index into `s` — and is
12066/// updated as escapes positioned before the cursor collapse:
12067///   - `-1` per collapsed `\<char>` escape whose backslash precedes the
12068///     cursor, `s - sstart < *misc` (c:6987); undone if the backslash is
12069///     kept literal in the default (non-EMACS) arm (c:7181-7183);
12070///   - additional `-6` for `\u`, `-10` (`-4`+`-6`) for `\U` (c:7073-7084),
12071///     then `+N` for the N output bytes emitted (c:7123-7124).
12072/// Octal/hex escapes get only the base `-1`, matching C's own incomplete
12073/// `/* HERE: GETKEY_UPDATE_OFFSET? */` handling (c:7155).
12074///
12075/// The offset is a byte index. For ASCII `$'...'` content (one byte per
12076/// char) it matches the C metafied-byte semantics exactly; the byte-vs-
12077/// char divergence for non-ASCII content is the pre-existing shared-cursor
12078/// limitation documented on `set_comp_sep` (compcore.rs), not new here.
12079///
12080/// Callers with no offset to track pass `misc = None`.
12081pub fn getkeystring_with(s: &str, how: u32, mut misc: Option<&mut i32>) -> (String, usize) {
12082    // c:utils.c:6915
12083    let update_off = (how & crate::ported::zsh_h::GETKEY_UPDATE_OFFSET as u32) != 0;
12084    let bash_dollar_quote = (how & crate::ported::zsh_h::GETKEY_DOLLAR_QUOTE as u32) != 0
12085        && crate::dash_mode::bash_mode();
12086    let mut result = String::new();
12087    let mut chars = s.chars().peekable();
12088    let mut consumed = 0;
12089    // c:7194 — C's `^` handler is `control = 1; continue;`, so the control bit
12090    // stays PENDING across loop iterations and a following `\M`/`\C` escape
12091    // chains onto it (`^\M-a` → c:7034 `meta = 1 + control` → meta==2 → 0x81).
12092    // This port's `^` arm consumes its base character inline, which cannot
12093    // express that, so the flag carries the state to the modifier arm instead.
12094    let mut pending_control = false;
12095    // (output length, control, meta) recorded by a `\C`/`\M` escape.
12096    let mut pending_mask: Option<(usize, u32, u32)> = None;
12097    // !!! WARNING: C applies a pending `\C`/`\M` modifier to `t[-1]` at the
12098    // bottom of the loop iteration that emitted the next character
12099    // (c:Src/utils.c:7261-7275), after that iteration decoded its own escape
12100    // (`\130`, `\x58`, `\e` …). The escape arms here leave the loop body from
12101    // many places, so the masks are applied to the last emitted unit at the
12102    // start of the next iteration and after the loop instead.
12103    let apply_pending_mask = |result: &mut String, pending: &mut Option<(usize, u32, u32)>| {
12104        let Some((mark, control, meta)) = *pending else {
12105            return;
12106        };
12107        if result.len() <= mark {
12108            return;
12109        }
12110        *pending = None;
12111        let mut units: Vec<char> = result.chars().collect();
12112        let Some(last) = units.pop() else {
12113            return;
12114        };
12115        let metafied = units.last() == Some(&'\u{83}');
12116        let mut byte = if metafied { (last as u32) ^ 32 } else { last as u32 };
12117        if byte > 0xff {
12118            return; // a multibyte character: left as emitted
12119        }
12120        if metafied {
12121            units.pop();
12122        }
12123        if meta == 2 {
12124            byte |= 0x80; // c:7261-7264
12125        }
12126        if control == 1 {
12127            byte = if byte == '?' as u32 { 0x7f } else { byte & 0x9f }; // c:7265-7271
12128        }
12129        if meta == 1 {
12130            byte |= 0x80; // c:7272-7275
12131        }
12132        *result = units.into_iter().collect();
12133        let b_ = byte as u8;
12134        if b_ < 0x80 {
12135            result.push(b_ as char);
12136        } else {
12137            // c:7289-7294 — a byte >= 0x80 is metafied.
12138            result.push('\u{83}');
12139            result.push(char::from(b_ ^ 32));
12140        }
12141    };
12142    // c:Src/utils.c:7276-7298 — under GETKEY_DOLLAR_QUOTE each chunk decoded
12143    // into the temporary buffer is copied out by a loop that, when
12144    // POSIX_STRINGS is set, stops at an embedded NUL and sets `ignoring`
12145    // (c:6920), which drops every later buffered chunk. `\u`/`\U`
12146    // (c:7117-7131) and tokens (c:7240-7255) bypass the buffer. `tbuf_mark`
12147    // is where the pending chunk starts in `result`.
12148    let posix_cut = (how & crate::ported::zsh_h::GETKEY_DOLLAR_QUOTE as u32) != 0
12149        && isset(crate::ported::zsh_h::POSIXSTRINGS);
12150    let mut ignoring = false;
12151    let mut tbuf_mark: Option<usize> = None;
12152    let flush_tbuf = |result: &mut String, mark: &mut Option<usize>, ignoring: &mut bool| {
12153        let Some(m) = mark.take() else { return };
12154        if !posix_cut || m > result.len() {
12155            return;
12156        }
12157        if *ignoring {
12158            result.truncate(m); // c:7286-7287
12159        } else if let Some(nul) = result[m..].find('\0') {
12160            *ignoring = true; // c:7284-7285
12161            result.truncate(m + nul);
12162        }
12163    };
12164    while let Some(c) = chars.next() {
12165        apply_pending_mask(&mut result, &mut pending_mask);
12166        flush_tbuf(&mut result, &mut tbuf_mark, &mut ignoring);
12167        if !(0xe184..=0xe1a1).contains(&(c as u32))
12168            && !(c == '\\' && matches!(chars.peek(), Some('u') | Some('U')))
12169        {
12170            tbuf_mark = Some(result.len());
12171        }
12172        consumed += c.len_utf8();
12173        // c:utils.c:7194 — `^X` caret notation. A bare `^` (not a backslash
12174        // escape) followed by any char applies the control mask to it, but
12175        // ONLY under GETKEY_CTRL — i.e. `print -b` / bindkey. Plain print
12176        // (GETKEYS_PRINT, no GETKEY_CTRL) keeps `^A` literal.
12177        if c == '^' && !pending_control && (how & GETKEY_CTRL) != 0 {
12178            // A `\M`/`\C` escape after the caret must be handled by the
12179            // modifier arm with control already pending — C reaches it via
12180            // `continue` rather than consuming the base char here. Gated on
12181            // GETKEY_EMACS because that is what enables the modifier arm at
12182            // all (c:7031/7043); without it C's `case 'M'` emits a literal
12183            // backslash instead.
12184            let mut la = chars.clone();
12185            if (how & GETKEY_EMACS) != 0
12186                && la.next() == Some('\\')
12187                && matches!(la.next(), Some('M') | Some('C'))
12188            {
12189                pending_control = true;
12190                continue;
12191            }
12192            if let Some(base) = chars.next() {
12193                consumed += base.len_utf8();
12194                let mut byte = base as u32;
12195                // c:7261-7267 — `^?` → 0x7f (DEL), else clear bits 5-6.
12196                if byte == '?' as u32 {
12197                    byte = 0x7f;
12198                } else if byte <= 0xff {
12199                    byte &= 0x9f;
12200                }
12201                if byte <= 0xff {
12202                    let b_ = byte as u8;
12203                    if b_ < 0x80 {
12204                        result.push(b_ as char);
12205                    } else {
12206                        result.push('\u{83}');
12207                        result.push(char::from(b_ ^ 32));
12208                    }
12209                } else if let Some(ch) = char::from_u32(byte) {
12210                    result.push(ch);
12211                }
12212                continue;
12213            }
12214            // c:7194 `s[1]` guard — a trailing `^` with no char is literal.
12215            result.push(c);
12216            continue;
12217        }
12218        if c != '\\' {
12219            result.push(c);
12220            continue;
12221        }
12222        // c is a backslash; record its byte start offset for
12223        // GETKEY_UPDATE_OFFSET (`s - sstart` in C).
12224        let bs_off = consumed - c.len_utf8();
12225        // c:utils.c:6987 — base offset decrement for a collapsed `\<char>`
12226        // escape whose backslash precedes the cursor. C gates on
12227        // `*s == '\\' && s[1]`, so a following char must exist (peek).
12228        let mut miscadded = false;
12229        if update_off && chars.peek().is_some() {
12230            if let Some(m) = misc.as_deref_mut() {
12231                if (bs_off as i32) < *m {
12232                    *m -= 1;
12233                    miscadded = true;
12234                }
12235            }
12236        }
12237        match chars.next() {
12238            Some('n') => {
12239                result.push('\n');
12240                consumed += 1;
12241            }
12242            Some('t') => {
12243                result.push('\t');
12244                consumed += 1;
12245            }
12246            Some('r') => {
12247                result.push('\r');
12248                consumed += 1;
12249            }
12250            // c:utils.c:7018-7024 — `\E` is EMACS-gated:
12251            //     case 'E':
12252            //         if (!(how & GETKEY_EMACS)) {
12253            //             *t++ = '\\', s--;
12254            //             if (miscadded) (*misc)++;
12255            //             continue;
12256            //         }
12257            //         /* FALL THROUGH */
12258            //     case 'e':
12259            //         *t++ = '\033';
12260            // Without EMACS `\E` stays a literal backslash + `E`; only `\e`
12261            // is unconditional. Folding `E` in with `e` made `${(g::)v}` on
12262            // `\E` yield ESC where zsh yields `\E`. Unlike the octal
12263            // `continue` arm (c:7159), C restores the miscadded decrement
12264            // here, since the backslash is kept.
12265            Some('E') if (how & GETKEY_EMACS) == 0 => {
12266                consumed += 1;
12267                if miscadded {
12268                    if let Some(m) = misc.as_deref_mut() {
12269                        *m += 1;
12270                    }
12271                }
12272                result.push('\\');
12273                result.push('E');
12274            }
12275            Some('e') | Some('E') => {
12276                result.push('\x1b');
12277                consumed += 1;
12278            }
12279            Some('a') => {
12280                result.push('\x07');
12281                consumed += 1;
12282            }
12283            Some('b') => {
12284                result.push('\x08');
12285                consumed += 1;
12286            }
12287            Some('f') => {
12288                result.push('\x0c');
12289                consumed += 1;
12290            }
12291            Some('v') => {
12292                result.push('\x0b');
12293                consumed += 1;
12294            }
12295            // c:utils.c:7140-7152 — `\\` (and `\'` under
12296            // GETKEY_DOLLAR_QUOTE) explicitly emit the trailing byte
12297            // bare. Outside DOLLAR_QUOTE, `\'` FALLTHROUGHs to default.
12298            // `\\` stays special because the default arm (c:7180-7184)
12299            // skips the leading-backslash emission iff `*s == '\\'`,
12300            // so `\\` reduces to `\` regardless of GETKEY_EMACS.
12301            //
12302            // Previous Rust port also had `Some('\'')` and `Some('"')`
12303            // arms that unconditionally dropped the backslash. That
12304            // contradicted C: under GETKEYS_ECHO (no GETKEY_EMACS),
12305            // `\'` must remain `\'` because echo doesn't strip
12306            // unknown escapes. Removed those arms — `\'` and `\"`
12307            // now flow through the default arm and honour GETKEY_EMACS.
12308            //
12309            // Root cause of `echo "${(qq)s}"` for `s="a'b"` emitting
12310            // `'a'''b'` instead of zsh's `'a'\''b'`.
12311            Some('\\') => {
12312                result.push('\\');
12313                consumed += 1;
12314            }
12315            // c:utils.c:7058-7063 — `\-` under GETKEY_BACKSLASH_MINUS:
12316            //     case '-':
12317            //         if (how & GETKEY_BACKSLASH_MINUS) {
12318            //             *misc  = 1;
12319            //             break;
12320            //         }
12321            //         goto def;
12322            // The escape emits NOTHING; it only reports its presence. Without
12323            // the flag it falls to the default arm below.
12324            Some('-') if (how & crate::ported::zsh_h::GETKEY_BACKSLASH_MINUS as u32) != 0 => {
12325                consumed += 1;
12326                if let Some(m) = misc.as_deref_mut() {
12327                    *m = 1;
12328                }
12329            }
12330            // c:utils.c:7072-7138 — `\u` (4-hex) / `\U` (8-hex) Unicode
12331            // codepoint escapes. `case 'U':` falls through into `case 'u':`
12332            // (c:7072-7076); only the digit count and the
12333            // GETKEY_UPDATE_OFFSET bookkeeping differ. The value is converted
12334            // by `ucs4tomb(wval, t)` (c:7101), i.e. in the CURRENT LOCALE, not
12335            // as blind UTF-8: under `LC_ALL=C` it fails with "character not
12336            // in range" and the escape is not decoded.
12337            Some(uc @ ('u' | 'U')) => {
12338                consumed += 1;
12339                // !!! BASH-MODE (no C counterpart) !!! bash keeps `\u` / `\U`
12340                // verbatim in `$'...'` when no hex digit follows.
12341                if bash_dollar_quote && !chars.peek().is_some_and(|c| c.is_ascii_hexdigit()) {
12342                    result.push('\\');
12343                    result.push(uc);
12344                    continue;
12345                }
12346                // c:utils.c:7073-7084 — `\U` extra `-4` (c:7073) then the
12347                // shared `-6` (c:7077), both gated at offset bs_off+1.
12348                if update_off {
12349                    if let Some(m) = misc.as_deref_mut() {
12350                        if ((bs_off + 1) as i32) < *m {
12351                            *m -= if uc == 'U' { 10 } else { 6 };
12352                        }
12353                    }
12354                }
12355                // c:7085-7095 — `wval = 0;` then up to 4 / 8 hex digits.
12356                // ZERO digits still emits codepoint 0: `\u` → one NUL byte,
12357                // `\uzz` → NUL + "zz".
12358                let mut wval: u32 = 0;
12359                for _ in 0..(if uc == 'u' { 4 } else { 8 }) {
12360                    match chars.peek().and_then(|c| c.to_digit(16)) {
12361                        Some(d) => {
12362                            chars.next();
12363                            consumed += 1;
12364                            wval = wval.wrapping_mul(16).wrapping_add(d);
12365                        }
12366                        None => break,
12367                    }
12368                }
12369                // c:7101 — `count = ucs4tomb(wval, t);`
12370                let mut tbuf = [0u8; 16];
12371                let count = ucs4tomb(wval, &mut tbuf);
12372                if count == -1 {
12373                    // c:7102-7121 — the conversion failed (ucs4tomb already
12374                    // raised the zerr). Under GETKEY_DOLLAR_QUOTE the rest of
12375                    // the input is copied through undecoded (c:7104-7115);
12376                    // otherwise the output ends here (c:7117-7119).
12377                    if (how & crate::ported::zsh_h::GETKEY_DOLLAR_QUOTE as u32) != 0 {
12378                        for rest in chars.by_ref() {
12379                            // c:7105-7108 — `if (s - sstart > *misc) (*misc)++;`
12380                            if update_off {
12381                                if let Some(m) = misc.as_deref_mut() {
12382                                    if (consumed as i32) > *m {
12383                                        *m += 1;
12384                                    }
12385                                }
12386                            }
12387                            consumed += rest.len_utf8();
12388                            result.push(rest);
12389                        }
12390                    }
12391                    flush_tbuf(&mut result, &mut tbuf_mark, &mut ignoring);
12392                    return (result, consumed); // c:7121
12393                }
12394                // c:7122-7124 — add one per output byte, checked at the last
12395                // consumed input byte.
12396                if update_off {
12397                    if let Some(m) = misc.as_deref_mut() {
12398                        if ((consumed - 1) as i32) < *m {
12399                            *m += count;
12400                        }
12401                    }
12402                }
12403                // c:7127-7137 — the converted bytes are metafied (`imeta`)
12404                // on the way out. Well-formed UTF-8 is a char a `String`
12405                // holds as-is; any other byte >= 0x80 (a non-UTF-8 locale's
12406                // encoding, or a surrogate's UTF-8 form) gets Meta + byte^32.
12407                let bytes = &tbuf[..count as usize];
12408                match std::str::from_utf8(bytes) {
12409                    Ok(v) => result.push_str(v),
12410                    Err(_) => {
12411                        for &b in bytes {
12412                            if b < 0x80 {
12413                                result.push(b as char);
12414                            } else {
12415                                result.push('\u{83}');
12416                                result.push(char::from(b ^ 32));
12417                            }
12418                        }
12419                    }
12420                }
12421            }
12422            // c:utils.c:7156-7178 — the NUMERIC-ESCAPE branch. C handles
12423            // `\x`, `\NNN` and `\0NNN` in ONE arm, and the shared tail is
12424            // what makes them agree, so this port keeps them together too:
12425            //
12426            //     if ((idigit(*s) && *s < '8') || *s == 'x') {
12427            //         if (!(how & GETKEY_OCTAL_ESC)) {
12428            //             if (*s == '0')
12429            //                 s++;
12430            //             else if (*s != 'x') {
12431            //                 *t++ = '\\', s--;
12432            //                 continue;
12433            //             }
12434            //         }
12435            //         if (s[1] && s[2] && s[3]) {
12436            //             svchar = s[3]; s[3] = '\0'; u = s;
12437            //         }
12438            //         *t++ = zstrtol(s + (*s == 'x'), &s,
12439            //                        (*s == 'x') ? 16 : 8);
12440            //         ...
12441            //         s--;
12442            //     }
12443            //
12444            // Three behaviours here were each missing when these were three
12445            // separate arms:
12446            //
12447            //  1. Without GETKEY_OCTAL_ESC a NON-ZERO digit is not an escape
12448            //     at all (c:7159): C emits a literal backslash and backs up so
12449            //     the digit is re-read as an ordinary char. This is INSIDE the
12450            //     numeric branch, so — unlike the default arm — GETKEY_EMACS
12451            //     does not suppress the backslash: `${(g:e:)v}` on `\101` is
12452            //     `\101`, not `101`.
12453            //  2. The value goes through `zstrtol`, which SKIPS leading blanks
12454            //     and accepts a `+`/`-` sign (c:2444-2450), and whose end
12455            //     pointer lands past everything it consumed — blanks included.
12456            //     So `\0 1` is byte 0o1, `\x 41` is 0x04 then `1`, `\x y` is
12457            //     NUL then `y`, and `\0-1` is 0xff (a NEGATIVE parse truncated
12458            //     to a byte). Peeking for "is the next char a digit" stopped at
12459            //     the blank and left it in the output.
12460            //  3. The base is chosen from the char AT `s` AFTER the `\0`
12461            //     introducer was skipped, so `\0x41` is HEX (`A`), not NUL
12462            //     followed by `x41`.
12463            //
12464            // c:7166-7168 windows the parse by NUL-ing `s[3]` when three chars
12465            // follow `s`, capping it at 3 significant chars (2 after an `x`),
12466            // then restores the byte — so `\1234` is 0o123 then `4`.
12467            //
12468            // `*t++ = zstrtol(...)` assigns a zlong to a char, truncating to
12469            // the low byte: `\777` (511) is 0xff, `\400` (256) is NUL. Parsing
12470            // straight into u8 made those an Err and emitted nothing at all.
12471            Some(d) if d.is_digit(8) || d == 'x' => {
12472                consumed += 1;
12473                // !!! BASH-MODE (no C counterpart) !!! bash keeps `\x` verbatim
12474                // in `$'...'` when no hex digit follows (zsh decodes a NUL).
12475                if bash_dollar_quote && d == 'x' && !chars.peek().is_some_and(|c| c.is_ascii_hexdigit()) {
12476                    result.push_str("\\x");
12477                    continue;
12478                }
12479                // c:7157-7161 — the pre-checks that run only without
12480                // GETKEY_OCTAL_ESC. `\0` is the octal introducer (`s++`);
12481                // any other bare digit is not an escape.
12482                let zero_intro = (how & GETKEY_OCTAL_ESC) == 0 && d == '0';
12483                if (how & GETKEY_OCTAL_ESC) == 0 && d != '0' && d != 'x' {
12484                    // c:7159 `*t++ = '\\', s--; continue;`
12485                    result.push('\\');
12486                    result.push(d);
12487                    continue;
12488                }
12489                // C's `s` after the pre-checks: the char AFTER the `\0`
12490                // introducer, else the matched char itself.
12491                //
12492                // Four chars of lookahead is all C can read: the window test
12493                // reaches `s[3]`, and the parse never sees past it. Bounding
12494                // the clone keeps this O(1) per escape — collecting the whole
12495                // tail here would make decoding an escape-dense string O(n²).
12496                let rest: Vec<char> = chars.clone().take(4).collect();
12497                let (p, after): (Option<char>, &[char]) = if zero_intro {
12498                    (rest.first().copied(), rest.get(1..).unwrap_or(&[]))
12499                } else {
12500                    (Some(d), &rest[..])
12501                };
12502                // c:7166 `if (s[1] && s[2] && s[3])` — needs three chars after
12503                // `s`; the window then spans `s` plus two more.
12504                let win: &[char] = if after.len() >= 3 { &after[..2] } else { after };
12505                let mut vis: Vec<char> = Vec::with_capacity(3);
12506                if let Some(pc) = p {
12507                    vis.push(pc);
12508                }
12509                vis.extend_from_slice(win);
12510                // c:7170 `zstrtol(s + (*s == 'x'), &s, (*s == 'x') ? 16 : 8)`.
12511                let (base, skip_p) = if p == Some('x') { (16, 1) } else { (8, 0) };
12512                let parse_src: String = vis[skip_p.min(vis.len())..].iter().collect();
12513                let (num, tail) = zstrtol(&parse_src, base);
12514                let used_chars = parse_src[..parse_src.len() - tail.len()].chars().count();
12515                // Advance past what C consumed. `vis[0]` is the already-taken
12516                // match char unless the `\0` introducer moved `s` forward.
12517                let vis_used = skip_p + used_chars;
12518                let advance = if zero_intro {
12519                    vis_used
12520                } else {
12521                    vis_used.saturating_sub(1)
12522                };
12523                for _ in 0..advance {
12524                    chars.next();
12525                    consumed += 1;
12526                }
12527                let val = (num as u64 & 0xff) as u8;
12528                // c:Src/utils.c — the escape is one raw BYTE; metafy high
12529                // bytes (c:7289-7294) so `\377` unmetafies to a single 0xff,
12530                // not the UTF-8 pair c3 bf.
12531                if val < 0x80 {
12532                    result.push(val as char);
12533                } else {
12534                    result.push('\u{83}');
12535                    result.push(char::from(val ^ 32));
12536                }
12537                // c:7172-7173 — under GETKEY_PRINTF_PERCENT a numeric escape
12538                // producing `%` gets a second `%`.
12539                if (how & crate::ported::zsh_h::GETKEY_PRINTF_PERCENT as u32) != 0 && val == b'%' {
12540                    result.push('%');
12541                }
12542            }
12543            // c:utils.c:7029-7052 + c:7255-7275 — `\C` / `\M` set the
12544            // control / meta modifiers (bindkey-style key escapes), an
12545            // optional `-` separator, then the next base char (possibly
12546            // through chained `\C`/`\M`) gets the mask applied:
12547            // control → `& 0x9f` (or `\C-?` → 0x7f), meta → `| 0x80`.
12548            // Gated on GETKEY_EMACS (c:7031/7043 `if (how & GETKEY_EMACS)`):
12549            // print / print -b / $'…' have it, echo does not (so echo keeps
12550            // `\C-a` literal via the default arm). Mirrors the same handler
12551            // already present in `getkeystring` (utils.rs) and
12552            // `getkeystring_dollar_quote` (lex.rs); print's `getkeystring_with`
12553            // path lacked it, so `print "\C-a"` emitted literal `C-a`.
12554            Some(mod_letter @ ('C' | 'M')) if (how & GETKEY_EMACS) != 0 => {
12555                consumed += 1;
12556                // c:7034 `meta = 1 + control;  /* preserve the order of ^ and
12557                // meta */` — meta is a COUNT, not a flag: 2 when `\M` was seen
12558                // while control was already pending, which makes c:7261-7264
12559                // apply `|0x80` BEFORE the control mask instead of after. The
12560                // two orders differ for `?`: `\M-\C-?` is 0xff, `\C-\M-?` is
12561                // 0x9f. Booleans could not express that.
12562                // A `^` already seen (c:7194 `control = 1; continue;`) arrives
12563                // here as pending state, so `^\M-a` gives meta = 1 + 1 = 2.
12564                let mut control: u32 = if pending_control {
12565                    pending_control = false;
12566                    1
12567                } else {
12568                    0
12569                };
12570                let mut meta: u32 = 0;
12571                if mod_letter == 'C' {
12572                    control = 1; // c:7046
12573                } else {
12574                    meta = 1 + control; // c:7034
12575                }
12576                // c:7032-7033 / 7044-7045 — `if (s[1] == '-') s++;` consumes at
12577                // most ONE separator per modifier. Looping over a RUN of dashes
12578                // ate the base character of `\M--` (meta applied to `-`, 0xad),
12579                // leaving an empty binding.
12580                if chars.peek() == Some(&'-') {
12581                    chars.next();
12582                    consumed += 1;
12583                }
12584                // Chained `\C`/`\M`, and the caret form after a modifier.
12585                loop {
12586                    let mut iter_clone = chars.clone();
12587                    if iter_clone.next() == Some('\\') {
12588                        if let Some(nx) = iter_clone.next() {
12589                            if nx == 'C' || nx == 'M' {
12590                                chars.next(); // consume '\'
12591                                chars.next(); // consume C/M
12592                                consumed += 2;
12593                                if nx == 'C' {
12594                                    control = 1; // c:7046
12595                                } else {
12596                                    meta = 1 + control; // c:7034
12597                                }
12598                                if chars.peek() == Some(&'-') {
12599                                    chars.next();
12600                                    consumed += 1;
12601                                }
12602                                continue;
12603                            }
12604                        }
12605                    }
12606                    // c:7194 — `else if (*s == '^' && !control && (how &
12607                    // GETKEY_CTRL) && s[1]) { control = 1; continue; }`. C's
12608                    // `case 'M'` only sets `meta` and `continue`s, so the MAIN
12609                    // loop reaches this `^` handler with meta still pending:
12610                    // `\M-^?` is control+meta applied to `?` (0xff). This arm
12611                    // consumed the base char itself, so `^` became the base and
12612                    // `\M-^?` bound TWO bytes (0xde 0x3f) — breaking the very
12613                    // common `bindkey '\M-^?' backward-kill-word`.
12614                    if control == 0 && (how & GETKEY_CTRL) != 0 && chars.peek() == Some(&'^') {
12615                        let mut it2 = chars.clone();
12616                        it2.next();
12617                        if it2.next().is_some() {
12618                            // c:7194 `s[1]` guard
12619                            chars.next();
12620                            consumed += 1;
12621                            control = 1;
12622                            continue;
12623                        }
12624                    }
12625                    break;
12626                }
12627                // c:7044-7047 — `control = 1; continue;` (and `meta = 1 +
12628                // control`, c:7034): the modifier does not read its base
12629                // character itself. The next loop iteration decodes whatever
12630                // follows — a plain char or a full escape such as `\130` /
12631                // `\x58` / `\e` — and c:7261-7275 masks the byte it emitted.
12632                // Reading a restricted base here turned `\C-\130` into
12633                // `\C-\1` + `30` (0x11 `3` `0`) where zsh gives 0x18.
12634                pending_mask = Some((result.len(), control, meta));
12635            }
12636            // c:utils.c:7180-7184 — default arm. With GETKEY_EMACS
12637            // set, drop the backslash; otherwise keep `\<char>`.
12638            Some(c) => {
12639                consumed += 1;
12640                // c:utils.c:7045 — `\c` under GETKEY_BACKSLASH_C
12641                // means TRUNCATE: drop the rest of the input,
12642                // suppress the trailing newline. Used by `echo`
12643                // (and `print` without -r). Set TLS flag so the
12644                // caller can detect + suppress the newline.
12645                if c == 'c' && (how & GETKEY_BACKSLASH_C) != 0 {
12646                    GETKEY_TRUNCATED.with(|cell| cell.set(true));
12647                    break;
12648                }
12649                // !!! BASH-MODE (no C counterpart) !!! bash `$'\cX'` is the
12650                // control character (`\c?` is DEL); any other unknown escape
12651                // keeps its backslash (`$'\z'` is `\z`, zsh drops it).
12652                if bash_dollar_quote {
12653                    if c == 'c' {
12654                        if let Some(x) = chars.next() {
12655                            consumed += 1;
12656                            result.push(if x == '?' { '\u{7f}' } else { ((x.to_ascii_uppercase() as u8) ^ 0x40) as char });
12657                            continue;
12658                        }
12659                    }
12660                    if !matches!(c, '\'' | '"' | '?' | '\\') {
12661                        result.push('\\');
12662                        result.push(c);
12663                        continue;
12664                    }
12665                }
12666                if (how & GETKEY_EMACS) == 0 {
12667                    // c:utils.c:7181-7183 — backslash kept literal (no EMACS),
12668                    // so the string does not collapse here: undo the base
12669                    // GETKEY_UPDATE_OFFSET decrement applied above.
12670                    if miscadded {
12671                        if let Some(m) = misc.as_deref_mut() {
12672                            *m += 1;
12673                        }
12674                    }
12675                    result.push('\\');
12676                }
12677                result.push(c);
12678            }
12679            None => {
12680                result.push('\\');
12681            }
12682        }
12683    }
12684    apply_pending_mask(&mut result, &mut pending_mask);
12685    flush_tbuf(&mut result, &mut tbuf_mark, &mut ignoring);
12686    (result, consumed)
12687}
12688
12689thread_local! {
12690    /// Sticky flag set by `getkeystring_with` when a `\c` escape
12691    /// truncates the input under `GETKEY_BACKSLASH_C`. The caller
12692    /// (bin_print / bin_echo) reads + clears via `getkey_truncated_take()`
12693    /// to suppress the trailing newline and skip any subsequent args
12694    /// in the print pass.
12695    pub static GETKEY_TRUNCATED: std::cell::Cell<bool> =
12696        const { std::cell::Cell::new(false) };
12697}
12698
12699/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12700/// standalone function.
12701/// C returns the `\c`-truncation signal through `getkeystring`'s `int *misc`
12702/// out-parameter (`Src/utils.c:6915`). Rust's `getkeystring_with` records it
12703/// in a thread-local instead; this reads and clears that flag.
12704/// Read + clear the `\c`-truncated flag set by the previous
12705/// `getkeystring_with` call. Returns true once after a truncation
12706/// fires, false otherwise.
12707pub fn getkey_truncated_take() -> bool {
12708    GETKEY_TRUNCATED.with(|c| {
12709        let v = c.get();
12710        c.set(false);
12711        v
12712    })
12713}
12714
12715/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12716/// standalone function.
12717/// Equivalent to the C expression `fdtable[fd]` — a read of the
12718/// `unsigned char *fdtable` global declared at `Src/exec.c:179` and grown by
12719/// `growfdtable` (`Src/utils.c:1965`). C indexes the array directly.
12720/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
12721/// the C expression `fdtable[fd]` (read). Returns `FDT_UNUSED` for
12722/// any fd that has not been explicitly set, matching the C source's
12723/// post-`growfdtable` zero-fill behaviour at Src/utils.c:1979.
12724pub fn fdtable_get(fd: i32) -> i32 {
12725    // c:utils.c:fdtable[fd]
12726    if fd < 0 {
12727        return FDT_UNUSED;
12728    }
12729    let g = fdtable_lock().lock().unwrap();
12730    g.get(fd as usize).copied().unwrap_or(FDT_UNUSED)
12731}
12732
12733/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12734/// standalone function.
12735/// Equivalent to the C statement `fdtable[fd] = kind;` over the
12736/// `unsigned char *fdtable` global (`Src/exec.c:179`), with the
12737/// `growfdtable` call (`Src/utils.c:1965`) inlined.
12738/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
12739/// the C statement `fdtable[fd] = kind;`. Inlines the `growfdtable`
12740/// call from Src/utils.c:1965 since C always invokes it immediately
12741/// before the assignment anyway. Also updates `MAX_ZSH_FD` to track
12742/// the highest assigned slot — C does this inside `check_fd_table`
12743/// at `Src/utils.c:1982` (`max_zsh_fd = fd;`). Previously the Rust
12744/// `fdtable_set` skipped this, leaving `max_zsh_fd = 0` after any
12745/// `addlockfd` / `addmodulefd` call, which broke `zcloselockfd`'s
12746/// `if (fd > max_zsh_fd)` guard.
12747pub fn fdtable_set(fd: i32, kind: i32) {
12748    // c:utils.c:fdtable[fd]
12749    if fd < 0 {
12750        return;
12751    }
12752    let mut g = fdtable_lock().lock().unwrap();
12753    if (fd as usize) >= g.len() {
12754        g.resize((fd as usize) + 1, FDT_UNUSED);
12755    }
12756    g[fd as usize] = kind;
12757    // c:1982 — `max_zsh_fd = fd;` (with `if (fd <= max_zsh_fd) return;`
12758    // guard at c:1971). Inline equivalent: bump only when this fd
12759    // exceeds the current max.
12760    let cur = MAX_ZSH_FD.load(Ordering::Relaxed);
12761    if fd > cur {
12762        MAX_ZSH_FD.store(fd, Ordering::Relaxed);
12763    }
12764}
12765
12766/// Port of `imeta()` from `Src/ztype.h:60`.
12767///
12768/// Port of `imeta()` macro from `Src/ztype.h:60` — `zistype(X, IMETA)`.
12769///
12770/// IMETA is set in the typtab at `Src/utils.c:4195-4201` for:
12771///   - `'\0'` (c:4195)
12772///   - `Meta` (0x83) (c:4196)
12773///   - `Marker` (0xa2) (c:4197)
12774///   - `Pound..=LAST_NORMAL_TOK` = `0x84..=0x9c` (c:4198)
12775///   - `Snull..=Nularg` = `0x9d..=0xa1` (c:4200)
12776///
12777/// The CANONICAL set is `{0x00, 0x83..=0xa2}`. The previous Rust
12778/// port used `b >= 0x83` (every byte 0x83..=0xff) which was WRONG
12779/// for bytes `0xa3..=0xff`: C `imeta()` returns false for those,
12780/// so metafy passes them through unchanged. Rust's broader range
12781/// caused `metafy` to corrupt UTF-8 multibyte content (e.g.,
12782/// `é` = `0xc3 0xa9`: both bytes are NOT IMETA in C, but Rust
12783/// escaped both as `Meta + (byte ^ 32)`, mangling the encoding
12784/// and breaking every downstream consumer that expects the raw
12785/// UTF-8 bytes to round-trip).
12786///
12787/// Use the closed-range form rather than the typtab lookup so
12788/// `metafy` / `imeta_byte` work in test contexts where the typtab
12789/// isn't initialised, AND so the fast-path doesn't go through a
12790/// Mutex lock per byte (`metafy` is called per-character on every
12791/// shell input line).
12792#[inline]
12793pub fn imeta_byte(b: u8) -> bool {
12794    // c:4195-4201 — canonical IMETA range.
12795    b == 0 || (0x83..=0xa2).contains(&b)
12796}
12797
12798// `pub fn convfloat` and `pub fn convfloat_underscore` re-export
12799// wrappers DELETED per PORT.md Rule C. C defines both in
12800// `Src/params.c:5690` and `:5765` — the canonical Rust home is
12801// `src/ported/params.rs` (`convfloat` at line 7356,
12802// `convfloat_underscore` matching). Caller convenience cost is
12803// `use crate::ported::params::convfloat;` instead of `utils::`;
12804// no Rust-side re-export needed.
12805
12806// ===========================================================
12807// Methods moved verbatim from src/ported/vm_helper because their
12808// C counterpart's source file maps 1:1 to this Rust module.
12809// Phase: drift
12810// ===========================================================
12811
12812// BEGIN moved-from-exec-rs
12813// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
12814
12815// END moved-from-exec-rs
12816
12817// ===========================================================
12818// Free ported moved verbatim from src/ported/vm_helper.
12819// ===========================================================
12820// BEGIN moved-from-exec-rs (free ported)
12821/// Port of `base64_decode()` from `Src/Zle/termquery.c:571`.
12822/// The only base64 decoder in zsh's C tree — it is NOT a utils.c function.
12823///
12824/// !!! DIVERGENT DUPLICATE !!! The faithful line-by-line port of that C body
12825/// lives at `src/ported/zle/termquery.rs:451`. This copy arrived with the
12826/// `vm_helper` move (see the marker above), decodes in fixed 4-byte chunks
12827/// rather than C's byte-at-a-time `while (len && (c = src[i]) != '=')` loop,
12828/// and has one caller (`src/fusevm_bridge.rs:11864`).
12829pub(crate) fn base64_decode(s: &str) -> Vec<u8> {
12830    let decode_char = |c: u8| -> Option<u8> {
12831        match c {
12832            b'A'..=b'Z' => Some(c - b'A'),
12833            b'a'..=b'z' => Some(c - b'a' + 26),
12834            b'0'..=b'9' => Some(c - b'0' + 52),
12835            b'+' => Some(62),
12836            b'/' => Some(63),
12837            _ => None,
12838        }
12839    };
12840    let bytes = s.as_bytes();
12841    let mut out = Vec::with_capacity(s.len() / 4 * 3);
12842    let mut i = 0;
12843    while i + 4 <= bytes.len() {
12844        let chunk = &bytes[i..i + 4];
12845        let pad = chunk.iter().filter(|&&c| c == b'=').count();
12846        let v0 = decode_char(chunk[0]).unwrap_or(0) as u32;
12847        let v1 = decode_char(chunk[1]).unwrap_or(0) as u32;
12848        let v2 = decode_char(chunk[2]).unwrap_or(0) as u32;
12849        let v3 = decode_char(chunk[3]).unwrap_or(0) as u32;
12850        let n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
12851        out.push(((n >> 16) & 0xff) as u8);
12852        if pad < 2 {
12853            out.push(((n >> 8) & 0xff) as u8);
12854        }
12855        if pad < 1 {
12856            out.push((n & 0xff) as u8);
12857        }
12858        i += 4;
12859    }
12860    out
12861}
12862// END moved-from-exec-rs (free ported)
12863
12864// ===========================================================
12865// Utility helpers moved from src/ported/vm_helper.
12866// All correspond to Src/utils.c logic (path/string/bslashquote helpers).
12867// ===========================================================
12868
12869// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12870// ─── RUST-ONLY ACCESSORS ───
12871//
12872// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
12873// RwLock<T>>` globals declared above. C zsh uses direct global
12874// access; Rust needs these wrappers because `OnceLock::get_or_init`
12875// is the only way to lazily construct shared state. These ported sit
12876// here so the body of this file reads in C source order without
12877// the accessor wrappers interleaved between real port ported.
12878// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12879
12880// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12881// ─── RUST-ONLY ACCESSORS ───
12882//
12883// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
12884// RwLock<T>>` globals declared above. C zsh uses direct global
12885// access; Rust needs these wrappers because `OnceLock::get_or_init`
12886// is the only way to lazily construct shared state. These ported sit
12887// here so the body of this file reads in C source order without
12888// the accessor wrappers interleaved between real port ported.
12889// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12890
12891// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12892// ─── RUST-ONLY ACCESSORS ───
12893//
12894// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
12895// RwLock<T>>` globals declared above. C zsh uses direct global
12896// access; Rust needs these wrappers because `OnceLock::get_or_init`
12897// is the only way to lazily construct shared state. These ported sit
12898// here so the body of this file reads in C source order without
12899// the accessor wrappers interleaved between real port ported.
12900// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12901
12902// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12903// ─── RUST-ONLY ACCESSORS ───
12904//
12905// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
12906// RwLock<T>>` globals declared above. C zsh uses direct global
12907// access; Rust needs these wrappers because `OnceLock::get_or_init`
12908// is the only way to lazily construct shared state. These ported sit
12909// here so the body of this file reads in C source order without
12910// the accessor wrappers interleaved between real port ported.
12911// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12912
12913// WARNING: NOT IN UTILS.C — Rust-only OnceLock get-or-init
12914// helpers. C dereferences each global directly.
12915/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12916/// standalone function.
12917/// `OnceLock` get-or-init accessor for the storage behind C's
12918/// `char *scriptname` global (`Src/utils.c:36`). C dereferences the global.
12919fn scriptname_lock() -> &'static crate::thread_shell_state::ThreadMutex<Option<String>> {
12920    &SCRIPTNAME
12921}
12922
12923// WARNING: NOT IN UTILS.C — see scriptname_lock above.
12924/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12925/// standalone function.
12926/// `OnceLock` get-or-init accessor for the storage behind C's
12927/// `char *argzero` global (`Src/params.c:72`).
12928fn argzero_lock() -> &'static Mutex<Option<String>> {
12929    ARGZERO.get_or_init(|| Mutex::new(None))
12930}
12931
12932// WARNING: NOT IN UTILS.C — see scriptname_lock above.
12933/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12934/// standalone function.
12935/// `OnceLock` get-or-init accessor for the storage behind C's
12936/// `char *scriptfilename` global (`Src/utils.c:41`).
12937fn scriptfilename_lock() -> &'static crate::thread_shell_state::ThreadMutex<Option<String>> {
12938    &SCRIPTFILENAME
12939}
12940
12941// WARNING: NOT IN UTILS.C — Rust-only OnceLock accessor for `posixzero`.
12942// C's `posixzero` lives in params.c:76; the canonical storage lives
12943// here for OnceLock initialisation parity with `argzero`.
12944/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12945/// standalone function.
12946/// `OnceLock` get-or-init accessor for the storage behind C's
12947/// `char *posixzero` global (`Src/params.c:76`).
12948fn posixzero_lock() -> &'static Mutex<Option<String>> {
12949    POSIXZERO.get_or_init(|| Mutex::new(None))
12950}
12951
12952// WARNING: NOT IN UTILS.C — see scriptname_lock above.
12953/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12954/// standalone function.
12955/// `OnceLock` get-or-init accessor for the storage behind C's
12956/// `int noerrs` global (`Src/exec.c:117`).
12957pub fn noerrs_lock() -> &'static Mutex<i32> {
12958    NOERRS.get_or_init(|| Mutex::new(0))
12959}
12960
12961// `locallevel_lock` removed — duplicate of canonical
12962// `LOCALLEVEL` (port of params.c:54). All
12963// accessors here now route through the canonical AtomicI32.
12964// WARNING: NOT IN UTILS.C — see scriptname_lock above.
12965/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12966/// standalone function.
12967/// `OnceLock` get-or-init accessor for the storage behind C's
12968/// `zlong lineno` global (`Src/params.c:123`).
12969fn lineno_lock() -> &'static Mutex<i32> {
12970    LINENO.get_or_init(|| Mutex::new(0))
12971}
12972
12973// WARNING: NOT IN UTILS.C — Rust-only cache for the SHINSTDIN
12974// option flag so error-emission doesn't pull in the option-table.
12975/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12976/// standalone function.
12977/// `OnceLock` get-or-init accessor for the SHINSTDIN option cache; the
12978/// option itself is table entry `Src/options.c:248`.
12979fn shinstdin_lock() -> &'static Mutex<bool> {
12980    SHINSTDIN_OPT.get_or_init(|| Mutex::new(false))
12981}
12982
12983/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12984/// standalone function.
12985/// `OnceLock` get-or-init accessor for the storage behind C's
12986/// `unsigned char *fdtable` global (`Src/exec.c:179`).
12987/// !!! RUST-ONLY HELPER — see WARNING block above. C source uses bare
12988/// `unsigned char *fdtable` global from Src/utils.c:~63.
12989fn fdtable_lock() -> &'static Mutex<Vec<i32>> {
12990    FDTABLE.get_or_init(|| Mutex::new(Vec::new()))
12991}
12992
12993#[cfg(test)]
12994mod tests {
12995    use super::*;
12996
12997    /// checkmailpath port (utils.c:1620). Drives mtime/atime/size through
12998    /// `libc::utimes` so the new-mail condition is deterministic in headless CI.
12999    #[test]
13000    fn checkmailpath_new_mail_conditions() {
13001        use std::io::Write as _;
13002        let _g = crate::test_util::global_state_lock();
13003
13004        // Unique temp file with content (size != 0).
13005        let dir = std::env::temp_dir();
13006        let path = dir.join(format!("zshrs_mailtest_{}", std::process::id()));
13007        {
13008            let mut f = std::fs::File::create(&path).unwrap();
13009            f.write_all(b"new message\n").unwrap();
13010        }
13011        let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
13012        // atime (times[0]) <= mtime (times[1]); both well past 0.
13013        let set_times = |atime: i64, mtime: i64| {
13014            let tv = [
13015                libc::timeval {
13016                    tv_sec: atime as libc::time_t,
13017                    tv_usec: 0,
13018                },
13019                libc::timeval {
13020                    tv_sec: mtime as libc::time_t,
13021                    tv_usec: 0,
13022                },
13023            ];
13024            assert_eq!(unsafe { libc::utimes(cpath.as_ptr(), tv.as_ptr()) }, 0);
13025        };
13026
13027        let saved_shout = *crate::ported::init::shout.lock().unwrap();
13028        let saved_lmc = LAST_MAILCHECK.load(Ordering::Relaxed);
13029        *crate::ported::init::shout.lock().unwrap() = 1; // interactive
13030        LAST_MAILCHECK.store(0, Ordering::Relaxed);
13031
13032        let p = path.to_str().unwrap().to_string();
13033
13034        // c:1671-1675 — unread + newer than last check → "You have new mail."
13035        set_times(1_000_000, 2_000_000);
13036        assert_eq!(
13037            checkmailpath(&[p.clone()]),
13038            vec!["You have new mail.".to_string()]
13039        );
13040
13041        // c:1676-1698 — `PATH?message`: the message (here a literal) is emitted.
13042        assert_eq!(
13043            checkmailpath(&[format!("{}?custom note", p)]),
13044            vec!["custom note".to_string()]
13045        );
13046
13047        // c:1672 — mtime older than the last check → nothing.
13048        LAST_MAILCHECK.store(9_000_000, Ordering::Relaxed);
13049        assert!(checkmailpath(&[p.clone()]).is_empty());
13050
13051        // c:1670 — non-interactive (no shout) → nothing, even when fresh.
13052        LAST_MAILCHECK.store(0, Ordering::Relaxed);
13053        *crate::ported::init::shout.lock().unwrap() = 0;
13054        assert!(checkmailpath(&[p.clone()]).is_empty());
13055
13056        // c:1634-1636 — empty component is reported (stderr), yields no message.
13057        *crate::ported::init::shout.lock().unwrap() = 1;
13058        assert!(checkmailpath(&["?orphan".to_string()]).is_empty());
13059
13060        *crate::ported::init::shout.lock().unwrap() = saved_shout;
13061        LAST_MAILCHECK.store(saved_lmc, Ordering::Relaxed);
13062        let _ = std::fs::remove_file(&path);
13063    }
13064
13065    /// zsh_errno_msg / zerrmsg `%e` rendering (utils.c:348-365). EINTR is the
13066    /// literal "interrupt"; EIO is verbatim strerror (keeps its capital); every
13067    /// other errno lowercases the first letter (`tulower`). The old
13068    /// io::Error-based path kept the capital and appended " (os error N)".
13069    #[test]
13070    fn zsh_errno_msg_matches_c_percent_e() {
13071        // c:355-358 — EINTR → "interrupt".
13072        assert_eq!(zsh_errno_msg(libc::EINTR), "interrupt");
13073
13074        // c:366 — non-EIO errno lowercases the first letter and never carries
13075        // the Rust "(os error N)" suffix.
13076        let acc = zsh_errno_msg(libc::EACCES);
13077        assert!(
13078            acc.chars().next().is_some_and(|c| c.is_lowercase()),
13079            "EACCES first letter must be lowercased: {acc:?}"
13080        );
13081        assert!(
13082            !acc.contains("(os error"),
13083            "must be strerror, not io::Error: {acc:?}"
13084        );
13085
13086        // c:359-364 — EIO is emitted verbatim (its message reads wrong
13087        // lowercased), so its first letter stays capitalized.
13088        let eio = zsh_errno_msg(libc::EIO);
13089        assert!(
13090            eio.chars().next().is_some_and(|c| c.is_uppercase()),
13091            "EIO must be verbatim strerror (capitalized): {eio:?}"
13092        );
13093    }
13094
13095    /// c:351-354 — zerrmsg's `%e` arm sets errflag |= ERRFLAG_ERROR on EINTR.
13096    #[test]
13097    fn zerrmsg_eintr_sets_errflag() {
13098        let _g = crate::test_util::global_state_lock();
13099        let saved = errflag.load(Ordering::Relaxed);
13100        errflag.store(0, Ordering::Relaxed);
13101        zerrmsg("test", Some(libc::EINTR)); // writes to stderr; side-effect is the flag
13102        assert_ne!(
13103            errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
13104            0,
13105            "EINTR must set ERRFLAG_ERROR"
13106        );
13107        // A non-EINTR errno must NOT set the flag.
13108        errflag.store(0, Ordering::Relaxed);
13109        zerrmsg("test", Some(libc::EACCES));
13110        assert_eq!(errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR, 0);
13111        errflag.store(saved, Ordering::Relaxed);
13112    }
13113
13114    /// zjoin port (utils.c:3622). ASCII delimiters join byte-for-byte; a meta
13115    /// delimiter (NUL is `imeta` per inittyptab c:4195) must be written in
13116    /// metafied form (`Meta` + `delim^32`), never as a raw byte.
13117    #[test]
13118    fn zjoin_ascii_and_meta_delims() {
13119        let v = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
13120
13121        // c:3632-3641 — ordinary (non-meta) delimiter: plain interposition.
13122        assert_eq!(zjoin(&v(&["a", "b", "c"]), ' '), "a b c");
13123        assert_eq!(zjoin(&v(&["foo"]), ':'), "foo"); // single elem, no delim
13124                                                     // c:3629-3630 — empty array → "".
13125        assert_eq!(zjoin(&v(&[]), ' '), "");
13126
13127        // c:3634-3637 — NUL is imeta, so the separator is metafied, NOT a raw
13128        // NUL byte (the prior `arr.join("\0")` bug). The Meta byte (0x83) can't
13129        // survive the UTF-8 String boundary, but the raw NUL must be gone.
13130        let joined = zjoin(&v(&["a", "b"]), '\0');
13131        assert!(
13132            !joined.as_bytes().contains(&0u8),
13133            "NUL delim must be metafied, not emitted as a raw NUL byte: {joined:?}"
13134        );
13135        assert_ne!(joined, "a\u{0}b", "must not raw-join on a meta delimiter");
13136        assert!(joined.starts_with('a') && joined.ends_with('b'));
13137    }
13138
13139    /// findsep port (utils.c:3784). Pure function — no global/cwd state.
13140    #[test]
13141    fn findsep_default_ifs_separators() {
13142        inittyptab(); // ISEP bits are set by the type table (c:4155)
13143                      // c:3814-3817 — advance to the first ISEP char, return 1.
13144        let mut s = "foo bar".to_string();
13145        let mut pos = 0usize;
13146        assert_eq!(findsep(&mut s, &mut pos, None, false), 1);
13147        assert_eq!(pos, 3); // points at the space
13148        assert_eq!(&s[..pos], "foo");
13149
13150        // c:3821 — already sitting on a separator: return 0, no advance.
13151        let mut s = " foo".to_string();
13152        let mut pos = 0usize;
13153        assert_eq!(findsep(&mut s, &mut pos, None, false), 0);
13154        assert_eq!(pos, 0);
13155    }
13156
13157    #[test]
13158    fn findsep_quote_strips_escaped_separator() {
13159        inittyptab(); // ISEP bits are set by the type table (c:4155)
13160                      // c:3795-3804 — `\<sep>` is not a separator; the backslash is
13161                      // stripped in place and the bare char is consumed into the word.
13162        let mut s = "foo\\ bar".to_string(); // foo<bslash><space>bar
13163        let mut pos = 0usize;
13164        let r = findsep(&mut s, &mut pos, None, true);
13165        assert_eq!(r, 1);
13166        assert_eq!(s, "foo bar"); // backslash removed
13167        assert_eq!(pos, s.len()); // whole thing is one word, no real sep
13168    }
13169
13170    #[test]
13171    fn findsep_quote_collapses_double_backslash() {
13172        // c:3796-3799 — `\\` collapses to a single literal backslash.
13173        let mut s = "a\\\\b".to_string(); // a <bslash><bslash> b
13174        let mut pos = 0usize;
13175        let r = findsep(&mut s, &mut pos, None, true);
13176        assert_eq!(r, 1);
13177        assert_eq!(s, "a\\b"); // one backslash left
13178        assert_eq!(pos, s.len()); // no separator present
13179    }
13180
13181    #[test]
13182    fn findsep_literal_multichar_separator() {
13183        // c:3836-3841 — explicit multi-byte literal separator.
13184        let mut s = "a::b".to_string();
13185        let mut pos = 0usize;
13186        assert_eq!(findsep(&mut s, &mut pos, Some(b"::"), false), 1);
13187        assert_eq!(pos, 1); // points at the "::"
13188        assert_eq!(&s[..pos], "a");
13189
13190        // c:3844 — separator absent → -1.
13191        let mut s = "abc".to_string();
13192        let mut pos = 0usize;
13193        assert_eq!(findsep(&mut s, &mut pos, Some(b"x"), false), -1);
13194    }
13195
13196    #[test]
13197    fn findsep_empty_separator_advances_one_char() {
13198        // c:3825-3834 — empty sep just steps past one character.
13199        let mut s = "ab".to_string();
13200        let mut pos = 0usize;
13201        assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), 1);
13202        assert_eq!(pos, 1);
13203
13204        let mut s = String::new();
13205        let mut pos = 0usize;
13206        assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), -1); // c:3833
13207    }
13208
13209    /// c:4033 — `subst_string_by_func` returns `getaparam("reply")`
13210    /// after the hook function finishes. The previous Rust port read
13211    /// `env::var("reply")` and split on NUL — wrong because `reply`
13212    /// is a shell-local PM_ARRAY in paramtab, never exported. This
13213    /// pin sets `reply` via `setaparam` (the paramtab write path)
13214    /// and exercises `subst_string_by_hook` end-to-end isn't viable
13215    /// in unit tests without a function-name hook, so we exercise
13216    /// just the getaparam plumbing.
13217    #[test]
13218    fn getaparam_reads_reply_from_paramtab_not_env() {
13219        let _g = crate::test_util::global_state_lock();
13220        // Stash any prior `reply` value.
13221        let saved = getaparam("reply");
13222
13223        // Write a known reply array via the canonical setaparam path.
13224        let payload = vec!["abbreviated".to_string(), "11".to_string()];
13225        let _ = setaparam("reply", payload.clone());
13226
13227        // The reply array must be reachable via getaparam — not env.
13228        // (env::var would return Err because setaparam never exports
13229        //  an un-flagged array to env.)
13230        assert_eq!(
13231            getaparam("reply"),
13232            Some(payload),
13233            "getaparam(\"reply\") must return paramtab array"
13234        );
13235
13236        // Restore.
13237        let _ = setaparam("reply", saved.unwrap_or_default());
13238    }
13239
13240    /// c:1133-1134 — `finddir` reads the global `home` variable (the
13241    /// canonical `$HOME` storage, not `getenv("HOME")`). The global
13242    /// is updated by `homesetfn` (Src/params.c:5118) whenever the user
13243    /// assigns `HOME=...` inside the shell.
13244    /// Regression target: a previous Rust port read `env::var("HOME")`
13245    /// so an in-shell `HOME=...` assignment would not retarget
13246    /// `~`-abbreviation until the user re-exported HOME.
13247    #[test]
13248    fn finddir_uses_paramtab_home_not_env() {
13249        let _g = crate::test_util::global_state_lock();
13250        // C's `homesetfn` (params.c:5118) UNUSED(Param pm) — the
13251        // function ignores its param-pointer argument and writes the
13252        // canonical `char *home` global directly. zshrs's port mirrors
13253        // that: `params::homesetfn(_pm, x)` ignores _pm and updates
13254        // `home_lock()`. So we pass a stack-default param; the paramtab
13255        // wiring (PM_SPECIAL dispatch) is not on the test path.
13256        let mut pm = crate::ported::zsh_h::param::default();
13257        let saved = crate::ported::params::homegetfn(&pm);
13258        let sentinel = "/tmp/zshrs-finddir-pin".to_string();
13259        homesetfn(&mut pm, sentinel.clone());
13260
13261        // `/tmp/zshrs-finddir-pin/x` must abbreviate to `~/x`.
13262        let abbrev = finddir(&format!("{}/x", sentinel));
13263        assert_eq!(
13264            abbrev.as_deref(),
13265            Some("~/x"),
13266            "finddir must consult canonical HOME (got {:?})",
13267            abbrev
13268        );
13269
13270        // Restore.
13271        homesetfn(&mut pm, saved);
13272    }
13273
13274    #[test]
13275    fn test_sepsplit() {
13276        let _g = crate::test_util::global_state_lock();
13277        assert_eq!(sepsplit("a:b:c", Some(":"), false), vec!["a", "b", "c"]);
13278        assert_eq!(sepsplit("a::b", Some(":"), false), vec!["a", "b"]);
13279        assert_eq!(sepsplit("a::b", Some(":"), true), vec!["a", "", "b"]);
13280    }
13281
13282    #[test]
13283    fn test_unmetafy_no_meta_byte_passes_through() {
13284        let _g = crate::test_util::global_state_lock();
13285        // No Meta byte → buffer unchanged, length unchanged.
13286        let mut buf = b"hello".to_vec();
13287        let n = unmetafy(&mut buf);
13288        assert_eq!(n, 5);
13289        assert_eq!(&buf, b"hello");
13290    }
13291
13292    #[test]
13293    fn test_unmetafy_collapses_meta_escapes() {
13294        let _g = crate::test_util::global_state_lock();
13295        // C: Meta byte (0x83) followed by `'a' ^ 32` (0x41 = 'A')
13296        // unmetafies to a single byte 'a' (0x61).
13297        // i.e. {0x83, 'a' ^ 32} → {'a'}.
13298        let mut buf = vec![0x83, b'a' ^ 32];
13299        let n = unmetafy(&mut buf);
13300        assert_eq!(n, 1);
13301        assert_eq!(buf, vec![b'a']);
13302    }
13303
13304    #[test]
13305    fn test_unmetafy_mixed_prefix_then_meta() {
13306        let _g = crate::test_util::global_state_lock();
13307        // Plain prefix, then Meta-escaped 0xFF (0x83, 0xFF ^ 32 = 0xDF).
13308        let mut buf = vec![b'X', b'Y', 0x83, 0xFF ^ 32, b'Z'];
13309        let n = unmetafy(&mut buf);
13310        assert_eq!(n, 4);
13311        assert_eq!(buf, vec![b'X', b'Y', 0xFF, b'Z']);
13312    }
13313
13314    #[test]
13315    fn test_unmetafy_returns_self_value() {
13316        let _g = crate::test_util::global_state_lock();
13317        // C returns `s` (the buffer) for chaining; Rust returns
13318        // the new length. Verify length matches a call that
13319        // collapses two Meta-escapes.
13320        let mut buf = vec![
13321            b'A',
13322            0x83,
13323            b'B' ^ 32, // → 'B'
13324            0x83,
13325            b'C' ^ 32, // → 'C'
13326            b'D',
13327        ];
13328        let n = unmetafy(&mut buf);
13329        assert_eq!(n, 4);
13330        assert_eq!(buf, b"ABCD".to_vec());
13331    }
13332
13333    #[test]
13334    fn test_imeta_byte_threshold() {
13335        let _g = crate::test_util::global_state_lock();
13336        // Canonical IMETA per Src/utils.c:4195-4201:
13337        //   - 0x00 (c:4195)
13338        //   - 0x83..=0xa2 (Meta through Marker — c:4196-4200)
13339        //
13340        // The previous version of this test asserted `imeta_byte(0xFF) == true`
13341        // based on the WRONG `b >= Meta` predicate the Rust port
13342        // originally used. C `imeta()` reads the typtab; 0xa3..=0xff
13343        // have NO typtab assignment so they're NOT IMETA, and
13344        // `metafy` must NOT escape them.
13345        assert!(imeta_byte(0x00), "c:4195 — NUL is IMETA");
13346        assert!(!imeta_byte(0x82), "0x82 is NOT IMETA (below Meta)");
13347        assert!(imeta_byte(Meta), "c:4196 — Meta (0x83) is IMETA");
13348        assert!(imeta_byte(0xa2), "c:4197 — Marker (0xa2) is IMETA");
13349        // c:4198-4200 upper bound — Nularg is the last IMETA byte.
13350        assert!(imeta_byte(0xa1), "c:4200 — Nularg (0xa1) is IMETA");
13351        // 0xa3..=0xff are NOT IMETA — UTF-8 multi-byte content
13352        // must pass through `metafy` unchanged.
13353        assert!(!imeta_byte(0xa3), "0xa3 NOT IMETA (above Marker)");
13354        assert!(!imeta_byte(0xc3), "0xc3 NOT IMETA (UTF-8 'é' lead)");
13355        assert!(!imeta_byte(0xFF), "0xFF NOT IMETA");
13356    }
13357
13358    #[test]
13359    fn test_meta_constant_value() {
13360        let _g = crate::test_util::global_state_lock();
13361        // Locked at 0x83 by Src/zsh.h. If this test fails, zsh
13362        // bumped the Meta sentinel and the encoding mapping needs
13363        // a full audit.
13364        assert_eq!(Meta, 0x83);
13365    }
13366
13367    #[test]
13368    #[cfg(unix)]
13369    fn test_mode_to_octal_canonical_bits() {
13370        let _g = crate::test_util::global_state_lock();
13371        // rwx for owner = 0o700.
13372        let mode = (S_IRUSR | S_IWUSR | S_IXUSR) as u32;
13373        assert_eq!(mode_to_octal(mode), 0o700);
13374        // rwx all = 0o777.
13375        let all = (S_IRUSR | S_IWUSR | S_IXUSR) as u32 * (1 + 8 + 64);
13376        // Use libc constants individually for portability.
13377        let m = (S_IRUSR
13378            | S_IWUSR
13379            | S_IXUSR
13380            | S_IRGRP
13381            | S_IWGRP
13382            | S_IXGRP
13383            | S_IROTH
13384            | S_IWOTH
13385            | S_IXOTH) as u32;
13386        assert_eq!(mode_to_octal(m), 0o777);
13387        let _ = all;
13388    }
13389
13390    #[test]
13391    #[cfg(unix)]
13392    fn test_mode_to_octal_setuid_setgid_sticky() {
13393        let _g = crate::test_util::global_state_lock();
13394        assert_eq!(mode_to_octal(S_ISUID as u32), 0o4000);
13395        assert_eq!(mode_to_octal(S_ISGID as u32), 0o2000);
13396        assert_eq!(mode_to_octal(S_ISVTX as u32), 0o1000);
13397        // All three: 0o7000.
13398        let all = (S_ISUID | S_ISGID | S_ISVTX) as u32;
13399        assert_eq!(mode_to_octal(all), 0o7000);
13400    }
13401
13402    #[test]
13403    #[cfg(unix)]
13404    fn test_mailstat_plain_file_returns_native_stat() {
13405        let _g = crate::test_util::global_state_lock();
13406        // Plain file path → *st fields mirror native stat,
13407        // not the maildir aggregation.
13408        let mut st: libc::stat = unsafe { std::mem::zeroed() };
13409        let rc = mailstat("/etc/hosts", &mut st);
13410        if rc == 0 {
13411            assert_eq!(
13412                st.st_nlink as u64,
13413                fs::metadata("/etc/hosts").unwrap().nlink()
13414            );
13415        }
13416    }
13417
13418    #[test]
13419    fn test_mailstat_nonexistent_returns_neg1() {
13420        let _g = crate::test_util::global_state_lock();
13421        let mut st: libc::stat = unsafe { std::mem::zeroed() };
13422        assert_eq!(mailstat("/nonexistent/path/does/not/exist", &mut st), -1);
13423    }
13424
13425    #[test]
13426    fn test_mailstat_directory_without_maildir_subdirs() {
13427        let _g = crate::test_util::global_state_lock();
13428        // /tmp is a directory but not a maildir (no cur/tmp/new) —
13429        // returns the partial aggregate (top dir's atime/mtime,
13430        // size=0 since cur/ wasn't found before we'd start summing).
13431        let mut st: libc::stat = unsafe { std::mem::zeroed() };
13432        let rc = mailstat("/tmp", &mut st);
13433        assert_eq!(rc, 0);
13434        assert_eq!(st.st_nlink, 1);
13435        assert_eq!(st.st_size, 0);
13436        assert_eq!(st.st_blocks, 0);
13437        // S_IFDIR bit should be cleared, S_IFREG set.
13438        #[cfg(unix)]
13439        {
13440            assert_eq!(st.st_mode & libc::S_IFDIR, 0);
13441            assert_ne!(st.st_mode & libc::S_IFREG, 0);
13442        }
13443    }
13444
13445    #[test]
13446    fn test_dupstrpfx_byte_counted() {
13447        let _g = crate::test_util::global_state_lock(); // c:161
13448                                                        // 5 bytes of ASCII = 5 chars, identical. Canonical
13449                                                        // port lives at `string.rs:161`; pin from utils.rs's test
13450                                                        // module via the qualified path.
13451        assert_eq!(dupstrpfx("hello", 3), "hel"); // c:161
13452        assert_eq!(dupstrpfx("hi", 10), "hi"); // c:161
13453        assert_eq!(dupstrpfx("anything", 0), ""); // c:161
13454    }
13455
13456    #[test]
13457    fn test_metafy_passes_through_ascii() {
13458        let _g = crate::test_util::global_state_lock();
13459        // ASCII bytes (< 0x83) stay untouched.
13460        assert_eq!(metafy("hello"), "hello");
13461        assert_eq!(metafy(""), "");
13462    }
13463
13464    #[test]
13465    fn test_metafy_imeta_predicate_matches_c_macro() {
13466        let _g = crate::test_util::global_state_lock();
13467        // Canonical C IMETA per Src/utils.c:4195-4201:
13468        //   - 0x00 (c:4195)
13469        //   - 0x83 (Meta, c:4196)
13470        //   - 0x84..=0x9c (Pound..LAST_NORMAL_TOK=Bang, c:4198)
13471        //   - 0x9d..=0xa1 (Snull..Nularg, c:4200)
13472        //   - 0xa2 (Marker, c:4197)
13473        //
13474        // The closed set is {0x00, 0x83..=0xa2}. Every other byte
13475        // (0x01..=0x82, 0xa3..=0xff) is NOT IMETA in C and so must
13476        // NOT be Meta-encoded by `metafy`. The previous Rust
13477        // hardcoded predicate `b >= 0x83` falsely marked
13478        // 0xa3..=0xff as IMETA, corrupting UTF-8 multibyte
13479        // content.
13480
13481        // NUL is IMETA (c:4195).
13482        assert!(imeta_byte(0x00), "c:4195 — '\\0' IS imeta");
13483
13484        // 0x01..=0x82 are NOT IMETA (no typtab assignment for them).
13485        for b in 0x01u8..=0x82 {
13486            assert!(!imeta_byte(b), "byte {:#x} should NOT be imeta", b);
13487        }
13488
13489        // 0x83..=0xa2 ARE IMETA (the canonical full range).
13490        for b in 0x83u8..=0xa2 {
13491            assert!(imeta_byte(b), "c:4196-4200 — byte {:#x} IS imeta", b);
13492        }
13493
13494        // 0xa3..=0xff are NOT IMETA. C `imeta()` returns false
13495        // for these so `metafy` must pass them through unchanged.
13496        // UTF-8 continuation bytes (0x80..=0xbf) and multi-byte
13497        // leads (0xc0..=0xff) live here and must round-trip.
13498        for b in 0xa3u8..=0xff {
13499            assert!(
13500                !imeta_byte(b),
13501                "byte {:#x} should NOT be imeta (no c:4195-4201 assignment)",
13502                b
13503            );
13504        }
13505    }
13506
13507    /// Pin: `metafy` passes UTF-8 multibyte bytes through unchanged
13508    /// when they fall outside the canonical IMETA range. Previously
13509    /// the broader `b >= 0x83` predicate corrupted every UTF-8
13510    /// continuation byte and lead byte that wasn't a token marker.
13511    #[test]
13512    fn metafy_preserves_utf8_high_bytes_outside_imeta_range() {
13513        let _g = crate::test_util::global_state_lock();
13514        // 'é' = U+00E9 = UTF-8 0xc3 0xa9. Both bytes are >= 0x83 BUT
13515        // both are also > 0xa2, so they're NOT IMETA per the typtab.
13516        // C `metafy` passes them through unchanged.
13517        let input = std::str::from_utf8(&[0xC3, 0xA9]).unwrap();
13518        let out = metafy(input);
13519        // Should round-trip the two bytes exactly (no Meta escape).
13520        let out_bytes = out.as_bytes();
13521        assert_eq!(
13522            out_bytes,
13523            &[0xC3, 0xA9],
13524            "c:4196-4200 — UTF-8 bytes 0xc3/0xa9 outside IMETA range must pass through"
13525        );
13526    }
13527
13528    #[test]
13529    fn test_ztrcmp_meta_aware() {
13530        let _g = crate::test_util::global_state_lock();
13531        // Two identical metafied strings → Equal.
13532        assert_eq!(ztrcmp("foo", "foo"), std::cmp::Ordering::Equal);
13533        // "foo" < "foz".
13534        assert_eq!(ztrcmp("foo", "foz"), std::cmp::Ordering::Less);
13535        // Prefix comparison: shorter < longer.
13536        assert_eq!(ztrcmp("foo", "foobar"), std::cmp::Ordering::Less);
13537        // Meta-encoded comparison: {0x83, 'a'^32} should compare as 'a'.
13538        let s_meta = unsafe { std::str::from_utf8_unchecked(&[0x83, b'a' ^ 32]) };
13539        let s_plain = "a";
13540        // Meta-encoded "a" should compare equal to plain "a".
13541        assert_eq!(ztrcmp(s_meta, s_plain), std::cmp::Ordering::Equal);
13542    }
13543
13544    #[test]
13545    fn test_skipwsep_skips_runs() {
13546        let _g = crate::test_util::global_state_lock();
13547        // 3 spaces + 'x' → returns ("x", 3).
13548        let (rest, n) = skipwsep("   x");
13549        assert_eq!(rest, "x");
13550        assert_eq!(n, 3);
13551        // No leading whitespace → 0 skipped.
13552        let (rest, n) = skipwsep("foo");
13553        assert_eq!(rest, "foo");
13554        assert_eq!(n, 0);
13555        // Mix of space/tab/newline.
13556        let (rest, n) = skipwsep(" \t\nbar");
13557        assert_eq!(rest, "bar");
13558        assert_eq!(n, 3);
13559    }
13560
13561    #[test]
13562    fn test_imeta_macro_threshold() {
13563        let _g = crate::test_util::global_state_lock(); // c:60
13564                                                        // `Src/ztype.h:60` `imeta(X) zistype(X, IMETA)` — typtab-driven
13565                                                        // predicate. Per `Src/utils.c:4195-4201`, IMETA is set on:
13566                                                        // NUL, Meta=0x83, Marker=0xa2, and the Pound..Nularg ITOK range
13567                                                        // (0x84..=0xa2). Routes through canonical
13568                                                        // `ztype_h::imeta(u8)`; init the typtab first since the
13569                                                        // canonical port reads through it.
13570        inittyptab(); // c:4148
13571        assert!(imeta(0x00), "c:4195 — NUL is IMETA"); // c:4195
13572        assert!(imeta(Meta), "c:4196 — Meta (0x83) is IMETA"); // c:4196
13573        assert!(imeta(0xa2), "c:4197 — Marker (0xa2) is IMETA"); // c:4197
13574        assert!(
13575            imeta(0x84),
13576            "c:4199-4201 — Pound (0x84) is IMETA via ITOK range"
13577        );
13578        assert!(
13579            imeta(0x9b),
13580            "c:4199-4201 — Dash sentinel within ITOK range is IMETA"
13581        );
13582        assert!(!imeta(0x82), "c:4170 — 0x82 is ICNTRL, not IMETA"); // c:4170
13583        assert!(!imeta(0xa3), "byte 0xa3 is past Marker — NOT IMETA");
13584        assert!(
13585            !imeta(0xff),
13586            "byte 0xff is NOT IMETA (the prior `>= Meta` over-report)"
13587        );
13588        assert!(!imeta(b' '), "space is not IMETA");
13589        assert!(!imeta(b'A'), "'A' is not IMETA");
13590    }
13591
13592    #[test]
13593    fn test_unmeta_routes_through_unmetafy() {
13594        let _g = crate::test_util::global_state_lock();
13595        // unmeta wraps the in-place unmetafy via a byte-vector
13596        // copy; the no-Meta fast path returns the source as-is.
13597        assert_eq!(unmeta("plain"), "plain");
13598    }
13599
13600    #[test]
13601    fn test_iwsep_includes_newline() {
13602        let _g = crate::test_util::global_state_lock(); // c:61
13603                                                        // The previous port omitted '\n' which broke wordcount on
13604                                                        // multi-line input. Routes through canonical
13605                                                        // `ztype_h::iwsep` (`Src/ztype.h:61`).
13606        assert!(iwsep(b'\n')); // c:61
13607        assert!(iwsep(b'\t')); // c:61
13608        assert!(iwsep(b' ')); // c:61
13609        assert!(!iwsep(b'a')); // c:61
13610    }
13611
13612    #[test]
13613    fn test_mailstat_aggregates_maildir() {
13614        let _g = crate::test_util::global_state_lock();
13615        // Create a temp maildir layout with 2 messages in new/ and 1
13616        // in cur/, verify the aggregate.
13617        let tmp = std::env::temp_dir().join(format!("zshrs_mailstat_test_{}", std::process::id()));
13618        let _ = fs::remove_dir_all(&tmp);
13619        fs::create_dir_all(tmp.join("cur")).unwrap();
13620        fs::create_dir_all(tmp.join("new")).unwrap();
13621        fs::create_dir_all(tmp.join("tmp")).unwrap();
13622        let mut f = fs::File::create(tmp.join("new").join("msg1")).unwrap();
13623        f.write_all(b"hello").unwrap();
13624        let mut f = fs::File::create(tmp.join("new").join("msg2")).unwrap();
13625        f.write_all(b"world!").unwrap();
13626        let mut f = fs::File::create(tmp.join("cur").join("msg3")).unwrap();
13627        f.write_all(b"third").unwrap();
13628        let mut st: libc::stat = unsafe { std::mem::zeroed() };
13629        let rc = mailstat(tmp.to_str().unwrap(), &mut st);
13630        assert_eq!(rc, 0, "maildir should stat");
13631        assert_eq!(st.st_blocks, 3, "3 messages total across new/ + cur/");
13632        assert_eq!(st.st_size, 5 + 6 + 5, "5+6+5 bytes total");
13633        let _ = fs::remove_dir_all(&tmp);
13634    }
13635
13636    #[test]
13637    fn test_spacesplit() {
13638        let _g = crate::test_util::global_state_lock();
13639        assert_eq!(spacesplit("a b c", false, false), vec!["a", "b", "c"]);
13640        assert_eq!(spacesplit("a  b", false, false), vec!["a", "b"]);
13641    }
13642
13643    /// `Src/utils.c:3741-3743` + `findsep` (c:3795-3811) — with `quote` a
13644    /// backslash before a separator keeps it inside the field (and is
13645    /// stripped), `\\` collapses to `\`, and without `quote` the backslash is
13646    /// an ordinary byte. This is the split `vared` applies to an array
13647    /// (zle_main.c:1878, `spacesplit(t, 1, 0, 1)`).
13648    #[test]
13649    fn spacesplit_quote_keeps_escaped_separators_in_the_field() {
13650        let _g = crate::test_util::global_state_lock();
13651        assert_eq!(spacesplit("a\\ b c", true, true), vec!["a b", "c"]);
13652        assert_eq!(spacesplit("a\\\\b c", true, true), vec!["a\\b", "c"]);
13653        assert_eq!(spacesplit("a\\ b c", true, false), vec!["a\\", "b", "c"]);
13654    }
13655
13656    #[test]
13657    fn test_sepjoin() {
13658        let _g = crate::test_util::global_state_lock();
13659        assert_eq!(
13660            sepjoin(&["a".into(), "b".into(), "c".into()], Some(":")),
13661            "a:b:c"
13662        );
13663        assert_eq!(sepjoin(&["a".into(), "b".into()], None), "a b");
13664    }
13665
13666    #[test]
13667    fn test_isident() {
13668        let _g = crate::test_util::global_state_lock(); // c:1288
13669                                                        // Canonical port lives at `params.rs:2056` (`Src/params.c:1288`).
13670        assert!(isident("foo")); // c:1288
13671        assert!(isident("_bar")); // c:1288
13672        assert!(isident("baz123")); // c:1288
13673        assert!(!isident("123abc")); // c:1288
13674        assert!(!isident("foo-bar")); // c:1288
13675    }
13676
13677    #[test]
13678    fn test_nicechar() {
13679        let _g = crate::test_util::global_state_lock();
13680        assert_eq!(nicechar('\n'), "\\n");
13681        assert_eq!(nicechar('\t'), "\\t");
13682        assert_eq!(nicechar('a'), "a");
13683    }
13684
13685    #[test]
13686    fn test_quotedzputs_single_quote_wrap() {
13687        let _g = crate::test_util::global_state_lock();
13688        assert_eq!(quotedzputs("simple"), "simple");
13689        assert_eq!(quotedzputs("has space"), "'has space'");
13690        assert_eq!(quotedzputs("it's"), "'it'\\''s'");
13691    }
13692
13693    #[test]
13694    fn test_quotestring_backslash() {
13695        let _g = crate::test_util::global_state_lock();
13696        assert_eq!(quotestring("hello", QT_BACKSLASH), "hello");
13697        assert_eq!(quotestring("has space", QT_BACKSLASH), "has\\ space");
13698        assert_eq!(quotestring("$var", QT_BACKSLASH), "\\$var");
13699    }
13700
13701    /// Pin: `ispecial(c)` matches the canonical SPECCHARS set at
13702    /// `Src/zsh.h:228` exactly: `"#$^*()=|{}[]\`<>?~;&\n\t \\'\""`.
13703    /// Previously the Rust local `ispecial` included `!` unconditionally
13704    /// which diverged from C — C only ISPECIAL-tags bangchar (default
13705    /// `!`) under BANGHIST + interactive (per `Src/utils.c:4257-4261`).
13706    ///
13707    /// This test exercises the path indirectly via `quotestring` with
13708    /// `QT_BACKSLASH` (which prepends `\` before every ispecial char).
13709    #[test]
13710    fn quotestring_backslash_only_specchars_no_bang_in_default() {
13711        let _g = crate::test_util::global_state_lock();
13712        // `!` is NOT in canonical SPECCHARS — should NOT be backslashed
13713        // in default non-interactive mode (matches C).
13714        assert_eq!(
13715            quotestring("a!b", QT_BACKSLASH),
13716            "a!b",
13717            "c:228 — `!` is not in SPECCHARS; only bangchar+BANGHIST adds it"
13718        );
13719        // `,` is NOT in canonical SPECCHARS until makecommaspecial(1).
13720        assert_eq!(
13721            quotestring("a,b", QT_BACKSLASH),
13722            "a,b",
13723            "c:228 — `,` not in SPECCHARS until makecommaspecial(1)"
13724        );
13725        // `^` IS in canonical SPECCHARS (per c:228 `"#$^..."`).
13726        assert_eq!(
13727            quotestring("a^b", QT_BACKSLASH),
13728            "a\\^b",
13729            "c:228 — `^` is in SPECCHARS"
13730        );
13731        // Open and close braces are in canonical SPECCHARS.
13732        assert_eq!(
13733            quotestring("a{b", QT_BACKSLASH),
13734            "a\\{b",
13735            "c:228 — open-brace is in SPECCHARS"
13736        );
13737        assert_eq!(
13738            quotestring("a}b", QT_BACKSLASH),
13739            "a\\}b",
13740            "c:228 — close-brace is in SPECCHARS"
13741        );
13742        // `#` is in canonical SPECCHARS (first char of c:228).
13743        assert_eq!(
13744            quotestring("a#b", QT_BACKSLASH),
13745            "a\\#b",
13746            "c:228 — `#` is the first char of SPECCHARS"
13747        );
13748        // `\\` (literal backslash) is in canonical SPECCHARS.
13749        assert_eq!(
13750            quotestring("a\\b", QT_BACKSLASH),
13751            "a\\\\b",
13752            "c:228 — `\\\\` in SPECCHARS"
13753        );
13754    }
13755
13756    /// c:6131-6134 — "Most quote styles other than backslash assume the
13757    /// quotes are to be added outside quotestring()." QT_SINGLE therefore
13758    /// returns the BODY: the caller (subst.c:4085-4087, text.c:1086-1088)
13759    /// supplies the `'…'` pair. The inner apostrophe still takes the
13760    /// close-reopen form from c:6372-6377.
13761    #[test]
13762    fn test_quotestring_single() {
13763        let _g = crate::test_util::global_state_lock();
13764        assert_eq!(quotestring("hello", QT_SINGLE), "hello");
13765        assert_eq!(quotestring("it's", QT_SINGLE), "it'\\''s");
13766    }
13767
13768    /// c:6131-6134 + c:6311-6312 — body only; `"` is backslash-escaped and
13769    /// the surrounding pair is the caller's (subst.c:4085-4087).
13770    #[test]
13771    fn test_quotestring_double() {
13772        let _g = crate::test_util::global_state_lock();
13773        assert_eq!(quotestring("hello", QT_DOUBLE), "hello");
13774        assert_eq!(quotestring("say \"hi\"", QT_DOUBLE), "say \\\"hi\\\"");
13775    }
13776
13777    /// c:6131-6134 — body only. C's caller writes the quote pair and then
13778    /// `val[0] = '$'` (subst.c:4085-4090), so `$'` never appears here.
13779    #[test]
13780    fn test_quotestring_dollars() {
13781        let _g = crate::test_util::global_state_lock();
13782        assert_eq!(quotestring("hello", QT_DOLLARS), "hello");
13783        assert_eq!(quotestring("line\nbreak", QT_DOLLARS), "line\\nbreak");
13784        assert_eq!(quotestring("tab\there", QT_DOLLARS), "tab\\there");
13785    }
13786
13787    #[test]
13788    fn test_quotestring_pattern() {
13789        let _g = crate::test_util::global_state_lock();
13790        assert_eq!(quotestring("*.txt", QT_BACKSLASH_PATTERN), "\\*.txt");
13791        assert_eq!(quotestring("file[1]", QT_BACKSLASH_PATTERN), "file\\[1\\]");
13792    }
13793
13794    #[test]
13795    fn test_quotetype_from_q_count() {
13796        let _g = crate::test_util::global_state_lock();
13797        assert_eq!(qflag_quotetype(1), QT_BACKSLASH);
13798        assert_eq!(qflag_quotetype(2), QT_SINGLE);
13799        assert_eq!(qflag_quotetype(3), QT_DOUBLE);
13800        assert_eq!(qflag_quotetype(4), QT_DOLLARS);
13801    }
13802
13803    #[test]
13804    fn test_tulower_tuupper() {
13805        let _g = crate::test_util::global_state_lock();
13806        assert_eq!(tulower('A'), 'a');
13807        assert_eq!(tuupper('a'), 'A');
13808        assert_eq!(tulower('1'), '1');
13809    }
13810
13811    #[test]
13812    fn test_wordcount_ifs_default() {
13813        let _g = crate::test_util::global_state_lock();
13814        // C: wordcount("a b c", NULL, 0) -> 3
13815        assert_eq!(wordcount("a b c", None, 0), 3);
13816        // Leading/trailing whitespace coalesced when mul <= 0.
13817        assert_eq!(wordcount("  a b  ", None, 0), 2);
13818        // Empty string with mul == 0 -> 0 words.
13819        assert_eq!(wordcount("", None, 0), 0);
13820        // Single word, no separators.
13821        assert_eq!(wordcount("foo", None, 0), 1);
13822    }
13823
13824    #[test]
13825    fn test_wordcount_with_explicit_sep() {
13826        let _g = crate::test_util::global_state_lock();
13827        // C: wordcount("a:b:c", ":", 0) -> 3 (3 fields, 2 separators)
13828        assert_eq!(wordcount("a:b:c", Some(":"), 0), 3);
13829        // Empty fields counted when mul != 0.
13830        assert_eq!(wordcount("a::b", Some(":"), 1), 3);
13831        // Without mul, consecutive empties collapse: a, b => 2... but
13832        // C's "if ((c || mul) && (sl || *(s+sl)))" — second `:` has
13833        // c=0 and mul=0 so doesn't increment. Result: a, b => 2.
13834        assert_eq!(wordcount("a::b", Some(":"), 0), 2);
13835    }
13836
13837    #[test]
13838    fn test_ucs4tomb_ascii() {
13839        let _g = crate::test_util::global_state_lock();
13840        let mut buf = [0u8; 8];
13841        // 'A' = 0x41, ASCII, single byte in any locale.
13842        let n = ucs4tomb('A' as u32, &mut buf);
13843        // wctomb may return 1 in C/POSIX locale; in UTF-8 locale also 1.
13844        assert_eq!(n, 1);
13845        assert_eq!(buf[0], b'A');
13846    }
13847
13848    #[test]
13849    fn test_is_mb_niceformat_plain_ascii() {
13850        let _g = crate::test_util::global_state_lock();
13851        // Plain printable ASCII — nothing needs nicechar escaping.
13852        assert_eq!(is_mb_niceformat("hello world"), 0);
13853    }
13854
13855    #[test]
13856    fn test_is_mb_niceformat_with_control_char() {
13857        let _g = crate::test_util::global_state_lock();
13858        // Tab is control (< 0x20) — needs nice escaping.
13859        assert_eq!(is_mb_niceformat("a\tb"), 1);
13860        // Bell character.
13861        assert_eq!(is_mb_niceformat("a\x07b"), 1);
13862    }
13863
13864    /// c:4856/4954 — `metafy` + `unmetafy` MUST round-trip for ASCII.
13865    /// A regression in the round-trip corrupts every metafied buffer
13866    /// the lexer/param-subst pipeline produces.
13867    #[test]
13868    fn metafy_unmetafy_round_trips_for_ascii() {
13869        let _g = crate::test_util::global_state_lock();
13870        let s = "hello world";
13871        let m = metafy(s);
13872        let mut buf = m.into_bytes();
13873        unmetafy(&mut buf);
13874        assert_eq!(std::str::from_utf8(&buf).unwrap(), s);
13875    }
13876
13877    /// `ztrlen` counts metafied characters (Meta-pairs as 1).
13878    /// Regression that double-counts Meta-pair bytes would break
13879    /// every fixed-width column path (printf %s).
13880    #[test]
13881    fn ztrlen_counts_ascii_one_per_byte() {
13882        let _g = crate::test_util::global_state_lock();
13883        assert_eq!(ztrlen(""), 0);
13884        assert_eq!(ztrlen("a"), 1);
13885        assert_eq!(ztrlen("hello"), 5);
13886    }
13887
13888    /// `Src/utils.c:5141-5149` — Meta-byte pair counts as 1 char.
13889    /// `*s++ == Meta` advances 2 bytes per iteration but increments
13890    /// `l` only once. Pin: a string with one Meta+X pair counts as 1.
13891    #[test]
13892    fn ztrlen_counts_meta_pair_as_one() {
13893        let _g = crate::test_util::global_state_lock();
13894        // META (0x83) + 0x20 = unmetafies to one '\0' byte (or a NUL).
13895        let meta = char::from_u32(Meta as u32).unwrap();
13896        let s: String = [meta, '\x20'].iter().collect();
13897        assert_eq!(
13898            ztrlen(&s),
13899            1,
13900            "c:5141-5148 — Meta+X pair counts as ONE char, not two"
13901        );
13902        // Mixed: "a" + META + "x" + "b" = 3 unmetafied chars.
13903        let mixed: String = ['a', meta, '\x20', 'b'].iter().collect();
13904        assert_eq!(
13905            ztrlen(&mixed),
13906            3,
13907            "c:5141 — three unmetafied chars from 'a' + Meta+X + 'b'"
13908        );
13909    }
13910
13911    /// `Src/utils.c:2579-2618` — `setblock_fd(turnonblocking, fd, modep)`.
13912    /// Signature pin: previously the Rust port had a 2-arg
13913    /// `(fd, blocking: bool) -> bool` signature, swapping the
13914    /// turnonblocking/fd argument order vs C AND collapsing the
13915    /// 3rd `*modep` out-param entirely. The fix restored canonical
13916    /// C order `(turnonblocking, fd)` with a `(bool, c_long)` tuple
13917    /// return mirroring `int return + long *modep`.
13918    ///
13919    /// Also pin the c:2599 regular-file short-circuit: C only
13920    /// operates on non-regular fds (pipes, sockets, ttys). A regular
13921    /// file returns `(false, -1)` immediately.
13922    #[cfg(unix)]
13923    #[test]
13924    fn setblock_fd_skips_regular_files_per_c_2599() {
13925        let _g = crate::test_util::global_state_lock();
13926        // Open a regular tempfile.
13927        let dir = tempfile::TempDir::new().unwrap();
13928        let f = fs::File::create(dir.path().join("regular")).unwrap();
13929        let fd = f.as_raw_fd();
13930        let (changed, mode) = setblock_fd(true, fd);
13931        assert!(
13932            !changed,
13933            "c:2599 — regular files short-circuit; setblock_fd must NOT report a change"
13934        );
13935        assert_eq!(mode, -1, "c:2614 — `*modep = -1` for regular files");
13936    }
13937
13938    /// `Src/utils.c:2606-2611` — `setblock_fd(turnonblocking=true, fd)`
13939    /// clears O_NONBLOCK on a pipe (non-regular fd). Returns
13940    /// `(true, prior_flags)` if the state was changed.
13941    #[cfg(unix)]
13942    #[test]
13943    fn setblock_fd_clears_o_nonblock_on_pipe() {
13944        let _g = crate::test_util::global_state_lock();
13945        // Create a pipe — non-regular fd.
13946        let mut pipefd: [libc::c_int; 2] = [0; 2];
13947        let r = unsafe { libc::pipe(pipefd.as_mut_ptr()) };
13948        assert_eq!(r, 0, "pipe(2) must succeed");
13949        let read_fd = pipefd[0];
13950        let write_fd = pipefd[1];
13951        // Force NONBLOCK on the read end.
13952        let cur = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
13953        unsafe {
13954            libc::fcntl(read_fd, libc::F_SETFL, cur | libc::O_NONBLOCK);
13955        }
13956        // Verify NONBLOCK is set.
13957        let now = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
13958        assert_ne!(
13959            now & libc::O_NONBLOCK,
13960            0,
13961            "test setup: NONBLOCK should be set"
13962        );
13963        // Call setblock_fd to ENABLE blocking (clear NONBLOCK).
13964        let (changed, _mode) = setblock_fd(true, read_fd);
13965        assert!(
13966            changed,
13967            "c:2611 — turnonblocking=true on a NONBLOCK pipe must report state change"
13968        );
13969        // Verify NONBLOCK is now cleared.
13970        let after = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
13971        assert_eq!(
13972            after & libc::O_NONBLOCK,
13973            0,
13974            "c:2611 — O_NONBLOCK must be cleared after turnonblocking=true"
13975        );
13976        // Cleanup.
13977        unsafe {
13978            libc::close(read_fd);
13979            libc::close(write_fd);
13980        }
13981    }
13982
13983    /// `Src/utils.c:2620-2625` — `setblock_stdin()`. C body
13984    /// `setblock_fd(1, 0, &mode)` enables BLOCKING on fd 0 (stdin).
13985    /// Previously the Rust port called `setblock_fd(0, false)`
13986    /// which DISABLES blocking — exact opposite. Pin via real fd
13987    /// inspection.
13988    #[cfg(unix)]
13989    #[test]
13990    fn setblock_stdin_enables_blocking_on_fd_zero() {
13991        let _g = crate::test_util::global_state_lock();
13992        // Set stdin to NONBLOCKING first to verify the function
13993        // ACTUALLY switches it back to blocking. Skip if stdin is
13994        // not a normal fd (some CI configurations).
13995        let cur = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
13996        if cur < 0 {
13997            return;
13998        }
13999        // Force NONBLOCK first.
14000        unsafe {
14001            libc::fcntl(0, libc::F_SETFL, cur | libc::O_NONBLOCK);
14002        }
14003        let after_set_nb = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
14004        if after_set_nb & libc::O_NONBLOCK == 0 {
14005            // System rejected the change (regular file? CI tty?) — skip.
14006            unsafe {
14007                libc::fcntl(0, libc::F_SETFL, cur);
14008            }
14009            return;
14010        }
14011        // Call setblock_stdin — should CLEAR O_NONBLOCK.
14012        setblock_stdin();
14013        let after_setblock = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
14014        assert_eq!(
14015            after_setblock & libc::O_NONBLOCK,
14016            0,
14017            "c:2624 — setblock_stdin must CLEAR O_NONBLOCK (enable blocking)"
14018        );
14019        // Restore original flags.
14020        unsafe {
14021            libc::fcntl(0, libc::F_SETFL, cur);
14022        }
14023    }
14024
14025    /// `Src/utils.c:2437-2519` — `zstrtol_underscore(s, base, false)`.
14026    /// Base-10 (explicit) parses canonical decimal.
14027    #[test]
14028    fn zstrtol_underscore_base_10_parses_decimal() {
14029        let _g = crate::test_util::global_state_lock();
14030        let (v, rest) = zstrtol_underscore("12345", 10, false);
14031        assert_eq!(v, 12345, "c:2471 — decimal accumulator");
14032        assert_eq!(rest, "", "rest is empty after full consumption");
14033        // With trailing non-digit.
14034        let (v, rest) = zstrtol_underscore("100abc", 10, false);
14035        assert_eq!(v, 100);
14036        assert_eq!(
14037            rest, "abc",
14038            "c:2467 — loop exits at first non-digit; rest carries on"
14039        );
14040    }
14041
14042    /// c:2452-2461 — base==0 autodetect: `0x`→16, `0b`→2, leading
14043    /// `0`→8 (always, unlike `zstrtoul_underscore` which honors
14044    /// OCTALZEROES). `zstrtol_underscore` does NOT consult
14045    /// OCTALZEROES — pure prefix detection.
14046    #[test]
14047    fn zstrtol_underscore_base_zero_autodetects_prefix() {
14048        let _g = crate::test_util::global_state_lock();
14049        // Hex.
14050        assert_eq!(
14051            zstrtol_underscore("0xff", 0, false).0,
14052            255,
14053            "c:2455 — 0x → base 16"
14054        );
14055        assert_eq!(zstrtol_underscore("0XFF", 0, false).0, 255);
14056        // Binary.
14057        assert_eq!(
14058            zstrtol_underscore("0b1010", 0, false).0,
14059            10,
14060            "c:2457 — 0b → base 2"
14061        );
14062        // Leading 0 → octal (always, no OCTALZEROES gate).
14063        assert_eq!(
14064            zstrtol_underscore("0777", 0, false).0,
14065            511,
14066            "c:2460 — leading 0 → octal (no OCTALZEROES gate for zstrtol)"
14067        );
14068        // Decimal default.
14069        assert_eq!(zstrtol_underscore("12345", 0, false).0, 12345);
14070    }
14071
14072    /// c:2447-2450 — leading `-` and `+` consumed; `-` triggers
14073    /// negation at the end (c:2497-2498).
14074    #[test]
14075    fn zstrtol_underscore_handles_sign_chars() {
14076        let _g = crate::test_util::global_state_lock();
14077        assert_eq!(
14078            zstrtol_underscore("-42", 10, false).0,
14079            -42,
14080            "c:2447 — leading `-` → negate"
14081        );
14082        assert_eq!(
14083            zstrtol_underscore("+42", 10, false).0,
14084            42,
14085            "c:2449 — leading `+` consumed, no negation"
14086        );
14087        // Mixed whitespace + sign.
14088        assert_eq!(
14089            zstrtol_underscore("  -100", 10, false).0,
14090            -100,
14091            "c:2444 — leading whitespace skipped, then sign"
14092        );
14093    }
14094
14095    /// c:2466-2492 — base 16 (hex) accumulator: digits 0-9 + letters
14096    /// a-f / A-F via `idigit(*s)` || `'a' ≤ *s < 'a'+base-10`.
14097    /// Pin both upper- and lower-case.
14098    #[test]
14099    fn zstrtol_underscore_base_16_accepts_letters() {
14100        let _g = crate::test_util::global_state_lock();
14101        assert_eq!(
14102            zstrtol_underscore("ff", 16, false).0,
14103            255,
14104            "c:2485 — base-16 'ff' → 255"
14105        );
14106        assert_eq!(
14107            zstrtol_underscore("FF", 16, false).0,
14108            255,
14109            "c:2485 — base-16 'FF' → 255 (upper case via `*s & 0x1f`)"
14110        );
14111        assert_eq!(
14112            zstrtol_underscore("DEADbeef", 16, false).0,
14113            0xDEADBEEF,
14114            "c:2485 — mixed case 32-bit hex"
14115        );
14116    }
14117
14118    /// c:2468/2482 — `underscore` flag enables digit-separator. With
14119    /// underscore=false, `_` is treated as end-of-digits. With
14120    /// underscore=true, `_` is skipped (c:2469-2470).
14121    #[test]
14122    fn zstrtol_underscore_underscore_flag() {
14123        let _g = crate::test_util::global_state_lock();
14124        // underscore=false: `_` terminates parse.
14125        let (v, rest) = zstrtol_underscore("1_000", 10, false);
14126        assert_eq!(v, 1, "c:2467 — underscore=false: `_` terminates");
14127        assert_eq!(rest, "_000");
14128        // underscore=true: `_` accepted and skipped.
14129        let (v, rest) = zstrtol_underscore("1_000_000", 10, true);
14130        assert_eq!(
14131            v, 1_000_000,
14132            "c:2469-2470 — underscore=true: `_` skipped during accumulation"
14133        );
14134        assert_eq!(rest, "", "fully consumed including `_`s");
14135    }
14136
14137    /// `Src/utils.c:2750-2774` — `timespec_diff_us(t1, t2)` returns
14138    /// the signed microsecond delta `t2 - t1`. Pin both sign
14139    /// conventions: t2 after t1 → positive; t2 before t1 → negative.
14140    #[test]
14141    fn timespec_diff_us_sign_matches_c_t2_minus_t1() {
14142        let _g = crate::test_util::global_state_lock();
14143        let t1 = std::time::Instant::now();
14144        std::thread::sleep(std::time::Duration::from_millis(2));
14145        let t2 = std::time::Instant::now();
14146        // c:2759 — `diff_sec = t2 - t1` for the t2 > t1 path. Result
14147        // positive (microseconds elapsed).
14148        let d = timespec_diff_us(&t1, &t2);
14149        assert!(d > 0, "c:2759 — t2 after t1 → positive delta");
14150        assert!(d >= 1000, "expected >= 1ms (1000us); got {}us", d);
14151        // c:2754 — reversed (t1 > t2): result negative.
14152        let r = timespec_diff_us(&t2, &t1);
14153        assert!(r < 0, "c:2770 — t1 after t2 → negative delta");
14154        assert_eq!(d, -r, "swap of args negates the result");
14155    }
14156
14157    /// c:2752 — `timespec_diff_us(t, t)` is 0 (identical Instants).
14158    #[test]
14159    fn timespec_diff_us_same_instant_returns_zero() {
14160        let _g = crate::test_util::global_state_lock();
14161        let t = std::time::Instant::now();
14162        assert_eq!(
14163            timespec_diff_us(&t, &t),
14164            0,
14165            "c:2752 — identical Instants → 0"
14166        );
14167    }
14168
14169    /// `Src/utils.c:6743-6779` — `ucs4toutf8(dest, wval)`. Encodes
14170    /// a Unicode codepoint to UTF-8. Pin canonical 1-, 2-, 3-, and
14171    /// 4-byte encodings.
14172    #[test]
14173    fn ucs4toutf8_encodes_canonical_lengths() {
14174        let _g = crate::test_util::global_state_lock();
14175        // c:6750 — 1 byte: ASCII range [0, 0x80).
14176        assert_eq!(
14177            ucs4toutf8(0x41),
14178            Some(b"A".to_vec()),
14179            "c:6750 — 0x41 → 'A' (1 byte)"
14180        );
14181        // c:6752 — 2 bytes: [0x80, 0x800). 'é' = U+00E9.
14182        assert_eq!(
14183            ucs4toutf8(0xe9),
14184            Some("é".as_bytes().to_vec()),
14185            "c:6752 — U+00E9 → 'é' (2 bytes)"
14186        );
14187        // c:6754 — 3 bytes: [0x800, 0x10000). '字' = U+5B57.
14188        assert_eq!(
14189            ucs4toutf8(0x5B57),
14190            Some("字".as_bytes().to_vec()),
14191            "c:6754 — U+5B57 → '字' (3 bytes)"
14192        );
14193        // c:6756 — 4 bytes: [0x10000, 0x200000). '𝄞' = U+1D11E.
14194        assert_eq!(
14195            ucs4toutf8(0x1D11E),
14196            Some("𝄞".as_bytes().to_vec()),
14197            "c:6756 — U+1D11E → '𝄞' (4 bytes)"
14198        );
14199    }
14200
14201    /// `Src/utils.c:6762-6764` — values `>= 0x80000000` are
14202    /// out-of-range; C emits `zerr("character not in range")` and
14203    /// returns `-1`. Surrogates (U+D800..U+DFFF) and values in
14204    /// 0x110000..0x80000000 are *encoded as raw bytes* by C
14205    /// (matches `wctomb(3)` extended UCS-4 range).
14206    #[test]
14207    fn ucs4toutf8_rejects_invalid_codepoints() {
14208        let _g = crate::test_util::global_state_lock();
14209        // c:6762-6764 — value >= 0x80000000 → None.
14210        assert_eq!(
14211            ucs4toutf8(0xFFFF_FFFE),
14212            None,
14213            "c:6763 — values >= 0x80000000 → None"
14214        );
14215        assert_eq!(
14216            ucs4toutf8(0x8000_0000),
14217            None,
14218            "c:6760-6764 — exactly 0x80000000 is out of range"
14219        );
14220        // Surrogates encode as 3-byte sequences per C bit-pattern
14221        // (c:6754 → len=3). The C source has no Unicode-validity
14222        // gate; that's a wctomb-only concern in callers.
14223        assert!(
14224            ucs4toutf8(0xD800).is_some(),
14225            "c:6754 — surrogates encode as 3-byte UTF-8 in C (no Unicode validity check)"
14226        );
14227    }
14228
14229    /// `Src/utils.c:6648-6723` — `dquotedztrdup(s)`. Default (non-
14230    /// CSHJUNKIEQUOTES) arm: wrap whole string in `"..."`. Backslash
14231    /// doubles, `"`/`$`/`` ` `` get escaped.
14232    #[test]
14233    fn dquotedztrdup_default_path_wraps_whole_string() {
14234        let _g = crate::test_util::global_state_lock();
14235        if isset(CSHJUNKIEQUOTES) {
14236            return; // Default-only test.
14237        }
14238        // c:6690 — `*p++ = '"';` then iterate.
14239        assert_eq!(
14240            dquotedztrdup("hello"),
14241            "\"hello\"",
14242            "c:6690+6711+6718 — wrap plain text in double quotes"
14243        );
14244        // c:6703-6708 — `$` gets `\$`.
14245        assert_eq!(
14246            dquotedztrdup("$var"),
14247            "\"\\$var\"",
14248            "c:6703-6708 — `$` → `\\$`"
14249        );
14250        // c:6703-6708 — `"` gets `\"`.
14251        assert_eq!(
14252            dquotedztrdup("a\"b"),
14253            "\"a\\\"b\"",
14254            "c:6703-6708 — `\"` → `\\\"`"
14255        );
14256    }
14257
14258    /// c:6697-6701 — backslash handling with `pending` state.
14259    /// Single `\` followed by an ordinary char emits just `\` (no
14260    /// doubling). The "pending" extra `\` fires only when the next
14261    /// char is itself a backslash or one of the escaped specials
14262    /// (`"`/`$`/`` ` ``), OR at end-of-string before the closing `"`.
14263    #[test]
14264    fn dquotedztrdup_pending_backslash_only_doubles_when_needed() {
14265        let _g = crate::test_util::global_state_lock();
14266        if isset(CSHJUNKIEQUOTES) {
14267            return;
14268        }
14269        // Mid-string `\` before ordinary char: stays as single `\`.
14270        assert_eq!(
14271            dquotedztrdup("a\\b"),
14272            "\"a\\b\"",
14273            "c:6711+6712 — `\\b` mid-string: no extra (pending=0 after b)"
14274        );
14275        // Trailing `\` triggers pending quirk: c:6716-6717 emits
14276        // an extra `\` before the closing `"`.
14277        let r = dquotedztrdup("a\\");
14278        assert!(
14279            r.ends_with("\\\\\""),
14280            "c:6716-6717 — trailing `\\` → emit extra `\\` before closing `\"`: got {:?}",
14281            r
14282        );
14283    }
14284
14285    /// `Src/utils.c:6464-6549` — `quotedzputs(s)`. Three branches:
14286    ///   - empty → `''`.
14287    ///   - has special chars (via `hasspecial`) → wrap in `'...'`
14288    ///     with `'\''` escape for embedded apostrophes.
14289    ///   - else → return unchanged.
14290    /// Pin all three.
14291    #[test]
14292    fn quotedzputs_empty_string_returns_double_quotes() {
14293        let _g = crate::test_util::global_state_lock();
14294        assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
14295    }
14296
14297    /// c:6514-6543 — needs-quote path: wrap in single quotes,
14298    /// escape embedded `'` as `'\''`.
14299    #[test]
14300    fn quotedzputs_wraps_specials_in_single_quotes() {
14301        let _g = crate::test_util::global_state_lock();
14302        inittyptab();
14303        // Space is special → wrap.
14304        assert_eq!(quotedzputs("hello world"), "'hello world'");
14305        // `=` is special → wrap.
14306        assert_eq!(quotedzputs("foo=bar"), "'foo=bar'");
14307        // Embedded apostrophe — `'\''` escape.
14308        assert_eq!(
14309            quotedzputs("it's"),
14310            "'it'\\''s'",
14311            "c:6517-6519 — apostrophe → '\\''"
14312        );
14313    }
14314
14315    /// c:6512 — no specials → return unchanged.
14316    #[test]
14317    fn quotedzputs_plain_alnum_returns_unchanged() {
14318        let _g = crate::test_util::global_state_lock();
14319        inittyptab();
14320        assert_eq!(
14321            quotedzputs("hello"),
14322            "hello",
14323            "c:6512 — no specials, no wrap"
14324        );
14325        assert_eq!(quotedzputs("abc123"), "abc123");
14326    }
14327
14328    /// c:6578-6637 — Bourne path "avoids empty quoted strings" by
14329    /// tracking `inquote`. Input `'x` should not produce empty `''`
14330    /// at the front; `x'` should not produce trailing `''`.
14331    #[test]
14332    fn quotedzputs_avoids_empty_quoted_runs_on_boundaries() {
14333        let _g = crate::test_util::global_state_lock();
14334        inittyptab();
14335        // Leading `'`: c:6587 with inquote=0 skips the close, emits
14336        // `\'`. Then `x`: c:6604 opens a quote, emits `x`, end emits
14337        // closing `'`. Result: `\''x'` — the visible doubled `''` is
14338        // `\'` (escaped) + `'` (opening single-quote), NOT an empty
14339        // single-quoted string.
14340        assert_eq!(
14341            quotedzputs("'x"),
14342            "\\''x'",
14343            "c:6587-6610 — leading `'` produces `\\'` + opening `'`"
14344        );
14345        // Trailing `'`: open quote at `x`, see `'` with inquote=1 → close
14346        // (`'`), emit `\'`. End-of-string: inquote=0, no trailing `'`.
14347        // Result: `'x'\\'` — NO trailing `''`.
14348        assert_eq!(
14349            quotedzputs("x'"),
14350            "'x'\\'",
14351            "c:6631-6637 — trailing `'` doesn't generate empty `''`"
14352        );
14353    }
14354
14355    /// c:6478-6492 — `is_mb_niceformat` arm of quotedzputs uses
14356    /// mb_niceformat with NICEFLAG_QUOTE so embedded `'` becomes
14357    /// `\'` inside the `$'...'` wrapper (preventing the quote from
14358    /// terminating the wrap). Previously sb_niceformat without
14359    /// NICEFLAG_QUOTE was used, which would have left `'` raw.
14360    #[test]
14361    fn quotedzputs_dollar_quote_escapes_apostrophe_inside_wrap() {
14362        let _g = crate::test_util::global_state_lock();
14363        inittyptab();
14364        // String with embedded `'` AND a control char so the
14365        // is_mb_niceformat arm is taken (controls trigger it).
14366        let r = quotedzputs("a\nb'c");
14367        // Expected: `$'a\nb\'c'` — the embedded `'` is `\'` inside
14368        // dollar-quotes, NOT terminating the wrap.
14369        assert!(
14370            r.starts_with("$'") && r.ends_with("'"),
14371            "c:6488 — must be wrapped in $'...'  got {:?}",
14372            r
14373        );
14374        assert!(
14375            r.contains("\\'"),
14376            "c:5413-5414 — embedded `'` must be `\\'` not raw `'`: got {:?}",
14377            r
14378        );
14379    }
14380
14381    /// c:6533-6576 — RCQUOTES branch: wrap everything in `'…'`,
14382    /// double embedded `'` as `''`.
14383    #[test]
14384    fn quotedzputs_rcquotes_doubles_apostrophe() {
14385        let _g = crate::test_util::global_state_lock();
14386        inittyptab();
14387        let prev = crate::ported::options::opt_state_get("rcquotes").unwrap_or(false);
14388        crate::ported::options::opt_state_set("rcquotes", true);
14389        // RCQUOTES: `it's` → `'it''s'` (doubled `'`, NOT `'\''`).
14390        let got = quotedzputs("it's");
14391        crate::ported::options::opt_state_set("rcquotes", prev);
14392        assert_eq!(
14393            got, "'it''s'",
14394            "c:6548-6553 — RCQUOTES: `'` → `''` (doubled)"
14395        );
14396    }
14397
14398    /// `Src/utils.c:2331-2337` — `strucpy(s, t)`. Appends `t` to
14399    /// `*s` and leaves `*s` pointing at the NUL terminator (Rust
14400    /// equivalent: append-in-place; `.len()` gives the new end).
14401    /// Pin: empty input → no-op; non-empty input → append.
14402    #[test]
14403    fn strucpy_appends_t_to_dest() {
14404        let _g = crate::test_util::global_state_lock();
14405        let mut dest = String::from("prefix-");
14406        strucpy(&mut dest, "suffix");
14407        assert_eq!(
14408            dest, "prefix-suffix",
14409            "c:2335 — strucpy appends t (NOT case-changes — c-name 'u' is pointer-walk, not upper)"
14410        );
14411        // Empty t — no change.
14412        let mut dest = String::from("alone");
14413        strucpy(&mut dest, "");
14414        assert_eq!(dest, "alone");
14415        // Empty dest + non-empty t.
14416        let mut dest = String::new();
14417        strucpy(&mut dest, "hello");
14418        assert_eq!(dest, "hello");
14419    }
14420
14421    /// `Src/utils.c:2341-2350` — `struncpy(s, t, n)`. Appends up to
14422    /// `n` bytes of `t` to `*s`. Pin: n=0 no-op; n < len(t) clips;
14423    /// n >= len(t) appends all.
14424    #[test]
14425    fn struncpy_appends_up_to_n_bytes() {
14426        let _g = crate::test_util::global_state_lock();
14427        // n < len(t).
14428        let mut dest = String::from("X");
14429        struncpy(&mut dest, "abcdef", 3);
14430        assert_eq!(dest, "Xabc", "c:2345 — clipped to n=3 bytes of `abcdef`");
14431        // n >= len(t).
14432        let mut dest = String::from("Y");
14433        struncpy(&mut dest, "abc", 100);
14434        assert_eq!(dest, "Yabc", "c:2345 — full copy when n >= len");
14435        // n=0 → no append.
14436        let mut dest = String::from("Z");
14437        struncpy(&mut dest, "abc", 0);
14438        assert_eq!(dest, "Z", "c:2345 — n=0 stops the copy loop immediately");
14439    }
14440
14441    /// `Src/utils.c:2280-2288` — `has_token(s)`. Returns true iff
14442    /// any byte in `s` triggers `itok()`. Pin: ASCII strings →
14443    /// false; strings containing token bytes (Pound 0x84, Bang
14444    /// 0x9c, Nularg 0xa1) → true.
14445    #[test]
14446    fn has_token_detects_typtab_token_bytes() {
14447        let _g = crate::test_util::global_state_lock();
14448        inittyptab();
14449        // c:2285 — pure ASCII has no token bytes.
14450        assert!(!has_token(""), "empty: no tokens");
14451        assert!(
14452            !has_token("hello world"),
14453            "c:2285 — ASCII text has no token bytes"
14454        );
14455        // Pound (0x84) — first token byte.
14456        let s: String = std::iter::once(crate::token_char::token_char_from_byte(0x84).unwrap()).collect();
14457        assert!(
14458            has_token(&s),
14459            "c:2285 — Pound (0x84) is itok → has_token=true"
14460        );
14461        // Bang (0x9c) — last_normal_tok.
14462        let s: String = std::iter::once(crate::token_char::token_char_from_byte(0x9c).unwrap()).collect();
14463        assert!(has_token(&s), "c:2285 — Bang (0x9c) is itok");
14464        // Nularg (0xa1) — upper bound.
14465        let s: String = std::iter::once(crate::token_char::token_char_from_byte(0xa1).unwrap()).collect();
14466        assert!(has_token(&s), "c:2285 — Nularg (0xa1) is itok");
14467    }
14468
14469    /// `Src/utils.c:2280` — Meta byte (0x83) is NOT a token. Pin
14470    /// the regression: previously the Rust port hardcoded `0x83`
14471    /// as a token byte. Now correctly excludes it.
14472    #[test]
14473    fn has_token_excludes_meta_byte() {
14474        let _g = crate::test_util::global_state_lock();
14475        inittyptab();
14476        // 0x83 is Meta, NOT itok. Should return false.
14477        let s: String = std::iter::once(0x83u8 as char).collect();
14478        assert!(
14479            !has_token(&s),
14480            "c:2285 — Meta (0x83) is NOT itok; previous hardcoded 0x83 list misfired"
14481        );
14482    }
14483
14484    /// `Src/utils.c:2284-2285` — C scans BYTES because its input is
14485    /// METAFIED (a raw >= 0x80 byte is stored as Meta + `byte ^ 32`), so a
14486    /// bare 0x84..=0xA1 there is always a token. zshrs keeps UTF-8 `str`
14487    /// and stores tokens as PUA CHARS (crate::token_char) — the representation
14488    /// `untokenize` (lex.rs:5990) tests with `c as u32` — so the byte walk
14489    /// mistook a UTF-8 CONTINUATION byte for a token. `—` U+2014 is E2 80
14490    /// 94, `→` U+2192 is E2 86 92, `“` U+201C is E2 80 9C: 0x94, 0x86,
14491    /// 0x92 and 0x9C all answer `itok`. This asserted-false set is what
14492    /// `s.bytes().any(itok)` got wrong; `lex.rs:6165` already scanned
14493    /// chars, this copy did not.
14494    #[test]
14495    fn has_token_ignores_utf8_continuation_bytes_in_itok_range() {
14496        let _g = crate::test_util::global_state_lock();
14497        inittyptab();
14498        for s in ["a—b", "a→b", "a“b”c", "—", "→"] {
14499            assert!(
14500                s.bytes().any(itok),
14501                "{s:?} must actually contain an ITOK-range BYTE, else this pins nothing"
14502            );
14503            assert!(
14504                !has_token(s),
14505                "c:2285 — {s:?} holds no token CHAR (crate::token_char); only \
14506                 UTF-8 continuation bytes that happen to land in the ITOK range"
14507            );
14508        }
14509        // Still true for a genuine token char, so the fix isn't a blanket
14510        // "non-ASCII is never a token".
14511        assert!(has_token("plain\u{e19c}"), "c:2285 — Bang (U+e19C) is a token");
14512    }
14513
14514    /// `Src/utils.c:5271` — `else if (itok(*s)) { s++; continue; }`. C may
14515    /// scan bytes there because its `s` is metafied; zshrs keeps UTF-8
14516    /// `str` with tokens as CHARS, so the byte walk silently DROPPED the
14517    /// continuation byte of any multibyte glyph landing in `0x84..=0xa1`.
14518    /// `functions f` printed `print "a<E2><80>b"` for a body holding `—`
14519    /// (U+2014 = E2 80 94, 0x94 = `Inang`). Same defect as
14520    /// `has_token_ignores_utf8_continuation_bytes_in_itok_range` above,
14521    /// at the printing site.
14522    #[test]
14523    fn zputs_ignores_utf8_continuation_bytes_in_itok_range() {
14524        let _g = crate::test_util::global_state_lock();
14525        inittyptab();
14526        for s in ["a—b", "a→b", "a“b”c", "字符", "aéb"] {
14527            let mut out: Vec<u8> = Vec::new();
14528            assert_eq!(zputs(s, &mut out), 0, "c:5280 — zputs returns 0");
14529            assert_eq!(
14530                out,
14531                s.as_bytes(),
14532                "c:5271 — {s:?} holds no token CHAR (crate::token_char); zputs \
14533                 must emit it verbatim, not drop UTF-8 continuation bytes"
14534            );
14535        }
14536        // Genuine token chars are still skipped (c:5271-5272), and a
14537        // Meta pair still decodes to the escaped byte (c:5269-5270), so
14538        // the fix is not a blanket "pass everything through".
14539        let mut out: Vec<u8> = Vec::new();
14540        let _ = zputs("a\u{e19c}b", &mut out);
14541        assert_eq!(out, b"ab", "c:5271 — Bang (U+e19C) is a token, skipped");
14542        let mut out: Vec<u8> = Vec::new();
14543        let _ = zputs("a\u{83}\u{c1}b", &mut out);
14544        assert_eq!(
14545            out,
14546            b"a\xe1b",
14547            "c:5270 — Meta + (0xe1 ^ 32) decodes back to the raw 0xe1"
14548        );
14549    }
14550
14551    /// `Src/utils.c:6082-6124` — `addunprintable(c)`. Renders bytes
14552    /// using shell-compatible C-string escapes (`\a`/`\b`/`\f`/`\n`/
14553    /// `\r`/`\t`/`\v` for the named controls + `\nnn` octal for
14554    /// others + `\0` for NUL). Previously emitted ZLE caret form —
14555    /// wrong convention.
14556    #[test]
14557    fn addunprintable_named_control_escapes() {
14558        let _g = crate::test_util::global_state_lock();
14559        // c:6106-6112 — named-escape per byte.
14560        assert_eq!(addunprintable('\x07'), "\\a", "c:6106 — BEL → \\a");
14561        assert_eq!(addunprintable('\x08'), "\\b", "c:6107 — BS → \\b");
14562        assert_eq!(addunprintable('\x0c'), "\\f", "c:6108 — FF → \\f");
14563        assert_eq!(addunprintable('\n'), "\\n", "c:6109 — LF → \\n");
14564        assert_eq!(addunprintable('\r'), "\\r", "c:6110 — CR → \\r");
14565        assert_eq!(addunprintable('\t'), "\\t", "c:6111 — TAB → \\t");
14566        assert_eq!(addunprintable('\x0b'), "\\v", "c:6112 — VT → \\v");
14567    }
14568
14569    /// c:6097-6103 — NUL renders as `\0`. (C peeks next byte for
14570    /// `\000` disambiguation; Rust port works one char at a time
14571    /// so emits the short `\0` form.)
14572    #[test]
14573    fn addunprintable_nul_renders_as_backslash_zero() {
14574        let _g = crate::test_util::global_state_lock();
14575        assert_eq!(addunprintable('\0'), "\\0", "c:6097-6098 — NUL → \\0");
14576    }
14577
14578    /// c:6114-6119 — `\nnn` 3-digit octal fallback for un-named
14579    /// control bytes. Pin the format: each digit is one octal digit
14580    /// (0-7), zero-padded to 3 positions.
14581    #[test]
14582    fn addunprintable_octal_fallback_for_unnamed_controls() {
14583        let _g = crate::test_util::global_state_lock();
14584        // 0x01 (SOH) = 001 octal.
14585        assert_eq!(addunprintable('\x01'), "\\001", "c:6116-6118 — SOH → \\001");
14586        // 0x1b (ESC) = 033 octal.
14587        assert_eq!(addunprintable('\x1b'), "\\033", "c:6116-6118 — ESC → \\033");
14588        // 0x7f (DEL) = 177 octal.
14589        assert_eq!(addunprintable('\x7f'), "\\177", "c:6116-6118 — DEL → \\177");
14590        // 0xff (high) = 377 octal.
14591        assert_eq!(
14592            addunprintable(char::from_u32(0xff).unwrap()),
14593            "\\377",
14594            "c:6116-6118 — 0xff → \\377"
14595        );
14596    }
14597
14598    /// `Src/utils.c:6072-6082` — `hasspecial(s)`. Pin canonical
14599    /// special chars from SPECCHARS (`Src/zsh.h:228`): `#$^*()=|{}[]
14600    /// \`<>?~;&\\n\\t \\\\\\'\\"`.
14601    #[test]
14602    fn hasspecial_recognises_canonical_special_chars() {
14603        let _g = crate::test_util::global_state_lock();
14604        // typtab access is read-only here (no flag mutation); concurrent
14605        // inittyptab() rebuilds are idempotent for the default flag set.
14606        inittyptab();
14607        // c:6075 — every char in SPECCHARS triggers true.
14608        for c in "#$^*()=|{}[]<>?~;&".chars() {
14609            let s = c.to_string();
14610            assert!(
14611                hasspecial(&s),
14612                "c:6075 — '{}' is in SPECCHARS, must be special",
14613                c
14614            );
14615        }
14616        // Whitespace specials.
14617        assert!(hasspecial(" "), "space in SPECCHARS");
14618        assert!(hasspecial("\t"), "tab in SPECCHARS");
14619        assert!(hasspecial("\n"), "newline in SPECCHARS");
14620        // Non-special chars → false.
14621        assert!(
14622            !hasspecial("hello"),
14623            "c:6075 — plain alphanumerics are NOT special"
14624        );
14625        assert!(!hasspecial("ABC012"));
14626    }
14627
14628    /// `Src/utils.c:6072-6075` — Meta-byte handling: `*s == Meta ?
14629    /// *++s ^ 32 : *s`. Pin Meta+X pair gets decoded before special-
14630    /// check. A Meta+'A' decodes to 'a' (not special), so a string
14631    /// containing only Meta+'A' returns false.
14632    #[test]
14633    fn hasspecial_decodes_meta_byte_before_check() {
14634        let _g = crate::test_util::global_state_lock();
14635        // Same as above — read-only after inittyptab.
14636        inittyptab();
14637        // Meta + 0x41 ('A') → 'a' (0x61) → not special.
14638        let bytes: Vec<u8> = vec![Meta, 0x41u8];
14639        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14640        assert!(
14641            !hasspecial(s),
14642            "c:6075 — Meta+0x41 decodes to 'a' which is NOT special"
14643        );
14644    }
14645
14646    /// `Src/utils.c:5849-5910` — `sb_niceformat(s)`. Pin: ASCII
14647    /// printable passes through unchanged; controls escape.
14648    /// C-equivalent call shape: `char *out; sb_niceformat(s, NULL, &out, 0);`
14649    #[test]
14650    fn sb_niceformat_passes_printable_ascii() {
14651        let _g = crate::test_util::global_state_lock();
14652        let mut out: Option<String> = None;
14653        let l = sb_niceformat("hello", None, Some(&mut out), 0);
14654        assert_eq!(
14655            out.as_deref(),
14656            Some("hello"),
14657            "c:5886 — nicechar_sel passes printable through"
14658        );
14659        assert_eq!(l, 5, "c:5928 — return length equals output bytes");
14660
14661        let mut out: Option<String> = None;
14662        sb_niceformat("", None, Some(&mut out), 0);
14663        assert_eq!(out.as_deref(), Some(""));
14664
14665        let mut out: Option<String> = None;
14666        sb_niceformat("ABC012!?@", None, Some(&mut out), 0);
14667        assert_eq!(out.as_deref(), Some("ABC012!?@"));
14668    }
14669
14670    /// c:5886 — controls get `\n`/`\t`/`^X`/`^?` escapes via nicechar_sel.
14671    #[test]
14672    fn sb_niceformat_escapes_controls() {
14673        let _g = crate::test_util::global_state_lock();
14674        let mut out: Option<String> = None;
14675        sb_niceformat("a\nb", None, Some(&mut out), 0);
14676        assert_eq!(out.as_deref(), Some("a\\nb"), "c:5886 — newline → \\n");
14677
14678        let mut out: Option<String> = None;
14679        sb_niceformat("a\tb", None, Some(&mut out), 0);
14680        assert_eq!(out.as_deref(), Some("a\\tb"));
14681
14682        let mut out: Option<String> = None;
14683        sb_niceformat("\x01", None, Some(&mut out), 0);
14684        assert_eq!(
14685            out.as_deref(),
14686            Some("^A"),
14687            "c:5886 — control char → ^X form"
14688        );
14689
14690        let mut out: Option<String> = None;
14691        sb_niceformat("\x7f", None, Some(&mut out), 0);
14692        assert_eq!(out.as_deref(), Some("^?"));
14693    }
14694
14695    /// `Src/utils.c:5872` — unmetafy step before formatting. Pin:
14696    /// metafied Meta+X pair gets unescaped first, then run through
14697    /// nicechar_sel.
14698    #[test]
14699    fn sb_niceformat_unmetafies_before_formatting() {
14700        let _g = crate::test_util::global_state_lock();
14701        // META + 0x41 ('A') → decodes to 'a' (0x61). 'a' is printable
14702        // → passes through unchanged.
14703        let bytes: Vec<u8> = vec![Meta, 0x41u8];
14704        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14705        let mut out: Option<String> = None;
14706        sb_niceformat(s, None, Some(&mut out), 0);
14707        assert_eq!(
14708            out.as_deref(),
14709            Some("a"),
14710            "c:5872 — unmetafy first: Meta+0x41 → 'a' → printable passthrough"
14711        );
14712        // META + 0x20 → decodes to NUL → "^@" form.
14713        let bytes: Vec<u8> = vec![Meta, 0x20u8];
14714        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14715        let mut out: Option<String> = None;
14716        sb_niceformat(s, None, Some(&mut out), 0);
14717        let r = out.unwrap_or_default();
14718        assert!(
14719            !r.is_empty(),
14720            "c:5872 — Meta+0x20 → NUL → must emit some escape, not empty"
14721        );
14722    }
14723
14724    /// `Src/utils.c:5937-5959` — `is_sb_niceformat(s)`. Returns 1
14725    /// if sb_niceformat would change the input, else 0.
14726    #[test]
14727    fn is_sb_niceformat_true_for_strings_with_controls() {
14728        let _g = crate::test_util::global_state_lock();
14729        assert_eq!(is_sb_niceformat("\n"), 1, "newline is nice");
14730        assert_eq!(is_sb_niceformat("a\tb"), 1);
14731        assert_eq!(is_sb_niceformat("\x01"), 1);
14732        // Non-control ASCII → 0.
14733        assert_eq!(is_sb_niceformat("hello"), 0);
14734        assert_eq!(is_sb_niceformat(""), 0);
14735    }
14736
14737    /// `Src/utils.c:5810-5826` — `metacharlenconv(x, c)`. Returns
14738    /// `(2, decoded)` for Meta+X pair, `(1, byte)` for plain byte.
14739    /// Pin both branches.
14740    #[test]
14741    fn metacharlenconv_plain_ascii_returns_one_byte() {
14742        let _g = crate::test_util::global_state_lock();
14743        // c:5823-5825 — plain byte.
14744        let (n, c) = metacharlenconv("a");
14745        assert_eq!((n, c), (1, Some('a')), "c:5823 — plain byte → (1, byte)");
14746        let (n, c) = metacharlenconv("X");
14747        assert_eq!((n, c), (1, Some('X')));
14748    }
14749
14750    /// c:5818-5821 — Meta+X pair: `*c = x[1] ^ 32; return 2`.
14751    #[test]
14752    fn metacharlenconv_meta_pair_xor_decodes() {
14753        let _g = crate::test_util::global_state_lock();
14754        // Meta + 0x41 ('A') → 'A' ^ 32 = 'a'.
14755        let bytes: Vec<u8> = vec![Meta, 0x41u8];
14756        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14757        let (n, c) = metacharlenconv(s);
14758        assert_eq!(
14759            (n, c),
14760            (2, Some('a')),
14761            "c:5820 — Meta+0x41 → 'a' (XOR 32), consumed 2 bytes"
14762        );
14763        // Empty input.
14764        let (n, c) = metacharlenconv("");
14765        assert_eq!((n, c), (0, None));
14766    }
14767
14768    /// `Src/utils.c:5832-5843` — `charlenconv(x, len, c)`. Returns
14769    /// `(0, NUL)` when len=0, `(1, byte)` otherwise.
14770    #[test]
14771    fn charlenconv_zero_len_returns_zero() {
14772        let _g = crate::test_util::global_state_lock();
14773        // c:5834 — `if (!len) { *c = '\0'; return 0; }`.
14774        let (n, c) = charlenconv("abc", 0);
14775        assert_eq!((n, c), (0, None), "c:5834-5837 — len=0 returns 0");
14776    }
14777
14778    /// c:5840-5842 — non-zero len: read one byte, return (1, byte).
14779    #[test]
14780    fn charlenconv_returns_first_byte_for_nonzero_len() {
14781        let _g = crate::test_util::global_state_lock();
14782        let (n, c) = charlenconv("abc", 3);
14783        assert_eq!((n, c), (1, Some('a')), "c:5841 — *c = *x; return 1");
14784        let (n, c) = charlenconv("xy", 2);
14785        assert_eq!((n, c), (1, Some('x')));
14786    }
14787
14788    /// `Src/utils.c:5474-5524` — `is_mb_niceformat(s)`. Predicate:
14789    /// would mb_niceformat produce a different output? Pin canonical
14790    /// branches:
14791    ///   - Pure ASCII printable → false (no escape needed).
14792    ///   - Contains control char → true.
14793    ///   - Pure UTF-8 wide chars → false under default PRINTEIGHTBIT-off
14794    ///     since is_wcs_nicechar treats them as printable.
14795    #[test]
14796    fn is_mb_niceformat_false_for_pure_printable_ascii() {
14797        let _g = crate::test_util::global_state_lock();
14798        assert_eq!(
14799            is_mb_niceformat("hello"),
14800            0,
14801            "c:5509 — printable ASCII needs no nice-format"
14802        );
14803        assert_eq!(
14804            is_mb_niceformat(""),
14805            0,
14806            "c:5486 — empty string has no chars to flag"
14807        );
14808        assert_eq!(is_mb_niceformat("ABC012!?@"), 0);
14809    }
14810
14811    /// c:5509 + `is_wcs_nicechar` — newline/tab/control chars trigger
14812    /// the predicate.
14813    #[test]
14814    fn is_mb_niceformat_true_for_strings_with_controls() {
14815        let _g = crate::test_util::global_state_lock();
14816        assert_eq!(is_mb_niceformat("a\nb"), 1, "c:5509 — newline is nice");
14817        assert_eq!(is_mb_niceformat("\t"), 1);
14818        assert_eq!(is_mb_niceformat("\x01"), 1, "c:5509 — control char is nice");
14819        assert_eq!(is_mb_niceformat("\x7f"), 1, "c:5509 — DEL is nice");
14820    }
14821
14822    /// `Src/utils.c:5366-5460` — `mb_niceformat(s)`. Multibyte-aware
14823    /// nice-formatter. Calls `wcs_nicechar` per char. Test uses
14824    /// C-equivalent call shape: `char *out; mb_niceformat(s, NULL, &out, 0);`
14825    #[test]
14826    fn mb_niceformat_preserves_printable_wide_chars() {
14827        let _g = crate::test_util::global_state_lock();
14828        let mut out: Option<String> = None;
14829        mb_niceformat("hello", None, Some(&mut out), 0);
14830        assert_eq!(
14831            out.as_deref(),
14832            Some("hello"),
14833            "c:5407 — printable ASCII passes through"
14834        );
14835
14836        let mut out: Option<String> = None;
14837        mb_niceformat("café", None, Some(&mut out), 0);
14838        assert_eq!(
14839            out.as_deref(),
14840            Some("café"),
14841            "c:5407 — Latin-1 'é' must NOT byte-mask to \\M-X"
14842        );
14843
14844        let mut out: Option<String> = None;
14845        mb_niceformat("字", None, Some(&mut out), 0);
14846        assert_eq!(
14847            out.as_deref(),
14848            Some("字"),
14849            "c:5407 — CJK printable passes through"
14850        );
14851
14852        let mut out: Option<String> = None;
14853        mb_niceformat("abcéxyz", None, Some(&mut out), 0);
14854        assert_eq!(out.as_deref(), Some("abcéxyz"));
14855    }
14856
14857    /// `Src/utils.c:5407` + `wcs_nicechar` — controls still escape.
14858    #[test]
14859    fn mb_niceformat_escapes_controls() {
14860        let _g = crate::test_util::global_state_lock();
14861        let mut out: Option<String> = None;
14862        mb_niceformat("\n", None, Some(&mut out), 0);
14863        assert_eq!(
14864            out.as_deref(),
14865            Some("\\n"),
14866            "c:5407 → wcs_nicechar c:625 — newline escapes"
14867        );
14868
14869        let mut out: Option<String> = None;
14870        mb_niceformat("\t", None, Some(&mut out), 0);
14871        assert_eq!(
14872            out.as_deref(),
14873            Some("\\t"),
14874            "c:5407 → wcs_nicechar c:628 — tab escapes"
14875        );
14876
14877        let mut out: Option<String> = None;
14878        mb_niceformat("a\nb", None, Some(&mut out), 0);
14879        assert_eq!(out.as_deref(), Some("a\\nb"));
14880    }
14881
14882    /// `Src/utils.c:4971-4983` — `metalen(s, len)`. **Input `len` is
14883    /// the UNMETAFIED char count; output is the METAFIED byte count.**
14884    /// Pin a metafied string with one Meta+X pair: 3 unmetafied chars
14885    /// → 4 metafied bytes.
14886    #[test]
14887    fn metalen_returns_metafied_byte_count() {
14888        let _g = crate::test_util::global_state_lock();
14889        // Pure ASCII: 5 chars → 5 bytes (no Meta encountered).
14890        assert_eq!(
14891            metalen("hello", 5),
14892            5,
14893            "c:4972 — ASCII: metafied bytes == unmetafied chars"
14894        );
14895        // [a, Meta, X, b] = 4 metafied bytes representing 3 chars.
14896        let bytes: Vec<u8> = vec![b'a', Meta, 0x41, b'b'];
14897        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14898        assert_eq!(
14899            metalen(s, 3),
14900            4,
14901            "c:4978 — 3 unmetafied chars + 1 Meta = 4 metafied bytes"
14902        );
14903        // Two Meta pairs: [Meta, X, Meta, Y] = 4 bytes for 2 chars.
14904        let bytes: Vec<u8> = vec![Meta, 0x41, Meta, 0x42];
14905        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14906        assert_eq!(
14907            metalen(s, 2),
14908            4,
14909            "c:4978 — 2 unmetafied chars (both Meta+X) = 4 metafied bytes"
14910        );
14911    }
14912
14913    /// c:4974 — `mlen = len;` is the initial value (no Meta found,
14914    /// returns the input length unchanged). Pin len=0 and pure-ASCII.
14915    #[test]
14916    fn metalen_returns_input_for_no_meta_chars() {
14917        let _g = crate::test_util::global_state_lock();
14918        assert_eq!(metalen("", 0), 0, "c:4974 — empty input returns 0");
14919        assert_eq!(
14920            metalen("abc", 3),
14921            3,
14922            "c:4974 — no Meta chars: output == input len"
14923        );
14924    }
14925
14926    /// `Src/utils.c:5070` — `if (!in || !*in) return 0;`.
14927    /// Empty input returns `('\0', 0)`. Pin: empty &str → 0 bytes consumed.
14928    #[test]
14929    fn unmeta_one_empty_input_returns_zero() {
14930        let _g = crate::test_util::global_state_lock();
14931        let (c, n) = unmeta_one("");
14932        assert_eq!(c, '\0', "c:5070 — empty input returns NUL char");
14933        assert_eq!(n, 0, "c:5070 — empty input consumes 0 bytes");
14934    }
14935
14936    /// `Src/utils.c:5081-5082` — Non-Meta byte: `*sz = 1; wc = byte`.
14937    #[test]
14938    fn unmeta_one_plain_ascii_consumes_one_byte() {
14939        let _g = crate::test_util::global_state_lock();
14940        for c in "aA0!~".chars() {
14941            let s = c.to_string();
14942            let (got, n) = unmeta_one(&s);
14943            assert_eq!(got, c, "c:5082 — '{}' decodes to itself", c);
14944            assert_eq!(n, 1, "c:5081 — non-Meta byte consumes 1");
14945        }
14946    }
14947
14948    /// `Src/utils.c:5077-5079` — Meta byte: `*sz = 2; wc = in[1] ^ 32`.
14949    /// Pin via constructed Meta byte sequence.
14950    #[test]
14951    fn unmeta_one_meta_pair_decodes_xor_32() {
14952        let _g = crate::test_util::global_state_lock();
14953        // META + 0x41 ('A') → 'A' ^ 32 = 0x61 ('a'). 2 bytes consumed.
14954        let bytes: Vec<u8> = vec![Meta, 0x41u8];
14955        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14956        let (got, n) = unmeta_one(s);
14957        assert_eq!(got, 'a', "c:5079 — Meta+0x41 decodes to 0x41^32 = 'a'");
14958        assert_eq!(n, 2, "c:5078 — Meta pair consumes 2 bytes");
14959        // META + 0x20 = 0x20^32 = 0x00 (NUL).
14960        let bytes: Vec<u8> = vec![Meta, 0x20u8];
14961        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14962        let (got, n) = unmeta_one(s);
14963        assert_eq!(
14964            got, '\0',
14965            "c:5079 — Meta+0x20 decodes to NUL (the canonical metafy-NUL pattern)"
14966        );
14967        assert_eq!(n, 2);
14968    }
14969
14970    /// Edge case: bare Meta byte at end with no follower. C source
14971    /// would read past the buffer (UB); Rust port's `bytes.len() > 1`
14972    /// guard handles this by falling through to the non-Meta arm.
14973    #[test]
14974    fn unmeta_one_trailing_meta_byte_falls_through() {
14975        let _g = crate::test_util::global_state_lock();
14976        let bytes: Vec<u8> = vec![Meta];
14977        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14978        let (_, n) = unmeta_one(s);
14979        // Defensive: Rust returns the Meta byte itself as char + 1.
14980        assert_eq!(n, 1, "trailing Meta — no panic, no read past buffer");
14981    }
14982
14983    /// `Src/utils.c:5185-5203` — `ztrsub(t, s)`. Count of unmetafied
14984    /// chars between two pointers in the same metafied string.
14985    /// Rust port takes (buf, start, end) byte-offsets since raw
14986    /// pointer subtraction across distinct &str isn't safe.
14987    /// Pin pure-ASCII subrange (no Meta) → returns end-start.
14988    #[test]
14989    fn ztrsub_ascii_no_meta_returns_byte_distance() {
14990        let _g = crate::test_util::global_state_lock();
14991        // "hello world" — substring [0..5) → "hello" → 5 chars.
14992        assert_eq!(
14993            ztrsub("hello world", 0, 5),
14994            5,
14995            "c:5189 — no Meta: returns t-s byte distance"
14996        );
14997        // [6..11) → "world" → 5 chars.
14998        assert_eq!(ztrsub("hello world", 6, 11), 5);
14999        // Empty range.
15000        assert_eq!(ztrsub("hello", 2, 2), 0);
15001    }
15002
15003    /// c:5191-5199 — each Meta+X pair decrements `l` by 1 (since the
15004    /// initial l = byte-distance counts both bytes but the pair is
15005    /// one logical char). Pin via constructed Meta string.
15006    #[test]
15007    fn ztrsub_subtracts_one_per_meta_pair() {
15008        let _g = crate::test_util::global_state_lock();
15009        // Build "a" + Meta + 0x20 + "b" = 4 bytes total.
15010        // Unmetafied: "a" + 1-char-from-meta + "b" = 3 chars.
15011        let meta_byte = Meta;
15012        let buf_bytes: Vec<u8> = vec![b'a', meta_byte, 0x20, b'b'];
15013        let buf = unsafe { std::str::from_utf8_unchecked(&buf_bytes) };
15014        // [0..4) byte distance = 4; Meta pair found → l-- → 3.
15015        assert_eq!(
15016            ztrsub(buf, 0, 4),
15017            3,
15018            "c:5192-5199 — Meta pair decrements distance by 1"
15019        );
15020    }
15021
15022    /// c:5189 — `int l = t - s;` start of count. Pin edge case
15023    /// where start > end (caller bug) doesn't underflow or panic;
15024    /// Rust port clamps via `start.min(end)`.
15025    #[test]
15026    fn ztrsub_clamps_inverted_range_to_zero() {
15027        let _g = crate::test_util::global_state_lock();
15028        assert_eq!(
15029            ztrsub("hello", 4, 2),
15030            0,
15031            "inverted range start>end → 0 (defensive; not in C contract)"
15032        );
15033        // Beyond-buffer end gets clamped.
15034        assert_eq!(
15035            ztrsub("hi", 0, 100),
15036            2,
15037            "end > buf.len() clamps to buffer length"
15038        );
15039    }
15040
15041    /// `Src/utils.c:5141` — trailing Meta byte with no following char
15042    /// is a malformed-string edge case. C has a DPUTS debug-only
15043    /// fprintf for "unexpected end" but otherwise the loop terminates
15044    /// because `*s` is now NUL. Rust port must not panic on this
15045    /// truncated input.
15046    #[test]
15047    fn ztrlen_handles_trailing_meta_byte_without_panic() {
15048        let _g = crate::test_util::global_state_lock();
15049        let meta = char::from_u32(Meta as u32).unwrap();
15050        let trailing: String = ['a', meta].iter().collect();
15051        // c:5141-5149 — increment l, then encounter Meta, then loop
15052        // condition `*s` is empty → terminate. Count: 'a' + Meta = 2
15053        // (Rust port counts Meta as 1 char when no follower).
15054        let r = ztrlen(&trailing);
15055        assert!(r >= 1, "trailing Meta must not panic; got {}", r);
15056    }
15057
15058    /// c:params.c:1288 — `isident` rejects digit-leading names.
15059    /// A regression accepting them lets `typeset 1foo=bar` install
15060    /// a poisoned param that no later expansion can address.
15061    #[test]
15062    fn isident_rejects_digit_leading_names() {
15063        let _g = crate::test_util::global_state_lock(); // c:1288
15064                                                        // c:1315-1319 — C `isident`: if first char is digit, ALL
15065                                                        // chars must be digit (all-digit names are valid positional
15066                                                        // params like $99). So `1foo` is rejected (digit-then-alpha)
15067                                                        // but `99` is accepted as a valid positional-param name.
15068                                                        // The previous utils.rs fake rejected all-digit names too,
15069                                                        // which was non-faithful — the canonical `params::isident`
15070                                                        // at `params.rs:2056` matches the C semantics.
15071        assert!(!isident("1foo")); // c:1318
15072        assert!(
15073            isident("99"), // c:1318
15074            "c:1315-1319 — all-digit names are valid positional params"
15075        );
15076        assert!(!isident("")); // c:1290
15077    }
15078
15079    /// `isident` MUST accept underscore-leading + alpha-leading names —
15080    /// `_foo`, `Foo_BAR_42` are valid POSIX shell idents.
15081    #[test]
15082    fn isident_accepts_underscore_and_alpha_leading() {
15083        let _g = crate::test_util::global_state_lock(); // c:1288
15084        assert!(isident("foo")); // c:1288
15085        assert!(isident("_foo")); // c:1288
15086        assert!(isident("Foo_BAR_42")); // c:1288
15087        assert!(isident("a")); // c:1288
15088    }
15089
15090    /// `isident` rejects whitespace/punctuation/$. A regression
15091    /// accepting them would let assignments install names no
15092    /// subsequent lookup can address (`typeset 'foo bar'=baz`).
15093    #[test]
15094    fn isident_rejects_special_chars() {
15095        let _g = crate::test_util::global_state_lock(); // c:1288
15096                                                        // c:1320-1322 — non-digit-leading names use itype_end with
15097                                                        // INAMESPC bits (alnum + `_` + `.`); other chars terminate.
15098                                                        // The previous utils.rs fake rejected `foo.` outright, which
15099                                                        // is incorrect — C accepts trailing `.` (it's an INAMESPC
15100                                                        // member). Whitespace, `-`, and `$` are NOT INAMESPC chars
15101                                                        // and remain rejected.
15102        assert!(!isident("foo bar")); // c:1322
15103        assert!(!isident("foo-bar")); // c:1322
15104        assert!(!isident("a$b")); // c:1322
15105    }
15106
15107    /// `convbase(0, 10)` MUST produce `"0"`, not `""`. The literal-zero
15108    /// edge case is a real C divergence point — regression here would
15109    /// make `$(( 0 ))` print nothing.
15110    #[test]
15111    fn convbase_zero_renders_as_zero_literal() {
15112        let _g = crate::test_util::global_state_lock();
15113        assert_eq!(convbase(0, 10), "0");
15114    }
15115
15116    /// `convbase` uses zsh's canonical `BASE#NUMBER` syntax for
15117    /// non-decimal output (matches `$(( [#16] 255 ))` → `16#FF`).
15118    /// This is the format `printf "%X"` and `$(( ... ))` both produce.
15119    #[test]
15120    fn convbase_uses_base_prefix_syntax_for_non_decimal() {
15121        let _g = crate::test_util::global_state_lock();
15122        assert_eq!(convbase(255, 16), "16#FF");
15123        assert_eq!(convbase(8, 8), "8#10");
15124        assert_eq!(convbase(5, 2), "2#101");
15125        // Base 10 has no prefix.
15126        assert_eq!(convbase(42, 10), "42");
15127    }
15128
15129    /// Negative number renders with leading `-`. Regression that drops
15130    /// the sign would silently flip arithmetic semantics in `$((...))`.
15131    #[test]
15132    fn convbase_preserves_negative_sign() {
15133        let _g = crate::test_util::global_state_lock();
15134        assert_eq!(convbase(-42, 10), "-42");
15135    }
15136
15137    /// `Src/utils.c:837` — `slashsplit` keeps an EMPTY leading segment
15138    /// when the input has a leading `/`. The caller `xsymlinks`
15139    /// (c:879) reads this empty as the signal "absolute path, start
15140    /// xbuf from /". Previous Rust test asserted filtering of all
15141    /// empties (matching the previous buggy port); that contract was
15142    /// wrong relative to C. Updated to assert the C contract.
15143    #[test]
15144    fn slashsplit_keeps_leading_empty_segment() {
15145        let _g = crate::test_util::global_state_lock();
15146        // c:851 with t==s on first iter → empty prefix segment.
15147        assert_eq!(
15148            slashsplit("/usr/local/bin"),
15149            vec![
15150                "".to_string(),
15151                "usr".to_string(),
15152                "local".to_string(),
15153                "bin".to_string()
15154            ],
15155            "c:851 — leading `/` produces empty first segment"
15156        );
15157        // Single `/`: one empty segment only.
15158        assert_eq!(
15159            slashsplit("/"),
15160            vec!["".to_string()],
15161            "c:854-857 — trailing `/` after first iter returns ['']"
15162        );
15163        // Consecutive slashes collapse (c:852-853 inner while-loop).
15164        assert_eq!(
15165            slashsplit("//foo"),
15166            vec!["".to_string(), "foo".to_string()],
15167            "c:852-853 — consecutive `/` collapse, leading empty kept"
15168        );
15169        // Empty input still empty result.
15170        assert_eq!(
15171            slashsplit(""),
15172            Vec::<String>::new(),
15173            "c:842 — empty input → empty array"
15174        );
15175    }
15176
15177    /// `Src/utils.c:837` — relative paths (no leading `/`) start
15178    /// at the first non-slash; trailing `/` doesn't add a segment
15179    /// (c:854-857 returns before tail emit).
15180    #[test]
15181    fn slashsplit_relative_path_no_trailing_empty() {
15182        let _g = crate::test_util::global_state_lock();
15183        assert_eq!(
15184            slashsplit("a/b/"),
15185            vec!["a".to_string(), "b".to_string()],
15186            "c:854-857 — trailing `/` doesn't add segment"
15187        );
15188        // Mid-slash collapse in relative path.
15189        assert_eq!(
15190            slashsplit("a//b"),
15191            vec!["a".to_string(), "b".to_string()],
15192            "c:852-853 — mid `//` collapses"
15193        );
15194    }
15195
15196    /// `equalsplit("foo=bar")` returns Some(("foo","bar")). Used by
15197    /// `typeset NAME=val` parsing. Regression returning None on a
15198    /// well-formed assignment would silently break every export/typeset.
15199    #[test]
15200    fn equalsplit_returns_first_equals_split() {
15201        let _g = crate::test_util::global_state_lock();
15202        assert_eq!(
15203            equalsplit("foo=bar"),
15204            Some(("foo".to_string(), "bar".to_string()))
15205        );
15206        assert_eq!(
15207            equalsplit("a=b=c"),
15208            Some(("a".to_string(), "b=c".to_string())),
15209            "splits on FIRST `=` only"
15210        );
15211    }
15212
15213    /// `equalsplit("foo")` returns None — no `=` to split on. Catches
15214    /// a regression returning Some(("foo","")) which would let bare
15215    /// names install with empty values silently.
15216    #[test]
15217    fn equalsplit_no_equals_returns_none() {
15218        let _g = crate::test_util::global_state_lock();
15219        assert_eq!(equalsplit("foo"), None);
15220        assert_eq!(equalsplit(""), None);
15221    }
15222
15223    /// c:5106 — `ztrcmp` is the canonical zsh string compare. Same
15224    /// inputs MUST produce same Ordering (deterministic). Regression
15225    /// to a randomised hash-order would mis-sort `${(o)array}` output.
15226    #[test]
15227    fn ztrcmp_deterministic_and_lexicographic() {
15228        let _g = crate::test_util::global_state_lock();
15229        assert_eq!(ztrcmp("abc", "abc"), std::cmp::Ordering::Equal);
15230        assert_eq!(ztrcmp("abc", "abd"), std::cmp::Ordering::Less);
15231        assert_eq!(ztrcmp("abd", "abc"), std::cmp::Ordering::Greater);
15232        // Empty strings.
15233        assert_eq!(ztrcmp("", ""), std::cmp::Ordering::Equal);
15234        assert_eq!(ztrcmp("", "a"), std::cmp::Ordering::Less);
15235    }
15236
15237    /// c:5106 — shorter-as-prefix sorts before longer (like strcmp).
15238    /// Catches a regression where length-then-content would mis-sort.
15239    #[test]
15240    fn ztrcmp_shorter_prefix_is_less() {
15241        let _g = crate::test_util::global_state_lock();
15242        assert_eq!(ztrcmp("a", "ab"), std::cmp::Ordering::Less);
15243        assert_eq!(ztrcmp("foo", "foob"), std::cmp::Ordering::Less);
15244    }
15245
15246    /// `Src/utils.c:5117-5122` — Meta+X pair decodes to `X ^ 32`
15247    /// for comparison purposes. Pin: two strings differing only in
15248    /// the metafied byte's decoded value compare correctly.
15249    #[test]
15250    fn ztrcmp_decodes_meta_byte_for_comparison() {
15251        let _g = crate::test_util::global_state_lock();
15252        // META+0x21 ('!') = NUL? Let me use safer chars.
15253        // META+'A' (0x41) decodes to 'A' ^ 32 = 'a' (0x61).
15254        // So a "META a" pair represents the byte 'a' (0x61).
15255        // Compare "Ma" (M=Meta+a-byte-pair) vs "b" (>a).
15256        let meta_byte = Meta;
15257        let s1_bytes: Vec<u8> = vec![meta_byte, 0x41u8]; // decodes to 0x61 = 'a'
15258        let s2_bytes: Vec<u8> = vec![b'b'];
15259        let s1 = unsafe { std::str::from_utf8_unchecked(&s1_bytes) };
15260        let s2 = unsafe { std::str::from_utf8_unchecked(&s2_bytes) };
15261        // 'a' (0x61) < 'b' (0x62) → Less.
15262        assert_eq!(
15263            ztrcmp(s1, s2),
15264            std::cmp::Ordering::Less,
15265            "c:5117-5118 — Meta+0x41 decodes to 'a' which < 'b'"
15266        );
15267    }
15268
15269    /// `Src/utils.c:531-539` — `is_nicechar`. Returns true for chars
15270    /// needing escape-formatting:
15271    ///   - c:534 — printable ASCII (0x20-0x7e) → false.
15272    ///   - c:536 — high-bit byte (>= 0x80) → !PRINTEIGHTBIT.
15273    ///   - c:538 — DEL/\n/\t/<0x20 → true.
15274    /// Pin the three branches.
15275    #[test]
15276    fn is_nicechar_printable_ascii_is_not_nice() {
15277        let _g = crate::test_util::global_state_lock();
15278        // c:534 — letters, digits, punctuation in 0x20-0x7e all NOT nice.
15279        for c in "abcXYZ012!?@~".chars() {
15280            assert!(
15281                !is_nicechar(c),
15282                "c:534 — '{}' (ASCII printable) must NOT be nice",
15283                c
15284            );
15285        }
15286        // c:534 — space (0x20) is printable.
15287        assert!(!is_nicechar(' '));
15288    }
15289
15290    /// c:538 — control chars (DEL, newline, tab, <0x20) ARE nice.
15291    #[test]
15292    fn is_nicechar_control_chars_are_nice() {
15293        let _g = crate::test_util::global_state_lock();
15294        assert!(is_nicechar('\n'), "c:538 — newline is nice");
15295        assert!(is_nicechar('\t'), "c:538 — tab is nice");
15296        assert!(is_nicechar('\x7f'), "c:538 — DEL is nice");
15297        assert!(is_nicechar('\x00'), "c:538 — NUL is nice (<0x20)");
15298        assert!(is_nicechar('\x07'), "c:538 — BEL is nice (<0x20)");
15299        assert!(is_nicechar('\x1b'), "c:538 — ESC is nice (<0x20)");
15300        assert!(is_nicechar('\x1f'), "c:538 — boundary 0x1f is nice");
15301    }
15302
15303    /// `Src/utils.c:2528-2570` — `zstrtoul_underscore(s)`. Pins
15304    /// the base-prefix dispatch: `0x` → 16, `0b` → 2, leading `0`
15305    /// only when OCTALZEROES is set. Tests the default state.
15306    #[test]
15307    fn zstrtoul_underscore_recognises_hex_binary_decimal() {
15308        let _g = crate::test_util::global_state_lock();
15309        // c:2538 — hex.
15310        assert_eq!(zstrtoul_underscore("0xff"), Some(255));
15311        assert_eq!(zstrtoul_underscore("0XFF"), Some(255));
15312        // c:2540 — binary.
15313        assert_eq!(zstrtoul_underscore("0b1010"), Some(10));
15314        assert_eq!(zstrtoul_underscore("0B11"), Some(3));
15315        // c:2537 — pure decimal (no leading zero).
15316        assert_eq!(zstrtoul_underscore("12345"), Some(12345));
15317        // c:2543 — leading-zero with OCTALZEROES off (default) → decimal.
15318        if !isset(OCTALZEROES) {
15319            assert_eq!(
15320                zstrtoul_underscore("0777"),
15321                Some(777),
15322                "c:2543 — OCTALZEROES off: leading-0 parses as decimal"
15323            );
15324            assert_eq!(zstrtoul_underscore("010"), Some(10));
15325        }
15326    }
15327
15328    /// `Src/utils.c:2547-2548` — underscores are stripped before
15329    /// parsing. C: `if (*s == '_') continue;`. Used to support
15330    /// human-readable big numbers like `1_000_000`.
15331    #[test]
15332    fn zstrtoul_underscore_strips_underscores() {
15333        let _g = crate::test_util::global_state_lock();
15334        assert_eq!(
15335            zstrtoul_underscore("1_000_000"),
15336            Some(1_000_000),
15337            "c:2547-2548 — `_` stripped from numeric input"
15338        );
15339        assert_eq!(zstrtoul_underscore("0xff_ff"), Some(0xffff));
15340        assert_eq!(zstrtoul_underscore("0b1010_1010"), Some(0xaa));
15341    }
15342
15343    /// `Src/utils.c:2533-2534` — leading `+` sign is consumed.
15344    /// `zulong` is unsigned so `-` is not handled here (caller
15345    /// handles negation at `zstrtol_underscore`).
15346    #[test]
15347    fn zstrtoul_underscore_consumes_leading_plus() {
15348        let _g = crate::test_util::global_state_lock();
15349        assert_eq!(zstrtoul_underscore("+42"), Some(42));
15350        assert_eq!(zstrtoul_underscore("+0xff"), Some(255));
15351    }
15352
15353    /// `Src/utils.c:467` — `if (ZISPRINT(c)) goto done;`. Printable
15354    /// ASCII passes through nicechar_sel unchanged. Pin a single-char
15355    /// printable input → single-char output.
15356    #[test]
15357    fn nicechar_sel_passes_printable_ascii_unchanged() {
15358        let _g = crate::test_util::global_state_lock();
15359        for c in "aA0!~".chars() {
15360            assert_eq!(
15361                nicechar_sel(c, false),
15362                c.to_string(),
15363                "c:467 — printable ASCII '{}' passes through",
15364                c
15365            );
15366        }
15367    }
15368
15369    /// `Src/utils.c:487-492` — `\n` becomes `\n` (backslash-n) and
15370    /// `\t` becomes `\t`. Both two-char outputs. Pin the canonical
15371    /// escapes.
15372    #[test]
15373    fn nicechar_sel_escapes_newline_and_tab() {
15374        let _g = crate::test_util::global_state_lock();
15375        assert_eq!(
15376            nicechar_sel('\n', false),
15377            "\\n",
15378            "c:487-489 — newline escape"
15379        );
15380        assert_eq!(nicechar_sel('\t', false), "\\t", "c:490-492 — tab escape");
15381    }
15382
15383    /// `Src/utils.c:493-501` — control chars (<0x20 except \n/\t)
15384    /// get `^X` prefix in non-quotable mode and `\C-X` in quotable.
15385    /// c:500 — `c += 0x40` adds 0x40 to map 0x01 → 0x41 ('A').
15386    #[test]
15387    fn nicechar_sel_control_chars_use_caret_or_c_prefix() {
15388        let _g = crate::test_util::global_state_lock();
15389        // Non-quotable: ^A
15390        assert_eq!(
15391            nicechar_sel('\x01', false),
15392            "^A",
15393            "c:499-500 — \\x01 → ^A non-quotable"
15394        );
15395        // Quotable: \C-A
15396        assert_eq!(
15397            nicechar_sel('\x01', true),
15398            "\\C-A",
15399            "c:495-500 — \\x01 → \\C-A quotable"
15400        );
15401        // ^G (BEL = 0x07 → 0x47 = 'G')
15402        assert_eq!(nicechar_sel('\x07', false), "^G");
15403        // ^[ (ESC = 0x1b → 0x5b = '[')
15404        assert_eq!(nicechar_sel('\x1b', false), "^[");
15405    }
15406
15407    /// `Src/utils.c:479-486` — DEL (0x7f) renders as `^?` or `\C-?`.
15408    #[test]
15409    fn nicechar_sel_del_renders_as_caret_question() {
15410        let _g = crate::test_util::global_state_lock();
15411        assert_eq!(nicechar_sel('\x7f', false), "^?", "c:485-486 — DEL is `^?`");
15412        assert_eq!(
15413            nicechar_sel('\x7f', true),
15414            "\\C-?",
15415            "c:481-486 — DEL is `\\C-?` quotable"
15416        );
15417    }
15418
15419    /// `Src/utils.c:469-478` — high-bit bytes (>= 0x80) under default
15420    /// PRINTEIGHTBIT-off path: write `\M-` then mask to low ASCII.
15421    /// If the masked char is printable, output is `\M-<char>`. Pin
15422    /// the canonical 0xc1 → \M-A and 0xe1 → \M-a cases.
15423    #[test]
15424    fn nicechar_sel_highbit_uses_meta_prefix_when_printeightbit_off() {
15425        let _g = crate::test_util::global_state_lock();
15426        if isset(PRINTEIGHTBIT) {
15427            return; // Test only valid when PRINTEIGHTBIT off (default).
15428        }
15429        // 0xc1 = 'A' + 0x80 → \M-A
15430        let c = char::from_u32(0xc1).unwrap();
15431        assert_eq!(
15432            nicechar_sel(c, false),
15433            "\\M-A",
15434            "c:472-477 — high-bit 0xc1 → \\M-A under PRINTEIGHTBIT-off"
15435        );
15436        // 0xe1 = 'a' + 0x80 → \M-a
15437        let c = char::from_u32(0xe1).unwrap();
15438        assert_eq!(nicechar_sel(c, false), "\\M-a");
15439    }
15440
15441    /// `Src/utils.c:593-705` — `wcs_nicechar_sel`. Wide-char variant
15442    /// of `nicechar_sel`: control chars escape, printable wides
15443    /// emit raw UTF-8, large codepoints get `\u`/`\U` hex escape.
15444    /// Pin every branch. Previously delegated to `nicechar_sel`
15445    /// which byte-masked via `c & 0xff` — every UTF-8 codepoint
15446    /// emerged mangled.
15447    #[test]
15448    fn wcs_nicechar_sel_printable_wide_emits_utf8() {
15449        let _g = crate::test_util::global_state_lock();
15450        // c:644-678 — printable wide char emits raw UTF-8.
15451        assert_eq!(
15452            wcs_nicechar_sel('a', None, None, false),
15453            "a",
15454            "ASCII printable"
15455        );
15456        assert_eq!(
15457            wcs_nicechar_sel('é', None, None, false),
15458            "é",
15459            "Latin-1 printable"
15460        );
15461        assert_eq!(
15462            wcs_nicechar_sel('字', None, None, false),
15463            "字",
15464            "CJK printable"
15465        );
15466    }
15467
15468    /// c:625-630 — `\n` and `\t` escape (same as nicechar_sel for ASCII).
15469    #[test]
15470    fn wcs_nicechar_sel_escapes_newline_and_tab() {
15471        let _g = crate::test_util::global_state_lock();
15472        assert_eq!(wcs_nicechar_sel('\n', None, None, false), "\\n");
15473        assert_eq!(wcs_nicechar_sel('\t', None, None, false), "\\t");
15474    }
15475
15476    /// c:617-623 — DEL (0x7f) renders as `^?` (non-quotable) or
15477    /// `\C-?` (quotable). Same as the byte version.
15478    #[test]
15479    fn wcs_nicechar_sel_del_uses_caret_question() {
15480        let _g = crate::test_util::global_state_lock();
15481        assert_eq!(wcs_nicechar_sel('\x7f', None, None, false), "^?");
15482        assert_eq!(wcs_nicechar_sel('\x7f', None, None, true), "\\C-?");
15483    }
15484
15485    /// c:631-638 — control chars (0x01-0x1f, except \n/\t) emit
15486    /// `^X` or `\C-X` with the +0x40 offset.
15487    #[test]
15488    fn wcs_nicechar_sel_control_chars_use_caret_prefix() {
15489        let _g = crate::test_util::global_state_lock();
15490        assert_eq!(wcs_nicechar_sel('\x01', None, None, false), "^A");
15491        assert_eq!(wcs_nicechar_sel('\x07', None, None, false), "^G");
15492        assert_eq!(wcs_nicechar_sel('\x1b', None, None, false), "^[");
15493        assert_eq!(wcs_nicechar_sel('\x01', None, None, true), "\\C-A");
15494    }
15495
15496    /// `Src/utils.c:656-663` — large non-printable codepoints get
15497    /// hex escape: `\U%.8x` (>= 0x10000), `\u%.4x` (>= 0x100).
15498    /// Both lowercase per C's `%x`. Use a non-printable wide char
15499    /// to test (most BMP chars are printable; pick a control area
15500    /// like U+0085 NEXT LINE).
15501    #[test]
15502    fn wcs_nicechar_sel_large_nonprintable_uses_hex_escape() {
15503        let _g = crate::test_util::global_state_lock();
15504        // U+0085 NEL — control in C1 range, not iswprint.
15505        let nel = char::from_u32(0x85).unwrap();
15506        // Since 0x85 < 0x100, falls into nicechar_sel fallback.
15507        // After u9_iswprint says false, cv < 0x80 is false (cv=0x85),
15508        // PRINTEIGHTBIT off → enters !is_printable && cv < 0x80 branch?
15509        // Actually 0x85 >= 0x80 → !print_eightbit true → enters non-print branch.
15510        // 0x85 not 0x7f, not \n/\t, not <0x20. Falls past all if-elses to
15511        // post-branch logic → !is_printable, cv >= 0x100 false, cv >= 0x100 false
15512        // → falls to nicechar_sel fallback.
15513        let r = wcs_nicechar_sel(nel, None, None, false);
15514        assert!(!r.is_empty(), "must emit something for U+0085");
15515        // U+200B ZERO WIDTH SPACE — non-printable, 0x100-0xffff range.
15516        let zwsp = char::from_u32(0x200B).unwrap();
15517        let r = wcs_nicechar_sel(zwsp, None, None, false);
15518        // u9_iswprint(0x200B) returns true via unicode-width 0-width.
15519        // Per the impl: u9_iswprint true → emit raw. So r should be
15520        // the raw zwsp char.
15521        assert!(!r.is_empty());
15522    }
15523
15524    /// `Src/utils.c:709-714` — `wcs_nicechar(c)` delegates to
15525    /// `wcs_nicechar_sel(c, widthp, swidep, 0)`. Pin the parity.
15526    #[test]
15527    fn wcs_nicechar_matches_wcs_nicechar_sel_with_zero() {
15528        let _g = crate::test_util::global_state_lock();
15529        for c in ['a', '\n', '\t', '\x7f', '\x01', 'é', '字'] {
15530            assert_eq!(
15531                wcs_nicechar(c, None, None),
15532                wcs_nicechar_sel(c, None, None, false),
15533                "c:711 — `wcs_nicechar(c, NULL, NULL)` must equal `wcs_nicechar_sel(c, NULL, NULL, 0)` for {:?}",
15534                c
15535            );
15536        }
15537    }
15538
15539    /// `Src/utils.c:1989-2012` — `movefd(fd)`. Three contracts:
15540    /// 1. fd == -1 → returned as-is (no syscalls).
15541    /// 2. fd >= 10 → returned unchanged (already high).
15542    /// 3. fd < 10 → dupped with F_DUPFD >= 10, original closed
15543    ///    unconditionally (even on dup failure).
15544    #[test]
15545    #[cfg(unix)]
15546    fn movefd_returns_minus_one_unchanged() {
15547        let _g = crate::test_util::global_state_lock();
15548        // c:1992 — `if (fd != -1 && fd < 10)` skipped for fd=-1.
15549        assert_eq!(movefd(-1), -1, "fd=-1 must be returned unchanged");
15550    }
15551
15552    /// c:1992 — `fd >= 10` skips the dup-and-close path.
15553    #[test]
15554    #[cfg(unix)]
15555    fn movefd_returns_high_fd_unchanged() {
15556        let _g = crate::test_util::global_state_lock();
15557        // Use a known-open fd. /dev/null open with fd guaranteed to be
15558        // <10 because new fds get the lowest free slot — but we can
15559        // dup it to 10 directly to test the early-return path.
15560        let f = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15561        if f < 0 {
15562            return;
15563        } // Skip on unusual systems.
15564        let high = unsafe { libc::fcntl(f, libc::F_DUPFD, 20) };
15565        unsafe {
15566            libc::close(f);
15567        }
15568        if high < 0 {
15569            return;
15570        }
15571        // movefd(20) must return 20 unchanged (no dup, no close).
15572        let r = movefd(high);
15573        assert_eq!(r, high, "c:1992 — fd >= 10 returned unchanged");
15574        unsafe {
15575            libc::close(high);
15576        }
15577    }
15578
15579    /// `Src/utils.c:1968-1983` — `check_fd_table(fd)`. Grows the
15580    /// fdtable Vec to `fd+1` slots (filling new entries with
15581    /// FDT_UNUSED) and bumps `MAX_ZSH_FD` to `fd`. Pin: a small
15582    /// fd grows the table to `fd+1` length; MAX_ZSH_FD reflects it.
15583    /// fd ≤ cur_max early-returns without growing (c:1971-1972).
15584    #[test]
15585    fn check_fd_table_grows_to_fd_plus_one() {
15586        let _g = crate::test_util::global_state_lock();
15587        // Reset globals so the test is deterministic regardless
15588        // of prior ordering — other tests may have grown the
15589        // table or bumped MAX_ZSH_FD.
15590        MAX_ZSH_FD.store(-1, Ordering::Relaxed);
15591        {
15592            let mut g = fdtable_lock().lock().unwrap();
15593            g.clear();
15594        }
15595
15596        // c:1971-1972 — fd ≤ cur_max is the early-return path.
15597        // With cur_max=-1, fd=-1 hits `fd <= cur_max` → true.
15598        assert!(check_fd_table(-1));
15599        assert_eq!(
15600            MAX_ZSH_FD.load(Ordering::Relaxed),
15601            -1,
15602            "fd ≤ cur_max early-returns; MAX_ZSH_FD untouched"
15603        );
15604
15605        // c:1974-1981 — fd > cur_max grows fdtable to fd+1 slots.
15606        assert!(check_fd_table(7));
15607        assert_eq!(
15608            MAX_ZSH_FD.load(Ordering::Relaxed),
15609            7,
15610            "c:1982 — MAX_ZSH_FD := fd"
15611        );
15612        let len = {
15613            let g = fdtable_lock().lock().unwrap();
15614            g.len()
15615        };
15616        assert!(
15617            len >= 8,
15618            "c:1975-1979 — fdtable grew to ≥ fd+1 = 8 slots, got {}",
15619            len
15620        );
15621
15622        // Idempotence under fd ≤ cur_max.
15623        assert!(check_fd_table(3));
15624        assert_eq!(
15625            MAX_ZSH_FD.load(Ordering::Relaxed),
15626            7,
15627            "fd ≤ cur_max keeps MAX_ZSH_FD at 7"
15628        );
15629    }
15630
15631    /// `Src/utils.c:4364,4367` — `wcsitype(c, IWORD/ISEP)` reads
15632    /// from the `wordchars`/`ifs` GLOBALS (writable by
15633    /// `wordcharssetfn`/`ifssetfn`). Previously read from
15634    /// `std::env::var` — the libc process env — which never
15635    /// reflects runtime `WORDCHARS=…` shell-side assignments.
15636    /// Pin: set wordchars via `wordcharssetfn`, verify `wcsitype`
15637    /// returns true for chars in the new set.
15638    #[test]
15639    fn wcsitype_iword_reads_from_canonical_wordchars_global() {
15640        let _g = crate::test_util::global_state_lock();
15641        // Test only meaningful in MULTIBYTE mode for non-ASCII chars;
15642        // ASCII chars (< 0x80) route through TYPTAB directly.
15643        // Skip if MULTIBYTE off.
15644        if !isset(MULTIBYTE) {
15645            return;
15646        }
15647        // Save and set WORDCHARS to a single non-ASCII char.
15648        // Snapshot the store `wordcharssetfn` writes: the `wordchars` global
15649        // itself. A paramtab read finds no WORDCHARS node in a bare test
15650        // process and yields "", and restoring that left an EMPTY WORDCHARS
15651        // behind for every later typtab test.
15652        let saved = crate::ported::params::wordchars_lock()
15653            .lock()
15654            .map(|g| g.clone())
15655            .unwrap_or_default();
15656        // wordcharssetfn ignores its `_pm` arg and re-enters paramtab's
15657        // read lock via inittyptab (sibling of the IFS deadlock above).
15658        // Use a stack-local dummy pm instead of going through paramtab's
15659        // write lock so the test never holds two paramtab locks at once.
15660        let mut dummy = crate::ported::zsh_h::param::default();
15661        let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
15662            wordcharssetfn(dummy, val);
15663        };
15664        do_set(&mut dummy, "é".to_string());
15665        // 'é' is alphanumeric per Unicode → IWORD returns true via
15666        // is_alphanumeric short-circuit at c:4353. So we can't pin
15667        // the WORDCHARS-specific path with 'é'. Use a non-alnum char.
15668        do_set(&mut dummy, ":".to_string());
15669        // ':' is ASCII so wcsitype routes through TYPTAB (which now
15670        // has IWORD on ':' because wordcharssetfn called inittyptab).
15671        assert!(
15672            wcsitype(':', IWORD as u32),
15673            "c:4364 — wordchars membership through canonical global"
15674        );
15675        // Restore.
15676        *crate::ported::params::wordchars_lock()
15677            .lock()
15678            .expect("wordchars poisoned") = saved;
15679        crate::ported::utils::inittyptab();
15680    }
15681
15682    /// `Src/utils.c:2090-2097` — `addmodulefd(fd, fdt)`. Stores the
15683    /// provided fdt in the fdtable slot for the given fd. Previously
15684    /// Rust port hardcoded FDT_MODULE and added CLOEXEC unconditionally.
15685    /// Pin: pass FDT_EXTERNAL, verify slot stores FDT_EXTERNAL (not
15686    /// FDT_MODULE).
15687    #[cfg(unix)]
15688    #[test]
15689    fn addmodulefd_respects_fdt_parameter() {
15690        let _g = crate::test_util::global_state_lock();
15691        // Open a real fd to use.
15692        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15693        if fd < 0 {
15694            return;
15695        }
15696        addmodulefd(fd, FDT_EXTERNAL);
15697        assert_eq!(
15698            fdtable_get(fd),
15699            FDT_EXTERNAL,
15700            "c:2095 — fdt parameter must be stored verbatim"
15701        );
15702        // Switch to FDT_MODULE.
15703        addmodulefd(fd, FDT_MODULE);
15704        assert_eq!(
15705            fdtable_get(fd),
15706            FDT_MODULE,
15707            "c:2095 — second call overwrites with new fdt"
15708        );
15709        // Cleanup.
15710        unsafe {
15711            libc::close(fd);
15712        }
15713    }
15714
15715    /// `Src/utils.c:2093` — `if (fd >= 0)`. Negative fd is silently
15716    /// ignored (no fdtable update). Pin the guard.
15717    #[test]
15718    fn addmodulefd_ignores_negative_fd() {
15719        let _g = crate::test_util::global_state_lock();
15720        // Should not panic, no side effect.
15721        addmodulefd(-1, FDT_MODULE);
15722        // Nothing to assert on side-effect-free path; just verify no panic.
15723    }
15724
15725    /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns -1 for
15726    /// non-lock fds, 0 for FDT_FLOCK/FDT_FLOCK_EXEC. Previously the
15727    /// Rust port always returned 0 — broke `zsystem flock -u <fd>`
15728    /// distinguishing "fd never flocked" from "successfully released".
15729    #[cfg(unix)]
15730    #[test]
15731    fn zcloselockfd_returns_minus_one_for_non_lock_fd() {
15732        let _g = crate::test_util::global_state_lock();
15733        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15734        if fd < 0 {
15735            return;
15736        }
15737        // No addlockfd call → fd is NOT in the flock table.
15738        let r = zcloselockfd(fd);
15739        assert_eq!(r, -1, "c:2160-2161 — non-lock fd must return -1, NOT 0");
15740        unsafe {
15741            libc::close(fd);
15742        }
15743    }
15744
15745    /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns 0 for an
15746    /// fd that was registered via `addlockfd`. Pin the end-to-end
15747    /// round-trip: addlockfd → zcloselockfd returns 0.
15748    #[cfg(unix)]
15749    #[test]
15750    fn zcloselockfd_returns_zero_for_flock_fd_then_closes() {
15751        let _g = crate::test_util::global_state_lock();
15752        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15753        if fd < 0 {
15754            return;
15755        }
15756        addlockfd(fd, true);
15757        let r = zcloselockfd(fd);
15758        assert_eq!(r, 0, "c:2162-2163 — FDT_FLOCK fd → zclose + return 0");
15759        // After zcloselockfd, the underlying close() has fired.
15760        // close() returning -1 with EBADF would confirm that, but
15761        // that's a libc side-effect; just verify the return value.
15762    }
15763
15764    /// `Src/utils.c:1982` — `check_fd_table` sets `max_zsh_fd = fd`
15765    /// when fd is new. `fdtable_set` inlines this to keep the guard
15766    /// in `zcloselockfd` (`if (fd > max_zsh_fd) return -1`) working
15767    /// against fds populated by `addmodulefd`/`addlockfd`. Pin: after
15768    /// `fdtable_set(fd, FDT_FLOCK)`, MAX_ZSH_FD >= fd.
15769    #[cfg(unix)]
15770    #[test]
15771    fn fdtable_set_bumps_max_zsh_fd() {
15772        let _g = crate::test_util::global_state_lock();
15773        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15774        if fd < 0 {
15775            return;
15776        }
15777        fdtable_set(fd, FDT_FLOCK);
15778        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
15779        assert!(
15780            max_fd >= fd,
15781            "c:1982 — max_zsh_fd ({}) must be >= newly-set fd ({})",
15782            max_fd,
15783            fd
15784        );
15785        // Cleanup.
15786        fdtable_set(fd, FDT_UNUSED);
15787        unsafe {
15788            libc::close(fd);
15789        }
15790    }
15791
15792    /// `Src/utils.c:2111-2121` — `addlockfd(fd, cloexec)`. Selects
15793    /// between FDT_FLOCK (when cloexec=true, lock dies on exec) and
15794    /// FDT_FLOCK_EXEC (when cloexec=false, lock survives exec).
15795    /// Previously the Rust port did `fcntl(F_SETFD, CLOEXEC)` —
15796    /// totally wrong semantics. Pin the FDT slot.
15797    #[cfg(unix)]
15798    #[test]
15799    fn addlockfd_selects_flock_category_per_cloexec_flag() {
15800        let _g = crate::test_util::global_state_lock();
15801        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15802        if fd < 0 {
15803            return;
15804        }
15805        // cloexec=true → FDT_FLOCK (lock dies on exec).
15806        addlockfd(fd, true);
15807        assert_eq!(
15808            fdtable_get(fd),
15809            FDT_FLOCK,
15810            "c:2117 — cloexec=true → FDT_FLOCK"
15811        );
15812        // cloexec=false → FDT_FLOCK_EXEC (lock survives exec).
15813        addlockfd(fd, false);
15814        assert_eq!(
15815            fdtable_get(fd),
15816            FDT_FLOCK_EXEC,
15817            "c:2119 — cloexec=false → FDT_FLOCK_EXEC"
15818        );
15819        // Cleanup.
15820        unsafe {
15821            libc::close(fd);
15822        }
15823    }
15824
15825    /// `Src/utils.c:1211` — `adduserdir` rejects paths `>= PATH_MAX`
15826    /// by removing the existing entry (treating as "can't use this
15827    /// value as a directory"). Pin with a path constructed to exceed
15828    /// PATH_MAX, verify no insertion happens.
15829    #[cfg(unix)]
15830    #[test]
15831    fn adduserdir_rejects_paths_at_or_above_path_max() {
15832        let _g = crate::test_util::global_state_lock();
15833        if !interact() {
15834            // c:1193 — non-interactive shells skip the table entirely.
15835            // Test inactive in non-interactive contexts.
15836            return;
15837        }
15838        // Ensure the entry doesn't exist beforehand.
15839        let name = "ZSHRS_TEST_PATHMAX_DIR";
15840        let _ = removenameddirnode(name);
15841        // Construct an oversized path.
15842        let mut over: String = "/".to_string();
15843        over.push_str(&"a".repeat(libc::PATH_MAX as usize));
15844        // adduserdir with the oversized path + AUTONAMEDIRS-off path
15845        // via always=true (so the AUTONAMEDIRS guard doesn't reject).
15846        adduserdir(name, &over, 0, true);
15847        // c:1211 — too-long → remove path triggered, no entry added.
15848        let tab = nameddirtab().lock().unwrap();
15849        assert!(
15850            !tab.contains_key(name),
15851            "c:1211 — strlen(t) >= PATH_MAX must NOT insert entry"
15852        );
15853    }
15854
15855    /// `Src/utils.c:760-786` — `pathprog`. Finds any regular file
15856    /// (not directory, no executable-bit check) in `$PATH`.
15857    /// Previously required `mode & 0o111 != 0` — silently missed
15858    /// non-executable autoload-function plaintext scripts. Pin
15859    /// using a known-existing file in /tmp.
15860    #[test]
15861    #[cfg(unix)]
15862    fn pathprog_finds_non_executable_files() {
15863        let _g = crate::test_util::global_state_lock();
15864        // Create a non-executable file in /tmp and add /tmp to PATH.
15865        let test_name = format!("zshrs_test_pathprog_{}", unsafe { libc::getpid() });
15866        let path = PathBuf::from("/tmp").join(&test_name);
15867        // Write content, NO executable bit.
15868        if fs::write(&path, b"plain content").is_err() {
15869            return; // /tmp may be unwritable on some systems.
15870        }
15871        // Verify it's not executable.
15872        let meta = fs::metadata(&path).unwrap();
15873        assert_eq!(
15874            meta.permissions().mode() & 0o111,
15875            0,
15876            "test setup: must be non-executable"
15877        );
15878        // Set PATH to /tmp.
15879        let saved_path = getsparam("PATH");
15880        assignsparam("PATH", "/tmp", 0);
15881        // pathprog should find the file.
15882        let r = pathprog(&test_name);
15883        // Cleanup.
15884        let _ = fs::remove_file(&path);
15885        if let Some(prev) = saved_path {
15886            assignsparam("PATH", &prev, 0);
15887        }
15888        assert_eq!(
15889            r,
15890            Some(path),
15891            "c:776 — pathprog finds non-executable files (only F_OK + !S_ISDIR)"
15892        );
15893    }
15894
15895    /// `Src/utils.c:776-778` — pathprog skips directories. Pin: a
15896    /// directory in $PATH with the queried name must NOT be returned.
15897    #[test]
15898    #[cfg(unix)]
15899    fn pathprog_skips_directories() {
15900        let _g = crate::test_util::global_state_lock();
15901        let test_name = format!("zshrs_test_pathprog_dir_{}", unsafe { libc::getpid() });
15902        let path = PathBuf::from("/tmp").join(&test_name);
15903        if fs::create_dir(&path).is_err() {
15904            return;
15905        }
15906        let saved_path = getsparam("PATH");
15907        assignsparam("PATH", "/tmp", 0);
15908        let r = pathprog(&test_name);
15909        // Cleanup.
15910        let _ = fs::remove_dir(&path);
15911        if let Some(prev) = saved_path {
15912            assignsparam("PATH", &prev, 0);
15913        }
15914        assert!(
15915            r.is_none(),
15916            "c:778 — pathprog must skip directories (!S_ISDIR)"
15917        );
15918    }
15919
15920    /// `Src/utils.c:4367` — `wcsitype(c, ISEP)` reads from canonical
15921    /// `ifs` global. Pin via `ifssetfn`. ASCII path routes through
15922    /// TYPTAB which gets ISEP bit from inittyptab's IFS walk
15923    /// (added earlier this session). End-to-end pin.
15924    #[test]
15925    fn wcsitype_isep_reads_from_canonical_ifs_global() {
15926        let _g = crate::test_util::global_state_lock();
15927        if !isset(MULTIBYTE) {
15928            return;
15929        }
15930        // Snapshot the store `ifssetfn` writes: the `ifs` global itself
15931        // (`None` is an unset IFS, c:Src/params.c:4748). A paramtab read finds
15932        // no IFS node in a bare test process and yields "", and restoring that
15933        // left an EMPTY IFS behind, so every later typtab test saw no
15934        // separators.
15935        let saved = crate::ported::params::ifs_lock()
15936            .lock()
15937            .map(|g| g.clone())
15938            .unwrap_or_default();
15939        // ifssetfn ignores its `_pm` arg (the C `gsu.s->setfn` receives
15940        // the param but the IFS callback only touches the global `ifs`
15941        // store + calls inittyptab). Calling through paramtab's write
15942        // lock would deadlock because inittyptab() re-takes paramtab's
15943        // read lock for the IFS walk (utils.rs:5028) — pthread rwlock
15944        // forbids same-thread write→read upgrade. Pass a stack-local
15945        // dummy pm instead so we never hold the paramtab lock during
15946        // ifssetfn.
15947        let mut dummy = crate::ported::zsh_h::param::default();
15948        let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
15949            ifssetfn(dummy, val);
15950        };
15951        do_set(&mut dummy, ":".to_string());
15952        assert!(
15953            wcsitype(':', ISEP as u32),
15954            "c:4367 — IFS membership through canonical global"
15955        );
15956        // Restore.
15957        *crate::ported::params::ifs_lock().lock().expect("ifs poisoned") = saved;
15958        crate::ported::utils::inittyptab();
15959    }
15960
15961    /// c:536 — high-bit byte (>= 0x80) is nice when PRINTEIGHTBIT is
15962    /// OFF (the default). Pin the default case; the alternate
15963    /// behavior under PRINTEIGHTBIT-on is harder to exercise from
15964    /// a unit test due to opt-state global mutation.
15965    #[test]
15966    fn is_nicechar_highbit_is_nice_when_printeightbit_off() {
15967        let _g = crate::test_util::global_state_lock();
15968        // Default state: PRINTEIGHTBIT is off → high-bit bytes are nice.
15969        // Use char with high-bit equivalent low byte.
15970        // 0xb5 (Meta+5 territory) — masked to 0xff → still 0xb5.
15971        let c = char::from_u32(0xb5).unwrap();
15972        // ZISPRINT(0xb5) is false (>0x7e). 0xb5 & 0x80 set → check PRINTEIGHTBIT.
15973        // Default PRINTEIGHTBIT off → returns true (nice).
15974        if !isset(PRINTEIGHTBIT) {
15975            assert!(
15976                is_nicechar(c),
15977                "c:536 — high-bit byte 0x{:x} must be nice when PRINTEIGHTBIT off",
15978                0xb5_u32
15979            );
15980        }
15981    }
15982
15983    /// `Src/utils.c:1969-1983` — `check_fd_table(fd)` grows the
15984    /// fdtable so it can index `fd` and bumps `max_zsh_fd`. The
15985    /// previous Rust port was a no-op shim that always returned true;
15986    /// real behavior is "fdtable.len() must be > fd" and
15987    /// "MAX_ZSH_FD >= fd" after the call.
15988    #[test]
15989    fn check_fd_table_grows_fdtable_and_bumps_max_zsh_fd() {
15990        let _g = crate::test_util::global_state_lock();
15991        // Snapshot prior state so we don't poison other tests.
15992        let saved_max = MAX_ZSH_FD.load(Ordering::Relaxed);
15993        // Pick a target fd well above the typical 0/1/2.
15994        let target = saved_max.max(50) + 7;
15995        check_fd_table(target);
15996        let new_max = MAX_ZSH_FD.load(Ordering::Relaxed);
15997        assert!(
15998            new_max >= target,
15999            "c:1982 — max_zsh_fd must be >= target after grow (got {})",
16000            new_max
16001        );
16002        // Calling again with a lower fd MUST NOT shrink max_zsh_fd
16003        // (c:1971-1972 early return).
16004        check_fd_table(target - 3);
16005        assert_eq!(
16006            MAX_ZSH_FD.load(Ordering::Relaxed),
16007            new_max,
16008            "c:1971 — fd <= max_zsh_fd path must not change max"
16009        );
16010        // Restore approximately — best we can do without exposing
16011        // fdtable internals; subsequent fdtable_set callers cope.
16012        MAX_ZSH_FD.store(saved_max, Ordering::Relaxed);
16013    }
16014
16015    /// `Src/utils.c:1971` — `if (fd <= max_zsh_fd) return;` early
16016    /// exit. Calling with a small fd (<= current max) must be a
16017    /// no-op.
16018    #[test]
16019    fn check_fd_table_small_fd_is_noop() {
16020        let _g = crate::test_util::global_state_lock();
16021        // Ensure max_zsh_fd is non-trivial.
16022        let _ = check_fd_table(100);
16023        let max_before = MAX_ZSH_FD.load(Ordering::Relaxed);
16024        check_fd_table(5);
16025        assert_eq!(
16026            MAX_ZSH_FD.load(Ordering::Relaxed),
16027            max_before,
16028            "c:1971 — small fd path must not touch max_zsh_fd"
16029        );
16030    }
16031
16032    /// `Src/utils.c:1969-1983` — defensive: negative fd shouldn't
16033    /// panic. C-side would have indexed past the start of `fdtable`
16034    /// (UB). Rust port should fail-soft.
16035    #[test]
16036    fn check_fd_table_negative_fd_does_not_panic() {
16037        let _g = crate::test_util::global_state_lock();
16038        // Should not panic; behavior beyond "no panic" is unspecified.
16039        let _ = check_fd_table(-1);
16040        let _ = check_fd_table(-100);
16041    }
16042
16043    /// `Src/zsh.h:3274` — under MULTIBYTE_SUPPORT, the C source
16044    /// defines `nicezputs(str, outs)` as a macro:
16045    ///   `(void)mb_niceformat((str), (outs), NULL, 0)`.
16046    /// So Rust `nicezputs(s)` must equal `mb_niceformat(s)` for
16047    /// every input. Pins the macro-equivalence after fixing the
16048    /// previous `chars().map(nicechar)` impl (which corrupted
16049    /// non-ASCII multibyte codepoints into `\M-X` mangle).
16050    #[test]
16051    fn nicezputs_matches_mb_niceformat_under_multibyte() {
16052        let _g = crate::test_util::global_state_lock();
16053        // C-equivalent pattern: write to a stream, compare to
16054        // mb_niceformat outstrp-form output. Inline rather than via a
16055        // Rust-only helper.
16056        for input in ["hello", "a\nb", "é", ""] {
16057            let mut nz_buf: Vec<u8> = Vec::new();
16058            let _ = nicezputs(input, &mut nz_buf);
16059            let nz = String::from_utf8(nz_buf).expect("utf8");
16060
16061            let mut mb_out: Option<String> = None;
16062            let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
16063            assert_eq!(
16064                nz,
16065                mb_out.unwrap_or_default(),
16066                "nicezputs and mb_niceformat must agree for {:?}",
16067                input
16068            );
16069        }
16070        // \n must produce literal `\n` (backslash + n).
16071        let mut nz_buf: Vec<u8> = Vec::new();
16072        let _ = nicezputs("a\nb", &mut nz_buf);
16073        let nz = String::from_utf8(nz_buf).expect("utf8");
16074        assert!(nz.contains("\\n"), "nicechar emits `\\n`, not raw 0x0a");
16075    }
16076
16077    /// `Src/utils.c:5530` — under MULTIBYTE_SUPPORT, `nicedup(s, heap)`
16078    /// body is `(void)mb_niceformat(s, NULL, &retstr, …); return retstr;`
16079    /// so the returned string MUST equal mb_niceformat output.
16080    #[test]
16081    fn nicedup_matches_mb_niceformat_under_multibyte() {
16082        let _g = crate::test_util::global_state_lock();
16083        for input in ["hello", "a\nb", "é"] {
16084            let nd = nicedup(input, 0);
16085            let mut mb_out: Option<String> = None;
16086            let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
16087            assert_eq!(
16088                nd,
16089                mb_out.unwrap_or_default(),
16090                "nicedup must equal mb_niceformat outstrp form for {:?}",
16091                input
16092            );
16093        }
16094        // nicedupstring delegates to nicedup(s, 1).
16095        assert_eq!(nicedupstring("hé\nllo"), nicedup("hé\nllo", 1));
16096    }
16097
16098    /// `Src/utils.c:734-744` — `zwcwidth(wc)` returns 1 when MULTIBYTE
16099    /// option is unset (c:738), regardless of the codepoint's actual
16100    /// display width. The previous Rust port skipped the option gate
16101    /// and always used the Unicode width table. Pin the option-gated
16102    /// behavior end-to-end: unset MULTIBYTE → width 1 for CJK; set →
16103    /// width 2 for CJK.
16104    #[test]
16105    fn zwcwidth_returns_1_when_multibyte_unset() {
16106        let _g = crate::test_util::global_state_lock();
16107        let saved = isset(MULTIBYTE);
16108        // c:738 — unset(MULTIBYTE) path returns 1 for everything.
16109        dosetopt(MULTIBYTE, 0, 1);
16110        assert_eq!(zwcwidth('a'), 1, "c:738 — ASCII width 1 under nomultibyte");
16111        assert_eq!(
16112            zwcwidth('字'),
16113            1,
16114            "c:738 — CJK collapses to 1 under nomultibyte (was 2 via Unicode-width)"
16115        );
16116        assert_eq!(
16117            zwcwidth('\u{200B}'),
16118            1,
16119            "c:738 — zero-width space → 1 under nomultibyte (was 0 via Unicode-width)"
16120        );
16121        // c:740-744 — MULTIBYTE on: Unicode-width table applies.
16122        dosetopt(MULTIBYTE, 1, 1);
16123        assert_eq!(zwcwidth('a'), 1, "ASCII width is 1");
16124        assert_eq!(zwcwidth('字'), 2, "c:740 — CJK width 2 under multibyte");
16125        // Restore prior state.
16126        dosetopt(MULTIBYTE, if saved { 1 } else { 0 }, 1);
16127    }
16128
16129    /// `Src/utils.c:5613` — `if (!isset(MULTIBYTE) || (unsigned char) *s <=
16130    /// 0x7f)` returns a SINGLE byte. The port had only the multibyte arm, so
16131    /// every walk built on this function kept stepping whole characters in a
16132    /// mode where zsh steps bytes — `${gr[2]}`, `${(s::)v}` and `$(( ##c ))`
16133    /// all read the wrong unit.
16134    ///
16135    /// Both the length and the returned unit are asserted: a version that
16136    /// fixed the length but re-encoded the unit as UTF-8 would still emit
16137    /// `c3 8e` where zsh emits the single byte `ce`.
16138    #[test]
16139    fn mb_metacharlenconv_steps_bytes_when_multibyte_unset() {
16140        let _g = crate::test_util::global_state_lock();
16141        let saved = isset(MULTIBYTE);
16142        // `α` is ce b1 — one character, two bytes.
16143        let alpha: &[u8] = &[0xce, 0xb1];
16144
16145        dosetopt(MULTIBYTE, 1, 1);
16146        let (n, wc, unit) = mb_metacharlenconv(alpha);
16147        assert_eq!(n, 2, "c:5635 — multibyte on: the whole character");
16148        assert_eq!(wc, Some('α'));
16149        assert_eq!(unit.as_bytes(), alpha, "the unit is the character verbatim");
16150
16151        dosetopt(MULTIBYTE, 0, 1);
16152        let (n, wc, unit) = mb_metacharlenconv(alpha);
16153        assert_eq!(n, 1, "c:5613 — multibyte off: one byte");
16154        assert_eq!(wc, Some('\u{ce}'), "c:5616 — the byte value is the scalar");
16155        assert_eq!(
16156            crate::ported::utils::unmetafy_str(&unit),
16157            vec![0xceu8],
16158            "the unit must decode back to the single byte 0xce, not to U+00CE's \
16159             UTF-8 (c3 8e) — half a character cannot be held verbatim"
16160        );
16161
16162        // ASCII takes the same one-byte path in either mode (c:5613).
16163        dosetopt(MULTIBYTE, 1, 1);
16164        assert_eq!(mb_metacharlenconv(b"abc").0, 1);
16165        dosetopt(MULTIBYTE, 0, 1);
16166        assert_eq!(mb_metacharlenconv(b"abc").0, 1);
16167
16168        dosetopt(MULTIBYTE, if saved { 1 } else { 0 }, 1);
16169    }
16170
16171    /// `Src/utils.c:6478-6492` — `quotedzputs(s, NULL)` under
16172    /// MULTIBYTE_SUPPORT detects "needs nice-format" inputs via
16173    /// `is_mb_niceformat(s)` and emits `$'<nice-formatted body>'`
16174    /// (the dollar-quoted form so embedded `\n`/`\t`/escape sequences
16175    /// round-trip through `typeset -p`/`set`/`set -x`). The previous
16176    /// Rust port skipped this branch entirely and single-quoted such
16177    /// strings — POSIX `'…'` is *strong* (no escapes interpreted),
16178    /// breaking round-trip for control bytes.
16179    #[test]
16180    fn quotedzputs_uses_dollar_quotes_for_control_chars() {
16181        let _g = crate::test_util::global_state_lock();
16182        // Ensure typtab is initialised — hasspecial depends on ISPECIAL
16183        // bits which are populated by inittyptab. Without this the
16184        // SPECCHARS arm short-circuits to "no specials".
16185        inittyptab();
16186        // Empty stays `''`.
16187        assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
16188        // Plain alphanumeric: no specials, no controls → bare.
16189        assert_eq!(
16190            quotedzputs("hello"),
16191            "hello",
16192            "c:6511-6517 — no SPECCHARS member → return unchanged"
16193        );
16194        // Control char `\n` needs `$'…'` to round-trip.
16195        let r = quotedzputs("a\nb");
16196        assert!(
16197            r.starts_with("$'") && r.ends_with('\''),
16198            "c:6488 — control char must use $'…' form (got {:?})",
16199            r
16200        );
16201        // Tab and ESC also force the niceformat arm.
16202        let r = quotedzputs("\t");
16203        assert!(
16204            r.starts_with("$'"),
16205            "c:6488 — TAB forces $'…' arm (got {:?})",
16206            r
16207        );
16208        let r = quotedzputs("\u{1b}[31m");
16209        assert!(
16210            r.starts_with("$'"),
16211            "c:6488 — ESC sequence forces $'…' arm (got {:?})",
16212            r
16213        );
16214        // Single quote forces single-quote arm via SPECCHARS membership
16215        // (Src/zsh.h:228 — SPECCHARS includes `'`). Embedded `'`
16216        // rewrites to `'\''`.
16217        let r = quotedzputs("a'b");
16218        assert!(
16219            r.contains("'\\''"),
16220            "c:6573-6587 — embedded ' → '\\''  (got {:?})",
16221            r
16222        );
16223    }
16224
16225    /// `Src/utils.c:2923-2945` — `read_loop` returns the requested
16226    /// length on full read, or the partial count on EOF. Pin: writing
16227    /// a known buffer to a pipe and reading it back returns the same
16228    /// content + correct length. Drives the no-side-effect path that
16229    /// the diagnostic-message fix doesn't touch.
16230    #[test]
16231    #[cfg(unix)]
16232    fn read_loop_round_trips_pipe_bytes() {
16233        let _g = crate::test_util::global_state_lock();
16234        // Create a pipe; write 16 bytes; read them back.
16235        let mut fds: [libc::c_int; 2] = [0; 2];
16236        unsafe {
16237            assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
16238        }
16239        let payload = b"hello-world-1234";
16240        let written = unsafe {
16241            libc::write(
16242                fds[1],
16243                payload.as_ptr() as *const libc::c_void,
16244                payload.len(),
16245            )
16246        };
16247        assert_eq!(written, payload.len() as isize, "write all 16 bytes");
16248        unsafe {
16249            libc::close(fds[1]);
16250        }
16251        let mut buf = [0u8; 16];
16252        let got = read_loop(fds[0], &mut buf).expect("read_loop ok");
16253        assert_eq!(got, payload.len(), "c:2929 — read_loop returns full length");
16254        assert_eq!(
16255            &buf[..],
16256            &payload[..],
16257            "c:2940-2941 — buffer copied verbatim"
16258        );
16259        unsafe {
16260            libc::close(fds[0]);
16261        }
16262    }
16263
16264    /// `Src/utils.c:2949-2970` — `write_loop` returns the requested
16265    /// length when the kernel accepts all bytes (the common case for
16266    /// a pipe with room). Pin the no-side-effect happy path.
16267    #[test]
16268    #[cfg(unix)]
16269    fn write_loop_writes_all_bytes_to_pipe() {
16270        let _g = crate::test_util::global_state_lock();
16271        let mut fds: [libc::c_int; 2] = [0; 2];
16272        unsafe {
16273            assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
16274        }
16275        let payload = b"abcdef";
16276        let got = write_loop(fds[1], payload).expect("write_loop ok");
16277        assert_eq!(
16278            got,
16279            payload.len(),
16280            "c:2955-2956 — write_loop returns full length on accept"
16281        );
16282        // Read back to verify.
16283        unsafe {
16284            libc::close(fds[1]);
16285        }
16286        let mut buf = [0u8; 6];
16287        let _ = unsafe { libc::read(fds[0], buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
16288        assert_eq!(&buf[..], &payload[..]);
16289        unsafe {
16290            libc::close(fds[0]);
16291        }
16292    }
16293
16294    /// `Src/utils.c:2935-2936` — `read_loop` on a closed/invalid fd
16295    /// returns an io::Error (the C path returns `ret` and emits the
16296    /// `zwarn` to stderr). Pin the error propagation; the zwarn
16297    /// emission is a stderr side-effect tested only by inspecting
16298    /// log output (out of scope here).
16299    #[test]
16300    #[cfg(unix)]
16301    fn read_loop_returns_error_on_invalid_fd() {
16302        let _g = crate::test_util::global_state_lock();
16303        let mut buf = [0u8; 4];
16304        // fd 9999 is essentially guaranteed-not-open in a test
16305        // process; the canonical "bad fd" error path.
16306        let r = read_loop(9999, &mut buf);
16307        assert!(
16308            r.is_err(),
16309            "c:2935 — invalid fd → io::Error (and zwarn to stderr)"
16310        );
16311    }
16312
16313    /// `Src/utils.c:2972-2988` — `read1char(echo)` reads from SHTTY,
16314    /// not stdin. With SHTTY uninitialised (default -1 in test
16315    /// environment), the function MUST return -1 immediately rather
16316    /// than blocking on a stdin read. The previous Rust port read
16317    /// from stdin and returned None — which in some test runners
16318    /// would block waiting for input.
16319    #[test]
16320    #[cfg(unix)]
16321    fn read1char_returns_minus_one_when_shtty_unset() {
16322        let _g = crate::test_util::global_state_lock();
16323        // Test environment: SHTTY is -1 (no controlling tty bound
16324        // by the port). C-side would `read(-1, ...)` which fails;
16325        // Rust port should fail-fast with -1.
16326        let saved = SHTTY.load(Ordering::Relaxed);
16327        SHTTY.store(-1, Ordering::Relaxed);
16328        let got = read1char(0); // echo=0
16329        assert_eq!(got, -1, "c:2978 — SHTTY=-1 → read fails → return -1");
16330        // Restore.
16331        SHTTY.store(saved, Ordering::Relaxed);
16332    }
16333
16334    /// `Src/utils.c:1989-2012` — `movefd(fd)` dups fd to >= 10 (so it
16335    /// stays out of the user fd range 0..=9), closes the original,
16336    /// AND marks the new fd as `FDT_INTERNAL` in fdtable. Previous
16337    /// Rust port omitted the fdtable_set call — internal-fd tracking
16338    /// (`closeallelse`, forkexec) silently never saw zshrs-internal
16339    /// fds. Pin: after movefd, fdtable[new_fd] == FDT_INTERNAL.
16340    #[test]
16341    #[cfg(unix)]
16342    fn movefd_marks_fdtable_internal() {
16343        let _g = crate::test_util::global_state_lock();
16344        // Open /dev/null to get a small (< 10) fd...
16345        let dev_null = CString::new("/dev/null").unwrap();
16346        let fd = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16347        assert!(fd >= 0, "open /dev/null returned -1");
16348        let new_fd = movefd(fd);
16349        assert!(
16350            new_fd >= 10,
16351            "c:1992-1994 — movefd dups to fd >= 10 (got {})",
16352            new_fd
16353        );
16354        // c:2009 — fdtable[new_fd] := FDT_INTERNAL.
16355        let entry = fdtable_get(new_fd);
16356        assert_eq!(
16357            entry, FDT_INTERNAL,
16358            "c:2009 — movefd must mark new_fd as FDT_INTERNAL (got {})",
16359            entry
16360        );
16361        unsafe {
16362            libc::close(new_fd);
16363        }
16364    }
16365
16366    /// `Src/utils.c:1989` — `movefd(-1)` is a defensive no-op: the
16367    /// `fd != -1 && fd < 10` gate at c:1992 fails, the c:2007
16368    /// `if (fd != -1)` post-check skips, and -1 is returned
16369    /// unchanged. Pin so a refactor of the early-out doesn't
16370    /// accidentally call fcntl(-1, ...).
16371    #[test]
16372    #[cfg(unix)]
16373    fn movefd_minus_one_returns_minus_one() {
16374        let _g = crate::test_util::global_state_lock();
16375        assert_eq!(
16376            movefd(-1),
16377            -1,
16378            "c:1992 — movefd(-1) bypasses both gates and returns -1"
16379        );
16380    }
16381
16382    /// `Src/utils.c:2021-2068` — `redup(x, y)` after a successful
16383    /// `dup2(x, y)` must:
16384    ///   * Copy fdtable[x] to fdtable[y] (c:2054).
16385    ///   * Promote FDT_FLOCK/FDT_FLOCK_EXEC to FDT_INTERNAL on the
16386    ///     dup target (c:2055-2056) — the lock doesn't transfer.
16387    ///   * Then close x (c:2064).
16388    ///
16389    /// Previous Rust port skipped the fdtable updates entirely.
16390    /// Pin the ownership-transfer with two fds the test allocates.
16391    #[test]
16392    #[cfg(unix)]
16393    fn redup_copies_fdtable_ownership_to_target() {
16394        let _g = crate::test_util::global_state_lock();
16395        // Open two distinct fds — both will land in the fdtable.
16396        let dev_null = CString::new("/dev/null").unwrap();
16397        let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16398        let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16399        assert!(x >= 0 && y >= 0, "open /dev/null returned -1");
16400        assert_ne!(x, y);
16401
16402        // Mark x as FDT_INTERNAL so we can observe the copy to y.
16403        check_fd_table(x);
16404        check_fd_table(y);
16405        fdtable_set(x, FDT_INTERNAL);
16406        fdtable_set(y, FDT_UNUSED);
16407
16408        let ret = redup(x, y);
16409        assert_eq!(ret, y, "c:2067 — successful redup returns y");
16410        // c:2054 — fdtable[y] inherited from fdtable[x].
16411        assert_eq!(
16412            fdtable_get(y),
16413            FDT_INTERNAL,
16414            "c:2054 — fdtable[y] = fdtable[x] (FDT_INTERNAL)"
16415        );
16416        // x is closed by c:2064.
16417        unsafe {
16418            libc::close(y);
16419        }
16420    }
16421
16422    /// `Src/utils.c:872-908` — `xsymlinks(path)` resolves `.`/`..`
16423    /// AND follows ONE LEVEL of symlinks via `readlink(2)`. Pin the
16424    /// symlink-following behavior with a temp-dir-managed symlink
16425    /// (the actual bug-fix pin — previous Rust port just normalised
16426    /// `.`/`..` without ever calling readlink).
16427    #[test]
16428    #[cfg(unix)]
16429    fn xsymlinks_follows_one_level_of_symlinks() {
16430        let _g = crate::test_util::global_state_lock();
16431        let tmp = std::env::temp_dir();
16432        let target = tmp.join(format!("zshrs_xsymlinks_target_{}", std::process::id()));
16433        let link = tmp.join(format!("zshrs_xsymlinks_link_{}", std::process::id()));
16434        // Create target as a regular dir to symlink to.
16435        let _ = fs::create_dir(&target);
16436        let _ = fs::remove_file(&link);
16437        std::os::unix::fs::symlink(&target, &link).unwrap();
16438
16439        let got = xsymlinks(link.to_str().unwrap()).unwrap();
16440        assert_eq!(
16441            got,
16442            target.to_string_lossy(),
16443            "c:908 — xsymlinks must follow the symlink to its target"
16444        );
16445
16446        // Cleanup.
16447        let _ = fs::remove_file(&link);
16448        let _ = fs::remove_dir(&target);
16449    }
16450
16451    /// `Src/utils.c:881-882` — `.` components are skipped.
16452    /// `Src/utils.c:883-896` — `..` components walk back one
16453    /// `/`-segment of xbuf (unless xbuf is empty or `/`).
16454    ///
16455    /// Use a temp directory we create ourselves — we can't pin
16456    /// `/tmp/...` literally because the macOS sandbox symlinks
16457    /// `/tmp -> /private/tmp`, which `xsymlinks` correctly follows
16458    /// (proving the bug-fix works). Test with a directory under
16459    /// the env tempdir + a non-existent sub so readlink fails on
16460    /// every component and the test exercises ONLY the c:881-896
16461    /// `.` / `..` paths.
16462    #[test]
16463    #[cfg(unix)]
16464    fn xsymlinks_normalises_dot_and_dotdot() {
16465        let _g = crate::test_util::global_state_lock();
16466        let tmp = fs::canonicalize(std::env::temp_dir()).unwrap();
16467        let base_dir = tmp.join(format!("zshrs_xs_norm_{}", std::process::id()));
16468        let _ = fs::create_dir(&base_dir);
16469        // `<base>/.` should be `<base>` (c:881 — `.` skipped).
16470        let arg = format!("{}/./.", base_dir.display());
16471        let got = xsymlinks(&arg).unwrap();
16472        assert_eq!(
16473            got,
16474            base_dir.to_string_lossy(),
16475            "c:881 — `.` segments collapse"
16476        );
16477        // `<base>/foo/..` should be `<base>` (c:891-895 — `..` walks back).
16478        let arg = format!("{}/foo/..", base_dir.display());
16479        let got = xsymlinks(&arg).unwrap();
16480        assert_eq!(
16481            got,
16482            base_dir.to_string_lossy(),
16483            "c:891-895 — `..` walks back one segment"
16484        );
16485        let _ = fs::remove_dir(&base_dir);
16486    }
16487
16488    /// `Src/utils.c:2055-2056` — when fdtable[x] is FDT_FLOCK or
16489    /// FDT_FLOCK_EXEC, the dup'd fd y gets promoted to FDT_INTERNAL
16490    /// (the dup doesn't carry the flock). Pin the promotion.
16491    ///
16492    /// Note on fdtable_flocks: C's c:2062-2063 decrements the
16493    /// flock count in `redup` BEFORE `zclose(x)`, and zclose ALSO
16494    /// decrements when it sees fdtable[fd] == FDT_FLOCK (c:2135).
16495    /// So C double-decrements for an FDT_FLOCK source fd in redup —
16496    /// the C comment at c:2058-2061 calls this "isn't expected to
16497    /// happen" (FDT_FLOCK fds aren't normally redup'd). We mirror
16498    /// C exactly: test asserts the count drops by 2 (1 → -1) to
16499    /// pin the faithful-to-C behavior. Reporting this as a C bug
16500    /// upstream is the right path; the port preserves it verbatim.
16501    #[test]
16502    #[cfg(unix)]
16503    fn redup_promotes_flock_to_internal_on_target() {
16504        let _g = crate::test_util::global_state_lock();
16505        let dev_null = CString::new("/dev/null").unwrap();
16506        let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16507        let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16508        assert!(x >= 0 && y >= 0);
16509        assert_ne!(x, y);
16510
16511        check_fd_table(x);
16512        check_fd_table(y);
16513        // Mark x as FDT_FLOCK.
16514        fdtable_set(x, FDT_FLOCK);
16515        FDTABLE_FLOCKS.store(2, Ordering::SeqCst); // start at 2 so double-decrement lands at 0
16516
16517        let _ = redup(x, y);
16518
16519        // c:2055-2056 — promoted to FDT_INTERNAL on y, NOT carried.
16520        assert_eq!(
16521            fdtable_get(y),
16522            FDT_INTERNAL,
16523            "c:2055-2056 — FDT_FLOCK on x promotes to FDT_INTERNAL on y"
16524        );
16525        // c:2062-2063 + zclose c:2135 — double decrement.
16526        assert_eq!(
16527            FDTABLE_FLOCKS.load(Ordering::SeqCst),
16528            0,
16529            "c:2062-2063 + c:2135 — flock count double-decremented (faithful to C)"
16530        );
16531        unsafe {
16532            libc::close(y);
16533        }
16534    }
16535
16536    /// `Src/utils.c:5217-5240` — `zreaddir(dir, ignoredots)` exposes
16537    /// the dot-filter as a parameter and returns one entry per call.
16538    /// Two paths:
16539    /// * `ignoredots=1` (the common case at c:590/655/1653/2884) —
16540    ///   `.` and `..` are filtered out.
16541    /// * `ignoredots=0` (used at c:4648 for spelling correction) —
16542    ///   `.` and `..` are RETAINED as valid candidates.
16543    #[test]
16544    #[cfg(unix)]
16545    fn zreaddir_honors_ignoredots_flag() {
16546        let _g = crate::test_util::global_state_lock();
16547        let tmp = std::env::temp_dir();
16548        let base = tmp.join(format!("zshrs_zreaddir_{}", std::process::id()));
16549        let _ = fs::create_dir(&base);
16550        // Create one real entry to make the test non-trivial.
16551        fs::write(base.join("file"), "x").unwrap();
16552
16553        // c:5232 — ignoredots=1: skip `.` and `..`, keep `file`.
16554        let mut dir = fs::read_dir(&base).unwrap();
16555        let mut with_skip: Vec<String> = Vec::new();
16556        while let Some(n) = zreaddir(&mut dir, 1) {
16557            with_skip.push(n);
16558        }
16559        assert!(
16560            with_skip.contains(&"file".to_string()),
16561            "c:5232 — real entry survives ignoredots=1"
16562        );
16563        assert!(
16564            !with_skip.contains(&".".to_string()),
16565            "c:5232 — `.` filtered with ignoredots=1"
16566        );
16567        assert!(
16568            !with_skip.contains(&"..".to_string()),
16569            "c:5232 — `..` filtered with ignoredots=1"
16570        );
16571
16572        // c:4648-equivalent — ignoredots=0: KEEP `.` and `..`.
16573        // (libstd's fs::read_dir filters them before exposing on
16574        // macOS/Linux, so the without_skip set is functionally
16575        // identical here — pin only that the API path doesn't error.)
16576        let mut dir2 = fs::read_dir(&base).unwrap();
16577        let mut without_skip: Vec<String> = Vec::new();
16578        while let Some(n) = zreaddir(&mut dir2, 0) {
16579            without_skip.push(n);
16580        }
16581        assert!(
16582            without_skip.contains(&"file".to_string()),
16583            "ignoredots=0 still yields real entries"
16584        );
16585
16586        let _ = fs::remove_file(base.join("file"));
16587        let _ = fs::remove_dir(&base);
16588    }
16589
16590    /// `Src/utils.c:1075` — `get_username()` uses `ztrdup_metafy` on
16591    /// `pswd->pw_name`. Previous Rust port returned the raw pw_name
16592    /// verbatim — fine for ASCII usernames, broken for high-bit
16593    /// bytes (downstream paramtab consumers assume metafied entries).
16594    /// Pin: ASCII usernames round-trip identically through metafy,
16595    /// AND the result is a non-empty string for the current uid.
16596    /// `Src/utils.c:3445-3460` — ztrftime zsh-specific %K/%L/%f
16597    /// extensions return values WITHOUT leading zeros (vs the
16598    /// strftime %H/%I/%d which pad).
16599    #[test]
16600    #[cfg(unix)]
16601    fn ztrftime_zsh_extensions_no_leading_zero() {
16602        let _g = crate::test_util::global_state_lock();
16603        // Pick a known time: Jan 5 2024 09:07:42.123456789 UTC.
16604        // Use SystemTime + an offset since we want deterministic values
16605        // independent of TZ; we just verify that the format substitutes
16606        // the right NUMBER OF DIGITS for the leading-zero case.
16607        use std::time::Duration;
16608        // 2024-01-05 09:07:42 UTC = 1704445662
16609        let t = UNIX_EPOCH + Duration::new(1704445662, 123_456_789);
16610        // %H gives leading zero "09"; %K should give "9" (in some TZ
16611        // hour will be different but the digit-count rule still holds).
16612        let h_padded = ztrftime("%H", t, false);
16613        let k_unpadded = ztrftime("%K", t, false);
16614        // Both represent the same hour. If h_padded starts with `0`,
16615        // k_unpadded must be 1-char and not start with `0`.
16616        if h_padded.starts_with('0') && h_padded.len() == 2 {
16617            assert!(
16618                !k_unpadded.starts_with('0'),
16619                "c:3445 — %K must strip the leading 0 from %H={}, got %K={}",
16620                h_padded,
16621                k_unpadded
16622            );
16623            assert_eq!(
16624                k_unpadded.len(),
16625                1,
16626                "c:3445 — %K should be 1 digit when hour < 10"
16627            );
16628        }
16629        // %f for day-of-month: t is Jan 5 → in any reasonable TZ
16630        // day is between 4 and 6; format should be 1 digit when day < 10.
16631        let d_padded = ztrftime("%d", t, false);
16632        let f_unpadded = ztrftime("%f", t, false);
16633        if d_padded.starts_with('0') && d_padded.len() == 2 {
16634            assert!(!f_unpadded.starts_with('0'));
16635            assert_eq!(
16636                f_unpadded.len(),
16637                1,
16638                "c:3457 — %f should be 1 digit when day < 10"
16639            );
16640        }
16641        // %3. fractional seconds: input nsec is 123456789 → first 3
16642        // digits should be 123. Note zsh's syntax is `%N.` where N is
16643        // the digit count (c:3374-3384 + c:3409).
16644        let frac = ztrftime("%3.", t, false);
16645        assert_eq!(
16646            frac, "123",
16647            "c:3409-3438 — %3. must emit first 3 digits of nsec"
16648        );
16649        // %. with no digit prefix defaults to 3 digits per c:3409.
16650        let frac_default = ztrftime("%.", t, false);
16651        assert_eq!(frac_default, "123", "c:3409 — %. defaults to 3 digits");
16652    }
16653
16654    #[test]
16655    #[cfg(unix)]
16656    fn get_username_returns_metafied_non_empty_string() {
16657        let _g = crate::test_util::global_state_lock();
16658        let name = get_username();
16659        // CI environments may have a username from `whoami(1)`. In
16660        // weird sandbox-only setups it might be empty, but on every
16661        // standard build it should be set.
16662        if name.is_empty() {
16663            return;
16664        }
16665        // ASCII round-trip via metafy is identity. If the username
16666        // had high-bit bytes (the bug case), the output would be
16667        // Meta-escaped — both forms are non-empty so this pin
16668        // doesn't reject either, just ensures the fn returns
16669        // SOMETHING usable (not an empty string).
16670        assert!(
16671            !name.is_empty(),
16672            "c:1086 — getpwuid result must yield a non-empty username"
16673        );
16674        // Sanity: metafy(name) == name for ASCII input (every byte
16675        // is below Meta range). Confirms the c:1086 step preserves
16676        // ASCII paths byte-for-byte.
16677        if name.bytes().all(|b| b < 0x80) {
16678            assert_eq!(
16679                metafy(&name),
16680                name,
16681                "c:1086 — ASCII metafy is identity (so pin holds for ASCII users)"
16682            );
16683        }
16684    }
16685
16686    // ═══════════════════════════════════════════════════════════════════
16687    // Pure-utility tests — zstrtol/zstrtoul (numeric parse) and
16688    // getkeystring (key-escape decode). Each test pinned against either
16689    // C zsh semantics or a direct zsh shell invocation where applicable.
16690    // ═══════════════════════════════════════════════════════════════════
16691
16692    // ── zstrtol: numeric string → (value, unconsumed_tail) ──────────
16693    /// `zstrtol("42", 10)` → (42, "") — full consumption.
16694    #[test]
16695    fn zstrtol_decimal_full_consumption() {
16696        let (v, t) = zstrtol("42", 10);
16697        assert_eq!(v, 42);
16698        assert_eq!(t, "");
16699    }
16700
16701    /// `zstrtol("42abc", 10)` → (42, "abc") — stops at first non-digit.
16702    #[test]
16703    fn zstrtol_decimal_stops_at_non_digit() {
16704        let (v, t) = zstrtol("42abc", 10);
16705        assert_eq!(v, 42);
16706        assert_eq!(t, "abc");
16707    }
16708
16709    /// `zstrtol("-7", 10)` → (-7, "") — sign handling.
16710    #[test]
16711    fn zstrtol_negative_decimal() {
16712        let (v, t) = zstrtol("-7", 10);
16713        assert_eq!(v, -7);
16714        assert_eq!(t, "");
16715    }
16716
16717    /// `zstrtol("+12", 10)` → (12, "") — explicit `+` sign.
16718    #[test]
16719    fn zstrtol_explicit_plus_sign_consumed() {
16720        let (v, t) = zstrtol("+12", 10);
16721        assert_eq!(v, 12);
16722        assert_eq!(t, "");
16723    }
16724
16725    /// `zstrtol("ff", 16)` → (255, "") — hex base.
16726    #[test]
16727    fn zstrtol_hex_base_16() {
16728        let (v, t) = zstrtol("ff", 16);
16729        assert_eq!(v, 255);
16730        assert_eq!(t, "");
16731    }
16732
16733    /// `zstrtol("FF", 16)` → (255, "") — case-insensitive hex.
16734    #[test]
16735    fn zstrtol_hex_uppercase() {
16736        let (v, t) = zstrtol("FF", 16);
16737        assert_eq!(v, 255);
16738    }
16739
16740    /// `zstrtol("1010", 2)` → (10, "") — binary base.
16741    #[test]
16742    fn zstrtol_binary_base_2() {
16743        let (v, t) = zstrtol("1010", 2);
16744        assert_eq!(v, 10);
16745        assert_eq!(t, "");
16746    }
16747
16748    /// `zstrtol("17", 8)` → (15, "") — octal base.
16749    #[test]
16750    fn zstrtol_octal_base_8() {
16751        let (v, t) = zstrtol("17", 8);
16752        assert_eq!(v, 15);
16753    }
16754
16755    /// `zstrtol("0x1A", 0)` → base-detect picks hex from `0x` prefix → 26.
16756    #[test]
16757    fn zstrtol_base_zero_detects_hex_prefix() {
16758        let (v, _) = zstrtol("0x1A", 0);
16759        assert_eq!(v, 26, "0x1A with base=0 → hex 26");
16760    }
16761
16762    /// `zstrtol("0b101", 0)` → base-detect picks binary → 5.
16763    #[test]
16764    fn zstrtol_base_zero_detects_binary_prefix() {
16765        let (v, _) = zstrtol("0b101", 0);
16766        assert_eq!(v, 5, "0b101 with base=0 → binary 5");
16767    }
16768
16769    /// `zstrtol("017", 0)` → base-detect: leading `0` → octal → 15.
16770    #[test]
16771    fn zstrtol_base_zero_leading_zero_means_octal() {
16772        let (v, _) = zstrtol("017", 0);
16773        assert_eq!(v, 15, "017 with base=0 → octal 15");
16774    }
16775
16776    /// `zstrtol("  42", 10)` → (42, "") — leading whitespace skipped.
16777    #[test]
16778    fn zstrtol_leading_whitespace_skipped() {
16779        let (v, _) = zstrtol("  42", 10);
16780        assert_eq!(v, 42);
16781    }
16782
16783    /// `zstrtol("0", 10)` → (0, "") — zero is valid input.
16784    #[test]
16785    fn zstrtol_zero_input() {
16786        let (v, t) = zstrtol("0", 10);
16787        assert_eq!(v, 0);
16788        assert_eq!(t, "");
16789    }
16790
16791    // ── zstrtol_underscore: digit-separator support ─────────────────
16792    /// `zstrtol_underscore("1_000_000", 10, true)` → (1_000_000, "")
16793    /// — underscores skipped during digit accumulation.
16794    #[test]
16795    fn zstrtol_underscore_separator_in_decimal() {
16796        let (v, _) = zstrtol_underscore("1_000_000", 10, true);
16797        assert_eq!(v, 1_000_000);
16798    }
16799
16800    /// Without underscore flag, `_` stops parsing.
16801    #[test]
16802    fn zstrtol_underscore_disabled_stops_at_underscore() {
16803        let (v, t) = zstrtol_underscore("123_456", 10, false);
16804        assert_eq!(v, 123);
16805        assert_eq!(t, "_456");
16806    }
16807
16808    // ── zstrtoul_underscore: unsigned variant ───────────────────────
16809    /// Parses a plain decimal unsigned.
16810    #[test]
16811    fn zstrtoul_underscore_basic_decimal() {
16812        let v = zstrtoul_underscore("12345");
16813        assert_eq!(v, Some(12345));
16814    }
16815
16816    /// Empty string → None (no number to parse).
16817    #[test]
16818    fn zstrtoul_underscore_empty_returns_none() {
16819        let v = zstrtoul_underscore("");
16820        assert_eq!(v, None);
16821    }
16822
16823    // ── getkeystring: shell-escape decode ───────────────────────────
16824    /// `\n` → newline, `\t` → tab, `\r` → CR.
16825    /// Anchor: `print -r -- $'\n\t\r'` produces the three bytes.
16826    #[test]
16827    fn getkeystring_decodes_common_escapes() {
16828        assert_eq!(getkeystring("\\n").0, "\n");
16829        assert_eq!(getkeystring("\\t").0, "\t");
16830        assert_eq!(getkeystring("\\r").0, "\r");
16831    }
16832
16833    /// `\\` → single backslash.
16834    #[test]
16835    fn getkeystring_double_backslash_yields_single() {
16836        assert_eq!(getkeystring("\\\\").0, "\\");
16837    }
16838
16839    /// `\xNN` hex escape → byte value.
16840    /// Anchor: `print -r -- $'\x41'` → "A" (0x41 = 65 = 'A').
16841    #[test]
16842    fn getkeystring_hex_escape_lowercase_x() {
16843        assert_eq!(getkeystring("\\x41").0, "A");
16844        assert_eq!(getkeystring("\\x7E").0, "~");
16845    }
16846
16847    /// `\u{NNNN}` Unicode escape → corresponding char.
16848    /// Anchor: `print -r -- $'é'` → "é".
16849    #[test]
16850    fn getkeystring_unicode_escape_u_four_digits() {
16851        assert_eq!(getkeystring("\\u00E9").0, "é");
16852    }
16853
16854    /// Plain text passes through unchanged.
16855    #[test]
16856    fn getkeystring_plain_text_passes_through() {
16857        assert_eq!(getkeystring("hello").0, "hello");
16858        assert_eq!(getkeystring("").0, "");
16859    }
16860
16861    /// Mixed plain + escapes → both handled.
16862    #[test]
16863    fn getkeystring_mixed_text_and_escapes() {
16864        assert_eq!(getkeystring("a\\nb").0, "a\nb");
16865        assert_eq!(
16866            getkeystring("line1\\nline2\\tindented").0,
16867            "line1\nline2\tindented"
16868        );
16869    }
16870
16871    // ── metafy / unmetafy round-trip on ASCII (identity) ────────────
16872    /// ASCII strings: metafy is identity (no meta-bytes to escape).
16873    #[test]
16874    fn metafy_then_unmetafy_ascii_roundtrips_to_input() {
16875        let s = "hello world";
16876        let m = metafy(s);
16877        assert_eq!(m, s);
16878        let mut bytes = m.into_bytes();
16879        let _len = unmetafy(&mut bytes);
16880        assert_eq!(String::from_utf8(bytes).unwrap(), "hello world");
16881    }
16882
16883    // ═══════════════════════════════════════════════════════════════════
16884    // quotestring — emit a quoted shell-safe form of the input string.
16885    // Each QT_* mode produces a different quoting style. Tests pin
16886    // each mode for both empty and non-empty input.
16887    // ═══════════════════════════════════════════════════════════════════
16888
16889    use crate::zsh_h::{
16890        QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_DOLLARS, QT_DOUBLE, QT_NONE,
16891        QT_SINGLE, QT_SINGLE_OPTIONAL,
16892    };
16893
16894    /// QT_NONE: no quoting; passes input through unchanged.
16895    #[test]
16896    fn quotestring_qt_none_passes_through_unchanged() {
16897        let _g = crate::test_util::global_state_lock();
16898        assert_eq!(quotestring("hello world", QT_NONE), "hello world");
16899        assert_eq!(quotestring("", QT_NONE), "");
16900        assert_eq!(quotestring("a*b?c", QT_NONE), "a*b?c");
16901    }
16902
16903    /// QT_NONE empty → empty.
16904    #[test]
16905    fn quotestring_qt_none_empty_is_empty() {
16906        let _g = crate::test_util::global_state_lock();
16907        assert_eq!(quotestring("", QT_NONE), "");
16908    }
16909
16910    /// QT_BACKSLASH on empty → "''" (single-quote pair).
16911    #[test]
16912    fn quotestring_qt_backslash_empty_yields_empty() {
16913        let _g = crate::test_util::global_state_lock();
16914        // c:6194 — shownull is 0 for plain QT_BACKSLASH, so empty → "".
16915        assert_eq!(quotestring("", QT_BACKSLASH), "");
16916    }
16917
16918    /// QT_BACKSLASH_SHOWNULL on empty → "''" too.
16919    #[test]
16920    fn quotestring_qt_backslash_shownull_empty_yields_empty_single_quotes() {
16921        let _g = crate::test_util::global_state_lock();
16922        assert_eq!(quotestring("", QT_BACKSLASH_SHOWNULL), "''");
16923    }
16924
16925    /// c:6194 — `if (!*s && shownull)`. `shownull` is 0 for QT_SINGLE (it is
16926    /// set only by QT_BACKSLASH_SHOWNULL at c:6165 and QT_SINGLE_OPTIONAL at
16927    /// c:6187), so an empty string yields an EMPTY body; the `''` comes from
16928    /// the caller's wrapper (subst.c:4085-4087).
16929    #[test]
16930    fn quotestring_qt_single_empty_yields_empty_body() {
16931        let _g = crate::test_util::global_state_lock();
16932        assert_eq!(quotestring("", QT_SINGLE), "");
16933    }
16934
16935    /// QT_SINGLE_OPTIONAL on empty → "''" too.
16936    #[test]
16937    fn quotestring_qt_single_optional_empty_yields_empty_single_quotes() {
16938        let _g = crate::test_util::global_state_lock();
16939        assert_eq!(quotestring("", QT_SINGLE_OPTIONAL), "''");
16940    }
16941
16942    /// c:6194 — `shownull` is 0 for QT_DOUBLE, so the body is empty and the
16943    /// `""` pair is added by the caller (subst.c:4085-4087).
16944    #[test]
16945    fn quotestring_qt_double_empty_yields_empty_body() {
16946        let _g = crate::test_util::global_state_lock();
16947        assert_eq!(quotestring("", QT_DOUBLE), "");
16948    }
16949
16950    /// c:6194 — same for QT_DOLLARS; C's caller adds `''` and then
16951    /// `val[0] = '$'` (subst.c:4085-4090).
16952    #[test]
16953    fn quotestring_qt_dollars_empty_yields_empty_body() {
16954        let _g = crate::test_util::global_state_lock();
16955        assert_eq!(quotestring("", QT_DOLLARS), "");
16956    }
16957
16958    /// QT_BACKSLASH_PATTERN escapes only pattern meta-chars.
16959    /// Input "a*b?c[d]" → "a\\*b\\?c\\[d\\]".
16960    #[test]
16961    fn quotestring_qt_backslash_pattern_escapes_glob_metas() {
16962        let _g = crate::test_util::global_state_lock();
16963        let r = quotestring("a*b?c[d]", QT_BACKSLASH_PATTERN);
16964        assert!(
16965            r.contains("\\*") && r.contains("\\?") && r.contains("\\[") && r.contains("\\]"),
16966            "all globs must be escaped; got {r:?}"
16967        );
16968    }
16969
16970    /// QT_BACKSLASH_PATTERN does NOT escape plain ASCII letters/digits.
16971    #[test]
16972    fn quotestring_qt_backslash_pattern_doesnt_escape_plain_chars() {
16973        let _g = crate::test_util::global_state_lock();
16974        let r = quotestring("plain", QT_BACKSLASH_PATTERN);
16975        assert_eq!(r, "plain", "plain text passes through");
16976    }
16977
16978    /// QT_BACKSLASH_PATTERN escapes `<`, `>`, `(`, `)`, `|`, `#`, `^`, `~`.
16979    #[test]
16980    fn quotestring_qt_backslash_pattern_escapes_all_meta_set() {
16981        let _g = crate::test_util::global_state_lock();
16982        for ch in ['<', '>', '(', ')', '|', '#', '^', '~'] {
16983            let s = format!("x{ch}y");
16984            let r = quotestring(&s, QT_BACKSLASH_PATTERN);
16985            assert!(
16986                r.contains(&format!("\\{ch}")),
16987                "{ch:?} must be escaped, got {r:?}"
16988            );
16989        }
16990    }
16991
16992    /// c:6131-6134 — QT_SINGLE returns the BODY; a word with nothing to
16993    /// escape passes straight through and the caller adds the `'…'` pair.
16994    #[test]
16995    fn quotestring_qt_single_returns_body_unwrapped() {
16996        let _g = crate::test_util::global_state_lock();
16997        assert_eq!(quotestring("simple", QT_SINGLE), "simple");
16998    }
16999
17000    // ═══════════════════════════════════════════════════════════════════
17001    // metafy / unmetafy edge cases — bytes that NEED meta-encoding
17002    // (NUL, Meta byte 0x83, Nularg 0xa1, etc.). Pin that metafy escapes
17003    // them via the Meta + (byte ^ 32) scheme and unmetafy reverses it.
17004    // ═══════════════════════════════════════════════════════════════════
17005
17006    /// `metafy` is identity for plain ASCII (no meta bytes).
17007    #[test]
17008    fn metafy_ascii_is_identity_byte_for_byte() {
17009        let s = "abc 123 XYZ!@#";
17010        assert_eq!(metafy(s), s);
17011    }
17012
17013    /// Empty string round-trips.
17014    #[test]
17015    fn metafy_empty_string_returns_empty() {
17016        assert_eq!(metafy(""), "");
17017    }
17018
17019    /// metafy → unmetafy is round-trip identity for ASCII input.
17020    #[test]
17021    fn metafy_unmetafy_roundtrip_with_punctuation() {
17022        let s = "Hello, World! 123 +-=";
17023        let m = metafy(s);
17024        let mut bytes = m.into_bytes();
17025        let _ = unmetafy(&mut bytes);
17026        let result = String::from_utf8(bytes).expect("valid utf-8");
17027        assert_eq!(result, s);
17028    }
17029
17030    /// Multi-byte UTF-8 chars get meta-encoded then unmetafied. The
17031    /// round-trip MUST preserve the original bytes. Inline the byte-
17032    /// level metafy here — `metafy()`'s String return goes through
17033    /// `from_utf8_lossy` for non-UTF-8 outputs (correct for String
17034    /// consumers like the lexer or pattern compile, but breaks byte-
17035    /// round-trip because U+FFFD inserts EF BF BD where the
17036    /// original Meta-escaped bytes lived). The unmetafy step
17037    /// reverses the Meta-escape to recover the source UTF-8 verbatim.
17038    #[test]
17039    fn metafy_unmetafy_roundtrip_with_utf8_multibyte_anchored() {
17040        let s = "日本語";
17041        // Inline of metafy() byte path (no String materialisation).
17042        let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
17043        for &b in s.as_bytes() {
17044            if imeta_byte(b) {
17045                bytes.push(Meta);
17046                bytes.push(b ^ 32);
17047            } else {
17048                bytes.push(b);
17049            }
17050        }
17051        let _ = unmetafy(&mut bytes);
17052        let result = String::from_utf8(bytes).expect("valid utf-8");
17053        assert_eq!(result, s, "UTF-8 round-trip must preserve content");
17054    }
17055
17056    // ─── zsh-corpus pins for quotestring per quote_type ─────────────
17057
17058    /// `quotestring(QT_NONE)` is identity on any input.
17059    #[test]
17060    fn quotestring_corpus_qt_none_is_identity() {
17061        let s = "anything `goes` $here";
17062        assert_eq!(quotestring(s, QT_NONE), s);
17063    }
17064
17065    /// c:6131-6134 — `quotestring(QT_SINGLE)` on a plain word is a
17066    /// pass-through: no `'` pair is added here (that is the caller's job at
17067    /// subst.c:4085-4087). Emitting the pair here is what broke completion
17068    /// inside a quoted word — see `multiquote_leaves_plain_word_unquoted`.
17069    #[test]
17070    fn quotestring_corpus_qt_single_returns_plain_word_verbatim() {
17071        let out = quotestring("hello", QT_SINGLE);
17072        assert_eq!(out, "hello", "QT_SINGLE returns the body, unwrapped");
17073    }
17074
17075    /// c:6131-6134 — same contract for QT_DOUBLE.
17076    #[test]
17077    fn quotestring_corpus_qt_double_returns_plain_word_verbatim() {
17078        let out = quotestring("hello", QT_DOUBLE);
17079        assert_eq!(out, "hello", "QT_DOUBLE returns the body, unwrapped");
17080    }
17081
17082    /// `quotestring(QT_SINGLE)` on string with apostrophe escapes the
17083    /// apostrophe by closing+escape+reopen: `it's` → `'it'\''s'`.
17084    #[test]
17085    fn quotestring_corpus_qt_single_escapes_apostrophe() {
17086        // Same precondition the sibling QT_BACKSLASH test states: `'` only
17087        // reaches the c:6313 `instring == QT_SINGLE && *u == '\''` arm after
17088        // passing c:6301 `ispecial(*u)`, which reads the live typtab. C
17089        // guarantees it is populated via the `inittyptab()` at init.c:1261
17090        // (ported at init.rs:1244); a unit test that takes neither
17091        // `global_state_lock()` nor this call sees a zeroed table.
17092        inittyptab();
17093        let out = quotestring("it's", QT_SINGLE);
17094        assert!(
17095            out.contains("\\'") || out.contains("'\\''"),
17096            "apostrophe gets escaped in single-quote form, got {out:?}",
17097        );
17098    }
17099
17100    /// `quotestring(QT_BACKSLASH)` escapes shell special chars.
17101    /// Pattern chars like `*`, `?`, `$`, `(`, `)` should be backslashed.
17102    #[test]
17103    fn quotestring_corpus_qt_backslash_escapes_glob_chars() {
17104        // `ispecial` reads the live typtab, as C's macro does, so the table
17105        // has to be populated first — C guarantees it via the `inittyptab`
17106        // call in `setupvals`. Same entry call the sibling typtab tests make.
17107        inittyptab();
17108        let out = quotestring("a*b?c", QT_BACKSLASH);
17109        assert!(out.contains("\\*"), "* gets backslashed, got {out:?}");
17110        assert!(out.contains("\\?"), "? gets backslashed, got {out:?}");
17111    }
17112
17113    /// c:6194 — `shownull` is 0 for QT_DOUBLE, so an empty input gives an
17114    /// empty body; the `""` pair comes from the caller.
17115    #[test]
17116    fn quotestring_corpus_qt_double_empty_yields_empty_body() {
17117        let out = quotestring("", QT_DOUBLE);
17118        assert_eq!(out, "", "empty double-quoted body is empty");
17119    }
17120
17121    /// c:6194 — same for QT_SINGLE.
17122    #[test]
17123    fn quotestring_corpus_qt_single_empty_yields_empty_body() {
17124        let out = quotestring("", QT_SINGLE);
17125        assert_eq!(out, "", "empty single-quoted body is empty");
17126    }
17127
17128    /// `quotestring(QT_BACKSLASH)` on plain alphanumeric is identity.
17129    /// "abc123" should round-trip with no backslashes added.
17130    #[test]
17131    fn quotestring_corpus_qt_backslash_plain_word_unchanged() {
17132        let out = quotestring("abc123", QT_BACKSLASH);
17133        assert_eq!(
17134            out, "abc123",
17135            "plain alphanumeric needs no escape, got {out:?}"
17136        );
17137    }
17138
17139    /// `ztrlen("hello")` = 5 (no meta chars).
17140    #[test]
17141    fn ztrlen_corpus_plain_ascii_byte_count() {
17142        assert_eq!(ztrlen("hello"), 5);
17143    }
17144
17145    /// `ztrlen("")` = 0.
17146    #[test]
17147    fn ztrlen_corpus_empty_is_zero() {
17148        assert_eq!(ztrlen(""), 0);
17149    }
17150
17151    // ═══════════════════════════════════════════════════════════════════
17152    // itype_end C-parity tests — pin `utils.c:4395-4495` per-itype-flag
17153    // behavior. One assertion per flag/path so a regression in the
17154    // char-class walk points at the exact branch that drifted.
17155    //
17156    // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"]
17157    // so the bug stays surfaced but CI doesn't fail until the fix.
17158    // ═══════════════════════════════════════════════════════════════════
17159
17160    /// `itype_end("abc123", IIDENT, false)` walks the full identifier.
17161    /// C utils.c:4395 — IIDENT matches letters/digits/`_` via TYPTAB.
17162    /// Requires `inittyptab()` for TYPTAB population — call at entry.
17163    #[test]
17164    fn itype_end_ident_walks_full_identifier() {
17165        let _g = crate::test_util::global_state_lock();
17166        inittyptab();
17167        use crate::ported::ztype_h::IIDENT;
17168        assert_eq!(itype_end("abc123", IIDENT as u32, false), 6);
17169    }
17170
17171    /// `itype_end("abc-def", IIDENT, false)` stops at `-` (non-IIDENT).
17172    #[test]
17173    fn itype_end_ident_stops_at_dash() {
17174        let _g = crate::test_util::global_state_lock();
17175        inittyptab();
17176        use crate::ported::ztype_h::IIDENT;
17177        assert_eq!(itype_end("abc-def", IIDENT as u32, false), 3);
17178    }
17179
17180    /// `itype_end("a", IIDENT, true)` with `once=true` stops after 1.
17181    #[test]
17182    fn itype_end_once_stops_after_first_match() {
17183        let _g = crate::test_util::global_state_lock();
17184        inittyptab();
17185        use crate::ported::ztype_h::IIDENT;
17186        assert_eq!(itype_end("abc", IIDENT as u32, true), 1);
17187    }
17188
17189    /// `itype_end("", IIDENT, false)` on empty string returns 0.
17190    #[test]
17191    fn itype_end_empty_string_returns_zero() {
17192        let _g = crate::test_util::global_state_lock();
17193        inittyptab();
17194        use crate::ported::ztype_h::IIDENT;
17195        assert_eq!(itype_end("", IIDENT as u32, false), 0);
17196    }
17197
17198    /// `itype_end(":", IIDENT, false)` on non-IIDENT first char = 0.
17199    #[test]
17200    fn itype_end_non_ident_first_char_returns_zero() {
17201        let _g = crate::test_util::global_state_lock();
17202        inittyptab();
17203        use crate::ported::ztype_h::IIDENT;
17204        assert_eq!(itype_end(":foo", IIDENT as u32, false), 0);
17205    }
17206
17207    /// `itype_end("ns.foo", INAMESPC, false)` — C utils.c:4399-4413
17208    /// INAMESPC special-cases ksh93 namespace `.` separators. With
17209    /// non-POSIXIDENTIFIERS / non-KSH emulation, dotted names should
17210    /// walk through the `.` to consume `ns.foo` as a single name.
17211    /// The Rust port may stop at `.` since dotted-namespace support
17212    /// requires POSIXIDENTIFIERS check + ksh emulation gating that
17213    /// isn't fully wired.
17214    #[test]
17215    fn itype_end_inamespc_walks_through_ksh93_dot() {
17216        let _g = crate::test_util::global_state_lock();
17217        inittyptab();
17218        use crate::ported::ztype_h::INAMESPC;
17219        // Expected: full "ns.foo" = 6 bytes walked.
17220        assert_eq!(itype_end("ns.foo", INAMESPC, false), 6);
17221    }
17222
17223    /// `itype_end("a b c", ISEP, false)` from a space char.
17224    /// ISEP matches IFS chars. Default IFS includes ` \t\n`.
17225    /// Walk should consume only the leading separator(s).
17226    #[test]
17227    fn itype_end_isep_walks_separator_run() {
17228        let _g = crate::test_util::global_state_lock();
17229        inittyptab();
17230        use crate::ported::ztype_h::ISEP;
17231        // " \t " then 'a' — 3 sep bytes.
17232        assert_eq!(itype_end(" \t a", ISEP as u32, false), 3);
17233    }
17234
17235    // ═══════════════════════════════════════════════════════════════════
17236    // unmetafy C-parity tests — pin `utils.c:4954-4983`. Meta-byte
17237    // collapse (0x83 + (b^0x20) → b) must be in-place + return new len.
17238    // ═══════════════════════════════════════════════════════════════════
17239
17240    /// `unmetafy("abc")` is a no-op for non-metafied input.
17241    #[test]
17242    fn unmetafy_pure_ascii_unchanged() {
17243        let _g = crate::test_util::global_state_lock();
17244        let mut v = b"abc".to_vec();
17245        let n = unmetafy(&mut v);
17246        assert_eq!(n, 3);
17247        assert_eq!(v, b"abc");
17248    }
17249
17250    /// `unmetafy("a\x83a")` → "a\x01" because Meta(0x83) + next ^ 0x20
17251    /// = 'a' ^ 0x20 = 0x41 ^ 0x20 = 0x61... wait, 'a'=0x61, 0x61 ^ 0x20
17252    /// = 0x41 = 'A'. So unmetafy("a\x83a") → "aA" (2 bytes).
17253    #[test]
17254    fn unmetafy_meta_pair_collapses_to_xor_byte() {
17255        let _g = crate::test_util::global_state_lock();
17256        // 'a' + Meta(0x83) + 'a' → 'a' + ('a' ^ 0x20) = 'a' + 'A' = "aA"
17257        let mut v: Vec<u8> = vec![b'a', 0x83, b'a'];
17258        let n = unmetafy(&mut v);
17259        assert_eq!(n, 2);
17260        assert_eq!(v, vec![b'a', b'A']);
17261    }
17262
17263    /// `unmetafy("")` returns 0 with empty buffer.
17264    #[test]
17265    fn unmetafy_empty_returns_zero() {
17266        let _g = crate::test_util::global_state_lock();
17267        let mut v: Vec<u8> = Vec::new();
17268        let n = unmetafy(&mut v);
17269        assert_eq!(n, 0);
17270    }
17271
17272    /// `unmetafy` with trailing lone Meta byte. C `unmetafy` checks
17273    /// `p < s.len()` before reading the byte after Meta — trailing
17274    /// Meta at EOF is preserved as a single literal Meta byte.
17275    #[test]
17276    fn unmetafy_trailing_lone_meta_preserved() {
17277        let _g = crate::test_util::global_state_lock();
17278        let mut v: Vec<u8> = vec![b'a', 0x83];
17279        let n = unmetafy(&mut v);
17280        // Rust port copies the Meta byte to t, then loop exits with
17281        // p == s.len() → no second-byte read. Final = "a\x83" (2).
17282        assert_eq!(n, 2);
17283        assert_eq!(v, vec![b'a', 0x83]);
17284    }
17285
17286    // ═══════════════════════════════════════════════════════════════════
17287    // nicechar / wcs_nicechar C-parity tests — pin display-form
17288    // conversion. Required by stradd, prompt expansion, completion etc.
17289    // ═══════════════════════════════════════════════════════════════════
17290
17291    /// `nicechar('a')` returns the printable char unchanged.
17292    #[test]
17293    fn nicechar_printable_ascii_passes_through() {
17294        let _g = crate::test_util::global_state_lock();
17295        assert_eq!(nicechar('a'), "a");
17296    }
17297
17298    /// `nicechar('\n')` returns `\n` escape sequence per C `nicechar`
17299    /// (utils.c:464+ — control chars get `^M` style or `\n` escape).
17300    /// zsh emits `\n` as literal `\n` (two chars) for non-quotable
17301    /// nicechar; the `nicechar_sel(c, false)` form is non-quotable.
17302    #[test]
17303    fn nicechar_newline_emits_escape() {
17304        let _g = crate::test_util::global_state_lock();
17305        let out = nicechar('\n');
17306        assert!(
17307            out == "\\n" || out == "\n",
17308            "newline should be escaped or literal; got {out:?}"
17309        );
17310    }
17311
17312    /// `wcs_nicechar('A', None, None)` returns "A" for printable wide
17313    /// chars per utils.c:689.
17314    #[test]
17315    fn wcs_nicechar_printable_ascii_passes_through() {
17316        let _g = crate::test_util::global_state_lock();
17317        assert_eq!(wcs_nicechar('A', None, None), "A");
17318    }
17319
17320    // ═══════════════════════════════════════════════════════════════════
17321    // Additional C-parity tests for Src/utils.c nicechar dispatch.
17322    // ═══════════════════════════════════════════════════════════════════
17323
17324    /// c:462 — nicechar for printable ASCII passes through.
17325    #[test]
17326    fn nicechar_printable_ascii_passes_through_pin() {
17327        let _g = crate::test_util::global_state_lock();
17328        assert_eq!(nicechar('a'), "a");
17329        assert_eq!(nicechar('Z'), "Z");
17330        assert_eq!(nicechar('5'), "5");
17331        assert_eq!(nicechar(' '), " ");
17332    }
17333
17334    /// c:487 — nicechar('\\n') returns '\\n' (backslash + n).
17335    #[test]
17336    fn nicechar_newline_returns_backslash_n() {
17337        let _g = crate::test_util::global_state_lock();
17338        assert_eq!(nicechar('\n'), "\\n");
17339    }
17340
17341    /// c:490 — nicechar('\\t') returns '\\t' (backslash + t).
17342    #[test]
17343    fn nicechar_tab_returns_backslash_t() {
17344        let _g = crate::test_util::global_state_lock();
17345        assert_eq!(nicechar('\t'), "\\t");
17346    }
17347
17348    /// c:479 — nicechar(0x7f) returns '^?' (DEL → ^?).
17349    #[test]
17350    fn nicechar_del_returns_caret_question() {
17351        let _g = crate::test_util::global_state_lock();
17352        assert_eq!(nicechar('\x7f'), "^?");
17353    }
17354
17355    /// c:493 — nicechar(0x01) returns '^A' (Ctrl-A).
17356    #[test]
17357    fn nicechar_ctrl_a_returns_caret_A() {
17358        let _g = crate::test_util::global_state_lock();
17359        assert_eq!(nicechar('\x01'), "^A");
17360    }
17361
17362    /// c:493 — nicechar(0x1b) returns '^[' (ESC).
17363    #[test]
17364    fn nicechar_esc_returns_caret_bracket() {
17365        let _g = crate::test_util::global_state_lock();
17366        assert_eq!(nicechar('\x1b'), "^[");
17367    }
17368
17369    /// c:462 — nicechar_sel quotable=true uses '\\C-' instead of '^'
17370    /// for control chars.
17371    #[test]
17372    fn nicechar_sel_quotable_uses_backslash_C() {
17373        let _g = crate::test_util::global_state_lock();
17374        let r = nicechar_sel('\x01', true);
17375        assert!(
17376            r.contains("\\C-") || r.contains("^"),
17377            "quotable should emit \\C- form, got {:?}",
17378            r
17379        );
17380    }
17381
17382    /// c:531 — is_nicechar('a') returns FALSE: printable chars don't
17383    /// NEED nice (escape) representation, they pass through verbatim.
17384    /// Per the C comment in utils.c:531: "Return whether the char needs
17385    /// nice representation".
17386    #[test]
17387    fn is_nicechar_printable_returns_false() {
17388        let _g = crate::test_util::global_state_lock();
17389        assert!(!is_nicechar('a'), "printable doesn't NEED nice repr");
17390        assert!(!is_nicechar('Z'));
17391        assert!(!is_nicechar('5'));
17392    }
17393
17394    /// c:531 — is_nicechar for any byte 0..127 is safe (no panic).
17395    #[test]
17396    fn is_nicechar_all_ascii_safe() {
17397        let _g = crate::test_util::global_state_lock();
17398        for c in 0u8..=127 {
17399            let _ = is_nicechar(c as char); // no panic
17400        }
17401    }
17402
17403    /// c:462 — nicechar deterministic.
17404    #[test]
17405    fn nicechar_is_deterministic() {
17406        let _g = crate::test_util::global_state_lock();
17407        for c in ['a', '\n', '\t', '\x7f', '\x01', '\x1b'] {
17408            let first = nicechar(c);
17409            for _ in 0..5 {
17410                assert_eq!(nicechar(c), first, "{:?} must be pure", c);
17411            }
17412        }
17413    }
17414
17415    /// c:706 — `pathprog("")` returns None (empty path invalid).
17416    #[test]
17417    fn pathprog_empty_returns_none() {
17418        let _g = crate::test_util::global_state_lock();
17419        assert!(pathprog("").is_none());
17420    }
17421
17422    /// c:776 — `pathprog("/nonexistent/zshrs_xyz")` returns None.
17423    #[test]
17424    fn pathprog_nonexistent_returns_none() {
17425        let _g = crate::test_util::global_state_lock();
17426        let r = pathprog("/__never_exists_zshrs_pathprog_xyz");
17427        assert!(r.is_none());
17428    }
17429
17430    // ═══════════════════════════════════════════════════════════════════
17431    // Additional C-parity tests for Src/utils.c
17432    // c:99 set_widearray / c:447 nicechar_sel / c:521 is_nicechar /
17433    // c:742 zwcwidth / c:836 findpwd / c:928 slashsplit /
17434    // c:1258 get_username / c:1426 getnameddir / c:1475 dircmp /
17435    // c:1576 callhookfunc
17436    // ═══════════════════════════════════════════════════════════════════
17437
17438    /// c:99 — `set_widearray("")` empty returns empty Vec.
17439    #[test]
17440    fn set_widearray_empty_returns_empty() {
17441        let _g = crate::test_util::global_state_lock();
17442        assert!(set_widearray("").is_empty());
17443    }
17444
17445    /// c:99 — `set_widearray` returns Vec<char>.
17446    #[test]
17447    fn set_widearray_returns_vec_char_type() {
17448        let _g = crate::test_util::global_state_lock();
17449        let _: Vec<char> = set_widearray("a");
17450    }
17451
17452    /// c:447 — `nicechar_sel('a', false)` returns "a" (printable pass-through).
17453    #[test]
17454    fn nicechar_sel_ascii_letter_passthrough() {
17455        assert_eq!(nicechar_sel('a', false), "a", "printable letter as-is");
17456    }
17457
17458    /// c:447 — `nicechar_sel` is pure.
17459    #[test]
17460    fn nicechar_sel_is_pure() {
17461        for c in ['a', '\t', '\n', '\x1b', '\x7f', '日'] {
17462            let first = nicechar_sel(c, false);
17463            for _ in 0..3 {
17464                assert_eq!(
17465                    nicechar_sel(c, false),
17466                    first,
17467                    "nicechar_sel({:?}, false) must be pure",
17468                    c
17469                );
17470            }
17471        }
17472    }
17473
17474    /// c:742 — `zwcwidth` returns usize (compile-time type pin).
17475    #[test]
17476    fn zwcwidth_returns_usize_type() {
17477        let _: usize = zwcwidth('a');
17478    }
17479
17480    /// c:742 — `zwcwidth('a')` ASCII letter returns 1.
17481    #[test]
17482    fn zwcwidth_ascii_returns_one() {
17483        assert_eq!(zwcwidth('a'), 1, "ASCII letter width = 1");
17484    }
17485
17486    /// c:836 — `findpwd("")` empty returns Option<String>.
17487    #[test]
17488    fn findpwd_empty_returns_option_string_type() {
17489        let _g = crate::test_util::global_state_lock();
17490        let _: Option<String> = findpwd("");
17491    }
17492
17493    /// c:928 — `slashsplit("")` empty returns empty Vec.
17494    #[test]
17495    fn slashsplit_empty_returns_empty() {
17496        assert!(slashsplit("").is_empty());
17497    }
17498
17499    /// c:928 — `slashsplit("a/b/c")` splits to 3 elements.
17500    #[test]
17501    fn slashsplit_simple_three_components() {
17502        let r = slashsplit("a/b/c");
17503        assert_eq!(r, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
17504    }
17505
17506    /// c:1475 — `dircmp("", "")` both empty returns bool (type pin + safe).
17507    #[test]
17508    fn dircmp_both_empty_returns_bool_type() {
17509        let _g = crate::test_util::global_state_lock();
17510        let _: bool = dircmp("", "");
17511    }
17512
17513    /// c:1426 — `getnameddir("")` empty returns None.
17514    #[test]
17515    fn getnameddir_empty_returns_none() {
17516        let _g = crate::test_util::global_state_lock();
17517        assert!(getnameddir("").is_none(), "empty → None");
17518    }
17519
17520    /// c:1258 — `get_username` returns String (compile-time type pin).
17521    #[test]
17522    fn get_username_returns_string_type() {
17523        let _g = crate::test_util::global_state_lock();
17524        let _: String = get_username();
17525    }
17526
17527    /// c:1921-1935 — adjustwinsize(1) seeds `$LINES`/`$COLUMNS` from the
17528    /// TIOCGWINSZ probe of SHTTY. The port gated the setiparam calls on
17529    /// LINES/COLUMNS being in the OS environment (a misread of C's
17530    /// zgetenv guard, which only covers env re-publication — the C param
17531    /// is always live via its zterm_lines valptr alias), so every
17532    /// interactive shell ran with LINES=0 → `$(( LINES / 3 ))` division
17533    /// by zero in history-search-multi-word pagination (_hsmw_main:81).
17534    #[test]
17535    fn adjustwinsize_seeds_lines_columns_from_shtty() {
17536        let _g = crate::test_util::global_state_lock();
17537        unsafe {
17538            let mut master: libc::c_int = -1;
17539            let mut slave: libc::c_int = -1;
17540            let mut ws: libc::winsize = std::mem::zeroed();
17541            ws.ws_row = 33;
17542            ws.ws_col = 77;
17543            if libc::openpty(
17544                &mut master,
17545                &mut slave,
17546                std::ptr::null_mut(),
17547                std::ptr::null_mut(),
17548                &mut ws,
17549            ) != 0
17550            {
17551                return; // no pty available (sandboxed CI) — skip
17552            }
17553            let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
17554            crate::ported::init::SHTTY.store(slave, Ordering::Relaxed);
17555            let _ = adjustwinsize(1);
17556            crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
17557            libc::close(slave);
17558            libc::close(master);
17559        }
17560        assert_eq!(
17561            crate::ported::params::getiparam("LINES"),
17562            33,
17563            "LINES must reflect the SHTTY winsize probe"
17564        );
17565        assert_eq!(
17566            crate::ported::params::getiparam("COLUMNS"),
17567            77,
17568            "COLUMNS must reflect the SHTTY winsize probe"
17569        );
17570    }
17571
17572    /// c:Src/utils.c:1858 / c:Src/params.c:362 — `zterm_columns` is ONE
17573    /// cell. `$COLUMNS` is that cell (`IPDEF5("COLUMNS", &zterm_columns,
17574    /// zlevar_gsu)`), and the TIOCGWINSZ probe that refreshes it lives
17575    /// only in `adjustwinsize` (c:1902). So a window that has been
17576    /// resized but whose SIGWINCH has not been handled yet — the normal
17577    /// state, since C blocks SIGWINCH by default (`Src/init.c:1458`
17578    /// `winch_block()`) — must still report the OLD geometry to every
17579    /// reader, script-side and C-side alike.
17580    ///
17581    /// `adjustcolumns()`/`adjustlines()` used to run their own
17582    /// `ioctl(1, TIOCGWINSZ)` on every call, so the two answers split:
17583    /// `_git`'s `${(r.COLUMNS-4.)…}` (c:Completion/Unix/Command/_git:6890)
17584    /// built its display strings at the pre-resize width while
17585    /// `calclist` (c:Src/Zle/compresult.c:1646/1701) counted them at the
17586    /// post-resize width. `git <TAB>` across a 24x80 -> 24x60 resize
17587    /// asked "see all 164 possibilities (251 lines)?" for 164 matches
17588    /// that each occupied one line.
17589    #[test]
17590    fn adjustcolumns_does_not_resample_the_tty_between_adjustwinsize_calls() {
17591        let _g = crate::test_util::global_state_lock();
17592        let saved_lines = ZTERM_LINES.load(Ordering::SeqCst);
17593        let saved_columns = ZTERM_COLUMNS.load(Ordering::SeqCst);
17594        let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
17595        let mut published: Option<(usize, usize)> = None;
17596        let mut after_resize: Option<(usize, usize)> = None;
17597        let mut after_second_probe: Option<(usize, usize)> = None;
17598        let mut params_after_resize: Option<(i64, i64)> = None;
17599        unsafe {
17600            let mut master: libc::c_int = -1;
17601            let mut slave: libc::c_int = -1;
17602            let mut ws: libc::winsize = std::mem::zeroed();
17603            ws.ws_row = 24;
17604            ws.ws_col = 80;
17605            if libc::openpty(
17606                &mut master,
17607                &mut slave,
17608                std::ptr::null_mut(),
17609                std::ptr::null_mut(),
17610                &mut ws,
17611            ) != 0
17612            {
17613                return; // no pty available (sandboxed CI) — skip
17614            }
17615            // The old implementation probed fd 1, so fd 1 has to BE the
17616            // pty for this to pin anything: otherwise a CI runner's piped
17617            // stdout makes the stale-fallback path accidentally agree.
17618            let saved_stdout = libc::dup(1);
17619            libc::dup2(slave, 1);
17620            crate::ported::init::SHTTY.store(slave, Ordering::Relaxed);
17621
17622            let _ = adjustwinsize(1);
17623            published = Some((adjustlines(), adjustcolumns()));
17624
17625            // Resize the window WITHOUT running the SIGWINCH handler.
17626            let mut ws2: libc::winsize = std::mem::zeroed();
17627            ws2.ws_row = 12;
17628            ws2.ws_col = 60;
17629            libc::ioctl(master, libc::TIOCSWINSZ, &ws2);
17630            after_resize = Some((adjustlines(), adjustcolumns()));
17631            params_after_resize = Some((
17632                crate::ported::params::getiparam("LINES"),
17633                crate::ported::params::getiparam("COLUMNS"),
17634            ));
17635
17636            // c:469 — the handler's `adjustwinsize(1)` is the one thing
17637            // that may move the number.
17638            let _ = adjustwinsize(1);
17639            after_second_probe = Some((adjustlines(), adjustcolumns()));
17640
17641            if saved_stdout >= 0 {
17642                libc::dup2(saved_stdout, 1);
17643                libc::close(saved_stdout);
17644            }
17645            crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
17646            libc::close(slave);
17647            libc::close(master);
17648        }
17649        ZTERM_LINES.store(saved_lines, Ordering::SeqCst);
17650        ZTERM_COLUMNS.store(saved_columns, Ordering::SeqCst);
17651
17652        assert_eq!(
17653            published,
17654            Some((24, 80)),
17655            "adjustwinsize(1) publishes the probed geometry"
17656        );
17657        assert_eq!(
17658            after_resize,
17659            Some((24, 80)),
17660            "an unhandled resize must NOT move zterm_lines/zterm_columns — \
17661             the probe belongs to adjustwinsize (c:1902), not to the readers"
17662        );
17663        assert_eq!(
17664            params_after_resize,
17665            Some((24, 80)),
17666            "$LINES/$COLUMNS and the accessors are one cell (c:Src/params.c:362-363)"
17667        );
17668        assert_eq!(
17669            after_second_probe,
17670            Some((12, 60)),
17671            "the SIGWINCH handler's adjustwinsize(1) is what republishes it (c:469)"
17672        );
17673    }
17674
17675    /// c:Src/params.c:4230 — `zlevarsetfn` does `*p = x` where `p` is
17676    /// `&zterm_columns` itself, so an explicit `COLUMNS=N` is visible to
17677    /// every C-side reader immediately, with no tty involved. The mirror
17678    /// has to live in `zlevarsetfn` and not in `adjustwinsize`, which
17679    /// early-returns at c:1900-1901 whenever `SHTTY == -1` — exactly the
17680    /// non-interactive case where `COLUMNS=N` is most often set.
17681    #[test]
17682    fn explicit_columns_assignment_reaches_the_zterm_columns_readers() {
17683        let _g = crate::test_util::global_state_lock();
17684        let saved_columns = ZTERM_COLUMNS.load(Ordering::SeqCst);
17685        let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
17686        crate::ported::init::SHTTY.store(-1, Ordering::Relaxed); // c:1900
17687                                                                 // A sentinel the assignment must overwrite. Without the c:4230
17688                                                                 // mirror the readers keep answering with this, whatever `$COLUMNS`
17689                                                                 // says.
17690        ZTERM_COLUMNS.store(999, Ordering::SeqCst);
17691
17692        setiparam("COLUMNS", 61); // c:4230
17693        let mirrored = ZTERM_COLUMNS.load(Ordering::SeqCst);
17694        let seen = adjustcolumns();
17695
17696        crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
17697        ZTERM_COLUMNS.store(saved_columns, Ordering::SeqCst);
17698
17699        assert_eq!(
17700            mirrored, 61,
17701            "c:Src/params.c:4230 — `*p = x` writes zterm_columns itself"
17702        );
17703        assert_eq!(
17704            seen, 61,
17705            "COLUMNS=61 must be what the zterm_columns readers see (c:Src/params.c:362)"
17706        );
17707    }
17708
17709    /// The same contract as the test above, but with `zterm_columns` ALREADY
17710    /// seeded — which is the only state that can catch the real defect.
17711    ///
17712    /// `adjustcolumns` (c:Src/utils.c:1858) is a cached read: a non-zero
17713    /// `zterm_columns` short-circuits every fallback. So with the global at 0
17714    /// the test above passes even when the assignment never reaches it,
17715    /// because `adjustcolumns` then falls through to `getsparam("COLUMNS")`
17716    /// and finds the parameter. Seed the global first and the fallback is
17717    /// unreachable, so the only way to observe 61 is the `setfn` dispatch
17718    /// C makes at `Src/params.c:2874` (`pm->gsu.i->setfn(pm, val.u.l)` →
17719    /// `zlevarsetfn`, c:4226-4230, whose `*p = x` IS the global).
17720    ///
17721    /// That dispatch was missing: `setnumvalue`'s PM_INTEGER arm stored
17722    /// `pm.u_val` directly, leaving `zlevarsetfn` with zero call sites in the
17723    /// tree. Any test that ran after one which probed a tty (there is one
17724    /// eight tests earlier in this very module) saw the stale probe value.
17725    #[test]
17726    fn explicit_columns_assignment_overrides_an_already_cached_zterm_columns() {
17727        let _g = crate::test_util::global_state_lock();
17728        let saved_columns = ZTERM_COLUMNS.load(Ordering::SeqCst);
17729        let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
17730        crate::ported::init::SHTTY.store(-1, Ordering::Relaxed); // c:1900
17731
17732        // Stand in for a completed `adjustwinsize` probe (c:1862).
17733        ZTERM_COLUMNS.store(77, Ordering::SeqCst);
17734        setiparam("COLUMNS", 61); // c:4230 via c:2874 setfn dispatch
17735        let seen = adjustcolumns();
17736        let global = ZTERM_COLUMNS.load(Ordering::SeqCst);
17737
17738        crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
17739        ZTERM_COLUMNS.store(saved_columns, Ordering::SeqCst);
17740
17741        assert_eq!(
17742            global, 61,
17743            "the assignment must land in the zterm_columns GLOBAL, not just \
17744             the parameter (c:Src/params.c:4230 `*p = x`)"
17745        );
17746        assert_eq!(
17747            seen, 61,
17748            "a cached zterm_columns must not survive an explicit COLUMNS="
17749        );
17750    }
17751}
17752
17753// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
17754// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
17755// pipeline. Extracted body of zerrmsg's `%e` arm (Src/utils.c:355-366):
17756// EINTR → "interrupt", EIO verbatim, else lowercased strerror.
17757// zerrmsg's errno branch routes through this so the prefix
17758// formatting matches C; module callers (zsh/system) embed the
17759// string in their own zwarnnam messages where C uses `%e`.
17760/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
17761/// standalone function.
17762/// C has no such function: the errno-to-message rendering is the `case 'e':`
17763/// arm inside `zerrmsg` at `Src/utils.c:352-369` — EINTR becomes the literal
17764/// `interrupt`, EIO is printed verbatim, everything else is `strerror(num)`
17765/// with `tulower(errmsg[0])` applied to the first byte (c:366). Rust needs it
17766/// as a value-returning function because callers format their own output.
17767/// Render an errno the way zsh's `%e` warning format does —
17768/// `zerrmsg` case `'e'` at `Src/utils.c:352-368`:
17769///
17770/// - `EINTR` → the literal string `interrupt` (C also sets
17771///   `errflag |= ERRFLAG_ERROR` and truncates the rest of the
17772///   format; every zshrs call site uses `%e` as the final
17773///   conversion so the truncation is a no-op here).
17774/// - `EIO` → `strerror(EIO)` verbatim ("I/O" reads wrong
17775///   lowercased, per the c:361-364 comment).
17776/// - everything else → `strerror(num)` with the first letter
17777///   lowercased (`tulower(errmsg[0])`, c:366).
17778///
17779/// `std::io::Error::from_raw_os_error(n)` Display is NOT this
17780/// format — it appends " (os error N)" and keeps the capital.
17781/// That mismatch was the residual byte-diff in zsh/system's
17782/// `sysopen`/`zsystem flock` warnings (docs/BUGS.md #316).
17783pub fn zsh_errno_msg(num: i32) -> String {
17784    if num == libc::EINTR {
17785        return "interrupt".to_string(); // c:355-358
17786    }
17787    let msg = unsafe {
17788        let p = libc::strerror(num); // c:360
17789        if p.is_null() {
17790            return String::new();
17791        }
17792        std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
17793    };
17794    if num == libc::EIO {
17795        return msg; // c:363-364
17796    }
17797    // c:366 — `fputc(tulower(errmsg[0]), file);`
17798    let mut chars = msg.chars();
17799    match chars.next() {
17800        Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
17801        None => msg,
17802    }
17803}
17804
17805// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
17806// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
17807// pipeline. Byte-exact inverse of metafy (Src/utils.c:4954 unmetafy):
17808// decodes Meta (\u{83}) + c^32 pairs back to raw bytes. C
17809// unmetafy works in place on char*; Rust strings are UTF-8 so
17810// the decode returns Vec<u8> for byte-exact writes.
17811/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
17812/// standalone function.
17813/// Byte-exact inverse of `metafy` for Rust's char-level metafied `String`.
17814/// C's `unmetafy` (`Src/utils.c:4954`) rewrites a `char *` in place; a UTF-8
17815/// `String` cannot hold raw bytes, so this returns `Vec<u8>` instead.
17816/// Decode a char-level metafied String back to RAW bytes for a write
17817/// or exec boundary. c:Src/utils.c:4954 unmetafy — `if (*t++ == Meta
17818/// && *p) t[-1] = *p++ ^ 32;` — transposed onto the char-level
17819/// encoding `meta_encode_byte` produces: U+0083 followed by a scalar
17820/// in U+0080..=U+00FF yields the single raw byte `payload ^ 32`;
17821/// everything else emits its UTF-8 bytes verbatim. A U+0083 followed
17822/// by anything outside that range is not our encoding and passes
17823/// through untouched. Bug #127.
17824pub fn unmetafy_str(s: &str) -> Vec<u8> {
17825    let mut out = Vec::with_capacity(s.len());
17826    let mut it = s.chars().peekable();
17827    let mut buf = [0u8; 4];
17828    while let Some(c) = it.next() {
17829        if c == '\u{83}' {
17830            if let Some(&n) = it.peek() {
17831                let nu = n as u32;
17832                if (0x80..=0xff).contains(&nu) {
17833                    out.push((nu as u8) ^ 32);
17834                    it.next();
17835                    continue;
17836                }
17837            }
17838        }
17839        out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
17840    }
17841    out
17842}