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, untokenize};
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_system_h::DEFAULT_WORDCHARS;
56use crate::ported::ztype_h::{
57    imeta, itok, iwsep, IALNUM, IALPHA, IBLANK, ICNTRL, IDIGIT, IIDENT, IMETA, INBLANK, INULL,
58    IPATTERN, ISEP, ISPECIAL, ITOK, IUSER, IWORD, IWSEP, TYPTAB, TYPTAB_FLAGS, ZISPRINT,
59    ZTF_BANGCHAR, ZTF_INIT, ZTF_INTERACT, ZTF_SP_COMMA,
60};
61
62/// Set a wide-char array from a multibyte source string.
63/// Port of `set_widearray(char *mb_array, Widechar_array wca)` from `Src/utils.c:69`.
64///
65/// ```c
66/// static void set_widearray(char *mb_array, Widechar_array wca) {
67///     if (wca->chars) free(wca->chars);
68///     wca->len = 0;
69///     if (mb_array) {
70///         while (*mb_array) {
71///             if (unsigned char *mb_array <= 0x7f) {
72///                 *wcptr++ = (wchar_t)*mb_array++;
73///                 continue;
74///             }
75///             mblen = mb_metacharlenconv(mb_array, &wci);
76///             if (!mblen) break;
77///             if (wci == WEOF) return;  // any non-convertible aborts
78///             *wcptr++ = (wchar_t)wci;
79///             mb_array += mblen;
80///         }
81///         wca->chars = malloc(...); wca->len = wcptr - tmpwcs;
82///     }
83/// }
84/// ```
85///
86/// Build a wide-char array from a metafied multibyte source string.
87/// C uses `mb_metacharlenconv()` to walk Meta-encoded sequences;
88/// Rust port unmetafies first, then collects chars (the
89/// equivalent: walk Unicode codepoints).
90///
91/// Returns the new vec; caller assigns to the appropriate slot
92/// (`WORDCHARS_w`, `IFS_w`, etc.). C aborts on non-convertible
93/// chars (returns without setting `wca->chars`); Rust port mirrors
94/// by returning empty Vec when conversion fails.
95/// WARNING: param names don't match C — Rust=(mb_array) vs C=(mb_array, wca)
96// Rust idiom replacement: `unmetafy` + `str::chars` covers the C
97// `mbsrtowcs`+`mbstate_t` conversion; the C `wca` out-param drops
98// since the Vec is returned by value.
99/// `set_widearray` — see implementation.
100pub fn set_widearray(mb_array: &str) -> Vec<char> {
101    let mut bytes = mb_array.as_bytes().to_vec();
102    unmetafy(&mut bytes);
103    match std::str::from_utf8(&bytes) {
104        Ok(s) => s.chars().collect(),
105        Err(_) => Vec::new(),
106    }
107}
108
109// =====================================================================
110// Port of `zwarning(const char *cmd, const char *fmt, va_list ap)` from `Src/utils.c:142`.
111// =====================================================================
112
113/// Port of `zwarning(const char *cmd, const char *fmt, va_list ap)` from `Src/utils.c:142`.
114///
115/// Internal helper that builds the diagnostic prefix and emits it +
116/// the formatted message to stderr. Direct C body translation:
117///
118/// ```c
119/// if (isatty(2)) zleentry(ZLE_CMD_TRASH);
120/// char *prefix = scriptname ? scriptname : (argzero ? argzero : "");
121/// if (cmd) {
122///     if (unset(SHINSTDIN) || locallevel) {
123///         nicezputs(prefix, stderr);
124///         fputc(':', stderr);
125///     }
126///     nicezputs(cmd, stderr);
127///     fputc(':', stderr);
128/// } else {
129///     nicezputs((isset(SHINSTDIN) && !locallevel) ? "zsh" : prefix, stderr);
130///     fputc(':', stderr);
131/// }
132/// zerrmsg(stderr, fmt, ap);
133/// ```
134/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd, fmt, ap)
135fn zwarning(cmd: Option<&str>, msg: &str) {
136    // c:96 — `if (isatty(2)) zleentry(ZLE_CMD_TRASH);`
137    // Flush any in-flight ZLE redraw state before the warning lands
138    // on stderr — without this, half-painted edit lines bleed into
139    // the diagnostic. Previously: `let _ = isatty(2);` (the result
140    // was discarded; the canonical zleentry port at init.rs:905 was
141    // never actually called from the warning path).
142    if unsafe { libc::isatty(2) } != 0 {
143        // c:96
144        let _ = zleentry(
145            // c:96
146            ZLE_CMD_TRASH, // c:96
147        );
148    }
149    let scriptname = scriptname_lock().lock().unwrap().clone();
150    let argzero = argzero_lock().lock().unwrap().clone();
151    let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
152    let prefix: String = scriptname.or(argzero).unwrap_or_default();
153    let stderr_handle = io::stderr();
154    let mut stderr_lock = stderr_handle.lock();
155    if let Some(cmd) = cmd {
156        // c:107-110 — `if (unset(SHINSTDIN) || locallevel) {
157        //                 nicezputs(prefix, stderr); fputc(':', stderr); }`
158        if unset(SHINSTDIN) || locallevel != 0 {
159            let _ = nicezputs(&prefix, &mut stderr_lock); // c:108
160            let _ = stderr_lock.write_all(b":");
161        }
162        let _ = nicezputs(cmd, &mut stderr_lock); // c:111
163        let _ = stderr_lock.write_all(b":");
164    } else {
165        // c:114 — `nicezputs((isset(SHINSTDIN) && !locallevel) ? "zsh" : prefix, stderr);`
166        let to_emit = if isset(SHINSTDIN) && locallevel == 0 {
167            "zsh"
168        } else {
169            prefix.as_str()
170        };
171        let _ = nicezputs(to_emit, &mut stderr_lock); // c:114
172        let _ = stderr_lock.write_all(b":");
173    }
174    // c:116 — `zerrmsg(stderr, fmt, ap)` — lineno prefix + message.
175    // Pre-built `msg: &str` covers C's va_list; zerrmsg port hasn't
176    // had its `(file, fmt, ap)` signature wired yet so the lineno
177    // prefix + write is inlined here against the same `unset(SHINSTDIN)`
178    // gate C uses at c:301.
179    let lineno = lineno() as i32;
180    if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
181        let _ = stderr_lock.write_all(format!("{}: ", lineno).as_bytes());
182    } else {
183        let _ = stderr_lock.write_all(b" ");
184    }
185    let _ = stderr_lock.write_all(msg.as_bytes());
186    let _ = stderr_lock.write_all(b"\n");
187    let _ = stderr_lock.flush();
188}
189
190// =====================================================================
191// Port of `zerr(VA_ALIST1(const char *fmt))` / `zerrnam` / `zwarn` / `zwarnnam` from utils.c:173
192// onward. Each is a thin wrapper: check errflag/noerrs guards, set
193// `ERRFLAG_ERROR` on the fatal variants, call `zwarning`.
194// =====================================================================
195
196/// Port of `zerr(VA_ALIST1(const char *fmt))` from `Src/utils.c:173`.
197///
198/// ```c
199/// if (errflag || noerrs) {
200///     if (noerrs < 2) errflag |= ERRFLAG_ERROR;
201///     return;
202/// }
203/// errflag |= ERRFLAG_ERROR;
204/// zwarning(NULL, fmt, ap);
205/// ```
206/// WARNING: param names don't match C — Rust=(msg) vs C=()
207pub fn zerr(msg: &str) {
208    // c:173
209    let noerrs = *noerrs_lock().lock().unwrap();
210    if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
211        // c:175
212        if noerrs < 2 {
213            // c:176
214            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:176
215        }
216        return; // c:177
217    }
218    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:194
219    zwarning(None, msg); // c:194
220}
221
222/// Port of `zerrnam(VA_ALIST2(const char *cmd, const char *fmt))` from `Src/utils.c:194`.
223///
224/// ```c
225/// if (errflag || noerrs) return;
226/// errflag |= ERRFLAG_ERROR;
227/// zwarning(cmd, fmt, ap);
228/// ```
229/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd)
230pub fn zerrnam(cmd: &str, msg: &str) {
231    // c:194
232    let noerrs = *noerrs_lock().lock().unwrap();
233    if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
234        // c:196
235        return;
236    }
237    errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:214
238    zwarning(Some(cmd), msg); // c:214
239}
240
241/// Port of `zwarn(VA_ALIST1(const char *fmt))` from `Src/utils.c:214`.
242///
243/// ```c
244/// if (errflag || noerrs) return;
245/// zwarning(NULL, fmt, ap);
246/// ```
247/// WARNING: param names don't match C — Rust=(msg) vs C=()
248pub fn zwarn(msg: &str) {
249    // c:214
250    let noerrs = *noerrs_lock().lock().unwrap();
251    if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
252        // c:216
253        return;
254    }
255    zwarning(None, msg); // c:231
256}
257
258/// Port of `zwarnnam(VA_ALIST2(const char *cmd, const char *fmt))` from `Src/utils.c:231`.
259///
260/// ```c
261/// if (errflag || noerrs) return;
262/// zwarning(cmd, fmt, ap);
263/// ```
264/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd)
265pub fn zwarnnam(cmd: &str, msg: &str) {
266    // c:231
267    let noerrs = *noerrs_lock().lock().unwrap();
268    if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
269        // c:233
270        return;
271    }
272    zwarning(Some(cmd), msg); // c:235
273}
274
275/// Port of `dputs(VA_ALIST1(const char *message))` from `Src/utils.c:253`.
276///
277/// C body (c:253-270):
278/// ```c
279/// mod_export void dputs(const char *message)
280/// {
281///     char *filename;
282///     FILE *file;
283///     if ((filename = getsparam_u("ZSH_DEBUG_LOG")) != NULL &&
284///         (file = fopen(filename, "a")) != NULL) {
285///         zerrmsg(file, message, ap);
286///         fclose(file);
287///     } else
288///         zerrmsg(stderr, message, ap);
289/// }
290/// ```
291///
292/// Previous Rust port was a FAKE — `eprintln!("BUG: {}", msg)` ignored
293/// `$ZSH_DEBUG_LOG` entirely (which is the whole point of dputs — to
294/// route debug output to a user-specified log file). Re-port now
295/// consults paramtab via `getsparam_u`, opens the log in append mode,
296/// and routes the message there; falls back to stderr per c:268.
297///
298/// Rust signature drift: takes `&str` instead of va_args; callers
299/// pre-format via Rust's `format!` (same pattern as `zerrmsg`).
300pub fn dputs(msg: &str) {
301    // c:253
302    // c:263 — `getsparam_u("ZSH_DEBUG_LOG")`. The `_u` variant
303    // unmetafies the result (utils.rs's getsparam_u port at
304    // params.rs:3831 wraps getsparam + unmeta).
305    let log_file = getsparam_u("ZSH_DEBUG_LOG"); // c:263
306                                                 // c:264 — `fopen(filename, "a")` — append mode.
307    let opened = log_file.as_ref().and_then(|p| {
308        // c:264
309        fs::OpenOptions::new()
310            .create(true)
311            .append(true)
312            .open(p)
313            .ok()
314    });
315    // Shared format logic: lineno prefix + msg + newline, matching
316    // zerrmsg at c:296-308. Built once, written to file or stderr.
317    let lineno = lineno() as i32;
318    let shinstdin = isset(SHINSTDIN);
319    let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
320    let prefix = if (!shinstdin || locallevel != 0) && lineno != 0 {
321        format!("{}: ", lineno)
322    } else {
323        String::new()
324    };
325    let line = format!("{}{}\n", prefix, msg);
326    if let Some(mut f) = opened {
327        // c:265 zerrmsg(file, ...)
328        let _ = f.write_all(line.as_bytes()); // c:265
329                                              // c:266 — `fclose(file)` — handled by Drop when `f` goes out of scope.
330    } else {
331        // c:267 else stderr
332        let _ = io::stderr().write_all(line.as_bytes()); // c:268 zerrmsg(stderr, ...)
333    }
334}
335
336// ---------------------------------------------------------------------------
337// Remaining 33 missing utils.c functions
338// ---------------------------------------------------------------------------
339
340// `zwarning` is defined earlier in this file as the real port of
341// utils.c:142 (private helper invoked by `zerr`/`zerrnam`/`zwarn`/
342// `zwarnnam`). The duplicate stub previously here has been deleted
343// — callers use the four public entry points instead.
344
345/// Port of `void zz_plural_z_alpha(void)` from Src/utils.c:282.
346///
347/// Cygwin-only no-op symbol the C source emits to work around a
348/// dllwrap bug that drops the last alphabetically-sorted exported
349/// symbol (zwarnnam). The Rust port has no equivalent linker
350/// problem; this is preserved as a no-op for symbol-table parity.
351pub fn zz_plural_z_alpha() {} // c:282
352
353/// Port of `zerrmsg(FILE *file, const char *fmt, va_list ap)` from `Src/utils.c:289`.
354///
355/// C body emits the formatted message + (when locallevel > 0 or
356/// SHINSTDIN unset) the line number prefix + "\n". The Rust port
357/// is invoked indirectly through `zwarning` — direct callers pass
358/// pre-formatted strings.
359/// WARNING: param names don't match C — Rust=(msg, errno) vs C=(file, fmt, ap)
360// Rust idiom replacement: pre-formatted `msg: &str` covers the C
361// va_list expansion; the C `file`+`fmt`+`ap` triplet collapses
362// because callers (zerr/zwarning) pre-format via Rust's `format!`.
363/// `zerrmsg` — see implementation.
364pub fn zerrmsg(msg: &str, errno: Option<i32>) {
365    // c:289
366    // c:296 — `lineno` is the parser-advanced line counter. The
367    // previous Rust port read `lineno_lock` (a Mutex<i32> in utils.rs)
368    // that ONLY fusevm_bridge::set_lineno wrote to; the actual
369    // parser uses lex::LEX_LINENO (thread_local Cell<u64>) updated
370    // by every newline scan. Result: error messages emitted from
371    // parse-time always printed line 0 (or last fusevm_bridge value)
372    // rather than the actual line of the syntax error.
373    //
374    // Route through lex::lineno() so the parser-advanced counter
375    // drives the error prefix.
376    let lineno = lineno() as i32;
377    let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
378    // c:301-308 — `if ((unset(SHINSTDIN) || locallevel) && lineno)
379    //                 fprintf(file, "%d: ", lineno); else fputc(' ', file);`
380    if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
381        eprint!("{}: ", lineno);
382    } else {
383        eprint!(" ");
384    }
385    if let Some(e) = errno {
386        // c:348-365 — `%e`: render the errno exactly as C's zerrmsg does,
387        // through `zsh_errno_msg` (EINTR→"interrupt", EIO verbatim, else
388        // strerror with a lowercased first letter). The previous
389        // `io::Error::from_raw_os_error` kept the capital and appended
390        // " (os error N)" — the byte-diff tracked as docs/BUGS.md #316.
391        if e == libc::EINTR {
392            // c:351-354 — EINTR sets the error flag and stops formatting.
393            errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
394        }
395        eprintln!("{}: {}", msg, zsh_errno_msg(e));
396    } else {
397        eprintln!("{}", msg);
398    }
399}
400
401/// Port of `void zsetupterm(void)` from Src/utils.c:390.
402///
403/// Reference-counts terminfo's `cur_term` setup. The C source
404/// guards with `#ifdef HAVE_SETUPTERM`; without terminfo this is a
405/// pure no-op. Rust's `term`-style state lives elsewhere (modules
406/// like `zle/termcap` initialize on demand), so this is a no-op
407/// counter for symbol-table parity.
408pub fn zsetupterm() {
409    // c:390
410    static TERM_COUNT: std::sync::atomic::AtomicI32 = // c:402
411        std::sync::atomic::AtomicI32::new(0);
412    let tc = TERM_COUNT.load(Ordering::Relaxed);
413    // c:395-396 — DPUTS(term_count < 0 || (term_count > 0 && !cur_term),
414    //                   "inconsistent term_count and/or cur_term");
415    // The Rust port has no `cur_term` analogue (terminfo state lives
416    // in zle/termcap module), so the second condition reduces to
417    // `term_count > 0 && true` if we treat cur_term as the count
418    // itself. Simplified to the bare `term_count < 0` invariant.
419    DPUTS!(tc < 0, "inconsistent term_count and/or cur_term"); // c:395-396
420    TERM_COUNT.fetch_add(1, Ordering::Relaxed); // c:402
421}
422
423/// Port of `void zdeleteterm(void)` from Src/utils.c:409.
424pub fn zdeleteterm() {
425    // c:409
426    static TERM_COUNT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
427    let tc = TERM_COUNT.load(Ordering::Relaxed);
428    // c:412-413 — DPUTS(term_count < 1 || !cur_term,
429    //                   "inconsistent term_count and/or cur_term");
430    DPUTS!(tc < 1, "inconsistent term_count and/or cur_term"); // c:412-413
431    if tc > 0 {
432        TERM_COUNT.fetch_sub(1, Ordering::Relaxed); // c:414
433    }
434}
435
436/// Port of `int putraw(int c)` from Src/utils.c:424. Writes a
437/// single byte to stdout for the termcap library, returning 0.
438pub fn putraw(c: char) -> i32 {
439    // c:424
440    print!("{}", c); // c:434
441    0 // c:434
442}
443
444/// Port of `int putshout(int c)` from Src/utils.c:434. Writes a
445/// single byte to `shout` (zsh's interactive stdout — falls back
446/// to stdout in zshrs's static-link path), returning 0.
447pub fn putshout(c: char) -> i32 {
448    // c:434
449    print!("{}", c); // c:434
450    0 // c:437
451}
452
453/// Port of `nicechar_sel(int c, int quotable)` from `Src/utils.c:462`.
454/// Renders one byte as its `^X` / `M-X` / `\\n` / `\\t` display form;
455/// `quotable=true` emits `\\C-X` instead of `^X` so the result is
456/// shell-quotable.
457pub fn nicechar_sel(c: char, quotable: bool) -> String {
458    // c:462
459    // c:466 — `c &= 0xff;` mask to byte before any classification.
460    let mut c = (c as u32) & 0xff;
461    let mut out = String::new();
462    // c:467 — `if (ZISPRINT(c)) goto done;`. Use the canonical
463    // `ZISPRINT` predicate (port of ztype.h:89 — IPRINT typtab bit
464    // AND != 0x7f). Previously used a custom `is_print` closure that
465    // wrongly accepted 0xa0+ as printable — diverged from C ZISPRINT
466    // for every high-bit byte under PRINTEIGHTBIT-off, breaking
467    // `\M-X` escape generation.
468    if !ZISPRINT(c as u8) {
469        // c:467
470        if c & 0x80 != 0 {
471            // c:469
472            if isset(PRINTEIGHTBIT) { // c:470
473                 // c:471 — goto done (output raw); c unchanged.
474            } else {
475                out.push_str("\\M-"); // c:472-474
476                c &= 0x7f; // c:475
477                if ZISPRINT(c as u8) {
478                    // c:476-477
479                    // c:477 — goto done after writing \M- + ASCII char.
480                    if let Some(ch) = char::from_u32(c) {
481                        out.push(ch);
482                    }
483                    return out;
484                }
485            }
486        }
487        if c == 0x7f {
488            // c:479
489            out.push_str(if quotable { "\\C-" } else { "^" }); // c:481-486
490            c = b'?' as u32;
491        } else if c == b'\n' as u32 {
492            // c:487
493            out.push('\\');
494            c = b'n' as u32;
495        } else if c == b'\t' as u32 {
496            // c:490
497            out.push('\\');
498            c = b't' as u32;
499        } else if c < 0x20 {
500            // c:493
501            out.push_str(if quotable { "\\C-" } else { "^" }); // c:494-499
502            c += 0x40;
503        }
504    }
505    if let Some(ch) = char::from_u32(c) {
506        // c:511
507        out.push(ch);
508    }
509    out
510}
511
512/// Port of `nicechar(int c)` from Src/utils.c:520. C body:
513///     `return nicechar_sel(c, 0);`
514pub fn nicechar(c: char) -> String {
515    // c:520
516    nicechar_sel(c, false) // c:523
517}
518
519/// Port of `is_nicechar(int c)` from `Src/utils.c:531-539`.
520/// ```c
521/// c &= 0xff;
522/// if (ZISPRINT(c)) return 0;
523/// if (c & 0x80)    return !isset(PRINTEIGHTBIT);
524/// return (c == 0x7f || c == '\n' || c == '\t' || c < 0x20);
525/// ```
526/// "Nice" means "needs escape-formatting when printed" — so
527/// returns true for control chars + (under PRINTEIGHTBIT off)
528/// high-bit bytes. The previous Rust port treated all `!c.is_ascii()`
529/// as nice unconditionally — divergent for users running with
530/// `setopt printeightbit` (very common for non-ASCII filenames).
531pub fn is_nicechar(c: char) -> bool {
532    let cu = (c as u32) & 0xff;
533    // c:534 — `if (ZISPRINT(c)) return 0;` — printable ASCII is not nice.
534    if ZISPRINT(cu as u8) {
535        return false;
536    }
537    // c:536 — high-bit byte path.
538    if (cu & 0x80) != 0 {
539        return !isset(PRINTEIGHTBIT);
540    }
541    // c:538 — ASCII control chars (DEL/\n/\t/<0x20).
542    cu == 0x7f || cu == b'\n' as u32 || cu == b'\t' as u32 || cu < 0x20
543}
544
545/// Initialize multibyte state (from utils.c mb_charinit) - no-op in Rust
546/// Port of `mb_charinit` from `Src/utils.c:553`.
547pub fn mb_charinit() {
548    // Rust handles UTF-8 natively
549}
550
551/// Port of `wcs_nicechar_sel(wchar_t c, size_t *widthp, char **swidep,
552/// int quotable)` from `Src/utils.c:593-705`. Four branches per C:
553///   1. c < 0x80 and not printable: control-char escape (\n, \t, ^X, \C-X)
554///      — same as `nicechar_sel`.
555///   2. c >= 0x80 printable AND PRINTEIGHTBIT set: emit raw UTF-8 bytes.
556///   3. c >= 0x80 fits in UTF-8: emit UTF-8 bytes (default-on for MULTIBYTE).
557///   4. c >= 0x10000: `\U%.8x` 8-digit hex; c >= 0x100: `\u%.4x` 4-digit hex.
558///
559/// Param mapping (Rule S1, faithful to C):
560/// - `widthp: Option<&mut usize>` ← C `size_t *widthp` (`None` ≡ `NULL`).
561///   Set to the display column width of the produced sequence.
562/// - `swidep: Option<&mut usize>` ← C `char **swidep` (`None` ≡ `NULL`).
563///   Set to the byte index in the returned string where the post-wide
564///   meta-prefixed bytes begin (i.e. boundary between display-width
565///   characters and trailing Meta+X encoding bytes that don't add to
566///   column position).
567pub fn wcs_nicechar_sel(
568    c: char,
569    widthp: Option<&mut usize>,
570    swidep: Option<&mut usize>,
571    quotable: bool,
572) -> String {
573    // c:593
574    let cv = c as u32;
575    // c:616 — `if (!WC_ISPRINT(c) && (c < 0x80 || !isset(PRINTEIGHTBIT)))`.
576    // The non-printable + (low-ASCII or PRINTEIGHTBIT-off) branch.
577    let print_eightbit = isset(PRINTEIGHTBIT);
578    let is_printable = u9_iswprint(c);
579    let buf: String;
580    if !is_printable && (cv < 0x80 || !print_eightbit) {
581        if cv == 0x7f {
582            // c:617-624 — DEL: `^?` / `\C-?`
583            buf = if quotable {
584                "\\C-?".to_string()
585            } else {
586                "^?".to_string()
587            };
588            // c:686 widthp = (s - buf) + wcw (wcw for '?' = 1).
589            if let Some(wp) = widthp {
590                *wp = buf.chars().count();
591            }
592            if let Some(sp) = swidep {
593                *sp = buf.len();
594            }
595            return buf;
596        } else if c == '\n' {
597            // c:625-627 — `\n` literal.
598            buf = "\\n".to_string();
599            if let Some(wp) = widthp {
600                *wp = 2;
601            }
602            if let Some(sp) = swidep {
603                *sp = buf.len();
604            }
605            return buf;
606        } else if c == '\t' {
607            // c:628-630 — `\t` literal.
608            buf = "\\t".to_string();
609            if let Some(wp) = widthp {
610                *wp = 2;
611            }
612            if let Some(sp) = swidep {
613                *sp = buf.len();
614            }
615            return buf;
616        } else if cv < 0x20 {
617            // c:631-638 — ^X / \C-X for controls (excluding \n, \t).
618            let cc = (cv + 0x40) as u8 as char;
619            buf = if quotable {
620                format!("\\C-{}", cc)
621            } else {
622                format!("^{}", cc)
623            };
624            if let Some(wp) = widthp {
625                *wp = buf.chars().count();
626            }
627            if let Some(sp) = swidep {
628                *sp = buf.len();
629            }
630            return buf;
631        }
632        // c:639-641 — c >= 0x80 non-printable falls through to ret=-1
633        // path (hex escape).
634    } else if cv < 0x80 {
635        // c:644-704 — printable ASCII: emit raw char. wcw=1.
636        buf = c.to_string();
637        if let Some(wp) = widthp {
638            *wp = 1;
639        }
640        if let Some(sp) = swidep {
641            *sp = buf.len();
642        }
643        return buf;
644    }
645    // c:644-678 — high-bit char: try UTF-8 encode first.
646    if u9_iswprint(c) {
647        // c:681-693 — printable wide char: emit raw UTF-8.
648        buf = c.to_string();
649        let wcw = zwcwidth(c) as usize;
650        if let Some(wp) = widthp {
651            *wp = wcw;
652        }
653        if let Some(sp) = swidep {
654            *sp = buf.len();
655        }
656        return buf;
657    }
658    // c:656-678 — non-printable wide: hex escape (or fall back to byte
659    // nicechar for c < 0x100).
660    if cv >= 0x10000 {
661        // c:656-659 — `\U%.8x` (10 chars).
662        buf = format!("\\U{:08x}", cv);
663        if let Some(wp) = widthp {
664            *wp = 10;
665        }
666        if let Some(sp) = swidep {
667            *sp = buf.len();
668        }
669        buf
670    } else if cv >= 0x100 {
671        // c:660-663 — `\u%.4x` (6 chars).
672        buf = format!("\\u{:04x}", cv);
673        if let Some(wp) = widthp {
674            *wp = 6;
675        }
676        if let Some(sp) = swidep {
677            *sp = buf.len();
678        }
679        buf
680    } else {
681        // c:664-674 — fall back to byte nicechar_sel.
682        buf = nicechar_sel(c, quotable);
683        // c:670-671 — `*widthp = ztrlen(buf);` (display width respects
684        // metafied chars). `ztrlen` counts visible cells.
685        if let Some(wp) = widthp {
686            *wp = ztrlen(&buf);
687        }
688        // c:672-673 — `*swidep = buf + strlen(buf);` (no trailing meta
689        // since nicechar_sel ASCII output).
690        if let Some(sp) = swidep {
691            *sp = buf.len();
692        }
693        buf
694    }
695}
696
697/// Port of `wcs_nicechar(wchar_t c, size_t *widthp, char **swidep)` from `Src/utils.c:709`.
698/// C body: `return wcs_nicechar_sel(c, widthp, swidep, 0);`
699pub fn wcs_nicechar(c: char, widthp: Option<&mut usize>, swidep: Option<&mut usize>) -> String {
700    // c:709
701    wcs_nicechar_sel(c, widthp, swidep, false) // c:711
702}
703
704/// Port of `int is_wcs_nicechar(wchar_t c)` from Src/utils.c:720.
705///
706/// "Return 1 if wcs_nicechar() would reformat this character for
707/// display." Mirrors the C condition: non-printable AND (low ASCII
708/// OR PRINTEIGHTBIT unset) for control chars; for high bytes, true
709/// when ≥0x100 or `is_nicechar` says so.
710pub fn is_wcs_nicechar(c: char) -> bool {
711    // c:720
712    let cv = c as u32;
713    let printable = !c.is_control() && cv >= 0x20;
714    let print_eight = isset(PRINTEIGHTBIT); // c:722
715    if !printable && (cv < 0x80 || !print_eight) {
716        if cv == 0x7f || c == '\n' || c == '\t' || cv < 0x20 {
717            // c:734
718            return true;
719        }
720        if cv >= 0x80 {
721            // c:734
722            return cv >= 0x100 || is_nicechar(c); // c:734
723        }
724    }
725    false // c:734
726}
727
728/// Get wide character width (from utils.c zwcwidth)
729/// Port of `int zwcwidth(wint_t wc)` from `Src/utils.c:734`.
730///
731/// C body (c:734-745):
732/// ```c
733/// int wcw;
734/// /* assume a single-byte character if not valid */
735/// if (wc == WEOF || unset(MULTIBYTE))                  // c:738
736///     return 1;
737/// wcw = WCWIDTH(wc);                                   // c:740
738/// /* if not printable, assume width 1 */
739/// if (wcw < 0)                                         // c:742
740///     return 1;
741/// return wcw;                                          // c:744
742/// ```
743///
744/// The previous Rust port skipped the `unset(MULTIBYTE)` early-
745/// return (c:738). When the MULTIBYTE option is OFF (set via
746/// `setopt nomultibyte` / `set +o multibyte`), C zsh treats every
747/// codepoint as a single-byte char and returns width 1 unconditionally.
748/// Without the option check, the Rust port would still report the
749/// Unicode-width-table answer (2 for CJK, 0 for combining marks)
750/// in single-byte mode — diverging from prompt/refresh layout that
751/// relies on the option as the source of truth.
752pub fn zwcwidth(wc: char) -> usize {
753    // c:734
754    // c:738 — `if (wc == WEOF || unset(MULTIBYTE)) return 1;`. WEOF
755    // path is Rust-impossible (char is always a valid scalar). The
756    // MULTIBYTE option gate maps to the canonical option register.
757    if !isset(MULTIBYTE) {
758        // c:738
759        return 1; // c:739
760    }
761    // c:740-744 — WCWIDTH(wc); negative result → width 1.
762    unicode_width::UnicodeWidthChar::width(wc).unwrap_or(1) // c:740-744
763}
764
765/// Port of `pathprog(char *prog, char **namep)` from `Src/utils.c:760-786`.
766/// ```c
767/// for (pp = path; *pp; pp++) {
768///     sprintf(buf, "%s/%s", *pp, prog);
769///     funmeta = unmeta(buf);
770///     if (access(funmeta, F_OK) == 0 && stat(funmeta, &st) >= 0 &&
771///         !S_ISDIR(st.st_mode)) {
772///         return funmeta;
773///     }
774/// }
775/// return NULL;
776/// ```
777/// C checks: (1) F_OK = exists, (2) stat succeeds, (3) NOT a directory.
778/// NO executable-bit check. Used by autoload / `which` paths that
779/// need to find any file in PATH, not just executables.
780///
781/// Previously the Rust port added a `mode & 0o111 != 0` executable
782/// check — divergent, made every `pathprog` lookup miss non-executable
783/// files (e.g. autoload-function plaintext scripts that don't have
784/// +x set).
785/// WARNING: param names don't match C — Rust=(prog) vs C=(prog, namep)
786pub fn pathprog(prog: &str) -> Option<PathBuf> {
787    // c:760
788    // The early-return on `prog` containing '/' is a Rust-port
789    // convenience NOT in C's pathprog. C unconditionally walks $PATH
790    // and prefixes each entry. The convenience is harmless since
791    // C's caller (`findcmd`) handles slashes separately before
792    // calling pathprog.
793    if prog.contains('/') {
794        let p = PathBuf::from(prog);
795        return if p.exists() { Some(p) } else { None };
796    }
797    if let Some(path_var) = getsparam("PATH") {
798        for dir in path_var.split(':') {
799            // c:773
800            let full_path = PathBuf::from(dir).join(prog);
801            // c:776 — `funmeta = unmeta(buf)`. The previous Rust port
802            // passed the raw composed path to `fs::metadata`, missing
803            // the unmeta step. Paths containing Meta-encoded bytes
804            // (from PATH entries or prog name with metafy lead bytes)
805            // would silently miss valid executables.
806            let unmeta_path = unmeta(full_path.to_str().unwrap_or("")); // c:776 unmeta(buf)
807                                                                        // c:777-779 — `access(F_OK) == 0 && stat >= 0 && !S_ISDIR`.
808                                                                        // is_file() folds existence + stat + not-dir into one.
809            if let Ok(meta) = fs::metadata(&unmeta_path) {
810                if meta.is_file() {
811                    // Return the unmeta'd path since that's what
812                    // C does (funmeta is the returned value).
813                    return Some(PathBuf::from(unmeta_path));
814                }
815            }
816        }
817    }
818    None
819}
820
821/// Port of `findpwd(char *s)` from `Src/utils.c:792`.
822///
823/// ```c
824/// char *findpwd(char *s)
825/// {
826///     char *t;
827///     if (*s == '/')
828///         return xsymlink(s, 0);
829///     s = tricat((pwd[1]) ? pwd : "", "/", s);
830///     t = xsymlink(s, 0);
831///     zsfree(s);
832///     return t;
833/// }
834/// ```
835///
836/// Resolve `s` to its canonical form. Absolute paths route through
837/// `xsymlink` directly; relative paths get prefixed with the
838/// current `pwd` first.
839///
840/// Signature note: C takes `s: char *`. The previous Rust port had
841/// no parameter (returning the cwd) — completely wrong. New port
842/// matches C: takes `&str`, returns `Option<String>` (xsymlink can
843/// return NULL).
844// get a symlink-free pathname for s relative to PWD                        // c:792
845/// `findpwd` — see implementation.
846pub fn findpwd(s: &str) -> Option<String> {
847    // c:792
848    if s.starts_with('/') {
849        // c:792
850        return xsymlink(s); // c:797
851    }
852    // C: tricat((pwd[1]) ? pwd : "", "/", s) — uses the global
853    // `pwd` (logical cwd; differs from realpath when chasing
854    // symlinks is disabled). The Rust port reads `$PWD` since
855    // shell-set `PWD` mirrors C's `pwd` global; falls back to
856    // `getcwd()` when unset.
857    let pwd = getsparam("PWD")
858        .or_else(|| {
859            std::env::current_dir()
860                .ok()
861                .map(|p| p.to_string_lossy().into_owned())
862        })
863        .unwrap_or_default(); // c:798
864    let prefix: &str = if pwd.len() > 1 { &pwd } else { "" }; // c:798 pwd[1]
865    let combined = format!("{}/{}", prefix, s); // c:798
866    xsymlink(&combined) // c:799
867}
868
869/// Validate an inherited `$PWD` exactly like zsh's ispwd() at
870/// src/zsh/Src/utils.c:809: PWD must be absolute, must stat to the
871/// same dev+inode as ".", and must contain no `.` or `..` components.
872/// When this returns false, callers should fall back to `getcwd()`.
873pub(crate) fn ispwd(pwd: &str) -> bool {
874    if !pwd.starts_with('/') {
875        return false;
876    }
877    let pwd_meta = match fs::metadata(pwd) {
878        Ok(m) => m,
879        Err(_) => return false,
880    };
881    let dot_meta = match fs::metadata(".") {
882        Ok(m) => m,
883        Err(_) => return false,
884    };
885    if pwd_meta.dev() != dot_meta.dev() || pwd_meta.ino() != dot_meta.ino() {
886        return false;
887    }
888    // Reject any component that is exactly `.` or `..` — the same loop
889    // zsh runs after the dev/ino check.
890    for comp in pwd.split('/') {
891        if comp == "." || comp == ".." {
892            return false;
893        }
894    }
895    true
896}
897
898// ---------------------------------------------------------------------------
899// Missing utility functions ported from utils.c
900// ---------------------------------------------------------------------------
901
902/// Split path into components (from utils.c slashsplit).
903///
904/// Port of `static char **slashsplit(char *s)` from `Src/utils.c:837`.
905///
906/// C body (c:837-863):
907/// ```c
908/// if (!*s) return zshcalloc(...);            // c:842 — empty input → empty
909/// for (t = s, t0 = 0; *t; t++)               // c:845 — count slashes
910///     if (*t == '/') t0++;
911/// q = r = zalloc(sizeof(char*) * (t0 + 2));
912/// while ((t = strchr(s, '/'))) {             // c:850
913///     *q++ = ztrduppfx(s, t - s);            // c:851 — emit prefix
914///     while (*t == '/') t++;                 // c:852-853 — collapse runs
915///     if (!*t) { *q = NULL; return r; }      // c:854-857 — trailing `/` ends
916///     s = t;
917/// }
918/// *q++ = ztrdup(s);                          // c:860 — final tail
919/// ```
920///
921/// Three behaviors that the previous `split('/').filter(non_empty)`
922/// Rust port got WRONG:
923///   1. **Leading `/` keeps an empty segment** (c:851 with `t == s`).
924///      C: `slashsplit("/usr")` → `["", "usr"]`; previous Rust dropped
925///      the empty, returning `["usr"]`. Caller `xsymlinks` (c:879)
926///      iterates the result building `xbuf` byte-by-byte — the
927///      empty leading segment is how it knows to start from `/`.
928///      Without it, absolute-path resolution silently became relative.
929///   2. **Consecutive slashes collapse** (c:852-853 inner while-loop).
930///      C: `"a//b"` → `["a", "b"]` (drop empty between). Filter-on-
931///      empty Rust gets this right by coincidence.
932///   3. **Trailing `/` drops** (c:854-857). C: `"a/b/"` → `["a", "b"]`.
933///      Filter-on-empty Rust gets this right by coincidence.
934///
935/// Pin: the leading-empty-segment behavior IS the C contract, not
936/// an oversight to filter out. Matches `xsymlinks` and any future
937/// path-walker port that reads slashsplit output.
938pub fn slashsplit(s: &str) -> Vec<String> {
939    // c:837
940    if s.is_empty() {
941        // c:842
942        return Vec::new();
943    }
944    let mut result = Vec::new();
945    let mut rest = s;
946    // c:850 — `while ((t = strchr(s, '/')))`.
947    while let Some(pos) = rest.find('/') {
948        result.push(rest[..pos].to_string()); // c:851 ztrduppfx
949        rest = &rest[pos..];
950        // c:852-853 — `while (*t == '/') t++;` collapse run.
951        while let Some(rest_after) = rest.strip_prefix('/') {
952            rest = rest_after;
953        }
954        // c:854-857 — if walked off end, return without emitting tail.
955        if rest.is_empty() {
956            return result;
957        }
958    }
959    // c:860 — `*q++ = ztrdup(s);` emit final tail.
960    result.push(rest.to_string());
961    result
962}
963
964/// Port of `static int xsymlinks(char *s)` from `Src/utils.c:872`.
965///
966/// Expands `.` and `..` components AND follows ONE LEVEL of
967/// symlinks (per C source comment at c:865-867: "expands .. or .
968/// expressions and one level of symlinks"). Used by the `:A`/`:P`
969/// modifier paths via `subst.rs:6896`.
970///
971/// The previous Rust port did `.`/`..` normalization ONLY, with a
972/// stale doc-comment claiming "Does NOT follow symlinks (matches
973/// the `physical = 0` mode in C)." That claim is FALSE: C
974/// `xsymlinks` IS the symlink-following form (it calls `readlink(2)`
975/// at c:908). The `physical = 0` no-symlink path is a different
976/// fn (`xsymlink` at utils.c:971, without the trailing `s`). Result:
977/// `:A` modifier output never resolved actual symlinks — `:A` on
978/// `/tmp/link -> /usr` returned `/tmp/link` instead of `/usr`.
979///
980/// Rewrite: walk components; for each non-`.`/`..` component, try
981/// `readlink` on the accumulated path. If it succeeds, the target
982/// replaces (or prepends to) the accumulator; otherwise the
983/// component appends as-is. C handles re-rooting (absolute symlink
984/// target replaces buf) and component-level concat.
985pub fn xsymlinks(s: &str) -> io::Result<String> {
986    // c:872
987    if s.is_empty() {
988        return Ok(String::new());
989    }
990
991    let path = if !s.starts_with('/') {
992        // c:879 — slashsplit
993        let cwd = std::env::current_dir()?;
994        format!("{}/{}", cwd.display(), s)
995    } else {
996        s.to_string()
997    };
998
999    let components: Vec<&str> = path.split('/').collect();
1000    // c:877 — `xbuflen = strlen(xbuf)`. Start with empty xbuf for
1001    // absolute paths (the leading "" from slashsplit handles that).
1002    let mut xbuf = String::new();
1003    for comp in components {
1004        match comp {
1005            "" | "." => continue, // c:881
1006            ".." => {
1007                // c:883
1008                if xbuf == "/" || xbuf.is_empty() {
1009                    // c:886-889
1010                    continue;
1011                }
1012                // c:891-895 — walk back one `/`-delimited component.
1013                if let Some(pos) = xbuf.rfind('/') {
1014                    xbuf.truncate(pos);
1015                }
1016            }
1017            c => {
1018                // c:905-907 — `memcpy xbuf2, xbuf` then append `/comp`.
1019                let candidate = format!("{}/{}", xbuf, c); // c:907
1020                                                           // c:908 — `readlink(unmeta(xbuf2), xbuf3, PATH_MAX)`.
1021                #[cfg(unix)]
1022                {
1023                    match fs::read_link(&candidate) {
1024                        // c:908
1025                        Ok(target) => {
1026                            // c:918-933 — successful readlink: target is
1027                            // either absolute (replaces xbuf wholesale) or
1028                            // relative (appends with `/`).
1029                            let t = target.to_string_lossy().into_owned();
1030                            if t.starts_with('/') {
1031                                // c:927
1032                                xbuf = t; // c:928
1033                            } else {
1034                                xbuf = format!("{}/{}", xbuf, t); // c:930-931
1035                            }
1036                            continue;
1037                        }
1038                        Err(_) => {
1039                            // c:909-916 — readlink failed (not a symlink),
1040                            // append the component verbatim.
1041                            xbuf = candidate; // c:910-912
1042                        }
1043                    }
1044                }
1045                #[cfg(not(unix))]
1046                {
1047                    xbuf = candidate;
1048                }
1049            }
1050        }
1051    }
1052
1053    if xbuf.is_empty() {
1054        Ok(if path.starts_with('/') {
1055            "/".to_string()
1056        } else {
1057            ".".to_string()
1058        })
1059    } else {
1060        Ok(xbuf)
1061    }
1062}
1063
1064/// Port of `xsymlink(char *s, int heap)` from `Src/utils.c:971`.
1065///
1066/// ```c
1067/// mod_export char *
1068/// xsymlink(char *s, int heap)
1069/// {
1070///     if (*s != '/')
1071///         return NULL;
1072///     *xbuf = '\0';
1073///     if (!chrealpath(&s, 'P', heap)) {
1074///         zwarn("path expansion failed, using root directory");
1075///         return heap ? dupstring("/") : ztrdup("/");
1076///     }
1077///     return s;
1078/// }
1079/// ```
1080///
1081/// Returns `Some(resolved)` on success, `None` if the path isn't
1082/// absolute (C: returns NULL). On resolve failure emits the same
1083/// "path expansion failed, using root directory" warning and
1084/// returns `Some("/")`.
1085///
1086/// C body (c:971-980):
1087/// ```c
1088/// char *xsymlink(char *s, int heap)
1089/// {
1090///     if (*s != '/') return NULL;
1091///     *xbuf = '\0';
1092///     if (!chrealpath(&s, 'P', heap)) {
1093///         zwarn("path expansion failed, using root directory");
1094///         return heap ? dupstring("/") : ztrdup("/");
1095///     }
1096///     return s;
1097/// }
1098/// ```
1099///
1100/// Previous Rust port was a FAKE — called `std::fs::canonicalize`
1101/// directly with the rationale "same semantics for symlink-
1102/// resolution". That bypassed the canonical `chrealpath` port
1103/// (hist.rs:2311) which handles the partial-prefix walk + xbuf
1104/// state that `fs::canonicalize` doesn't replicate. Re-port now
1105/// matches C line-by-line.
1106///
1107/// Rust signature drift: `heap` param dropped — Rust strings always
1108/// live on the heap; the C `heap` flag toggles between zhalloc
1109/// (heap arena) and ztrdup (process-wide) allocation which has no
1110/// distinction in Rust. Always defaults to `false` (ztrdup arm) at
1111/// the chrealpath call.
1112pub fn xsymlink(path: &str) -> Option<String> {
1113    // c:971
1114    // c:973 — `if (*s != '/') return NULL;`
1115    if !path.starts_with('/') {
1116        // c:973
1117        return None;
1118    }
1119    // c:974 — `*xbuf = '\0';`  Reset the xbuf cursor; no-op in Rust
1120    // (xbuf is an internal chrealpath buffer not exposed at this level).
1121    // c:975 — `if (!chrealpath(&s, 'P', heap))`
1122    match chrealpath(path, b'P', false) {
1123        // c:975
1124        Some(r) => Some(r), // c:979 return s
1125        None => {
1126            // c:976 failure arm
1127            // c:977 — `zwarn("path expansion failed, using root directory");`
1128            zwarn("path expansion failed, using root directory"); // c:977
1129                                                                  // c:978 — `return heap ? dupstring("/") : ztrdup("/");`
1130            Some("/".to_string()) // c:978
1131        }
1132    }
1133}
1134
1135/// Port of `void print_if_link(char *s, int all)` from Src/utils.c:985.
1136///
1137/// "Print arrow + symlink target(s) iff `s` is an absolute path
1138/// pointing through symlinks." When `all` is set, follows the
1139/// chain (`xsymlinks` loop, c:992); otherwise emits just the final
1140/// realpath if it differs from the input. Always relative to
1141/// stdout. The Rust port writes via `print!` to mirror C's
1142/// `printf`/`zputs(stdout)` calls.
1143pub fn print_if_link(s: &str, all: bool) {
1144    // c:985
1145    if !s.starts_with('/') {
1146        // c:987
1147        return;
1148    }
1149    if all {
1150        // c:988
1151        let mut start = s.to_string();
1152        loop {
1153            // c:992
1154            match xsymlinks(&start) {
1155                Ok(target) if !target.is_empty() && target != start => {
1156                    print!(" -> "); // c:994
1157                    print!("{}", if target.is_empty() { "/" } else { &target }); // c:995
1158                    start = target; // c:998-999
1159                }
1160                _ => break, // c:1002
1161            }
1162        }
1163    } else {
1164        // c:1006
1165        // c:1007-1011 — HAVE_MEMCCPY arm: copy s into a PATH_MAX-1 buffer
1166        // and DPUTS1 if the input overflows. Rust's String has no fixed
1167        // PATH_MAX limit but the C parity-check still fires when a
1168        // pathological caller passes a path that wouldn't fit C's
1169        // s_at_entry[PATH_MAX+1] buffer.
1170        DPUTS1!(
1171            // c:1009
1172            s.len() >= libc::PATH_MAX as usize, // c:1008 memccpy returns NULL
1173            "path longer than PATH_MAX: {}",
1174            s // c:1009
1175        );
1176        let s_at_entry = s.to_string(); // c:1013
1177                                        // c:1015 — `if (chrealpath(&s, 'P', 0) && strcmp(s, s_at_entry))`
1178                                        // The previous Rust port called std::fs::canonicalize directly —
1179                                        // a fake that bypassed the canonical chrealpath port (hist.rs:2311).
1180        let mut resolved = s.to_string(); // c:1015 &s in/out
1181        if let Some(r) = chrealpath(&resolved, b'P', false) {
1182            // c:1015
1183            resolved = r;
1184            if resolved != s_at_entry {
1185                // c:1015 strcmp(s, s_at_entry)
1186                print!(" -> "); // c:1016
1187                print!("{}", if resolved.is_empty() { "/" } else { &resolved });
1188                // c:1017
1189            }
1190        }
1191    }
1192    let _ = io::stdout().flush();
1193}
1194
1195/// Port of `void fprintdir(char *s, FILE *f)` from Src/utils.c:1031.
1196///
1197/// "print a directory" — abbreviates `s` via `finddir` (so a path
1198/// matching `$HOME` or a `nameddirtab` entry shows as `~name/...`)
1199/// and returns the rendering. The C source writes to a FILE*; Rust
1200/// returns the string for the caller to print.
1201pub fn fprintdir(s: &str) -> String {
1202    // c:1031
1203    match finddir(s) {
1204        // c:1031
1205        None => unmeta(s),          // c:1036
1206        Some(rendered) => rendered, // c:1038-1040
1207    }
1208}
1209
1210/// Port of `char *substnamedir(char *s)` from Src/utils.c:1053.
1211///
1212/// C body (c:1053-1061):
1213/// ```c
1214/// Nameddir d = finddir(s);
1215/// if (!d)
1216///     return quotestring(s, QT_BACKSLASH);
1217/// return zhtricat("~", d->node.nam, quotestring(s + strlen(d->dir),
1218///                                                QT_BACKSLASH));
1219/// ```
1220///
1221/// The previous Rust port was a FAKE — it called `finddir(s)` (Rust
1222/// signature returns `Option<String>` of the already-formatted
1223/// `~name/rest`) and returned that string unchanged in the Some-arm,
1224/// missing the `quotestring(..., QT_BACKSLASH)` C applies to the
1225/// residue. C's `finddir` returns a `Nameddir` pointer; the
1226/// `~name` + quoted-residue join lives here in `substnamedir`.
1227///
1228/// This re-port duplicates the HOME-first + nameddirtab-scan logic
1229/// (finddir_scan at c:1106) so it can take the (name, dir_prefix)
1230/// split BEFORE pre-joining, then apply quotestring to the residue
1231/// per c:1059.
1232pub fn substnamedir(s: &str) -> String {
1233    // c:1053
1234    // C `finddir` at c:1127 runs the homenode through the SAME
1235    // best-diff scan as the nameddirtab (c:1164-1165) — home COMPETES
1236    // with `hash -d` entries by `diff`, it does not preempt them.
1237    // Duplicate that competition here without the pre-format, so we
1238    // can apply quotestring on just the residue (c:1059).
1239    let home = getsparam("HOME").unwrap_or_default(); // c:1133
1240    let mut finddir_best: i32 = 0; // c:1163
1241    let mut best: Option<(String, String)> = None; // finddir_last
1242    let home_diff = if home.len() == 1 { 0 } else { home.len() as i32 }; // c:1140-1141
1243    if home_diff > 0 {
1244        if let Some(rest) = s.strip_prefix(&home) {
1245            if rest.is_empty() || rest.starts_with('/') {
1246                finddir_best = home_diff;
1247                // HOME is the implicit homenode (no name).
1248                best = Some((String::new(), rest.to_string()));
1249            }
1250        }
1251    }
1252    // c:1106 finddir_scan — best-diff walk over nameddirtab.
1253    if let Some((name, rest, diff)) = finddir_scan(s) {
1254        if diff > finddir_best {
1255            best = Some((name, rest));
1256        }
1257    }
1258    match best {
1259        // c:1059 — `zhtricat("~", d->node.nam, quotestring(s + strlen(d->dir), QT_BACKSLASH))`
1260        Some((name, rest)) => format!("~{}{}", name, quotestring(&rest, QT_BACKSLASH)),
1261        None => quotestring(s, QT_BACKSLASH), // c:1058
1262    }
1263}
1264
1265// ===========================================================
1266// Direct ports of utility entries from Src/utils.c.
1267// ===========================================================
1268
1269/// Cached current-user lookup.
1270/// Port of `get_username()` from Src/utils.c:1075 — `getpwuid(3)`
1271/// against the current real uid, with the result cached so a
1272/// re-call after setuid sees the new identity. The C source uses
1273/// a `cached_uid`+`cached_username` pair guarded by a uid match;
1274/// the Rust port uses an `OnceLock` keyed on uid for the same
1275/// invalidate-on-uid-change behaviour.
1276pub fn get_username() -> String {
1277    static CACHE: Mutex<Option<(u32, String)>> = Mutex::new(None);
1278
1279    let current_uid = unsafe { libc::getuid() };
1280    let mut guard = CACHE.lock().unwrap();
1281    if let Some((uid, name)) = &*guard {
1282        if *uid == current_uid {
1283            return name.clone();
1284        }
1285    }
1286    let name = unsafe {
1287        let pw = libc::getpwuid(current_uid);
1288        if pw.is_null() {
1289            String::new() // c:1088 — `ztrdup("")` fallback
1290        } else {
1291            // c:1086 — `cached_username = ztrdup_metafy(pswd->pw_name);`.
1292            // `ztrdup_metafy` calls `metafy((char *)s, -1, META_DUP)`
1293            // (utils.c:4929). The previous Rust port returned the raw
1294            // pw_name verbatim — fine for ASCII usernames, but high-bit
1295            // bytes (e.g. an `émile` username on some systems) would
1296            // surface to callers un-metafied. zsh's downstream pipeline
1297            // (param expansion, prompt rendering) assumes paramtab
1298            // entries are metafied; an un-metafied high-bit byte breaks
1299            // the Meta-escape contract.
1300            let raw = std::ffi::CStr::from_ptr((*pw).pw_name)
1301                .to_string_lossy()
1302                .into_owned();
1303            metafy(&raw) // c:1086 ztrdup_metafy
1304        }
1305    };
1306    *guard = Some((current_uid, name.clone()));
1307    name
1308}
1309
1310/// Port of `finddir_scan(HashNode hn, UNUSED(int flags))` from Src/utils.c:1103-1112 — ScanFunc the
1311/// C source registers with `scanhashtable(nameddirtab, …)`. Keeps the
1312/// entry with the LARGEST `diff` (zsh.h:2152 — `strlen(dir) -
1313/// strlen(nam)`, the abbreviation savings — NOT the raw dir length)
1314/// whose dir is a boundary-prefix of the path (`!dircmp`, c:1075-1086)
1315/// and which is not ND_NOABBREV (PWD/OLDPWD entries, c:1108).
1316/// WARNING: param names don't match C — Rust=(path) vs C=(hn, flags);
1317/// the C statics finddir_full/finddir_last/finddir_best are threaded
1318/// as the argument and the `(name, rest, diff)` return instead.
1319pub fn finddir_scan(path: &str) -> Option<(String, String, i32)> {
1320    // c:1106
1321    let table = nameddirtab().lock().ok()?;
1322    let mut best: Option<(String, String, i32)> = None;
1323    for (name, nd) in table.iter() {
1324        // c:1107-1108 — `nd->diff > finddir_best && !dircmp(nd->dir,
1325        // finddir_full) && !(nd->node.flags & ND_NOABBREV)`.
1326        if (nd.node.flags & crate::ported::zsh_h::ND_NOABBREV) != 0 {
1327            continue;
1328        }
1329        if best.as_ref().is_some_and(|b| nd.diff <= b.2) {
1330            continue;
1331        }
1332        if let Some(rest) = path.strip_prefix(nd.dir.as_str()) {
1333            if rest.is_empty() || rest.starts_with('/') {
1334                best = Some((name.clone(), rest.to_string(), nd.diff));
1335            }
1336        }
1337    }
1338    best
1339}
1340
1341/// Port of `Nameddir finddir(char *s)` from Src/utils.c:1127.
1342///
1343/// "See if a path has a named directory as its prefix." Compares
1344/// `s` against `$HOME` first (longest implicit named dir), then
1345/// scans the global `nameddirtab`, then falls back to
1346/// `subst_string_by_hook("zsh_directory_name", "d", s)`.
1347///
1348/// Rust signature returns `Option<String>` (the abbreviated path
1349/// `~name/rest`) instead of the C `Nameddir` pointer.
1350pub fn finddir(path: &str) -> Option<String> {
1351    // c:1127
1352    // c:1138 — `homenode.dir = home ? home : "";`. Reads the C global
1353    // `char *home` (params.c:91) DIRECTLY — not via paramtab. zshrs
1354    // ports the global as `params::home_lock()` accessed by
1355    // `homegetfn` (which ignores its &param arg per
1356    // `c:5109 UNUSED(Param pm)`). Pass a default param so the read
1357    // works even before paramtab["HOME"] has been hydrated (during
1358    // early init / unit-test environments).
1359    let _default_pm = crate::ported::zsh_h::param::default();
1360    let home = crate::ported::params::homegetfn(&_default_pm); // c:1138 home
1361    // c:1139-1141 + 1164-1165 — the home node (nam="", diff =
1362    // strlen(home), root's 1 → 0) runs through the SAME best-diff scan
1363    // as the table (`finddir_scan(&homenode.node, 0);
1364    // scanhashtable(nameddirtab, …)`): it COMPETES with `hash -d`
1365    // entries instead of preempting them, so a named dir with a larger
1366    // diff wins (e.g. ~ZPWR over ~/.zpwr). The previous early return
1367    // on the $HOME prefix hid every named dir under $HOME.
1368    let mut finddir_best: i32 = 0; // c:1163
1369    let mut best: Option<(String, String)> = None; // finddir_last
1370    let home_diff = if home.len() == 1 { 0 } else { home.len() as i32 }; // c:1140-1141
1371    if home_diff > 0 {
1372        if let Some(rest) = path.strip_prefix(&home) {
1373            if rest.is_empty() || rest.starts_with('/') {
1374                finddir_best = home_diff;
1375                best = Some((String::new(), rest.to_string()));
1376            }
1377        }
1378    }
1379    if let Some((name, rest, diff)) = finddir_scan(path) {
1380        // c:1165 — scanhashtable: only a strictly larger diff replaces
1381        // the home candidate (finddir_scan's `nd->diff > finddir_best`).
1382        if diff > finddir_best {
1383            finddir_best = diff;
1384            best = Some((name, rest));
1385        }
1386    }
1387    // c:1167-1175 — zsh_directory_name hook — returns ["name", "len"];
1388    // wins only when its match length beats finddir_best (c:1169).
1389    if let Some(reply) = subst_string_by_hook("zsh_directory_name", Some("d"), path) {
1390        if reply.len() >= 2 {
1391            // c:1170
1392            if let Ok(len) = reply[1].parse::<i32>() {
1393                if len > finddir_best && (len as usize) <= path.len() {
1394                    return Some(format!("~[{}]{}", reply[0], &path[len as usize..]));
1395                }
1396            }
1397        }
1398    }
1399    best.map(|(name, rest)| format!("~{}{}", name, rest)) // c:1177
1400}
1401
1402/// Port of `void adduserdir(char *s, char *t, int flags, int always)`
1403/// from Src/utils.c:1187.
1404///
1405/// Adds (or removes when `t` is empty / non-absolute) an entry in
1406/// the global `nameddirtab`. ND_USERNAME entries from `getpwnam`
1407/// don't override explicit assignments. AUTONAMEDIRS gating, the
1408/// trailing-slash trim, and PWD/OLDPWD ND_NOABBREV stamp are all
1409/// preserved. Routes through `crate::ported::hashnameddir`.
1410pub fn adduserdir(name: &str, dir: &str, flags: i32, always: bool) {
1411    // c:1187
1412
1413    if !interact() {
1414        return;
1415    } // c:1193
1416    if let Ok(t) = nameddirtab().lock() {
1417        if (flags & ND_USERNAME) != 0 && t.contains_key(name) {
1418            // c:1199
1419            return;
1420        }
1421        if !always && !isset(AUTONAMEDIRS) && !t.contains_key(name) {
1422            // c:1207
1423            return;
1424        }
1425    }
1426    // c:1211 — `if (!t || *t != '/' || strlen(t) >= PATH_MAX)`. C
1427    // rejects paths >= PATH_MAX as too long (would overflow path-
1428    // expansion buffers downstream). PATH_MAX is platform-dependent
1429    // (4096 on Linux, 1024 on macOS); libc::PATH_MAX exposes it.
1430    if dir.is_empty() || !dir.starts_with('/') || dir.len() >= libc::PATH_MAX as usize
1431    // c:1211 strlen(t) >= PATH_MAX
1432    {
1433        let _ = removenameddirnode(name); // c:1214
1434        return;
1435    }
1436    let mut trimmed = dir.trim_end_matches('/').to_string(); // c:1224-1226
1437    if trimmed.is_empty() {
1438        trimmed = dir.to_string(); // c:1227-1233
1439    }
1440    let final_flags = if name == "PWD" || name == "OLDPWD" {
1441        // c:1237
1442        flags | ND_NOABBREV
1443    } else {
1444        flags
1445    };
1446    // c:1239 — `nd = (Nameddir) zshcalloc(sizeof *nd); nd->node.flags = …;
1447    //           nd->dir = ztrdup(t); addnode(nameddirtab, ztrdup(s), nd);`
1448    let nd = nameddir {
1449        node: hashnode {
1450            next: None,
1451            nam: name.to_string(),
1452            flags: final_flags,
1453        },
1454        dir: trimmed,
1455        diff: 0,
1456    };
1457    crate::ported::hashnameddir::addnameddirnode(name, nd);
1458}
1459
1460/// Port of `char *getnameddir(char *name)` from Src/utils.c:1247.
1461///
1462/// Looks up `name` in `nameddirtab`; if absent, checks for a
1463/// scalar parameter whose value starts with `/` and registers it
1464/// via `adduserdir`; finally falls back to `getpwnam(name)` for
1465/// `~user`-style lookups when USE_GETPWNAM is enabled.
1466pub fn getnameddir(name: &str) -> Option<String> {
1467    // c:1247
1468    if let Ok(t) = nameddirtab().lock() {
1469        if let Some(nd) = t.get(name) {
1470            // c:1254
1471            return Some(nd.dir.clone());
1472        }
1473    }
1474    // c:1260 — `if ((s = getsparam(name)) && *s == '/')`. paramtab read.
1475    if let Some(s) = getsparam(name) {
1476        if s.starts_with('/') {
1477            adduserdir(name, &s, 0, true); // c:1264
1478            return Some(s);
1479        }
1480    }
1481    #[cfg(unix)]
1482    {
1483        // c:1268 — getpwnam fallback.
1484        let cn = CString::new(name).ok()?;
1485        let pw = unsafe { libc::getpwnam(cn.as_ptr()) };
1486        if !pw.is_null() {
1487            let raw_dir = unsafe {
1488                std::ffi::CStr::from_ptr((*pw).pw_dir)
1489                    .to_string_lossy()
1490                    .into_owned()
1491            };
1492            // c:1273-1274 — `isset(CHASELINKS) ? xsymlink(pw->pw_dir, 0)
1493            // : ztrdup(pw->pw_dir);`. Resolve symlinks when the option
1494            // is set. Previously omitted in the Rust port — silently
1495            // returned the raw passwd-db dir even when the user had
1496            // `setopt chaselinks`.
1497            let dir = if isset(CHASELINKS) {
1498                xsymlink(&raw_dir).unwrap_or(raw_dir)
1499            } else {
1500                raw_dir
1501            };
1502            // c:1276 — `adduserdir(name, dir, ND_USERNAME, 1);`
1503            // Cache the lookup so subsequent `~user` expansions hit the
1504            // nameddirtab fast-path at c:1254 instead of round-tripping
1505            // through getpwnam every time.
1506            adduserdir(name, &dir, ND_USERNAME, true); // c:1276
1507            return Some(dir);
1508        }
1509    }
1510    None
1511}
1512
1513/// Compare directory paths (from utils.c dircmp)
1514/// Port of `dircmp(char *s, char *t)` from `Src/utils.c:1296`.
1515pub fn dircmp(s: &str, t: &str) -> bool {
1516    let s = s.trim_end_matches('/');
1517    let t = t.trim_end_matches('/');
1518    s == t
1519}
1520
1521// Add a function to the list of pre-prompt functions.                     // c:1332
1522/// Register a callback to run before each prompt.
1523/// Port of `addprepromptfn(voidvoidfnptr_t func)` from Src/utils.c:1319.
1524pub fn addprepromptfn(func: fn()) {
1525    // c:1319
1526    PREPROMPT_FNS.lock().unwrap().push(func);
1527}
1528
1529// Remove a function from the list of pre-prompt functions.                // c:1332
1530/// Remove a previously-registered pre-prompt callback.
1531/// Port of `delprepromptfn(voidvoidfnptr_t func)` from Src/utils.c:1332.
1532pub fn delprepromptfn(func: fn()) {
1533    // c:1332
1534    let mut list = PREPROMPT_FNS.lock().unwrap();
1535    if let Some(pos) = list.iter().position(|f| *f as usize == func as usize) {
1536        list.remove(pos);
1537    }
1538}
1539
1540// Add a function to the list of timed functions.                           // c:1367
1541/// Port of `void addtimedfn(voidvoidfnptr_t func, time_t when)` from
1542/// `Src/utils.c:1371`. Faithful walk of the timedfns LinkList:
1543/// allocate a `Timedfn`, lazy-init the list when empty, otherwise scan
1544/// from `firstnode` and insert BEFORE the first node whose `when` is
1545/// greater than ours. The standard linklist API only inserts AFTER a
1546/// node, so the C loop carries `ln` as the previous node and inserts
1547/// before `next` once `when < next->when`. Note: zsh's `time_t` is
1548/// signed and historically negative-`when` was supported, so we keep
1549/// `i64`.
1550pub fn addtimedfn(func: fn(), when: i64) {
1551    // c:1371
1552    let mut list = TIMED_FNS.lock().unwrap(); // c:1365 timedfns
1553    let tfdat: (i64, fn()) = (when, func); // c:1373-1375 Timedfn tfdat
1554    if list.is_empty() {
1555        // c:1377 !timedfns
1556        list.push(tfdat); // c:1378-1379 znewlinklist + zaddlinknode
1557        return;
1558    }
1559    if list.is_empty() {
1560        // c:1394 !ln (firstnode of empty list)
1561        list.push(tfdat); // c:1395 zaddlinknode
1562        return; // c:1396
1563    }
1564    let mut idx: usize = 0; // c:1381 LinkNode ln = firstnode(timedfns)
1565    loop {
1566        // c:1398 for(;;)
1567        let next = idx + 1; // c:1400 LinkNode next = nextnode(ln)
1568        if next >= list.len() {
1569            // c:1401 !next
1570            list.push(tfdat); // c:1402 zaddlinknode
1571            return; // c:1403
1572        }
1573        let tfdat2_when = list[next].0; // c:1405 tfdat2 = getdata(next)
1574        if when < tfdat2_when {
1575            // c:1406 when < tfdat2->when
1576            list.insert(next, tfdat); // c:1407 zinsertlinknode(timedfns, ln, tfdat)
1577            return; // c:1408
1578        }
1579        idx = next; // c:1410 ln = next
1580    }
1581}
1582
1583/// Remove a registered timed function (first occurrence only).
1584/// Port of `deltimedfn(voidvoidfnptr_t func)` from Src/utils.c:1430.
1585pub fn deltimedfn(func: fn()) {
1586    // c:1430
1587    let mut list = TIMED_FNS.lock().unwrap();
1588    if let Some(pos) = list.iter().position(|(_, f)| *f as usize == func as usize) {
1589        list.remove(pos);
1590    }
1591}
1592
1593/// Invoke a hook function by name plus any `<name>_functions` array.
1594/// Port of `callhookfunc(char *name, LinkList lnklst, int arrayp, int *retval)` from Src/utils.c:1469. Returns 0 if at
1595/// least one hook ran, 1 otherwise — the C source uses the same
1596/// stat semantics so the prompt machinery can detect "did periodic
1597/// fire". Hook dispatch goes through the executor singleton (which
1598/// owns the function table); we look up `name` and then walk the
1599/// `<name>_functions` array exactly as the C source does at
1600/// Src/utils.c:1469.
1601/// Direct port of
1602/// `int callhookfunc(char *name, LinkList lnklst, int arrayp, int *retval)`
1603/// at `Src/utils.c:1469`. C signature mapping:
1604///   - `char *name`     → `name: &str`
1605///   - `LinkList lnklst` → `lnklst: Option<&[String]>` (Rust-shape
1606///     stand-in for the C-shape `LinkList` — both are an ordered list
1607///     of meta-fied strings; the LinkList port itself diverges)
1608///   - `int arrayp`     → `arrayp: i32`
1609///   - `int *retval`    → `retval: *mut i32` (out-param; NULL is fine
1610///     when the caller doesn't need the doshfunc return value)
1611///
1612/// Returns `stat` — 0 if at least one shfunc fired, 1 otherwise
1613/// (matches `c:1495 stat = 0` after every dispatch). When `retval` is
1614/// non-null, `*retval` is set to the most recent `doshfunc`-returned
1615/// status, mirroring the C body's `*retval = ret` semantics.
1616pub fn callhookfunc(name: &str, lnklst: Option<&[String]>, arrayp: i32, retval: *mut i32) -> i32 {
1617    let mut stat: i32 = 1; // c:1475
1618    let mut ret: i32 = 0; // c:1475
1619
1620    // Build the doshfunc arg list `[fname ($0), $1, $2, …]`. A provided
1621    // `lnklst` already carries the ORIGINAL hook name at [0] and the real
1622    // positional args at [1..] (matching C, where the caller does
1623    // `addlinknode(args, name)` before the args). So substitute `fname` for
1624    // [0] — `fname` is the hook name on the direct path (c:1487, identity)
1625    // and a `${name}_functions` member on the array path (c:1504-1508,
1626    // `arg0 = [*arrptr] + argarr[1..]`) — and KEEP [1..].
1627    //
1628    // The prior code prepended `fname` to the WHOLE list (name included),
1629    // so every hook invoked with args saw `$1` == its own name (e.g.
1630    // `preexec`'s `$1` was "preexec" instead of the command line). c:1483.
1631    let mk_args = |fname: &str| -> Vec<String> {
1632        let mut v: Vec<String> = vec![fname.to_string()];
1633        if let Some(extra) = lnklst {
1634            if extra.len() > 1 {
1635                v.extend_from_slice(&extra[1..]);
1636            }
1637        }
1638        v
1639    };
1640
1641    // c:1495 — `if ((shfunc = getshfunc(name))) { doshfunc(...); stat = 0; }`
1642    let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
1643        .read()
1644        .ok()
1645        .and_then(|t| t.get(name).cloned());
1646    if let Some(mut shf) = shf_clone {
1647        let args = mk_args(name);
1648        // c:1487 — `ret = doshfunc(shfunc, lnklst, 1);`. Direct
1649        // doshfunc invocation; body_runner routes through the host
1650        // body-only entry to avoid double-wrapping the scope.
1651        let name_for_body = name.to_string();
1652        let body_args = args.clone();
1653        let body_runner = move || -> i32 {
1654            crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
1655        };
1656        ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
1657        stat = 0; // c:1504
1658    }
1659
1660    if arrayp != 0 {
1661        // c:1507-1525 — fire every `${name}_functions` hook in order.
1662        let arr_name = format!("{}_functions", name); // c:1511
1663        let arr = crate::ported::params::paramtab()
1664            .read()
1665            .ok()
1666            .and_then(|t| t.get(&arr_name).and_then(|p| p.u_arr.clone()))
1667            .unwrap_or_default(); // c:1512 getaparam
1668        for fn_name in arr {
1669            // c:1514
1670            let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
1671                .read()
1672                .ok()
1673                .and_then(|t| t.get(&fn_name).cloned());
1674            if let Some(mut shf) = shf_clone {
1675                // c:1518
1676                let args = mk_args(&fn_name);
1677                // c:1508 — `newret = doshfunc(shfunc, arg0, 1);`.
1678                let name_for_body = fn_name.clone();
1679                let body_args = args.clone();
1680                let body_runner = move || -> i32 {
1681                    crate::ported::exec::run_function_body(&name_for_body, &body_args[1..])
1682                        .unwrap_or(0)
1683                };
1684                ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
1685                stat = 0; // c:1520
1686            }
1687        }
1688    }
1689
1690    // c:1528 — `if (retval) *retval = ret;`
1691    if !retval.is_null() {
1692        unsafe {
1693            *retval = ret;
1694        }
1695    }
1696    let _ = ret;
1697
1698    stat // c:1530
1699}
1700
1701// do pre-prompt stuff                                                      // c:1530
1702/// Run pre-prompt machinery: precmd, periodic, prepromptfns.
1703/// Port of `preprompt()` from Src/utils.c:1530. Rust port skips
1704/// the `PROMPT_SP` heuristic + mailcheck (those need terminal +
1705/// MAIL state plumbing not yet present); fires the `precmd` hook
1706/// + `precmd_functions` array, the `periodic` hook on its
1707/// PERIOD-second cadence, and walks the prepromptfns registry.
1708pub fn preprompt() {
1709    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1710    // Stamp duration + exit status onto the SQLite history row hend()
1711    // added for the just-finished command (no-op when nothing is
1712    // pending). preprompt runs immediately after execode returns and
1713    // before the next ZLE read, so the elapsed time is command
1714    // wall-time, not prompt idle.
1715    crate::history::history_sqlite_finish(crate::ported::builtin::LASTVAL.load(Ordering::Relaxed));
1716    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1717    // Native p10k engine (src/extensions/p10k): snapshot `$?` and close
1718    // the command timer BEFORE the precmd hook runs, so precmd's own
1719    // commands can't clobber what the status / command_execution_time
1720    // segments show. Mirrors p10k's `_p9k_save_status` precmd ordering.
1721    crate::p10k::note_command_finished(
1722        crate::ported::builtin::LASTVAL.load(Ordering::Relaxed) as i64,
1723    );
1724    // c:1532 `static time_t lastperiodic;` — periodic-hook last-fire timestamp.
1725    static LAST_PERIODIC: AtomicI64 = AtomicI64::new(0);
1726    // `lastmailcheck` is module-scoped (LAST_MAILCHECK below) because C makes it
1727    // a file-static shared between checkmail() and checkmailpath() (c:1447).
1728
1729    // c:1535-1536 — `zlong period = getiparam("PERIOD"); zlong mailcheck
1730    //                = getiparam("MAILCHECK");`
1731    let period = crate::ported::params::getiparam("PERIOD");
1732    let mailcheck = crate::ported::params::getiparam("MAILCHECK");
1733
1734    // c:1538-1543 — `winch_unblock(); ... winch_block();` — let any
1735    // pending SIGWINCH fire so the prompt picks up the latest column
1736    // count, then re-block to prevent the resize-handler from
1737    // interrupting prompt rendering mid-paint.
1738    crate::ported::signals_h::winch_unblock();
1739    crate::ported::signals_h::winch_block();
1740
1741    // c:1545-1567 — PROMPT_SP heuristic (move the prompt to a fresh line
1742    // when the previous command left dangling output). DEFERRED — a faithful
1743    // port must call `countprompt(str, &w, 0, -1)` (c:1560) to size the
1744    // PROMPT_EOL_MARK, but `countprompt` measures visible width by skipping
1745    // `Inpar`/`Outpar` (0x88/0x8a) spans, whereas zshrs's `promptexpand`
1746    // emits raw terminal escapes / readline `\x01`/`\x02` markers instead.
1747    // Until the expander is reworked to emit Inpar/Outpar (so `countprompt`
1748    // measures correctly), this can't be ported without reimplementing
1749    // countprompt's job inline — which it must not. Left as a no-op.
1750
1751    // Reap any children that exited while the shell sat idle. zsh's
1752    // SIGCHLD handler reaps asynchronously during the ZLE read (signals
1753    // are unqueued there), but zshrs runs the interactive event loop
1754    // inside a queued-signal window (init.rs `queue_signals()` at loop
1755    // top), so zhandler QUEUES each SIGCHLD instead of reaping it
1756    // (signals.rs:373). Disowned / background jobs (`&!`, `&`, and
1757    // `<(…)` process substitutions) therefore piled up as zombies
1758    // between prompts — every zpwr/zinit/p10k async `&!` per prompt
1759    // leaked a zombie, slowing startup and eventually exhausting the
1760    // process table. Mirror zhandler's reap+route here, right before
1761    // the scanjobs notification pass, so `waitpid(-1)` clears the
1762    // zombies and the DONE bits land for scanjobs to print/delete. No
1763    // foreground job is live at preprompt, so reaping -1 can't steal a
1764    // fg wait's status.
1765    {
1766        let reaped = crate::ported::signals::wait_for_processes();
1767        if !reaped.is_empty() {
1768            if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
1769                if let Ok(mut guard) = jt.lock() {
1770                    for (pid, status) in reaped {
1771                        let _ = crate::ported::jobs::update_bg_job(&mut guard, pid, status);
1772                    }
1773                }
1774            }
1775        }
1776    }
1777
1778    // c:1569-1572 — `if (unset(NOTIFY)) scanjobs();` — sync job-status
1779    // print before prompt, via the canonical scanjobs port
1780    // (Src/jobs.c:1993).
1781    if !crate::ported::zsh_h::isset(crate::ported::zsh_h::NOTIFY) {
1782        if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
1783            let mut guard = jt.lock().unwrap();
1784            crate::ported::jobs::scanjobs(&mut guard);
1785        }
1786    }
1787
1788    // c:1573-1574 — `if (errflag) return;` — bail if a previous error
1789    // already set the global flag.
1790    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
1791        return;
1792    }
1793
1794    // c:1576-1580 — `callhookfunc("precmd", NULL, 1, NULL);` + errflag bail.
1795    callhookfunc("precmd", None, 1, std::ptr::null_mut());
1796    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
1797        return;
1798    }
1799
1800    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1801    // Native p10k engine (src/extensions/p10k): rebuild PROMPT/RPROMPT
1802    // after the precmd hook so user precmd state is fresh. Equivalent
1803    // to the zsh theme's `_p9k_precmd` running as the last precmd
1804    // hook (p10k.zsh registers _p9k_precmd via add-zsh-hook). No-op
1805    // unless the powerlevel10k theme source was intercepted.
1806    crate::p10k::preprompt_render();
1807
1808    // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1809    // Fire the `async_precmd` hooks on the worker pool AFTER the prompt is
1810    // built/rendered, so they never block the paint. Non-blocking: submits to
1811    // the pool and returns immediately; the hooks write into the shared param
1812    // table which the NEXT prompt render reads. (Same call shape as the p10k
1813    // render above — the ShellExecutor-using logic lives in the extension.)
1814    crate::async_precmd::fire_async_precmd();
1815
1816    // c:1582-1589 — periodic-hook dispatch on PERIOD cadence.
1817    if period > 0 {
1818        let now = std::time::SystemTime::now()
1819            .duration_since(UNIX_EPOCH)
1820            .map(|d| d.as_secs() as i64)
1821            .unwrap_or(0);
1822        if now > LAST_PERIODIC.load(Ordering::Relaxed) + period
1823            && callhookfunc("periodic", None, 1, std::ptr::null_mut()) == 0
1824        {
1825            LAST_PERIODIC.store(now, Ordering::Relaxed);
1826        }
1827    }
1828    // c:1588-1589 — `if (errflag) return;` post-periodic bail.
1829    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
1830        return;
1831    }
1832
1833    // c:1591-1611 — mail check: `if (mailcheck && difftime(now,
1834    // lastmailcheck) > mailcheck)` walk MAILPATH (or scalar MAIL) via
1835    // checkmailpath. Faithful port — checkmailpath already exists at
1836    // utils.rs:1623, getaparam exists for the MAILPATH array.
1837    let currentmailcheck = std::time::SystemTime::now()
1838        .duration_since(UNIX_EPOCH)
1839        .map(|d| d.as_secs() as i64)
1840        .unwrap_or(0);
1841    if mailcheck > 0 && (currentmailcheck - LAST_MAILCHECK.load(Ordering::Relaxed)) > mailcheck {
1842        // c:1597 — `if (mailpath && *mailpath && **mailpath)`
1843        let mailpath = crate::ported::params::getaparam("MAILPATH");
1844        let has_mailpath = mailpath
1845            .as_ref()
1846            .map(|p| !p.is_empty() && p.first().map(|s| !s.is_empty()).unwrap_or(false))
1847            .unwrap_or(false);
1848        if has_mailpath {
1849            // c:1598 `checkmailpath(mailpath);` — emit each message to the
1850            // terminal (C writes directly to `shout`).
1851            for m in checkmailpath(mailpath.as_deref().unwrap()) {
1852                println!("{}", m);
1853            }
1854        } else {
1855            // c:1600-1608 — `if ((mailfile = getsparam("MAIL")) && *mailfile)
1856            //                  { x[0]=mailfile; x[1]=NULL; checkmailpath(x); }`
1857            crate::ported::signals::queue_signals(); // c:1600
1858            if let Some(mailfile) = crate::ported::params::getsparam("MAIL") {
1859                if !mailfile.is_empty() {
1860                    let x = vec![mailfile]; // c:1604-1605
1861                    for m in checkmailpath(&x) {
1862                        println!("{}", m); // c:1606
1863                    }
1864                }
1865            }
1866            crate::ported::signals::unqueue_signals(); // c:1608
1867        }
1868        LAST_MAILCHECK.store(currentmailcheck, Ordering::Relaxed); // c:1610
1869    }
1870
1871    // c:1613-1618 — `if (prepromptfns) for (...) ppnode->func();`
1872    let snapshot: Vec<fn()> = PREPROMPT_FNS.lock().unwrap().clone();
1873    for f in snapshot {
1874        f();
1875    }
1876}
1877
1878/// `static time_t lastmailcheck;` (Src/utils.c:1447) — the timestamp of the
1879/// previous mail check. `checkmail()` reads it (mtime/atime comparisons happen
1880/// against the PREVIOUS check) and writes it after walking the paths.
1881pub static LAST_MAILCHECK: AtomicI64 = AtomicI64::new(0);
1882
1883/// Direct port of `void checkmailpath(char **s)` from `Src/utils.c:1620`.
1884/// Walks each `PATH` / `PATH?message` MAILPATH component:
1885///   - empty component → `zerr` (c:1634-1636)
1886///   - `mailstat` failure → `zerr` unless ENOENT (c:1637-1639)
1887///   - directory → list entries (`PATH/entry[?message]`) and recurse
1888///     (c:1640-1669)
1889///   - file, interactive (`shout`) only: "You have new mail." or the
1890///     `?`-suffixed message expanded with `$_` bound to the file
1891///     (`parsestr`+`singsub`, c:1670-1699); and, under `MAILWARNING`,
1892///     "The mail in PATH has been read." (c:1700-1704)
1893///
1894/// **Signature note:** C is `void` and writes to `shout`; the zshrs caller
1895/// drives output, so this returns the messages (in C emission order) instead.
1896/// The new-mail / read-warning conditions are now ported faithfully — the
1897/// previous adhoc port used an unfaithful "mtime within 60s" heuristic and its
1898/// result was discarded by the caller, so the feature did nothing.
1899pub fn checkmailpath(s: &[String]) -> Vec<String> {
1900    // c:1620
1901    let mut messages = Vec::new();
1902    let interactive = *crate::ported::init::shout.lock().unwrap() != 0; // c:1670 `else if (shout)`
1903    let lastmailcheck = LAST_MAILCHECK.load(Ordering::Relaxed);
1904
1905    for path in s {
1906        // c:1627-1633 — split on the first '?': `file` before, `u` after (or None).
1907        let (file, u) = match path.find('?') {
1908            Some(p) => (&path[..p], Some(&path[p + 1..])),
1909            None => (path.as_str(), None),
1910        };
1911
1912        if file.is_empty() {
1913            // c:1634-1636 — `if (**s == 0) zerr("empty MAILPATH component: %s", *s);`
1914            zerr(&format!("empty MAILPATH component: {}", path));
1915            continue;
1916        }
1917
1918        let mut st: libc::stat = unsafe { std::mem::zeroed() };
1919        let real = unmeta(file); // c:1637 — mailstat(unmeta(*s), &st)
1920        if mailstat(&real, &mut st) == -1 {
1921            // c:1638-1639 — report anything other than "no such file".
1922            let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1923            if e != libc::ENOENT {
1924                zerr(&format!("{}: {}", zsh_errno_msg(e), path));
1925            }
1926        } else if (st.st_mode as libc::mode_t & libc::S_IFMT) == libc::S_IFDIR {
1927            // c:1640-1669 — a directory: enumerate entries and recurse.
1928            match fs::read_dir(&real) {
1929                Err(_) => {
1930                    let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1931                    zerr(&format!("{}: {}", zsh_errno_msg(e), path)); // c:1647
1932                }
1933                Ok(mut rd) => {
1934                    let mut arr = Vec::new();
1935                    // c:1653-1662 — `while ((fn = zreaddir(lock, 1)) && !errflag)`.
1936                    while let Some(fname) = zreaddir(&mut rd, 1) {
1937                        if errflag.load(Ordering::Relaxed) != 0 {
1938                            break;
1939                        }
1940                        // c:1654-1657 — keep the `?message` suffix on each entry.
1941                        arr.push(match u {
1942                            Some(u) => format!("{}/{}?{}", path, fname, u),
1943                            None => format!("{}/{}", path, fname),
1944                        });
1945                    }
1946                    messages.extend(checkmailpath(&arr)); // c:1667 — recurse
1947                }
1948            }
1949        } else if interactive {
1950            // c:1671-1672 — new mail: non-empty, not yet read, newer than the
1951            // last check.
1952            if st.st_size != 0 && st.st_atime <= st.st_mtime && st.st_mtime >= lastmailcheck {
1953                match u {
1954                    // c:1673-1675 — `fprintf(shout, "You have new mail.\n");`
1955                    None => messages.push("You have new mail.".to_string()),
1956                    // c:1676-1698 — custom message: bind `$_` to the file, then
1957                    // parsestr + singsub the message (prompt-style expansion).
1958                    Some(u) => {
1959                        let usav = crate::ported::init::zunderscore.lock().unwrap().clone();
1960                        crate::ported::exec::setunderscore(path); // c:1685 setunderscore(*s)
1961                        if let Ok(parsed) = crate::ported::lex::parsestr(u) {
1962                            // c:1688-1691 — parse ok → singsub then emit.
1963                            messages.push(crate::ported::subst::singsub(&parsed));
1964                        }
1965                        crate::ported::exec::setunderscore(&usav); // c:1694-1697 restore $_
1966                    }
1967                }
1968            }
1969            // c:1700-1704 — MAILWARNING: file read since the last check.
1970            if crate::ported::zsh_h::isset(crate::ported::zsh_h::MAILWARNING)
1971                && st.st_atime > st.st_mtime
1972                && st.st_atime > lastmailcheck
1973                && st.st_size != 0
1974            {
1975                messages.push(format!("The mail in {} has been read.", unmeta(file)));
1976            }
1977        }
1978    }
1979    messages
1980}
1981
1982/// Port of `printprompt4()` from `Src/utils.c:1718`.
1983///
1984/// Render the PS4 / PROMPT4 prefix and write it to stderr. zsh's
1985/// implementation reads `prompt4` global, suppresses XTRACE during
1986/// promptexpand (so subshells inside `%(?…)` don't recursively trace),
1987/// then fprintf's the expanded prefix to xtrerr. zshrs uses the same
1988/// suppress-XTRACE-around-expand pattern; ksh/sh emulation defaults
1989/// to `+ ` per Src/init.c:1192, zsh default to `+%N:%i> `.
1990///
1991/// The C source's caller (Src/exec.c::tracingcond etc.) follows this
1992/// with the per-line/per-arg fprintf — same shape mirrored at the two
1993/// zshrs call sites in fusevm_bridge.rs (BUILTIN_XTRACE_LINE / ARGS).
1994pub(crate) fn printprompt4() {
1995    // c:utils.c:1720 — `if (!isset(XTRACE)) return;`. C tests
1996    // `xtrerr` first then conditionally; the read-the-option early-
1997    // return path is equivalent for our purposes since we don't ship
1998    // the `xtrerr` separate-stream support.
1999    if !isset(XTRACE) {
2000        return;
2001    }
2002    // c:utils.c:1722-1724 — `if (prompt4) { ... s = dupstring(prompt4);`
2003    // C `prompt4` is a global initialized in init.c:1192 from the
2004    // emulation bits; PS4/PROMPT4 paramtab entries alias it via
2005    // IPDEF7R/IPDEF7 (params.c:381, 421). Read from paramtab to
2006    // honour user-set values, fall back to the same emulation
2007    // default C uses at init.c:1192.
2008    let posix = EMULATION(EMULATE_KSH | EMULATE_SH); // c:init.c:1192
2009    let prefix_template = getsparam("PS4")
2010        .or_else(|| getsparam("PROMPT4"))
2011        .unwrap_or_else(|| {
2012            if posix {
2013                "+ ".to_string() // c:init.c:1192
2014            } else {
2015                "+%N:%i> ".to_string() // c:init.c:1193
2016            }
2017        });
2018    // c:utils.c:1723,1726,1730 — `t = opts[XTRACE]; opts[XTRACE] = 0;
2019    //                              promptexpand(...);  opts[XTRACE] = t;`
2020    let saved = isset(XTRACE);
2021    opt_state_set(&opt_name(XTRACE), false);
2022    let prefix = crate::prompt::expand_prompt(&prefix_template);
2023    opt_state_set(&opt_name(XTRACE), saved);
2024    // c:utils.c:1736 — `fprintf(xtrerr, "%s", s)`. Append to the xtrerr
2025    // line buffer rather than writing straight to stderr; the emitter
2026    // appends the command/args and flushes the whole line in one write,
2027    // matching C's single `fflush(xtrerr)` (so concurrent pipeline-stage
2028    // trace lines never interleave).
2029    crate::fusevm_bridge::xtrerr_fputs(&prefix);
2030}
2031
2032/// Port of `freestr(void *a)` from `Src/utils.c:1739`.
2033///
2034/// C body:
2035/// ```c
2036/// void freestr(void *a) { zsfree(a); }
2037/// ```
2038/// The C function is registered as the `freenode` callback for
2039/// hashtables holding plain string values. The Rust port consumes
2040/// `a` by value; Rust's `Drop` runs the equivalent of `zsfree` when
2041/// the `String` is moved into this fn and falls out of scope at the
2042/// closing brace — the no-op body is the correct port. Param name
2043/// `a` matches C exactly per Rule E.
2044pub fn freestr(_a: String) {}
2045
2046/// Port of `int gettempfile(const char *prefix, int use_heap, char **tempname)`
2047/// from Src/utils.c:2231. Creates a fresh tempfile with `O_RDWR|O_CREAT|O_EXCL`
2048/// and mode 0600 under umask 0177; returns `(fd, path)` on success.
2049///
2050/// C signature uses an out-param for the filename and returns the fd; Rust
2051/// returns both as a tuple. Matches the C control-flow: open with O_EXCL,
2052/// retry up to 16 times on EEXIST (the non-mkstemp branch, c:2255-2269).
2053pub fn gettempfile(prefix: Option<&str>) -> Option<(i32, String)> {
2054    // c:2231
2055    #[cfg(unix)]
2056    {
2057        queue_signals(); // c:2239
2058        let old_umask = unsafe { libc::umask(0o177) }; // c:2240
2059        let mut failures = 0; // c:2255
2060        let mut result: Option<(i32, String)> = None;
2061        loop {
2062            let fn_ = match gettempname(prefix, false) {
2063                // c:2260
2064                Some(n) => n,
2065                None => break,
2066            };
2067            let cn = match CString::new(fn_.clone()) {
2068                Ok(c) => c,
2069                Err(_) => break,
2070            };
2071            let fd = unsafe {
2072                libc::open(
2073                    cn.as_ptr(),
2074                    libc::O_RDWR | libc::O_CREAT | libc::O_EXCL, // c:2264
2075                    0o600 as libc::c_int,
2076                )
2077            };
2078            if fd >= 0 {
2079                result = Some((fd, fn_));
2080                break;
2081            }
2082            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2083            if err != libc::EEXIST {
2084                // c:2269
2085                break;
2086            }
2087            failures += 1;
2088            if failures >= 16 {
2089                // c:2269
2090                break;
2091            }
2092        }
2093        unsafe {
2094            libc::umask(old_umask);
2095        } // c:2273
2096        unqueue_signals(); // c:2274
2097        result
2098    }
2099    #[cfg(not(unix))]
2100    {
2101        let _ = prefix;
2102        None
2103    }
2104}
2105
2106/// Port of `void gettyinfo(struct ttyinfo *ti)` from Src/utils.c:1746.
2107///
2108/// The saved baseline terminal mode — C's `struct ttyinfo shttyinfo`
2109/// (Src/init.c). Captured ONCE while the terminal is still in its
2110/// original (cooked / canonical) mode, before ZLE ever puts it into
2111/// raw mode via `zsetterm()`. `trashzle()` restores this baseline via
2112/// `settyinfo(&shttyinfo)` so that external commands the user runs
2113/// (`cat`, `less`, …) execute with a cooked terminal — where `^D` is
2114/// EOF, `^C` is SIGINT, and line editing works. Without it the tty
2115/// stayed in ZLE raw mode across command execution, so `cat` never
2116/// saw EOF on `^D`.
2117#[cfg(unix)]
2118pub static SHTTYINFO: std::sync::Mutex<Option<libc::termios>> = std::sync::Mutex::new(None);
2119
2120/// Reads the current termios from the global `SHTTY`. Returns
2121/// `None` when SHTTY is closed or the call fails (matching C's
2122/// silent return when SHTTY == -1).
2123/// C body (single statement): `fdgettyinfo(SHTTY, ti);`
2124/// Rust returns `Option<termios>` instead of taking an out-ptr;
2125/// the SHTTY=-1 case naturally returns Err from fdgettyinfo → None.
2126#[cfg(unix)]
2127pub fn gettyinfo() -> Option<libc::termios> {
2128    // c:1746
2129    fdgettyinfo(SHTTY.load(Ordering::Relaxed)).ok() // c:1748
2130}
2131
2132/// Emit the `$PS4` xtrace prefix to stderr.
2133/// Read terminal mode from a file descriptor.
2134/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1753. C source uses
2135/// `tcgetattr(SHTTY, &ti->tio)`; we return the populated termios
2136/// or an io::Error on failure (caller equivalent to zsh's `zerr`).
2137#[cfg(unix)]
2138/// WARNING: param names don't match C — Rust=(fd) vs C=(SHTTY, ti)
2139pub fn fdgettyinfo(fd: i32) -> io::Result<libc::termios> {
2140    let mut tio: libc::termios = unsafe { std::mem::zeroed() };
2141    if unsafe { libc::tcgetattr(fd, &mut tio) } == -1 {
2142        Err(io::Error::last_os_error())
2143    } else {
2144        Ok(tio)
2145    }
2146}
2147
2148#[cfg(not(unix))]
2149/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from `Src/utils.c:1753`.
2150/// WARNING: param names don't match C — Rust=(_fd) vs C=(SHTTY, ti)
2151pub fn fdgettyinfo(_fd: i32) -> std::io::Result<()> {
2152    Err(std::io::Error::new(
2153        std::io::ErrorKind::Unsupported,
2154        "no termios",
2155    ))
2156}
2157
2158/// Port of `void settyinfo(struct ttyinfo *ti)` from Src/utils.c:1778.
2159///
2160/// Restores the termios state on the global `SHTTY` with EINTR
2161/// retry; no-op when SHTTY is closed.
2162/// C body (single statement): `fdsettyinfo(SHTTY, ti);`
2163/// Rust returns bool; SHTTY=-1 yields Err → false from fdsettyinfo.
2164#[cfg(unix)]
2165pub fn settyinfo(ti: &libc::termios) -> bool {
2166    // c:1778
2167    fdsettyinfo(SHTTY.load(Ordering::Relaxed), ti).is_ok() // c:1780
2168}
2169
2170/// Apply terminal mode to a file descriptor, with EINTR retry.
2171/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1785. C source loops
2172/// `while (tcsetattr(SHTTY, TCSADRAIN, &ti->tio) == -1 && errno
2173/// == EINTR)` — same retry shape here.
2174#[cfg(unix)]
2175pub fn fdsettyinfo(SHTTY2: i32, ti: &libc::termios) -> io::Result<()> {
2176    loop {
2177        if unsafe { libc::tcsetattr(SHTTY2, libc::TCSADRAIN, ti) } != -1 {
2178            return Ok(());
2179        }
2180        let err = io::Error::last_os_error();
2181        if err.kind() != io::ErrorKind::Interrupted {
2182            return Err(err);
2183        }
2184    }
2185}
2186
2187#[cfg(not(unix))]
2188/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from `Src/utils.c:1785`.
2189pub fn fdsettyinfo(SHTTY: i32, ti: &()) -> std::io::Result<()> {
2190    Err(std::io::Error::new(
2191        std::io::ErrorKind::Unsupported,
2192        "no termios",
2193    ))
2194}
2195
2196// window size changed                                                     // c:1831
2197/// Port of `adjustlines(int signalled)` from Src/utils.c:1831 — TIOCGWINSZ
2198/// lookup that seeds `$LINES`. The C variant updates the global
2199/// `zterm_lines` and returns whether it changed; this Rust port
2200/// returns the row count directly. Falls back to `$LINES` env var,
2201/// then 24, mirroring the C source's `tclines > 0 ? tclines : 24`
2202/// fallback at line 1844.
2203/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2204pub fn adjustlines() -> usize {
2205    // c:1831
2206    #[cfg(unix)]
2207    {
2208        unsafe {
2209            let mut ws: libc::winsize = std::mem::zeroed();
2210            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_row > 0 {
2211                return ws.ws_row as usize;
2212            }
2213        }
2214    }
2215    // c:1844 fallback — paramtab `$LINES`, not OS env.
2216    getsparam("LINES")
2217        .and_then(|s| s.parse().ok())
2218        .unwrap_or(24)
2219}
2220
2221/// Port of `adjustcolumns(int signalled)` from Src/utils.c:1856 — TIOCGWINSZ
2222/// lookup that seeds `$COLUMNS`. The C variant updates the global
2223/// `zterm_columns` and returns whether it changed; this Rust port
2224/// returns the column count directly. Falls back to `$COLUMNS` env
2225/// var, then 80, mirroring the C source's `tccolumns > 0 ? tccolumns : 80`
2226/// fallback at line 1869.
2227/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2228pub fn adjustcolumns() -> usize {
2229    // c:1856
2230    #[cfg(unix)]
2231    {
2232        unsafe {
2233            let mut ws: libc::winsize = std::mem::zeroed();
2234            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
2235                return ws.ws_col as usize;
2236            }
2237        }
2238    }
2239    // c:1820 fallback — `if (zterm_columns <= 0) zterm_columns =
2240    //                    tccolumns > 0 ? tccolumns : 80`. C consults
2241    //                    `getsparam("COLUMNS")` (paramtab), not OS env.
2242    getsparam("COLUMNS")
2243        .and_then(|s| s.parse().ok())
2244        .unwrap_or(80)
2245}
2246
2247// window size changed                                                      // c:1824
2248/// Port of `void adjustwinsize(int from)` from `Src/utils.c:1889`.
2249/// SIGWINCH handler + LINES/COLUMNS-update entry. Reads the tty's
2250/// current geometry via `TIOCGWINSZ` ioctl, updates the cached
2251/// `zterm_lines`/`zterm_columns`, and writes `$LINES` / `$COLUMNS`
2252/// when they're already set in the environment.
2253/// ```c
2254/// void
2255/// adjustwinsize(int from)
2256/// {
2257///     static int getwinsz = 1;
2258///     int ttyrows = shttyinfo.winsize.ws_row;
2259///     int ttycols = shttyinfo.winsize.ws_col;
2260///     int resetzle = 0;
2261///     if (getwinsz || from == 1) {
2262///         if (SHTTY == -1) return;
2263///         if (ioctl(SHTTY, TIOCGWINSZ, &shttyinfo.winsize) == 0) {
2264///             resetzle = (ttyrows != ... || ttycols != ...);
2265///             ...
2266///         } else {
2267///             shttyinfo.winsize.ws_row = zterm_lines;
2268///             shttyinfo.winsize.ws_col = zterm_columns;
2269///         }
2270///     }
2271///     switch (from) {
2272///     case 0: case 1:
2273///         getwinsz = 0;
2274///         if (adjustlines(from) && zgetenv("LINES")) setiparam("LINES", zterm_lines);
2275///         if (adjustcolumns(from) && zgetenv("COLUMNS")) setiparam("COLUMNS", zterm_columns);
2276///         getwinsz = 1;
2277///         break;
2278///     case 2: resetzle = adjustlines(0); break;
2279///     case 3: resetzle = adjustcolumns(0); break;
2280///     }
2281///     if (interact && resetzle) zleentry(ZLE_CMD_REFRESH);
2282/// }
2283/// ```
2284pub fn adjustwinsize(from: i32) -> (usize, usize) {
2285    // c:1889
2286
2287    // c:1891 — `static int getwinsz = 1;`
2288    let getwinsz = ADJUSTWINSIZE_GETWINSZ.load(Ordering::SeqCst);
2289
2290    let mut ttyrows: i32 = 0;
2291    let mut ttycols: i32 = 0;
2292
2293    // c:1898-1917 — TIOCGWINSZ probe.
2294    if getwinsz != 0 || from == 1 {
2295        // c:1898
2296        let shtty = SHTTY.load(Ordering::Relaxed);
2297        if shtty == -1 {
2298            // c:1900
2299            return (adjustcolumns(), adjustlines()); // c:1901
2300        }
2301        #[cfg(unix)]
2302        unsafe {
2303            let mut ws: libc::winsize = std::mem::zeroed();
2304            if libc::ioctl(shtty, libc::TIOCGWINSZ, &mut ws as *mut _) == 0 {
2305                // c:1902
2306                ttyrows = ws.ws_row as i32; // c:1907
2307                ttycols = ws.ws_col as i32; // c:1908
2308            }
2309        }
2310    }
2311
2312    let mut resetzle = 0i32;
2313    match from {
2314        // c:1921
2315        0 | 1 => {
2316            // c:1922-1923
2317            ADJUSTWINSIZE_GETWINSZ.store(0, Ordering::SeqCst); // c:1924
2318            // c:1931-1934 — C: `if (adjustlines(from) && zgetenv("LINES"))
2319            // setiparam("LINES", zterm_lines);` (same for COLUMNS). The
2320            // zgetenv gate only guards re-publishing to an ENV-exported
2321            // LINES — in C the param itself is ALWAYS live because its
2322            // valptr aliases the `zterm_lines` global that adjustlines
2323            // just wrote (createparamtable IPDEF5 → zlevarsetfn/intvargetfn
2324            // on &zterm_lines). zshrs params store their own u_val copy
2325            // (no valptr aliasing), so the probe result must be written
2326            // through setiparam unconditionally — gating on the env left
2327            // `$LINES`/`$COLUMNS` at 0 for every interactive shell
2328            // (LINES is not in the environment; zsh doesn't export it).
2329            // Effect: `$(( LINES / 3 ))` = 0 → division-by-zero in
2330            // history-search-multi-word pagination (_hsmw_main:81), and
2331            // every LINES/COLUMNS-derived layout was wrong. Recursion is
2332            // safe: setiparam → zlevarsetfn → adjustwinsize(2|3) never
2333            // re-enters this arm.
2334            let lines = if ttyrows > 0 {
2335                ttyrows as i64 // c:1837 — zterm_lines = shttyinfo.winsize.ws_row
2336            } else {
2337                adjustlines() as i64 // c:1844 — tclines/24 fallback chain
2338            };
2339            setiparam("LINES", lines); // c:1932
2340            let cols = if ttycols > 0 {
2341                ttycols as i64 // c:1862 — zterm_columns = ws_col
2342            } else {
2343                adjustcolumns() as i64 // c:1869 — tccolumns/80 fallback chain
2344            };
2345            setiparam("COLUMNS", cols); // c:1934
2346            ADJUSTWINSIZE_GETWINSZ.store(1, Ordering::SeqCst); // c:1935
2347        }
2348        2 => {
2349            // c:1937
2350            resetzle = adjustlines() as i32; // c:1938
2351        }
2352        3 => {
2353            // c:1940
2354            resetzle = adjustcolumns() as i32; // c:1941
2355        }
2356        _ => {}
2357    }
2358
2359    // c:1946-1958 — resetzle + zleentry(ZLE_CMD_REFRESH) when interact.
2360    if from >= 2 && resetzle != 0 {
2361        // ZLE refresh dispatch via zleentry(ZLE_CMD_REFRESH) lands here
2362        // once the C signal handler shape ports.
2363        let _ = ttyrows;
2364        let _ = ttycols;
2365    }
2366
2367    (adjustcolumns(), adjustlines())
2368}
2369
2370/// Port of `static int getwinsz` from `Src/utils.c:1891`. Local
2371/// reentry guard inside adjustwinsize — bumped to 0 around the
2372/// setiparam recursion so the recursive call short-circuits.
2373pub static ADJUSTWINSIZE_GETWINSZ: std::sync::atomic::AtomicI32 =
2374    std::sync::atomic::AtomicI32::new(1); // c:1891
2375
2376/// Port of `check_fd_table(int fd)` from `Src/utils.c:1968-1983`.
2377/// ```c
2378/// if (fd <= max_zsh_fd) return;
2379/// if (fd >= fdtable_size) {
2380///     int old_size = fdtable_size;
2381///     while (fd >= fdtable_size)
2382///         fdtable = zrealloc(fdtable, (fdtable_size *= 2) * sizeof(*fdtable));
2383///     memset(fdtable + old_size, 0, (fdtable_size - old_size) * sizeof(*fdtable));
2384/// }
2385/// max_zsh_fd = fd;
2386/// ```
2387/// C semantics: GROW the `fdtable` array so it can index `fd`, then
2388/// update `max_zsh_fd`. Returns void.
2389///
2390/// The Rust port keeps the same signature for name-parity but the
2391/// fdtable global isn't yet modeled — so this is a no-op shim. The
2392/// previous Rust impl was an `fcntl(F_GETFD)` validity check —
2393/// COMPLETELY DIFFERENT SEMANTICS from C (which doesn't validate
2394/// the fd at all, just grows the table). Fixed to no-op + bool
2395/// return for caller compatibility (no live callers).
2396pub fn check_fd_table(fd: i32) -> bool {
2397    // c:1969
2398    // c:1971-1972 — `if (fd <= max_zsh_fd) return;`
2399    let cur_max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:1971
2400    if fd <= cur_max {
2401        // c:1971
2402        return true; // c:1972 (return; in C)
2403    }
2404    if fd < 0 {
2405        // defensive — fdtable index must be ≥0
2406        return false;
2407    }
2408    // c:1974-1981 — `if (fd >= fdtable_size) { while (fd >= fdtable_size)
2409    //                fdtable_size *= 2; zrealloc; memset(new_slots, 0); }`.
2410    // Rust Vec::resize handles the realloc + zero-fill in one call (the
2411    // expansion bumps capacity geometrically too).
2412    {
2413        let mut g = fdtable_lock().lock().unwrap();
2414        if (fd as usize) >= g.len() {
2415            g.resize((fd as usize) + 1, FDT_UNUSED); // c:1975-1979 grow
2416        }
2417    }
2418    // c:1982 — `max_zsh_fd = fd;`.
2419    MAX_ZSH_FD.store(fd, Ordering::Relaxed); // c:1982
2420    true
2421}
2422
2423/// Port of `movefd(int fd)` from `Src/utils.c:1989-2012`.
2424/// ```c
2425/// if (fd != -1 && fd < 10) {
2426///     int fe = fcntl(fd, F_DUPFD, 10);
2427///     zclose(fd);          // unconditional close, even when fe == -1
2428///     fd = fe;
2429/// }
2430/// if (fd != -1) {
2431///     check_fd_table(fd);
2432///     fdtable[fd] = FDT_INTERNAL;
2433/// }
2434/// return fd;
2435/// ```
2436/// Two divergences fixed this iteration:
2437///   1. Old Rust closed the source fd ONLY on success; C closes
2438///      unconditionally per the c:1999-2004 comment "probably better
2439///      to avoid a leak."
2440///   2. Old Rust added `FD_CLOEXEC` after the dup; C does NOT —
2441///      CLOEXEC is added later by `addmodulefd`/`addlockfd` callers
2442///      that need it. movefd itself does not.
2443pub fn movefd(fd: i32) -> i32 {
2444    #[cfg(unix)]
2445    {
2446        let mut fd = fd;
2447        if fd != -1 && fd < 10 {
2448            // c:1992
2449            let fe = unsafe { libc::fcntl(fd, libc::F_DUPFD, 10) }; // c:1994
2450            unsafe { libc::close(fd) }; // c:2004 zclose(fd) — unconditional
2451            fd = fe; // c:2005
2452        }
2453        // c:2007-2010 — `if (fd != -1) { check_fd_table(fd); fdtable[fd]
2454        // = FDT_INTERNAL; }`. The fdtable global IS now modeled (port
2455        // of utils.c:63 — `Vec<u8>` behind `fdtable_lock`). Mark the
2456        // new fd as FDT_INTERNAL so the rest of the shell knows the
2457        // shell is using it (a later `addmodulefd` / `addlockfd`
2458        // upgrade may reclassify; raw external code shouldn't touch
2459        // it). The previous port skipped this so internal-fd tracking
2460        // (which `closeallelse` and forkexec rely on) silently
2461        // never saw zshrs-internal fds.
2462        if fd != -1 {
2463            // c:2007
2464            check_fd_table(fd); // c:2008
2465            fdtable_set(fd, FDT_INTERNAL); // c:2009
2466        }
2467        fd
2468    }
2469    #[cfg(not(unix))]
2470    {
2471        fd
2472    }
2473}
2474
2475/// Port of `redup(int x, int y)` from `Src/utils.c:2021`.
2476///
2477/// C signature: `int redup(int x, int y)`.
2478/// "Move fd x to y. If x == -1, fd y is closed. Returns y for
2479/// success, -1 for failure." (c:2014-2016 docstring.)
2480///
2481/// Body mirrors c:2023-2068: when `x == -1`, close `y` (return `y`);
2482/// when `x == y`, no-op (return `y`); otherwise `dup2(x, y)` +
2483/// `close(x)`.
2484///
2485/// C body fdtable updates (c:2053-2063) that the previous Rust port
2486/// SKIPPED with a stale "fdtable global not yet ported" comment —
2487/// fdtable IS now ported and these updates are load-bearing:
2488///   * `fdtable[y] = fdtable[x]` — the new fd inherits the old fd's
2489///     ownership category (FDT_INTERNAL / FDT_MODULE / etc.).
2490///   * If the inherited type is `FDT_FLOCK` / `FDT_FLOCK_EXEC`, promote
2491///     to `FDT_INTERNAL` (the dup'd fd doesn't carry the flock).
2492///   * If `fdtable[x] == FDT_FLOCK`, decrement `fdtable_flocks` (the
2493///     original lock-holding fd is about to be closed).
2494///
2495/// Without these updates, redup'd fds had stale `FDT_UNUSED` ownership
2496/// and `closeallelse(FDT_EXTERNAL)` etc. couldn't classify them.
2497pub fn redup(x: i32, y: i32) -> i32 {
2498    // c:2021
2499    let mut ret = y; // c:2023
2500    #[cfg(unix)]
2501    {
2502        if x < 0 {
2503            // c:2047
2504            zclose(y); // c:2048
2505        } else if x != y {
2506            // c:2049
2507            // c:2053-2057 — successful dup2: copy fdtable + FLOCK promote.
2508            if unsafe { libc::dup2(x, y) } == -1 {
2509                // c:2050
2510                ret = -1; // c:2051
2511            } else {
2512                check_fd_table(y); // c:2053
2513                let kind_x = fdtable_get(x); // c:2054
2514                let kind_y = if kind_x == FDT_FLOCK || kind_x == FDT_FLOCK_EXEC {
2515                    FDT_INTERNAL // c:2055-2056
2516                } else {
2517                    kind_x // c:2054
2518                };
2519                fdtable_set(y, kind_y);
2520            }
2521            // c:2062-2063 — `if (fdtable[x] == FDT_FLOCK) fdtable_flocks--;`
2522            // The original lock-holding fd is about to be closed, so the
2523            // flock-count tracker must drop. Even on dup2 failure C still
2524            // closes x, so this runs in both arms.
2525            if fdtable_get(x) == FDT_FLOCK {
2526                // c:2062
2527                FDTABLE_FLOCKS.fetch_sub(1, Ordering::SeqCst); // c:2063
2528            }
2529            zclose(x); // c:2064
2530        }
2531    }
2532    #[cfg(not(unix))]
2533    {
2534        let _ = (x, y);
2535    }
2536    ret // c:2067
2537}
2538
2539/// Port of `addmodulefd(int fd, int fdt)` from `Src/utils.c:2090-2097`.
2540/// ```c
2541/// if (fd >= 0) {
2542///     check_fd_table(fd);
2543///     fdtable[fd] = fdt;
2544/// }
2545/// ```
2546/// Two divergences fixed this iteration:
2547///   1. C accepts an `fdt` parameter (FDT_MODULE / FDT_INTERNAL /
2548///      FDT_EXTERNAL — see callers in Src/Modules/{random,socket,tcp,
2549///      db_gdbm}.c). Rust port hardcoded `FDT_MODULE` — silently
2550///      ignored caller intent. Now takes `fdt` per C.
2551///   2. C does NOT set `FD_CLOEXEC`. Rust port was adding it
2552///      unconditionally — divergent. CLOEXEC is the caller's
2553///      responsibility based on the fdt semantics.
2554pub fn addmodulefd(fd: i32, fdt: i32) {
2555    // c:2091
2556    // c:2093 — `if (fd >= 0)`.
2557    if fd >= 0 {
2558        // c:2094 — `check_fd_table(fd)` — grow fdtable to cover fd.
2559        check_fd_table(fd); // c:2094
2560                            // c:2095 — `fdtable[fd] = fdt`. Routes through the canonical
2561                            // helper which mirrors C's direct `fdtable[fd] = fdt` write.
2562        fdtable_set(fd, fdt); // c:2095
2563    }
2564}
2565
2566/// Port of `addlockfd(int fd, int cloexec)` from `Src/utils.c:2111-2121`.
2567/// ```c
2568/// if (cloexec) {
2569///     if (fdtable[fd] != FDT_FLOCK)
2570///         fdtable_flocks++;
2571///     fdtable[fd] = FDT_FLOCK;
2572/// } else {
2573///     fdtable[fd] = FDT_FLOCK_EXEC;
2574/// }
2575/// ```
2576/// Critical divergence fixed: C updates `fdtable[fd]` to FDT_FLOCK
2577/// (cloexec=true) or FDT_FLOCK_EXEC (cloexec=false). Previous Rust
2578/// port did the OPPOSITE — called `fcntl(F_SETFD, FD_CLOEXEC)` when
2579/// `cloexec` was true. That's wrong on two counts:
2580///   1. C's "cloexec" parameter selects the FDT category, not a
2581///      libc fcntl flag.
2582///   2. FDT_FLOCK means "lock survives across exec via fd inheritance"
2583///      — the OPPOSITE of close-on-exec. The Rust port was adding
2584///      CLOEXEC for the very case where the fd should be inheritable.
2585pub fn addlockfd(fd: i32, cloexec: bool) {
2586    // c:2112
2587    if cloexec {
2588        // c:2114
2589        // c:2115-2117 — track flock count, set FDT_FLOCK.
2590        if fdtable_get(fd) != FDT_FLOCK {
2591            FDTABLE_FLOCKS.fetch_add(1, Ordering::SeqCst);
2592        }
2593        fdtable_set(fd, FDT_FLOCK);
2594    } else {
2595        // c:2118
2596        // c:2119 — FDT_FLOCK_EXEC means flock inherited by exec.
2597        fdtable_set(fd, FDT_FLOCK_EXEC);
2598    }
2599}
2600
2601// FDTABLE_FLOCKS canonical declaration lives at utils.rs:5219
2602// (the exec.c global with same name). Reuse, don't redeclare.
2603
2604/// Close the given fd, and clear it from fdtable.                          // c:2123
2605/// Port of `int zclose(int fd)` from `Src/utils.c:2127`.
2606pub fn zclose(fd: i32) -> i32 {
2607    // c:2127
2608    if fd >= 0 {
2609        // c:2129
2610        // c:2130-2133 — comment carry: "Careful: we allow closing of
2611        // arbitrary fd's, beyond max_zsh_fd. In that case we don't
2612        // try anything clever."
2613        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed); // c:2134
2614        if fd <= max_fd {
2615            // c:2134
2616            // !!! ZSHRS THREADING ADAPTATION — deviates from C here !!!
2617            // C closes blindly; it is single-threaded, so a shell-recorded
2618            // fd number can never be concurrently recycled by someone else.
2619            // zshrs runs 18 worker threads whose Rust-owned fds (ReadDir
2620            // dirfds, SQLite, File) recycle freed numbers immediately — a
2621            // stale zclose then closes a FOREIGN fd, and std panics in
2622            // ReadDir::drop ("unexpected error during closedir: EBADF",
2623            // observed from the compinit bg scan). An in-range fd the
2624            // fdtable does NOT own is by definition not ours to close.
2625            if fdtable_get(fd) == FDT_UNUSED {
2626                tracing::debug!(fd, "zclose: skipping unowned in-range fd (thread-safety guard)");
2627                return 0;
2628            }
2629            if fdtable_get(fd) == FDT_FLOCK {
2630                // c:2135
2631                FDTABLE_FLOCKS.fetch_sub(1, Ordering::Relaxed);
2632                // c:2136
2633            }
2634            fdtable_set(fd, FDT_UNUSED); // c:2137
2635                                         // c:2138-2139 — shrink max_zsh_fd past trailing UNUSED slots.
2636            let mut m = MAX_ZSH_FD.load(Ordering::Relaxed);
2637            while m > 0 && fdtable_get(m) == FDT_UNUSED {
2638                m -= 1;
2639            }
2640            MAX_ZSH_FD.store(m, Ordering::Relaxed);
2641            // c:2140-2143 — coproc fd tracking.
2642            if fd == coprocin.load(Ordering::Relaxed) {
2643                coprocin.store(-1, Ordering::Relaxed);
2644            }
2645            if fd == coprocout.load(Ordering::Relaxed) {
2646                coprocout.store(-1, Ordering::Relaxed);
2647            }
2648        }
2649        #[cfg(unix)]
2650        unsafe {
2651            return libc::close(fd);
2652        } // c:2145
2653        #[cfg(not(unix))]
2654        return 0;
2655    }
2656    -1 // c:2147
2657}
2658
2659/// Port of `int zcloselockfd(int fd)` from `Src/utils.c:2155-2164`.
2660/// ```c
2661/// if (fd > max_zsh_fd) return -1;
2662/// if (fdtable[fd] != FDT_FLOCK && fdtable[fd] != FDT_FLOCK_EXEC)
2663///     return -1;
2664/// zclose(fd);
2665/// return 0;
2666/// ```
2667/// "Close an fd returning 0 if used for locking; return -1 if it
2668/// isn't." Caller (`bin_zsystem_flock -u`) uses the -1 return to
2669/// distinguish "fd not in the flock table" from "successfully
2670/// released a lock."
2671///
2672/// Previously the Rust port skipped the FDT_FLOCK / FDT_FLOCK_EXEC
2673/// check entirely and always returned 0 — meaning `zsystem flock -u
2674/// <unlocked-fd>` reported success on any fd. Now that the
2675/// `addlockfd` port (this iteration) populates the fdtable with the
2676/// canonical FDT_FLOCK / FDT_FLOCK_EXEC slots, the check can fire
2677/// faithfully.
2678pub fn zcloselockfd(fd: i32) -> i32 {
2679    // c:2156
2680    let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2681    // c:2158 — `if (fd > max_zsh_fd) return -1;`.
2682    if fd > max_fd {
2683        return -1;
2684    }
2685    // c:2160-2161 — `if (fdtable[fd] != FDT_FLOCK && != FDT_FLOCK_EXEC)
2686    //                   return -1;`.
2687    let slot = fdtable_get(fd);
2688    if slot != FDT_FLOCK && slot != FDT_FLOCK_EXEC {
2689        return -1;
2690    }
2691    zclose(fd); // c:2162
2692    0 // c:2163
2693}
2694
2695/// Port of `char *gettempname(const char *prefix, int use_heap)` from Src/utils.c:2178.
2696///
2697/// Returns a unique tempfile name templated like C `mktemp(3)` —
2698/// `{prefix}.XXXXXX`. Falls back to `getsparam("TMPPREFIX")` and
2699/// then `DEFAULT_TMPPREFIX` when `prefix` is None. Does NOT create
2700/// the file (matches C — only `gettempfile` creates).
2701pub fn gettempname(prefix: Option<&str>, _use_heap: bool) -> Option<String> {
2702    // c:2178
2703    let suffix = if prefix.is_some() {
2704        ".XXXXXX"
2705    } else {
2706        "XXXXXX"
2707    }; // c:2178
2708    queue_signals(); // c:2182
2709    let prefix_owned: String = match prefix {
2710        // c:2183
2711        Some(p) => p.to_string(),
2712        // c:2184 — `getsparam("TMPPREFIX")`. Read from paramtab (not OS
2713        //          env); fall back to compile-time default when unset.
2714        None => getsparam("TMPPREFIX")
2715            .unwrap_or_else(|| crate::ported::config_h::DEFAULT_TMPPREFIX.to_string()),
2716    };
2717    let template = format!("{}{}", prefix_owned, suffix); // c:2186-2188
2718                                                          // C uses mktemp(3) which mutates the X's into a unique name              // c:2192/2219
2719                                                          // without creating the file. Rust has no mktemp; emulate with
2720                                                          // pid+timestamp. Caller is responsible for O_EXCL open.
2721    let pid = std::process::id();
2722    let nanos = std::time::SystemTime::now()
2723        .duration_since(UNIX_EPOCH)
2724        .map(|d| d.as_nanos())
2725        .unwrap_or(0);
2726    let unique = format!("{:x}{:x}", pid, nanos & 0xffffff);
2727    let name = template.replace("XXXXXX", &unique);
2728    unqueue_signals(); // c:2221
2729    Some(name)
2730}
2731
2732/// Check if metafied - port from zsh/Src/utils.c has_token()
2733// Check if a string contains a token                                       // c:2282
2734/// Port of `has_token(const char *s)` from `Src/utils.c:2282` — used by
2735/// `ecstrcode` (parse.rs:989) to flip the token-marker bit on the
2736/// encoded string offset. Token markers live in 0x83..=0x9f (Pound,
2737/// Stringg, Hat, Star, ..., Bnull, Nularg). Earlier impl checked
2738/// only 0x83 (Meta) which missed Dash/Equals/Inbrack/Inbrace/etc.,
2739/// so any string containing those (e.g. `dart-lang/dart` →
2740/// `dart\u{9b}lang/dart`) got encoded with the no-token bit set,
2741/// breaking byte parity with C's wordcode-emitter output.
2742/// Port of `int has_token(const char *s)` from `Src/utils.c:2280-2288`.
2743/// ```c
2744/// while (*s)
2745///     if (itok(*s++)) return 1;
2746/// return 0;
2747/// ```
2748/// Routes through the canonical `itok()` typtab-driven predicate.
2749/// Previous Rust port used hardcoded `0x83..=0x9f` which was:
2750///   - Wrong on the low end: includes `0x83` (Meta, NOT a token).
2751///   - Wrong on the high end: missing `0xa0..=0xa1` (part of the
2752///     Snull..Nularg range that `itok` legitimately covers).
2753/// The full canonical ITOK range per `Src/zsh.h:152-159` is
2754/// `0x84..=0xa1` (Pound..Nularg), with possible gaps in the middle
2755/// (Snull at 0x9d follows Bang at 0x9c).
2756pub fn has_token(s: &str) -> bool {
2757    // c:2282
2758    s.bytes().any(itok) // c:2285
2759}
2760
2761// Delete a character in a string                                           // c:2294
2762/// Remove character from string (from utils.c chuck)
2763pub fn chuck(s: &mut String, pos: usize) {
2764    // c:2294
2765    if pos < s.len() {
2766        s.remove(pos);
2767    }
2768}
2769
2770/// Convert character to lowercase
2771/// To-lowercase that respects locale.
2772/// Port of `tulower(int c)` from Src/utils.c.
2773pub fn tulower(c: char) -> char {
2774    // c:2302
2775    c.to_lowercase().next().unwrap_or(c)
2776}
2777
2778/// Convert character to uppercase
2779/// To-uppercase that respects locale.
2780/// Port of `tuupper(int c)` from Src/utils.c.
2781pub fn tuupper(c: char) -> char {
2782    // c:2310
2783    c.to_uppercase().next().unwrap_or(c)
2784}
2785
2786/// Port of `void ztrncpy(char *s, char *t, int len)` from `Src/utils.c:2320`.
2787///
2788/// C body (c:2320-2326):
2789/// ```c
2790/// while (len--)
2791///     *s++ = *t++;
2792/// *s = '\0';
2793/// ```
2794///
2795/// Copy `len` bytes from `t` into `s`, then NUL-terminate. C does no
2796/// bounds check; the caller must ensure `s` has `len + 1` bytes of
2797/// capacity. Rust port preserves the (s, t, len) parameter shape:
2798/// `s: &mut String` (output), `t: &str` (source), `len: usize` (copy
2799/// count). The C NUL terminator at c:2325 is implicit in Rust's
2800/// length-prefixed String. The Rust port clamps `len` to `t.len()` to
2801/// avoid the out-of-bounds read C's `*s++ = *t++` would perform when
2802/// `len > strlen(t)` (UB in C; explicitly bounded in Rust).
2803pub fn ztrncpy(s: &mut String, t: &str, len: usize) {
2804    // c:2320
2805    s.clear(); // C overwrites from start of *s
2806    let take = len.min(t.len()); // c:2322 while (len--)
2807    s.push_str(&t[..take]); // c:2322 *s++ = *t++
2808                            // c:2325 — `*s = '\0';` (implicit in Rust String length)
2809}
2810
2811/// Port of `void strucpy(char **s, char *t)` from `Src/utils.c:2331-2337`.
2812/// ```c
2813/// char *u = *s;
2814/// while ((*u++ = *t++));
2815/// *s = u - 1;       // leave *s pointing at the NUL terminator
2816/// ```
2817/// Body: `strcpy(*s, t)` + advance `*s` to point at the new
2818/// NUL terminator. The "u" in the name is a C convention for the
2819/// local pointer-walker, NOT "upper-case" — the function does NOT
2820/// change case.
2821///
2822/// Rust API: param name `s` matches C exactly per Rule E (despite Rust
2823/// convention of `dest` for output buffers). The C pointer-advance
2824/// `*s = u - 1` translates to "the new end of s is at s.len()", which
2825/// is implicit in Rust's owning-String model.
2826pub fn strucpy(s: &mut String, t: &str) {
2827    // c:2331
2828    s.push_str(t); // c:2335 `*u++ = *t++` loop
2829                   // c:2336 — `*s = u - 1;` — pointer-advance is implicit in Rust
2830                   // (`s.len()` is the new end-of-string position).
2831}
2832
2833/// Port of `void struncpy(char **s, char *t, int n)` from `Src/utils.c:2341-2350`.
2834/// ```c
2835/// char *u = *s;
2836/// while (n-- && (*u = *t++)) u++;
2837/// *s = u;
2838/// if (n > 0) *u = '\0';
2839/// ```
2840/// Body: copy up to `n` bytes from `t` to `*s`, NUL-terminate.
2841/// Note c:2348 — "just one null-byte will do, unlike strncpy(3)";
2842/// doesn't pad with NULs.
2843///
2844/// Param names match C exactly per Rule E.
2845pub fn struncpy(s: &mut String, t: &str, n: usize) {
2846    // c:2341
2847    // c:2345 — `while (n-- && (*u = *t++)) u++;` — copy up to n
2848    // bytes, stop at NUL (which in Rust &str is the end-of-string).
2849    let take = n.min(t.len());
2850    s.push_str(&t[..take]);
2851}
2852
2853/// Array length - port from arrlen()
2854/// Port of `arrlen(char **s)` from `Src/utils.c:2357`.
2855pub fn arrlen<T>(s: &[T]) -> usize {
2856    // c:2357
2857    s.len()
2858}
2859
2860// Return TRUE iff arrlen(s) >= lower_bound, but more efficiently.          // c:2382
2861/// Check if array length >= n (from utils.c arrlen_ge)
2862pub fn arrlen_ge<T>(arr: &[T], n: usize) -> bool {
2863    // c:2369
2864    arr.len() >= n
2865}
2866
2867// Return TRUE iff arrlen(s) > lower_bound, but more efficiently.           // c:2400
2868/// Check if array length > n (from utils.c arrlen_gt)
2869pub fn arrlen_gt<T>(arr: &[T], n: usize) -> bool {
2870    // c:2382
2871    arr.len() > n
2872}
2873
2874// Return TRUE iff arrlen(s) <= upper_bound, but more efficiently.          // c:2409
2875/// Check if array length <= n (from utils.c arrlen_le)
2876pub fn arrlen_le<T>(arr: &[T], n: usize) -> bool {
2877    // c:2391
2878    arr.len() <= n
2879}
2880
2881// Return TRUE iff arrlen(s) < upper_bound, but more efficiently.           // c:2400
2882/// Check if array length < n (from utils.c arrlen_lt)
2883pub fn arrlen_lt<T>(arr: &[T], n: usize) -> bool {
2884    // c:2400
2885    arr.len() < n
2886}
2887
2888/// Direct port of `int skipparens(char inpar, char outpar, char **s)`
2889/// from `Src/utils.c:2409`. Walks `*s` past a balanced `inpar`/`outpar`
2890/// pair, advancing the input slice in place. Returns 0 on full
2891/// balance, `-1` when `**s != inpar` (no opening paren), or positive
2892/// when the scan ran off the end of `*s` with `level > 0`
2893/// (unbalanced).
2894///
2895/// Rust signature: `s: &mut &str` mirrors C's `char **s` out-arg —
2896/// the caller's slice handle is rewritten to point past the closing
2897/// paren (or to the empty tail on unbalanced input).
2898/// WARNING: param names don't match C — Rust=(open, close, s) vs C=(inpar, outpar, s)
2899pub fn skipparens(open: char, close: char, s: &mut &str) -> i32 {
2900    // c:2409
2901    let mut chars = s.char_indices();
2902    // c:2412 — `if (**s != inpar) return -1;`
2903    match chars.next() {
2904        Some((_, c)) if c == open => {}
2905        _ => return -1,
2906    }
2907    // c:2414 — `for (level = 1; *++*s && level;)`
2908    let mut level: i32 = 1;
2909    let mut consumed = open.len_utf8();
2910    for (i, c) in chars {
2911        consumed = i + c.len_utf8();
2912        if c == open {
2913            level += 1; // c:2416-2417
2914        } else if c == close {
2915            level -= 1; // c:2418-2419
2916        }
2917        if level == 0 {
2918            break;
2919        }
2920    }
2921    *s = &s[consumed..];
2922    // c:2421 — `return level;`. 0 on balanced match, positive on
2923    // ran-off-end (unbalanced input).
2924    level
2925}
2926
2927/// zlong with base autodetection. Returns the parsed value AND a
2928/// `&str` slice pointing to the first unconsumed character —
2929/// matching C's `char **t` out-arg.
2930///
2931/// C signature: `zlong zstrtol(const char *s, char **t, int base)`.
2932/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
2933/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Otherwise `base`
2934/// must be in [2, 36]; values outside that range emit `zerr` and
2935/// return 0.
2936///
2937/// On overflow, mirrors C's "number truncated after N digits"
2938/// behaviour: emits a `zwarn` and returns the truncated value
2939/// (last in-range computation).
2940/// Port of `zstrtol(const char *s, char **t, int base)` from `Src/utils.c:2427`.
2941/// WARNING: param names don't match C — Rust=(s, base) vs C=(s, t, base)
2942pub fn zstrtol(s: &str, base: i32) -> (i64, &str) {
2943    // c:2427
2944    zstrtol_underscore(s, base, false) // c:2427
2945}
2946
2947/// Port of `zstrtol_underscore(const char *s, char **t, int base, int underscore)` from `Src/utils.c:2438`.
2948///
2949/// C signature: `zlong zstrtol_underscore(const char *s, char **t,
2950///                                         int base, int underscore)`.
2951///
2952/// Convert string to zlong with optional `_` digit-separator support.
2953/// `underscore != 0` allows underscores to appear inside the digit
2954/// run (skipped during accumulation). Returns the parsed value AND
2955/// the unconsumed-tail slice (matching C's `*t = (char *)s` writeback
2956/// at line 2516).
2957///
2958/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
2959/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Bases outside [2, 36]
2960/// emit `zerr` and return `(0, original_s)`. Overflow truncates and
2961/// emits a `zwarn` per c:2510-2512.
2962pub fn zstrtol_underscore(s: &str, base: i32, underscore: bool) -> (i64, &str) {
2963    // c:2438
2964    let bytes = s.as_bytes();
2965    let mut i = 0usize;
2966    // c:2444-2445 — skip leading `inblank` whitespace.
2967    while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
2968        i += 1;
2969    }
2970    // c:2447-2450 — handle sign.
2971    let neg = if i < bytes.len() && (bytes[i] == b'-') {
2972        // c:2447 IS_DASH
2973        i += 1;
2974        true
2975    } else if i < bytes.len() && bytes[i] == b'+' {
2976        // c:2449
2977        i += 1;
2978        false
2979    } else {
2980        false
2981    };
2982
2983    // c:2452-2461 — base autodetect when base == 0.
2984    let mut base = base;
2985    if base == 0 {
2986        // c:2452
2987        if i >= bytes.len() || bytes[i] != b'0' {
2988            // c:2453
2989            base = 10; // c:2454
2990        } else {
2991            i += 1; // c:2455 ++s
2992            if i < bytes.len() && (bytes[i] == b'x' || bytes[i] == b'X') {
2993                // c:2455
2994                base = 16;
2995                i += 1;
2996            } else if i < bytes.len() && (bytes[i] == b'b' || bytes[i] == b'B') {
2997                // c:2457
2998                base = 2;
2999                i += 1;
3000            } else {
3001                // c:2459
3002                base = 8; // c:2460
3003            }
3004        }
3005    }
3006    let inp_idx = i; // c:2462 inp = s
3007    if base < 2 || base > 36 {
3008        // c:2463
3009        zerr(&format!(
3010            "invalid base (must be 2 to 36 inclusive): {}",
3011            base
3012        )); // c:2464
3013        return (0, s); // c:2465 return 0
3014    }
3015
3016    let mut calc: u64 = 0; // c:2441
3017    let mut trunc_idx: Option<usize> = None; // c:2440 trunc = NULL
3018    if base <= 10 {
3019        // c:2466
3020        // c:2467-2479 — digit accumulator, low bases.
3021        let max_d = b'0' + base as u8;
3022        while i < bytes.len() {
3023            let c = bytes[i];
3024            if c >= b'0' && c < max_d {
3025                if trunc_idx.is_none() {
3026                    let newcalc = calc
3027                        .wrapping_mul(base as u64)
3028                        .wrapping_add((c - b'0') as u64);
3029                    if newcalc < calc {
3030                        // c:2473
3031                        trunc_idx = Some(i); // c:2474
3032                    } else {
3033                        calc = newcalc;
3034                    }
3035                }
3036                i += 1;
3037            } else if underscore && c == b'_' {
3038                // c:2467 underscore && *s == '_'
3039                i += 1;
3040            } else {
3041                break;
3042            }
3043        }
3044    } else {
3045        // c:2480
3046        // c:2481-2495 — digit accumulator, high bases (>10).
3047        while i < bytes.len() {
3048            let c = bytes[i];
3049            let digit = if c.is_ascii_digit() {
3050                // c:2481 idigit
3051                Some((c - b'0') as u32)
3052            } else if c >= b'a' && c < b'a' + base as u8 - 10 {
3053                // c:2482
3054                Some(((c & 0x1f) + 9) as u32) // c:2486 (*s & 0x1f) + 9
3055            } else if c >= b'A' && c < b'A' + base as u8 - 10 {
3056                // c:2483
3057                Some(((c & 0x1f) + 9) as u32)
3058            } else if underscore && c == b'_' {
3059                // c:2484
3060                i += 1;
3061                continue;
3062            } else {
3063                None
3064            };
3065            match digit {
3066                Some(d) => {
3067                    if trunc_idx.is_none() {
3068                        let newcalc = calc.wrapping_mul(base as u64).wrapping_add(d as u64);
3069                        if newcalc < calc {
3070                            // c:2489
3071                            trunc_idx = Some(i); // c:2490
3072                        } else {
3073                            calc = newcalc;
3074                        }
3075                    }
3076                    i += 1;
3077                }
3078                None => break,
3079            }
3080        }
3081    }
3082
3083    // c:2504-2511 — special-case: signed-overflow into top bit.
3084    // The lowest negative number triggers the first test but is
3085    // representable correctly; check it explicitly.
3086    if trunc_idx.is_none() && (calc as i64) < 0 {
3087        // c:2504
3088        let top_bit_only = calc & !(1u64 << 63); // c:2506
3089        if !neg || top_bit_only != 0 {
3090            // c:2505
3091            trunc_idx = Some(i.saturating_sub(1)); // c:2508
3092            calc /= base as u64; // c:2509
3093        }
3094    }
3095
3096    if let Some(t) = trunc_idx {
3097        // c:2512
3098        let digits = t.saturating_sub(inp_idx);
3099        let inp_str = &s[inp_idx..];
3100        zwarn(&format!(
3101            "number truncated after {} digits: {}",
3102            digits, inp_str
3103        )); // c:2513
3104    }
3105
3106    let result = if neg {
3107        // c:2517
3108        -(calc as i64)
3109    } else {
3110        calc as i64
3111    };
3112    (result, &s[i..])
3113}
3114
3115/// Parse unsigned integer with underscore support
3116/// Port from zsh/Src/utils.c zstrtoul_underscore() lines 2528-2575
3117/// Port of `zstrtoul_underscore(const char *s, zulong *retval)` from
3118/// `Src/utils.c:2527-2590`. C body:
3119/// ```c
3120/// if (*s == '+') s++;
3121/// if      (*s != '0')                    base = 10;
3122/// else if (*++s == 'x' || *s == 'X')     base = 16, s++;
3123/// else if (*s == 'b' || *s == 'B')       base = 2,  s++;
3124/// else                                   base = isset(OCTALZEROES) ? 8 : 10;
3125/// ```
3126/// The leading-zero case is option-gated on OCTALZEROES — without
3127/// the option (default), `0777` parses as decimal 777, NOT octal 511.
3128/// Previously the Rust port always treated leading-zero as octal —
3129/// divergent for the default shell setup.
3130pub fn zstrtoul_underscore(s: &str) -> Option<u64> {
3131    // c:2529
3132    let s = s.trim();
3133    let s = s.strip_prefix('+').unwrap_or(s); // c:2533-2534
3134
3135    let (base, rest) = if s.starts_with("0x") || s.starts_with("0X") {
3136        // c:2538
3137        (16, &s[2..])
3138    } else if s.starts_with("0b") || s.starts_with("0B") {
3139        // c:2540
3140        (2, &s[2..])
3141    } else if s.starts_with('0') && s.len() > 1 && isset(OCTALZEROES)
3142    // c:2543
3143    {
3144        (8, &s[1..])
3145    } else {
3146        (10, s)
3147    };
3148
3149    let rest = rest.replace('_', "");
3150    u64::from_str_radix(&rest, base).ok()
3151}
3152
3153/// Port of `int setblock_fd(int turnonblocking, int fd, long *modep)`
3154/// from `Src/utils.c:2579`. Toggles O_NONBLOCK on `fd`.
3155///
3156/// **Signature matches C exactly** (param order `turnonblocking, fd`):
3157/// - `turnonblocking=1` → clear O_NONBLOCK (enable blocking).
3158/// - `turnonblocking=0` → set O_NONBLOCK (disable blocking).
3159///
3160/// Returns:
3161/// - `(true, mode)` if the fd's state was changed.
3162/// - `(false, -1)` if the fd is a regular file (C skips regular
3163///   files entirely — only operates on pipes/sockets/ttys per
3164///   `c:2599 if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`).
3165/// - `(false, mode)` if state was already correct.
3166///
3167/// Rust returns `(state_changed, modep_value)` as a tuple to mirror
3168/// C's `int` return + `long *modep` out-param.
3169pub fn setblock_fd(turnonblocking: bool, fd: i32) -> (bool, libc::c_long) {
3170    // c:2579
3171    #[cfg(unix)]
3172    unsafe {
3173        // c:2598-2600 — `if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`.
3174        let mut st: libc::stat = std::mem::zeroed();
3175        if libc::fstat(fd, &mut st) != 0 {
3176            return (false, -1);
3177        }
3178        // c:2599 — skip regular files (no nonblock concept).
3179        let mode_bits = st.st_mode as u32;
3180        if (mode_bits & libc::S_IFMT as u32) == libc::S_IFREG as u32 {
3181            return (false, -1); // c:2614 *modep = -1; return 0
3182        }
3183        // c:2601 — `*modep = fcntl(fd, F_GETFL, 0);`
3184        let modep = libc::fcntl(fd, libc::F_GETFL, 0) as libc::c_long;
3185        if modep < 0 {
3186            return (false, -1); // c:2602 if (*modep != -1)
3187        }
3188        const NONBLOCK: libc::c_long = libc::O_NONBLOCK as libc::c_long;
3189        if !turnonblocking {
3190            // c:2603-2606 — want to KNOW if blocking was off; set it on.
3191            if (modep & NONBLOCK) != 0 {
3192                return (true, modep); // already nonblock — no-op, but "off"
3193            }
3194            if libc::fcntl(fd, libc::F_SETFL, modep | NONBLOCK) == 0 {
3195                return (true, modep); // c:2606
3196            }
3197        } else {
3198            // c:2607-2611 — want to clear NONBLOCK if currently set.
3199            if (modep & NONBLOCK) != 0 && libc::fcntl(fd, libc::F_SETFL, modep & !NONBLOCK) == 0 {
3200                return (true, modep); // c:2611 state changed
3201            }
3202        }
3203        (false, modep)
3204    }
3205    #[cfg(not(unix))]
3206    {
3207        let _ = (turnonblocking, fd);
3208        (false, -1)
3209    }
3210}
3211
3212/// Port of `int setblock_stdin(void)` from `Src/utils.c:2620-2625`.
3213/// ```c
3214/// long mode;
3215/// return setblock_fd(1, 0, &mode);
3216/// ```
3217/// `turnonblocking=1, fd=0` — turn ON blocking on stdin (clear
3218/// O_NONBLOCK).
3219pub fn setblock_stdin() -> i32 {
3220    // c:2624 — `return setblock_fd(1, 0, &mode);`.
3221    let (changed, _mode) = setblock_fd(true, 0); // c:2624
3222    changed as i32
3223}
3224
3225/// Port of `int read_poll(int fd, int *readchar, int polltty, zlong microseconds)`
3226/// from `Src/utils.c:2645-2738`.
3227///
3228/// Check whether input is pending on `fd` within `timeout_us`
3229/// microseconds. Faithful port of all three C paths:
3230/// 1. The `polltty` non-canonical-tty path (VMIN/VTIME termios
3231///    setup so a raw tty can be polled — c:2665-2696).
3232/// 2. The `HAVE_SELECT` wait (c:2697-2705). Rust uses `poll(2)`
3233///    as the idiom substitution for `select(2)`; `poll` return
3234///    semantics are mapped onto C's `ret` so the `ret < 0`
3235///    error-fallback still triggers.
3236/// 3. The final `setblock_fd` + non-blocking `read` fallback that
3237///    captures a byte when `select`/`poll` errored (c:2718-2729).
3238///
3239/// `readchar` is C's `int *readchar` out-param: when a byte is
3240/// actually consumed during the poll it is written through
3241/// `readchar` (so `read -t -k` on a raw tty gets the byte). `polltty`
3242/// is C's `int polltty` truthiness. Returns C's `(ret > 0)`.
3243///
3244/// WARNING: param order differs from C — Rust=(fd, readchar,
3245/// polltty, timeout_us) vs C=(fd, readchar, polltty, microseconds);
3246/// C's `microseconds` is `timeout_us` here.
3247pub fn read_poll(fd: i32, readchar: &mut i32, polltty: bool, timeout_us: i64) -> bool {
3248    // c:2645
3249    #[cfg(unix)]
3250    {
3251        let mut ret: i32 = -1; // c:2647
3252        let mut mode: libc::c_long = -1; // c:2648
3253        // C reassigns the `polltty` param; hold it as the VMIN-derived int.
3254        let mut polltty: i32 = polltty as i32; // c:2645
3255        let mut ti: Option<libc::termios> = None; // c:2659 `struct ttyinfo ti;`
3256
3257        // c:2662-2663 — `if (fd < 0 || (polltty && !isatty(fd))) polltty = 0;`
3258        if fd < 0 || (polltty != 0 && unsafe { libc::isatty(fd) } == 0) {
3259            polltty = 0; // no tty to poll
3260        }
3261
3262        // c:2665-2696 — HAS_TIO && !__CYGWIN__: non-canonical VMIN poll setup.
3263        if polltty != 0 && fd >= 0 {
3264            // c:2686 — `gettyinfo(&ti);` (operates on global SHTTY, as in C).
3265            if let Some(mut t) = gettyinfo() {
3266                // c:2687 — `if ((polltty = ti.tio.c_cc[VMIN]))`
3267                polltty = t.c_cc[libc::VMIN] as i32;
3268                if polltty != 0 {
3269                    t.c_cc[libc::VMIN] = 0; // c:2688
3270                    // c:2690 — termios timeout is 10ths of a second.
3271                    t.c_cc[libc::VTIME] = (timeout_us / 100_000) as libc::cc_t; // c:2690
3272                    settyinfo(&t); // c:2691
3273                }
3274                ti = Some(t);
3275            } else {
3276                // gettyinfo failed (SHTTY closed) — nothing to poll via VMIN.
3277                polltty = 0;
3278            }
3279        }
3280
3281        // c:2697-2705 — HAVE_SELECT wait, using poll(2) as the select(2) idiom.
3282        let timeout_ms = (timeout_us / 1000) as i32; // c:2698-2699 (expire_tv)
3283        if fd > -1 {
3284            // c:2701-2703
3285            let mut fds = [libc::pollfd {
3286                fd: fd as RawFd,
3287                events: libc::POLLIN,
3288                revents: 0,
3289            }];
3290            let raw = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
3291            ret = if raw > 0 {
3292                // Map poll revents onto select's "fd is readable" ret>0.
3293                if (fds[0].revents & libc::POLLIN) != 0 {
3294                    1
3295                } else {
3296                    0
3297                }
3298            } else {
3299                raw // 0 = timeout, -1 = error (triggers fallback below)
3300            };
3301        } else {
3302            // c:2705 — `select(0, NULL, NULL, NULL, &expire_tv)`: pure sleep.
3303            ret = unsafe { libc::poll(std::ptr::null_mut(), 0, timeout_ms) };
3304        }
3305
3306        // c:2718 — `if (fd >= 0 && ret < 0 && !errflag)`
3307        if fd >= 0 && ret < 0 && errflag.load(Ordering::Relaxed) == 0 {
3308            // c:2723 — final attempt: non-blocking read of a single char.
3309            // `(polltty || setblock_fd(0, fd, &mode))`
3310            let can_read = if polltty != 0 {
3311                true // polltty short-circuits setblock_fd; mode stays -1
3312            } else {
3313                let (changed, m) = setblock_fd(false, fd); // c:2723
3314                mode = m; // C writes *modep unconditionally
3315                changed
3316            };
3317            if can_read {
3318                let mut c: u8 = 0;
3319                let n = unsafe {
3320                    libc::read(fd, &mut c as *mut u8 as *mut libc::c_void, 1)
3321                };
3322                if n > 0 {
3323                    *readchar = c as i32; // c:2724
3324                    ret = 1; // c:2725
3325                }
3326            }
3327            // c:2727-2728 — `if (mode != -1) fcntl(fd, F_SETFL, mode);`
3328            if mode != -1 {
3329                unsafe {
3330                    libc::fcntl(fd, libc::F_SETFL, mode);
3331                }
3332            }
3333        }
3334
3335        // c:2730-2736 — HAS_TIO: restore canonical VMIN=1 / VTIME=0.
3336        if polltty != 0 {
3337            if let Some(mut t) = ti {
3338                t.c_cc[libc::VMIN] = 1; // c:2732
3339                t.c_cc[libc::VTIME] = 0; // c:2733
3340                settyinfo(&t); // c:2734
3341            }
3342        }
3343
3344        ret > 0 // c:2737
3345    }
3346    #[cfg(not(unix))]
3347    {
3348        let _ = (fd, readchar, polltty, timeout_us);
3349        false
3350    }
3351}
3352
3353/// Compute time difference in microseconds (from utils.c timespec_diff_us)
3354/// Port of `timespec_diff_us(const struct timespec *t1, const struct timespec *t2)` from `Src/utils.c:2752`.
3355/// Rust idiom replacement: `Instant::duration_since().as_micros()`
3356/// replaces the C tv_sec/tv_nsec arithmetic + sign-flip dance.
3357pub fn timespec_diff_us(t1: &std::time::Instant, t2: &std::time::Instant) -> i64 {
3358    if *t2 > *t1 {
3359        t2.duration_since(*t1).as_micros() as i64
3360    } else {
3361        -(t1.duration_since(*t2).as_micros() as i64)
3362    }
3363}
3364
3365/// Port of `int zmonotime(time_t *tloc)` from Src/utils.c:2780.
3366///
3367/// "Like time(), but uses the monotonic clock." Reads CLOCK_MONOTONIC
3368/// and returns `tv_sec`; writes the same value through `tloc` if
3369/// non-NULL (Rust: `Some(&mut t)`).
3370pub fn zmonotime(tloc: Option<&mut i64>) -> i64 {
3371    // c:2780
3372    #[cfg(unix)]
3373    {
3374        // c:2782 — `struct timespec ts;`
3375        let mut ts = libc::timespec {
3376            // c:2782
3377            tv_sec: 0,
3378            tv_nsec: 0,
3379        };
3380        // c:2783 — `zgettime_monotonic_if_available(&ts);`
3381        unsafe {
3382            libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); // c:2783
3383        }
3384        // c:2784-2785 — `if (tloc) *tloc = ts.tv_sec;`
3385        if let Some(t) = tloc {
3386            // c:2784
3387            *t = ts.tv_sec as i64; // c:2785
3388        }
3389        ts.tv_sec as i64 // c:2786 return ts.tv_sec
3390    }
3391    #[cfg(not(unix))]
3392    {
3393        let _ = tloc;
3394        0
3395    }
3396}
3397
3398/// Check if string looks like a number
3399/// Check whether a string parses as a decimal integer.
3400/// Sleep for a given number of seconds (fractional)
3401/// Sleep for a fractional number of seconds.
3402/// Port of `int zsleep(long us)` from Src/utils.c:2797.
3403///
3404/// "Sleep for the given number of microseconds." Wraps
3405/// `nanosleep(2)` with EINTR retry, returning 1 on completion, 0
3406/// on permanent error.
3407pub fn zsleep(us: i64) -> i32 {
3408    // c:2797
3409    let mut sleeptime = libc::timespec {
3410        // c:2797-2803
3411        tv_sec: (us / 1_000_000) as libc::time_t,
3412        tv_nsec: ((us % 1_000_000) * 1000) as libc::c_long,
3413    };
3414    #[cfg(unix)]
3415    {
3416        loop {
3417            // c:2804
3418            let mut rem = libc::timespec {
3419                tv_sec: 0,
3420                tv_nsec: 0,
3421            };
3422            let ret = unsafe { libc::nanosleep(&sleeptime, &mut rem) }; // c:2806
3423            if ret == 0 {
3424                // c:2808
3425                return 1;
3426            }
3427            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
3428            if err != libc::EINTR {
3429                // c:2810
3430                return 0; // c:2811
3431            }
3432            sleeptime = rem; // c:2812
3433        }
3434    }
3435    #[cfg(not(unix))]
3436    {
3437        let _ = sleeptime;
3438        std::thread::sleep(std::time::Duration::from_micros(us.max(0) as u64));
3439        1
3440    }
3441}
3442
3443/// Sleep random amount up to max microseconds (from utils.c zsleep_random)
3444/// Port of `int zsleep_random(long max_us, time_t end_time)`
3445/// from Src/utils.c:2833.
3446///
3447/// "Sleep for time (fairly) randomly up to max_us microseconds.
3448/// Don't let the time extend beyond end_time. end_time is compared
3449/// to the current *monotonic* clock time. Return 1 if that seemed
3450/// to work, else 0."
3451pub fn zsleep_random(max_us: i64, end_time: i64) -> i32 {
3452    // c:2833
3453    let now = zmonotime(None); // c:2833
3454    let r16 = unsafe { libc::rand() } & 0xFFFF; // c:2845
3455    let mut r: i64 = (max_us >> 16) * (r16 as i64); // c:2852
3456    while r != 0 && now + (r / 1_000_000) > end_time {
3457        // c:2858
3458        r >>= 1; // c:2859
3459    }
3460    if r != 0 {
3461        // c:2860
3462        zsleep(r) // c:2861
3463    } else {
3464        0 // c:2862
3465    }
3466}
3467
3468/// Port of `int checkrmall(char *s)` from `Src/utils.c:2867`.
3469///
3470/// C body (c:2867-2919): count files in directory `s` (capped at 100,
3471/// honoring GLOBDOTS), emit one of 4 prompts based on count, optionally
3472/// sleep 10s under RMSTARWAIT, then `getquery("ny", 1)` for confirm.
3473///
3474/// Previous Rust port was a FAKE — single generic prompt, no file
3475/// count, no shout/errflag checks, no RMSTARWAIT, wrong valid-chars
3476/// order ("yn" instead of "ny" so 'n' isn't the default-first-arm).
3477/// Lost 9 of the 10 C semantic branches.
3478pub fn checkrmall(s: &str) -> bool {
3479    // c:2867
3480    // c:2871-2872 — `if (!shout) return 1;` — no controlling tty for
3481    // prompt output, default to yes. Rust analogue: if stderr isn't
3482    // a tty, skip the prompt and approve.
3483    let shout_is_tty = unsafe { libc::isatty(2) != 0 }; // c:2871
3484    if !shout_is_tty {
3485        // c:2871
3486        return true; // c:2872
3487    }
3488    // c:2873-2878 — `if (*s != '/')` build absolute via pwd prefix.
3489    let s_owned: String; // c:2873
3490    let s_abs: &str = if !s.starts_with('/') {
3491        // c:2873
3492        let pwd = getsparam("PWD").unwrap_or_default(); // c:2874 pwd[1]
3493        s_owned = if pwd.len() > 1 {
3494            // c:2874 if (pwd[1])
3495            crate::ported::string::tricat(&pwd, "/", s) // c:2875 zhtricat
3496        } else {
3497            // c:2876
3498            crate::ported::string::dyncat("/", s) // c:2877
3499        };
3500        s_owned.as_str()
3501    } else {
3502        s
3503    };
3504    let max_count: i32 = 100; // c:2879
3505    let mut count: i32 = 0; // c:2870 count = 0
3506                            // c:2880-2892 — opendir + readdir loop, count entries (skip
3507                            // dotfiles when !GLOBDOTS), cap at max_count.
3508    let ignoredots = !isset(GLOBDOTS); // c:2881
3509                                       // c:2880 — `if (!(dir = opendir(unmeta(s)))) return;` then
3510                                       //          `while ((fn = zreaddir(dir, 1))) { ... }`.
3511    if let Ok(mut dir) = fs::read_dir(s_abs) {
3512        while let Some(fname) = zreaddir(&mut dir, 1) {
3513            if ignoredots && fname.starts_with('.') {
3514                // c:2885
3515                continue; // c:2886
3516            }
3517            count += 1; // c:2887
3518            if count > max_count {
3519                // c:2888
3520                break; // c:2889
3521            }
3522        }
3523        // c:closedir(dir) auto on drop
3524    }
3525    // c:2893-2904 — four-arm prompt based on file count.
3526    let stderr_h = io::stderr();
3527    let mut stderr_w = stderr_h.lock();
3528    if count > max_count {
3529        // c:2893
3530        let _ = write!(
3531            stderr_w,
3532            "zsh: sure you want to delete more than {} files in ",
3533            max_count
3534        ); // c:2894
3535    } else if count == 1 {
3536        // c:2896
3537        let _ = write!(stderr_w, "zsh: sure you want to delete the only file in ");
3538    // c:2897
3539    } else if count > 0 {
3540        // c:2898
3541        let _ = write!(
3542            stderr_w,
3543            "zsh: sure you want to delete all {} files in ",
3544            count
3545        ); // c:2899
3546    } else {
3547        // c:2901
3548        // c:2902 — `We don't know how many files the glob will expand to`
3549        let _ = write!(stderr_w, "zsh: sure you want to delete all the files in ");
3550        // c:2903
3551    }
3552    // c:2905 — `nicezputs(s, shout)` — escape non-printables in path.
3553    let _ = nicezputs(s_abs, &mut stderr_w); // c:2905
3554                                             // c:2906-2912 — RMSTARWAIT: print "? (waiting ten seconds)",
3555                                             // flush, beep, sleep 10, then newline.
3556    if isset(RMSTARWAIT) {
3557        // c:2906
3558        let _ = write!(stderr_w, "? (waiting ten seconds)"); // c:2907
3559        let _ = stderr_w.flush(); // c:2908
3560        drop(stderr_w); // release lock around zbeep + sleep
3561        zbeep(); // c:2909
3562        std::thread::sleep(std::time::Duration::from_secs(10)); // c:2910
3563        let _ = writeln!(io::stderr()); // c:2911 fputc('\n', shout)
3564    } else {
3565        drop(stderr_w);
3566    }
3567    // c:2913-2914 — `if (errflag) return 0;`
3568    if errflag.load(Ordering::Relaxed) != 0 {
3569        // c:2913
3570        return false; // c:2914
3571    }
3572    // c:2915-2917 — emit "[yn]? ", flush, beep.
3573    let _ = io::stderr().write_all(b" [yn]? "); // c:2915 nicezputs of " [yn]? "
3574    let _ = io::stderr().flush(); // c:2916
3575    zbeep(); // c:2917
3576             // c:2918 — `return (getquery("ny", 1) == 'y');`. Default-first-
3577             // char is 'n' (no) — getquery's first valid char is the default
3578             // returned when the user just hits Enter.
3579    getquery(Some("ny"), 1) == b'y' as i32 // c:2918
3580}
3581
3582/// Port of `ssize_t read_loop(int fd, char *buf, size_t len)` from
3583/// `Src/utils.c:2923`.
3584///
3585/// C body (c:2923-2945):
3586/// ```c
3587/// while (1) {
3588///     ssize_t ret = read(fd, buf, len);
3589///     if (ret == len) break;
3590///     if (ret <= 0) {
3591///         if (ret < 0) {
3592///             if (errno == EINTR) continue;          // c:2933
3593///             if (fd != SHTTY)                       // c:2935
3594///                 zwarn("read failed: %e", errno);   // c:2936
3595///         }
3596///         return ret;
3597///     }
3598///     buf += ret; len -= ret;
3599/// }
3600/// ```
3601///
3602/// The previous Rust port omitted the `zwarn("read failed: %e", errno)`
3603/// emission at c:2935-2936. That's a real diagnostic divergence:
3604/// any non-SHTTY read failure in zsh emits a stderr message; the
3605/// Rust port silently propagated the io::Error to the caller, which
3606/// in most call sites was discarded (`let _ = read_loop(...)`).
3607/// Users debugging an interrupted file read (e.g. `< /broken/fd`)
3608/// would see no error message at all.
3609/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
3610pub fn read_loop(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
3611    // c:2923
3612    #[cfg(unix)]
3613    {
3614        let mut total = 0;
3615        while total < buf.len() {
3616            let n = unsafe {
3617                libc::read(
3618                    // c:2928
3619                    fd,
3620                    buf[total..].as_mut_ptr() as *mut libc::c_void,
3621                    buf.len() - total,
3622                )
3623            };
3624            if n <= 0 {
3625                // c:2931
3626                if n < 0 {
3627                    let e = io::Error::last_os_error();
3628                    if e.kind() == io::ErrorKind::Interrupted {
3629                        // c:2933
3630                        continue; // c:2934
3631                    }
3632                    // c:2935-2936 — `if (fd != SHTTY) zwarn("read failed: %e", errno);`
3633                    let shtty = SHTTY.load(Ordering::Relaxed);
3634                    if fd != shtty {
3635                        // c:2935
3636                        zwarn(
3637                            // c:2936
3638                            &format!("read failed: {}", e),
3639                        );
3640                    }
3641                    return Err(e);
3642                }
3643                break;
3644            }
3645            total += n as usize;
3646        }
3647        Ok(total)
3648    }
3649    #[cfg(not(unix))]
3650    {
3651        let _ = (fd, buf);
3652        Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
3653    }
3654}
3655
3656/// Port of `ssize_t write_loop(int fd, const char *buf, size_t len)` from
3657/// `Src/utils.c:2949`.
3658///
3659/// C body (c:2949-2970):
3660/// ```c
3661/// while (1) {
3662///     ssize_t ret = write(fd, buf, len);
3663///     if (ret == len) break;
3664///     if (ret < 0) {
3665///         if (errno == EINTR) continue;
3666///         if (fd != SHTTY)                            // c:2960
3667///             zwarn("write failed: %e", errno);       // c:2961
3668///         return -1;
3669///     }
3670///     buf += ret; len -= ret;
3671/// }
3672/// ```
3673///
3674/// Same divergence as `read_loop`: previous Rust port omitted the
3675/// `zwarn("write failed: %e", errno)` emission for non-SHTTY fds.
3676/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
3677pub fn write_loop(fd: i32, buf: &[u8]) -> io::Result<usize> {
3678    // c:2949
3679    #[cfg(unix)]
3680    {
3681        let mut total = 0;
3682        while total < buf.len() {
3683            let n = unsafe {
3684                libc::write(
3685                    // c:2954
3686                    fd,
3687                    buf[total..].as_ptr() as *const libc::c_void,
3688                    buf.len() - total,
3689                )
3690            };
3691            if n <= 0 {
3692                if n < 0 {
3693                    let e = io::Error::last_os_error();
3694                    if e.kind() == io::ErrorKind::Interrupted {
3695                        // c:2958
3696                        continue; // c:2959
3697                    }
3698                    // c:2960-2961 — `if (fd != SHTTY) zwarn("write failed: %e", errno);`
3699                    let shtty = SHTTY.load(Ordering::Relaxed);
3700                    if fd != shtty {
3701                        // c:2960
3702                        zwarn(
3703                            // c:2961
3704                            &format!("write failed: {}", e),
3705                        );
3706                    }
3707                    return Err(e);
3708                }
3709                break;
3710            }
3711            total += n as usize;
3712        }
3713        Ok(total)
3714    }
3715    #[cfg(not(unix))]
3716    {
3717        let _ = (fd, buf);
3718        Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
3719    }
3720}
3721
3722/// Read a single character (from utils.c read1char).
3723///
3724/// Port of `static int read1char(int echo)` from `Src/utils.c:2972`.
3725///
3726/// C body (c:2972-2988):
3727/// ```c
3728/// char c;
3729/// int q = queue_signal_level();
3730/// dont_queue_signals();
3731/// while (read(SHTTY, &c, 1) != 1) {
3732///     if (errno != EINTR || errflag || retflag || breaks || contflag) {
3733///         restore_queue_signals(q);
3734///         return -1;
3735///     }
3736/// }
3737/// restore_queue_signals(q);
3738/// if (echo) write_loop(SHTTY, &c, 1);
3739/// return (unsigned char) c;
3740/// ```
3741///
3742/// Returns the byte read (0..=255) on success, `-1` on error. Three
3743/// divergences in the previous Rust port:
3744///   1. **Read from stdin** instead of SHTTY (the tty fd). zsh reads
3745///      single chars during `read -k`, `bindkey`, query prompts —
3746///      always against the controlling terminal, not the standard
3747///      input which may be a pipe.
3748///   2. **No `echo` parameter.** When `echo=1` C writes the byte
3749///      back to SHTTY so the user sees what they typed.
3750///   3. **No `errflag` / `retflag` / `breaks` / `contflag` early-exit.**
3751///      A SIGINT-driven errflag should abort the read; previous Rust
3752///      port had no break path.
3753/// WARNING: returns i32 not `Option<char>` to match C's signature
3754/// (`(unsigned char) c` for success, `-1` for error).
3755pub fn read1char(echo: i32) -> i32 {
3756    // c:2972
3757    #[cfg(unix)]
3758    {
3759        let shtty = SHTTY.load(Ordering::Relaxed);
3760        if shtty < 0 {
3761            return -1;
3762        }
3763        let mut c: u8 = 0;
3764        loop {
3765            let rc = unsafe { libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1) };
3766            if rc == 1 {
3767                // c:2978
3768                break;
3769            }
3770            // c:2979 — `if (errno != EINTR || errflag || retflag || breaks
3771            //              || contflag) return -1;`
3772            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
3773            if err != libc::EINTR {
3774                return -1;
3775            }
3776            // Check errflag interrupt flag (the most observable of the
3777            // four guards; retflag/breaks/contflag are control-flow
3778            // states that don't apply outside the exec engine).
3779            if errflag.load(Ordering::Relaxed) != 0 {
3780                return -1;
3781            }
3782        }
3783        // c:2985-2986 — `if (echo) write_loop(SHTTY, &c, 1);`
3784        if echo != 0 {
3785            // c:2985
3786            let _ = write_loop(shtty, &[c]); // c:2986
3787        }
3788        // c:2987 — `return (unsigned char) c;`
3789        c as i32
3790    }
3791    #[cfg(not(unix))]
3792    {
3793        let _ = echo;
3794        -1
3795    }
3796}
3797
3798/// Port of `int noquery(int purge)` from Src/utils.c:2992.
3799///
3800/// "If anything has been typed before the query, return without
3801/// asking. Optionally also purge the input queue." Returns the
3802/// number of bytes pending on `SHTTY` (via FIONREAD ioctl); when
3803/// `purge` is set, drains them before returning.
3804pub fn noquery(purge: bool) -> i32 {
3805    // c:2992
3806    let mut val: libc::c_int = 0; // c:2992
3807    #[cfg(unix)]
3808    {
3809        let shtty = SHTTY.load(Ordering::Relaxed); // c:2999
3810        if shtty == -1 {
3811            return 0;
3812        }
3813        unsafe {
3814            // c:2999
3815            libc::ioctl(shtty, libc::FIONREAD, &mut val as *mut libc::c_int);
3816        }
3817        if purge {
3818            // c:3000
3819            let mut c: u8 = 0;
3820            for _ in 0..val {
3821                // c:3001
3822                let _ = unsafe {
3823                    // c:3002
3824                    libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1)
3825                };
3826            }
3827        }
3828    }
3829    val // c:3009
3830}
3831
3832/// Port of `int getquery(char *valid_chars, int purge)` from
3833/// `Src/utils.c:3014`.
3834///
3835/// Read a single keystroke from the TTY in raw mode and return its
3836/// character code (as `int`, mirroring C). If `valid_chars` is `None`
3837/// (C `NULL`) any byte is accepted. If `valid_chars` is `Some(s)`, only
3838/// chars in `s` (plus `\n` which maps to `s[0]`) are accepted; other
3839/// input rings the bell and re-prompts. `Y`/`N` are pre-normalised to
3840/// `y`/`n` per c:3041-3045.
3841///
3842/// `purge != 0` triggers `noquery(purge)`'s queue-purge path — if input
3843/// is queued, returns `'n'` without reading.
3844pub fn getquery(valid_chars: Option<&str>, purge: i32) -> i32 {
3845    // c:3014
3846    // c:3016 — `int c, d, nl = 0;`
3847    let mut c: i32;
3848    let mut d: i32;
3849    let mut nl: i32 = 0;
3850    // c:3017 — `int isem = !strcmp(term, "emacs");`
3851    // Stub: `term` is the $TERM environment global, declared in
3852    // `Src/init.c` (extern in zsh.h). Local stub reads $TERM from
3853    // paramtab; absent → empty string.
3854    let term: String = getsparam("TERM").unwrap_or_default();
3855    let isem: bool = term == "emacs";
3856    // c:3018 — `struct ttyinfo ti;`
3857    // Rust: termios is the canonical TTY-state type returned by gettyinfo.
3858    let mut ti: libc::termios; // c:3018
3859
3860    attachtty(mypgrp.load(Ordering::Relaxed)); // c:3020 attachtty(mypgrp)
3861
3862    // c:3022 — `gettyinfo(&ti);`
3863    ti = match gettyinfo() {
3864        Some(t) => t,
3865        None => return -1,
3866    };
3867    // c:3023-3030 — `ti.tio.c_lflag &= ~ECHO; if (!isem) {
3868    //                  ti.tio.c_lflag &= ~ICANON;
3869    //                  ti.tio.c_cc[VMIN] = 1;
3870    //                  ti.tio.c_cc[VTIME] = 0; }`
3871    #[cfg(unix)]
3872    {
3873        ti.c_lflag &= !(libc::ECHO); // c:3024
3874        if !isem {
3875            // c:3025
3876            ti.c_lflag &= !(libc::ICANON); // c:3026
3877            ti.c_cc[libc::VMIN] = 1; // c:3027
3878            ti.c_cc[libc::VTIME] = 0; // c:3028
3879        }
3880    }
3881    // c:3037 — `settyinfo(&ti);`
3882    settyinfo(&ti);
3883
3884    // c:3039 — `if (noquery(purge))`
3885    if noquery(purge != 0) != 0 {
3886        // Stub: `shttyinfo` is the canonical saved-TTY-state global,
3887        // declared in `Src/init.c` (extern in zsh.h:1856). Without the
3888        // global tracked here we re-fetch current termios as a degraded
3889        // best-effort restore.
3890        if !isem {
3891            // c:3040
3892            if let Some(saved) = gettyinfo() {
3893                // c:3041 settyinfo(&shttyinfo)
3894                settyinfo(&saved);
3895            }
3896        }
3897        let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"n\n"); // c:3042
3898        return b'n' as i32; // c:3043
3899    }
3900
3901    // c:3046-3061 — `while ((c = read1char(0)) >= 0) { ... }`
3902    c = -1;
3903    loop {
3904        let cc = read1char(0); // c:3046
3905        if cc < 0 {
3906            c = cc;
3907            break;
3908        }
3909        c = cc;
3910        if c == b'Y' as i32 {
3911            // c:3047
3912            c = b'y' as i32; // c:3048
3913        } else if c == b'N' as i32 {
3914            // c:3049
3915            c = b'n' as i32; // c:3050
3916        }
3917        if valid_chars.is_none() {
3918            // c:3051
3919            break; // c:3052
3920        }
3921        if c == b'\n' as i32 {
3922            // c:3053
3923            // c:3054 — `c = *valid_chars;`
3924            c = valid_chars.unwrap().bytes().next().unwrap_or(b'\n') as i32;
3925            nl = 1; // c:3055
3926            break; // c:3056
3927        }
3928        // c:3058 — `if (strchr(valid_chars, c))`
3929        if valid_chars.unwrap().bytes().any(|b| b as i32 == c) {
3930            nl = 1; // c:3059
3931            break; // c:3060
3932        }
3933        zbeep(); // c:3062
3934    }
3935    if c >= 0 {
3936        // c:3064
3937        // c:3065-3066 — `char buf = (char)c; write_loop(SHTTY, &buf, 1);`
3938        let buf = [c as u8]; // c:3065
3939        let _ = write_loop(SHTTY.load(Ordering::Relaxed), &buf); // c:3066
3940    }
3941    if nl != 0 {
3942        // c:3068
3943        let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n"); // c:3069
3944    }
3945
3946    if isem {
3947        // c:3071
3948        if c != b'\n' as i32 {
3949            // c:3072
3950            // c:3073 — `while ((d = read1char(1)) >= 0 && d != '\n');`
3951            loop {
3952                d = read1char(1);
3953                if d < 0 || d == b'\n' as i32 {
3954                    break;
3955                }
3956            }
3957        }
3958    } else if c != b'\n' as i32 && valid_chars.is_none() {
3959        // c:3075
3960        // c:3077-3094 — MULTIBYTE_SUPPORT branch: drain trailing bytes
3961        // of an incomplete multibyte sequence.
3962        if isset(MULTIBYTE) && c >= 0 {
3963            // c:3077
3964            // c:3083-3093 — `for (;;) { ret = mbrlen(&cc, 1, &mbs);
3965            //                          if (ret != MB_INCOMPLETE) break;
3966            //                          c = read1char(1); if (c < 0) break;
3967            //                          cc = (char)c; }`
3968            // Rust: model MB_INCOMPLETE detection by checking whether
3969            // the byte buffer so far forms a complete UTF-8 prefix.
3970            // `cc` carries the current byte under examination.
3971            let mut cc: u8 = c as u8; // c:3082
3972            let mut accum: Vec<u8> = vec![cc];
3973            loop {
3974                // mbrlen returns MB_INCOMPLETE when the partial buffer
3975                // doesn't yet form a complete UTF-8 character. Rust
3976                // equivalent: from_utf8 errors with error_len() == None
3977                // when more bytes are needed.
3978                let incomplete = match std::str::from_utf8(&accum) {
3979                    Ok(_) => false,
3980                    Err(e) => e.error_len().is_none(),
3981                };
3982                if !incomplete {
3983                    // c:3088
3984                    break;
3985                }
3986                let nc = read1char(1); // c:3089
3987                if nc < 0 {
3988                    // c:3090
3989                    break; // c:3091
3990                }
3991                cc = nc as u8; // c:3092
3992                accum.push(cc);
3993            }
3994            let _ = cc;
3995        }
3996        // c:3097 — `write_loop(SHTTY, "\n", 1);`
3997        let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n");
3998    }
3999
4000    // c:3101 — `settyinfo(&shttyinfo);` — restore saved TTY state.
4001    // Stub-degraded path: refetch current termios. The proper port
4002    // requires wiring an `shttyinfo` global in init.rs.
4003    if let Some(saved) = gettyinfo() {
4004        settyinfo(&saved);
4005    }
4006
4007    c // c:3102 return c
4008}
4009
4010// `spscan` (Src/utils.c:3109) — canonical port lives below the
4011// thread_local block in this file. The pre-existing 3-arg
4012// `(name, candidates[], threshold) → Option<String>` shape was a
4013// drift fake (C is `void spscan(HashNode hn, scanflags)`); deleted
4014// because it had zero call sites and conflicted with the faithful
4015// port spckword needs.
4016
4017// spellcheck a word                                                       // c:3123
4018// fix s ; if hist is nonzero, fix the history list too                    // c:3124
4019
4020// File-static state shared with the (inlined) spscan callback. C uses
4021// raw file-statics at utils.c:3045-3050 (`best`, `d`, `guess`, `ic`,
4022// `spckpat`, `spnamepat`); Rust port mirrors them as thread_locals
4023// (per-evaluator per PORT_PLAN.md bucket-1) so concurrent worker
4024// threads each have their own correction state.
4025thread_local! {
4026    /// Port of `static int d;` from `Src/utils.c:3045`. Best dist seen.
4027    static SPCK_D: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
4028    /// Port of `static char *best;` from `Src/utils.c:3046`. Best-match.
4029    static SPCK_BEST: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4030    /// Port of `static char *guess;` from `Src/utils.c:3047`. Word to fix.
4031    static SPCK_GUESS: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4032    /// Port of `static Patprog spckpat;` from `Src/utils.c:3049`.
4033    static SPCK_PAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4034        = const { std::cell::RefCell::new(None) };
4035    /// Port of `static Patprog spnamepat;` from `Src/utils.c:3050`.
4036    static SPCK_NAMEPAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4037        = const { std::cell::RefCell::new(None) };
4038}
4039
4040/// Port of `static void spscan(HashNode hn, UNUSED(int scanflags))` from
4041/// `Src/utils.c:3074`. Inlined per-call below because hashtable.rs's
4042/// scan helpers don't model C's `scanhashtable(table, ..., scanfn, ...)`
4043/// callback shape — Rust call sites iterate the table directly. C
4044/// body:
4045/// ```c
4046/// int nd = spdist(hn->nam, guess, (int) strlen(guess) / 4 + 1);
4047/// if (nd < d && (!spckpat || !pattry(spckpat, hn->nam))) {
4048///     best = hn->nam;
4049///     d = nd;
4050/// }
4051/// ```
4052fn spscan(name: &str) {
4053    // c:3074
4054    let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4055    if guess.is_empty() {
4056        return;
4057    }
4058    let thresh = guess.len() / 4 + 1; // c:3076 `(int) strlen(guess) / 4 + 1`
4059    let nd = spdist(name, &guess, thresh) as i32; // c:3076
4060    let d = SPCK_D.with(|c| c.get());
4061    if nd < d {
4062        // c:3077
4063        // c:3078 — `if (!spckpat || !pattry(spckpat, hn->nam))`.
4064        let allow = SPCK_PAT.with(|p| {
4065            p.borrow()
4066                .as_ref()
4067                .map(|prog| !crate::ported::pattern::pattry(prog, name))
4068                .unwrap_or(true)
4069        });
4070        if allow {
4071            SPCK_BEST.with(|b| *b.borrow_mut() = Some(name.to_string())); // c:3079
4072            SPCK_D.with(|c| c.set(nd)); // c:3080
4073        }
4074    }
4075}
4076
4077/// Port of `void spckword(char **s, int hist, int cmd, int ask)` from
4078/// `Src/utils.c:3128`.
4079///
4080/// Spell-check `*s`. If a near-match is found in the appropriate
4081/// hashtable (params for `$x`, command tables for command position,
4082/// directory entries otherwise) AND the user accepts the correction
4083/// (`ask=0` auto-accepts), replace `*s` in place with the corrected
4084/// form and (if `hist!=0`) rewrite the history entry too.
4085///
4086/// Faithful 1:1 line-by-line port. Interactive prompting (c:3273-3287)
4087/// is stubbed to auto-accept when `ask=1` since `getquery` /
4088/// `promptexpand` / `shout` / `zbeep` aren't yet wired in zshrs —
4089/// flagged with WARNING at the prompt site.
4090///
4091/// Caller updates: previous Rust signature `(word, candidates[], threshold)
4092/// → Option<String>` is gone — `lex.rs` builds candidate lists itself,
4093/// but C's spckword scans the canonical hashtables directly. The new
4094/// signature matches C exactly: takes `&mut String` for in-place fix.
4095pub fn spckword(s: &mut String, hist: i32, cmd: i32, ask: i32) {
4096    use crate::ported::hashtable::{
4097        aliastab_lock, cmdnamtab_lock, fillcmdnamtable, pathchecked, reswdtab_lock, shfunctab_lock,
4098    };
4099    use crate::ported::params::{getsparam, paramtab};
4100    use crate::ported::pattern::{patcompile, PAT_HEAPDUP};
4101    use crate::ported::zsh_h::{
4102        isset, Dash, Equals, Stringg as StringTok, Tilde, AUTOCD, HASHLISTALL,
4103    };
4104    // c:3130-3133 — locals.
4105    let _t: Option<String>;
4106    let mut ic: char = '\0'; // c:3131
4107    let mut preflen: usize = 0; // c:3132
4108                                // c:3133 — `autocd = cmd && isset(AUTOCD) && strcmp(*s, ".") && strcmp(*s, "..")`.
4109    let autocd = cmd != 0 && isset(AUTOCD) && s != "." && s != "..";
4110
4111    // c:3135-3136 — `if (!(*s)[0] || !(*s)[1]) return;`
4112    if s.len() < 2 {
4113        return;
4114    }
4115    // c:3137-3140 — HISTFLAG_NOEXEC or leading %/- skip.
4116    let bytes = s.as_bytes();
4117    let first = bytes[0] as char;
4118    let histdone = crate::ported::hist::histdone.load(std::sync::atomic::Ordering::Relaxed); // c:3137
4119    if (histdone & HISTFLAG_NOEXEC) != 0
4120        || (if cmd != 0 {
4121            first == '%' // c:3139 — leading % is a job
4122        } else {
4123            first == '-' || first == Dash // c:3139 — leading hyphen is an option
4124        })
4125    {
4126        return; // c:3140
4127    }
4128    // c:3141-3142 — `if (!strcmp(*s, "in")) return;`.
4129    if s == "in" {
4130        return; // c:3142
4131    }
4132    // c:3143-3155 — `cmd` branch: skip if it's already a known
4133    // function/builtin/cmdname/alias/reswd. Optional HASHLISTALL
4134    // re-fill of cmdnamtab on miss.
4135    if cmd != 0 {
4136        // c:3143
4137        let known = shfunctab_lock() // c:3144
4138            .read()
4139            .map(|t| t.get(s).is_some())
4140            .unwrap_or(false)
4141            || BUILTINS // c:3145
4142                .iter()
4143                .any(|b| b.node.nam == *s)
4144            || cmdnamtab_lock() // c:3146
4145                .read()
4146                .map(|t| t.get(s).is_some())
4147                .unwrap_or(false)
4148            || aliastab_lock() // c:3147
4149                .read()
4150                .map(|t| t.get(s).is_some())
4151                .unwrap_or(false)
4152            || reswdtab_lock() // c:3148
4153                .read()
4154                .map(|t| t.get(s).is_some())
4155                .unwrap_or(false);
4156        if known {
4157            return; // c:3149
4158        }
4159        // c:3150-3154 — HASHLISTALL: bulk-hash $PATH then retry.
4160        if isset(HASHLISTALL) {
4161            // c:3150
4162            let path: Vec<String> =
4163                getsparam("PATH") // c:3151
4164                    .map(|p| p.split(':').map(String::from).collect())
4165                    .unwrap_or_default();
4166            fillcmdnamtable(&path); // c:3151
4167            if cmdnamtab_lock() // c:3152
4168                .read()
4169                .map(|t| t.get(s).is_some())
4170                .unwrap_or(false)
4171            {
4172                return; // c:3153
4173            }
4174        }
4175    }
4176    // c:3156-3165 — Tilde/Equals/String prefix skip + itok/Dash
4177    // detok + early-return on any other tokenized char.
4178    let mut start = 0usize;
4179    let bytes = s.as_bytes(); // re-bind after potential string ops
4180    if !bytes.is_empty() {
4181        let c0 = bytes[0] as char;
4182        if c0 == Tilde || c0 == Equals || c0 == StringTok {
4183            // c:3157
4184            start = 1; // c:3158 t++
4185        }
4186    }
4187    // Scan from `start` for tokenized bytes.
4188    {
4189        let mut buf = s.clone().into_bytes();
4190        let mut i = start;
4191        let mut had_dash_only = true; // accumulator
4192        while i < buf.len() {
4193            let b = buf[i];
4194            if itok(b) {
4195                // c:3160
4196                if b as char == Dash {
4197                    // c:3161
4198                    buf[i] = b'-'; // c:3162
4199                } else {
4200                    return; // c:3164
4201                }
4202            } else {
4203                had_dash_only = had_dash_only && false;
4204            }
4205            i += 1;
4206        }
4207        let _ = had_dash_only;
4208        *s = String::from_utf8_lossy(&buf).into_owned();
4209    }
4210    // c:3166 — `best = NULL;`
4211    SPCK_BEST.with(|b| *b.borrow_mut() = None);
4212    SPCK_D.with(|c| c.set(100)); // initialised at each table-scan branch in C; mirror up-front.
4213                                 // c:3167-3169 — `for (t = *s; *t; t++) if (*t == '/') break;`
4214                                 // `t` is the position of the first slash (or end of string).
4215    let t_pos = s.find('/').unwrap_or(s.len()); // c:3167
4216                                                // c:3170-3171 — `if (**s == Tilde && !*t) return;`
4217    let bytes = s.as_bytes();
4218    if !bytes.is_empty() && (bytes[0] as char) == Tilde && t_pos == bytes.len() {
4219        // c:3170
4220        return; // c:3171
4221    }
4222
4223    // c:3173-3178 — compile CORRECT_IGNORE pattern if set.
4224    if let Some(ci) = getsparam("CORRECT_IGNORE") {
4225        // c:3173
4226        let prog = patcompile(
4227            &{
4228                let mut __pat_tok = (&ci).to_string();
4229                crate::ported::glob::tokenize(&mut __pat_tok);
4230                __pat_tok
4231            },
4232            PAT_HEAPDUP,
4233            None,
4234        ); // c:3176
4235        SPCK_PAT.with(|p| *p.borrow_mut() = prog);
4236    } else {
4237        SPCK_PAT.with(|p| *p.borrow_mut() = None); // c:3178
4238    }
4239    // c:3180-3185 — compile CORRECT_IGNORE_FILE pattern if set.
4240    if let Some(ci) = getsparam("CORRECT_IGNORE_FILE") {
4241        // c:3180
4242        let prog = patcompile(
4243            &{
4244                let mut __pat_tok = (&ci).to_string();
4245                crate::ported::glob::tokenize(&mut __pat_tok);
4246                __pat_tok
4247            },
4248            PAT_HEAPDUP,
4249            None,
4250        ); // c:3183
4251        SPCK_NAMEPAT.with(|p| *p.borrow_mut() = prog);
4252    } else {
4253        SPCK_NAMEPAT.with(|p| *p.borrow_mut() = None); // c:3185
4254    }
4255
4256    let bytes = s.as_bytes();
4257    let first = if bytes.is_empty() {
4258        '\0'
4259    } else {
4260        bytes[0] as char
4261    };
4262
4263    // c:3187-3193 — `**s == String && !*t`: $-prefixed name → scan paramtab.
4264    if first == StringTok && t_pos == bytes.len() {
4265        // c:3187
4266        // c:3188 — `guess = *s + 1;` strip leading $.
4267        let guess = s[1..].to_string();
4268        // c:3189-3190 — `if (itype_end(guess, INAMESPC, 1) == guess) return;`
4269        if itype_end(&guess, crate::ported::ztype_h::INAMESPC, true) == 0 {
4270            // c:3189
4271            return; // c:3190
4272        }
4273        ic = StringTok; // c:3191
4274        SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4275        SPCK_D.with(|c| c.set(100)); // c:3192
4276        if let Ok(t) = paramtab().read() {
4277            // c:3193 scanhashtable(paramtab, ..., spscan, ...)
4278            for k in t.keys() {
4279                spscan(k);
4280            }
4281        }
4282    // c:3194-3202 — `**s == Equals`: =cmd → hashcmd; then scan aliases+cmdnam.
4283    } else if first == Equals {
4284        // c:3194
4285        if t_pos != bytes.len() {
4286            // c:3195
4287            return; // c:3196
4288        }
4289        // c:3197-3198 — `if (hashcmd(guess = *s + 1, pathchecked)) return;`
4290        let guess = s[1..].to_string();
4291        let path: Vec<String> = getsparam("PATH")
4292            .map(|p| p.split(':').map(String::from).collect())
4293            .unwrap_or_default();
4294        let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4295        if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4296            return; // c:3198
4297        }
4298        SPCK_D.with(|c| c.set(100)); // c:3199
4299        ic = Equals; // c:3200
4300        SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4301        if let Ok(t) = aliastab_lock().read() {
4302            // c:3201
4303            for (k, _) in t.iter() {
4304                spscan(k);
4305            }
4306        }
4307        if let Ok(t) = cmdnamtab_lock().read() {
4308            // c:3202
4309            for (k, _) in t.iter() {
4310                spscan(k);
4311            }
4312        }
4313    // c:3203-3248 — default branch: filename / dir spell-check.
4314    } else {
4315        // c:3203
4316        let mut guess = s.clone(); // c:3204
4317                                   // c:3205-3218 — Tilde / String inline-expand prefix handling.
4318        if !guess.is_empty()
4319            && ((guess.as_bytes()[0] as char) == Tilde
4320                || (guess.as_bytes()[0] as char) == StringTok)
4321        {
4322            // c:3205
4323            ic = guess.as_bytes()[0] as char; // c:3207
4324            if t_pos + 1 >= s.len() {
4325                // c:3208 — `if (!*++t) return;`
4326                return; // c:3209
4327            }
4328            // c:3210-3214 — `noerrs=2; singsub(&guess); noerrs = ne;`
4329            let saved_noerrs =
4330                crate::ported::exec::noerrs.load(std::sync::atomic::Ordering::Relaxed);
4331            crate::ported::exec::noerrs.store(2, std::sync::atomic::Ordering::Relaxed); // c:3212
4332            guess = crate::ported::subst::singsub(&guess); // c:3213
4333            crate::ported::exec::noerrs.store(saved_noerrs, std::sync::atomic::Ordering::Relaxed);
4334            if guess.is_empty() {
4335                return; // c:3216 `if (!guess) return;`
4336            }
4337            // c:3217 — `preflen = strlen(guess) - strlen(t);` t = original
4338            // s[t_pos..] (the post-slash remainder).
4339            let t_len = s.len() - t_pos;
4340            preflen = guess.len().saturating_sub(t_len);
4341        }
4342        // c:3219-3220 — `if (access(unmeta(guess), F_OK) == 0) return;`
4343        let cstr = match std::ffi::CString::new(unmeta(&guess).as_str()) {
4344            Ok(c) => c,
4345            Err(_) => return,
4346        };
4347        if unsafe { libc::access(cstr.as_ptr(), libc::F_OK) } == 0 {
4348            // c:3219
4349            return; // c:3220
4350        }
4351        // c:3221 — `best = spname(guess);`
4352        // The Rust spname has a signature-drift adaptation taking
4353        // `(name, dir)`; pass the parent dir extracted from guess.
4354        let path_obj = std::path::Path::new(&guess);
4355        let parent = path_obj
4356            .parent()
4357            .and_then(|p| p.to_str())
4358            .filter(|s| !s.is_empty())
4359            .unwrap_or(".");
4360        let basename = path_obj.file_name().and_then(|n| n.to_str()).unwrap_or("");
4361        let best = spname(basename, parent);
4362        SPCK_BEST.with(|b| *b.borrow_mut() = best);
4363        SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess.clone()));
4364        // c:3222-3247 — command-position default branch: hashcmd +
4365        // scan tables + autocd cdpath scan.
4366        if t_pos == s.len() && cmd != 0 {
4367            // c:3222
4368            // c:3223-3224 — hashcmd shortcut.
4369            let path: Vec<String> = getsparam("PATH")
4370                .map(|p| p.split(':').map(String::from).collect())
4371                .unwrap_or_default();
4372            let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4373            if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4374                return; // c:3224
4375            }
4376            SPCK_D.with(|c| c.set(100)); // c:3225
4377                                         // c:3226-3230 — scan reswd, alias, shfunc, builtin, cmdnam.
4378            if let Ok(t) = reswdtab_lock().read() {
4379                // c:3226
4380                for (k, _) in t.iter() {
4381                    spscan(k);
4382                }
4383            }
4384            if let Ok(t) = aliastab_lock().read() {
4385                // c:3227
4386                for (k, _) in t.iter() {
4387                    spscan(k);
4388                }
4389            }
4390            if let Ok(t) = shfunctab_lock().read() {
4391                // c:3228
4392                for (k, _) in t.iter() {
4393                    spscan(k);
4394                }
4395            }
4396            // c:3229 — builtintab scan: BUILTINS is a static array.
4397            for b in BUILTINS.iter() {
4398                spscan(&b.node.nam);
4399            }
4400            if let Ok(t) = cmdnamtab_lock().read() {
4401                // c:3230
4402                for (k, _) in t.iter() {
4403                    spscan(k);
4404                }
4405            }
4406            // c:3231-3247 — autocd $cdpath scan: for each cdpath
4407            // entry, find the closest filename match via mindist.
4408            // Strict `<` (not `<=` as in spscan) so a cdpath dir
4409            // wins only when STRICTLY better than the existing best
4410            // — preferring earlier cdpath dirs on ties.
4411            if autocd {
4412                // c:3232 — `if (cd_able_vars(unmeta(guess))) return;`
4413                let unmeta_guess = crate::ported::utils::unmeta(&guess);
4414                if crate::ported::builtin::cd_able_vars(&unmeta_guess).is_some() {
4415                    return; // c:3233
4416                }
4417                // c:3234 — `for (pp = cdpath; *pp; pp++)`. Read CDPATH.
4418                let cdpath: Vec<String> = crate::ported::params::paramtab()
4419                    .read()
4420                    .ok()
4421                    .and_then(|t| t.get("cdpath").and_then(|pm| pm.u_arr.clone()))
4422                    .unwrap_or_default();
4423                let mut cur_d = SPCK_D.with(|c| c.get());
4424                let mut cur_best = SPCK_BEST.with(|b| b.borrow().clone());
4425                for pp in cdpath.iter() {
4426                    // c:3235-3245 — `mindist(*pp, *s, bestcd, 1)`.
4427                    if let Some((bestcd, thisdist)) = mindist(pp, &s) {
4428                        // c:3239 — STRICT `<` (not `<=`) per C comment
4429                        // at c:3236-3239.
4430                        if (thisdist as i32) < cur_d {
4431                            cur_best = Some(bestcd);
4432                            cur_d = thisdist as i32;
4433                        }
4434                    }
4435                }
4436                SPCK_BEST.with(|b| *b.borrow_mut() = cur_best);
4437                SPCK_D.with(|c| c.set(cur_d));
4438            }
4439        }
4440    }
4441    // c:3250-3251 — `if (errflag) return;`
4442    if (errflag.load(std::sync::atomic::Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
4443        return; // c:3251
4444    }
4445    // c:3252 — `if (best && strlen(best) > 1 && strcmp(best, guess))`.
4446    let best = SPCK_BEST.with(|b| b.borrow().clone());
4447    let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4448    let Some(mut best) = best else {
4449        return;
4450    };
4451    if best.len() <= 1 || best == guess {
4452        return;
4453    }
4454    // c:3253-3272 — assemble the prefixed/de-tokenized replacement.
4455    if ic != '\0' {
4456        // c:3254
4457        if preflen > 0 {
4458            // c:3256
4459            // c:3258-3259 — `if (strncmp(guess, best, preflen)) return;`
4460            if !best.starts_with(&guess[..preflen.min(guess.len())]) {
4461                return; // c:3259
4462            }
4463            // c:3261-3263 — `u = ...s[0..t-*s] + best[preflen..]`.
4464            let t_off = s.len() - (s.len() - t_pos);
4465            let mut u = String::with_capacity(t_off + best.len() - preflen + 1);
4466            u.push_str(&s[..t_off]);
4467            u.push_str(&best[preflen..]);
4468            best = u;
4469        } else {
4470            // c:3264 — `u = "\0" + best;` (prepend NUL placeholder).
4471            best = format!("\0{}", best);
4472        }
4473        // c:3269-3271 — `best = u; guess = *s; *guess = *best = ztokens[ic - Pound];`
4474        let pound = crate::ported::zsh_h::Pound as u8;
4475        let zt = crate::ported::lex::ztokens.as_bytes();
4476        let token_char = if (ic as u8) >= pound {
4477            let idx = (ic as u8 - pound) as usize;
4478            if idx < zt.len() {
4479                zt[idx] as char
4480            } else {
4481                ic
4482            }
4483        } else {
4484            ic
4485        };
4486        // Set first char of both `*s` (the original) and `best`.
4487        if !s.is_empty() {
4488            let mut sb = s.clone().into_bytes();
4489            sb[0] = token_char as u8;
4490            *s = String::from_utf8_lossy(&sb).into_owned();
4491        }
4492        if !best.is_empty() {
4493            let mut bb = best.into_bytes();
4494            bb[0] = token_char as u8;
4495            best = String::from_utf8_lossy(&bb).into_owned();
4496        }
4497    }
4498    // c:3273-3289 — interactive prompt (`ask`) or auto-accept.
4499    let x: char;
4500    if ask != 0 {
4501        // c:3273
4502        // WARNING — DIVERGENCE: `noquery()`, `shout`, `promptexpand`,
4503        // `zputs(stream)`, `zbeep`, `getquery("nyae", 0)` aren't yet
4504        // wired in zshrs (interactive ZLE prompt machinery). Default
4505        // to 'n' (decline) when ask=1 — preserves the C behavior of
4506        // declining when shout is NULL (c:3286-3287). Re-enable the
4507        // interactive flow when promptexpand/getquery land.
4508        x = 'n';
4509    } else {
4510        x = 'y'; // c:3289
4511    }
4512    // c:3290-3300 — apply chosen action.
4513    if x == 'y' {
4514        // c:3290
4515        *s = best; // c:3291 `*s = dupstring(best);`
4516        if hist != 0 {
4517            // c:3292
4518            // c:3293 — `hwrep(best);` (history rewrite). Stubbed: hist
4519            // rewrite plumbing isn't yet hooked into the lex caller.
4520        }
4521    } else if x == 'a' {
4522        // c:3294
4523        crate::ported::hist::histdone
4524            .fetch_or(HISTFLAG_NOEXEC, std::sync::atomic::Ordering::Relaxed); // c:3295
4525    } else if x == 'e' {
4526        // c:3296
4527        crate::ported::hist::histdone.fetch_or(
4528            HISTFLAG_NOEXEC | crate::ported::zsh_h::HISTFLAG_RECALL,
4529            std::sync::atomic::Ordering::Relaxed,
4530        ); // c:3297
4531    }
4532    // c:3299-3300 — `if (ic) **s = ic;` — restore prefix sigil.
4533    if ic != '\0' && !s.is_empty() {
4534        let mut sb = s.clone().into_bytes();
4535        sb[0] = ic as u8;
4536        *s = String::from_utf8_lossy(&sb).into_owned();
4537    }
4538}
4539
4540/// Port of `ztrftimebuf(int *bufsizeptr, int decr)` from `Src/utils.c:3312`.
4541///
4542/// ```c
4543/// static int ztrftimebuf(int *bufsizeptr, int decr) {
4544///     if (*bufsizeptr <= decr) return 1;
4545///     *bufsizeptr -= decr;
4546///     return 0;
4547/// }
4548/// ```
4549///
4550/// "Helper for ztrftime: try to fit decr more bytes (plus a NUL)
4551/// in the buffer, and a new string length to decrement from that.
4552/// Returns 0 if the new length fits, 1 otherwise."
4553///
4554/// Previous Rust port had wrong semantics — returned `needed.max(256)`
4555/// (a buffer-sizing helper) instead of the C "decrement-and-check"
4556/// semantics. New port matches C: takes &mut bufsize + decr,
4557/// returns i32 (0 = fit, 1 = doesn't fit).
4558pub fn ztrftimebuf(bufsizeptr: &mut i32, decr: i32) -> i32 {
4559    if *bufsizeptr <= decr {
4560        return 1;
4561    }
4562    *bufsizeptr -= decr;
4563    0
4564}
4565
4566/// Format time struct (from utils.c ztrftime)
4567/// Port of `ztrftime(char *buf, int bufsize, char *fmt, struct tm *tm, long nsec)` from `Src/utils.c:3337`.
4568/// WARNING: param names don't match C — Rust=(fmt, time) vs C=(buf, bufsize, fmt, tm, nsec)
4569// Rust idiom replacement: `SystemTime::duration_since(UNIX_EPOCH)`
4570// + libc::localtime covers the C tm-struct populate; the C source's
4571// 192-line body builds the fmt-walk by hand because C has no
4572// strftime extension support — Rust delegates to libc::strftime via
4573// the strftime crate equivalent inline.
4574/// `ztrftime` — see implementation.
4575///
4576/// `use_gmt` controls whether time fields are interpreted as UTC (true,
4577/// libc::gmtime) or local time (false, libc::localtime). C's ztrftime
4578/// takes a `struct tm *` produced by the caller via gmtime/localtime
4579/// (e.g. Src/Modules/stat.c:201 picks between them based on STF_GMT);
4580/// since the Rust signature takes `SystemTime` we hoist that choice into
4581/// this bool param.
4582pub fn ztrftime(fmt: &str, time: std::time::SystemTime, use_gmt: bool) -> String {
4583    // C takes `struct tm *` + nsec directly, so negative time_t
4584    // (pre-1970) flows through localtime untouched. SystemTime
4585    // pre-epoch surfaces as duration_since Err — recover the signed
4586    // seconds rather than collapsing to 0 (the old unwrap_or_default
4587    // pinned every pre-epoch timestamp to the epoch).
4588    let (secs, nsec): (i64, u64) = match time.duration_since(UNIX_EPOCH) {
4589        Ok(d) => (d.as_secs() as i64, d.subsec_nanos() as u64),
4590        Err(e) => {
4591            let d = e.duration();
4592            let s = -(d.as_secs() as i64);
4593            let n = d.subsec_nanos() as u64;
4594            // sub-second part rolls the seconds down one: -1.25s
4595            // before epoch is secs=-2, nsec=750ms in tm terms.
4596            if n > 0 {
4597                (s - 1, 1_000_000_000 - n)
4598            } else {
4599                (s, 0)
4600            }
4601        }
4602    };
4603
4604    #[cfg(unix)]
4605    unsafe {
4606        let tm = if use_gmt {
4607            libc::gmtime(&secs)
4608        } else {
4609            libc::localtime(&secs)
4610        };
4611        if tm.is_null() {
4612            return String::new();
4613        }
4614        let tm_ref = &*tm;
4615
4616        // c:3398-3406 — pre-pass: walk fmt and substitute zsh-specific
4617        // extensions BEFORE delegating to libc::strftime. The C source
4618        // handles these inline; Rust pre-rewrites them into literal
4619        // numbers so libc::strftime sees only standard specifiers.
4620        //   %K  = 24-hr clock, no leading zero (0-23)
4621        //   %L  = 12-hr clock, no leading zero (1-12)
4622        //   %f  = day of month, no leading zero (1-31)
4623        //   %.N = fractional seconds, N digits (0-9)
4624        let mut preprocessed = String::with_capacity(fmt.len());
4625        let bytes = fmt.as_bytes();
4626        let mut i = 0;
4627        while i < bytes.len() {
4628            if bytes[i] == b'%' && i + 1 < bytes.len() {
4629                // c:3374-3384 — parse optional `N.` prefix (digit count
4630                // for the `%.` fractional-seconds specifier).
4631                let mut j = i + 1;
4632                while j < bytes.len() && bytes[j].is_ascii_digit() {
4633                    j += 1;
4634                }
4635                let digs: u32 = if j > i + 1 {
4636                    std::str::from_utf8(&bytes[i + 1..j])
4637                        .unwrap_or("3")
4638                        .parse()
4639                        .unwrap_or(3)
4640                } else {
4641                    3
4642                };
4643                // c:3408 — `switch (*fmt++)` examines the specifier
4644                // char (after the digit prefix).
4645                if j < bytes.len() && bytes[j] == b'.' {
4646                    // c:3409 — fractional-seconds. Default 3 digits if
4647                    // none specified (j == i+1).
4648                    let digs = digs.min(9);
4649                    let trunc_div: u64 = 10u64.pow(9 - digs);
4650                    let val = nsec / trunc_div;
4651                    preprocessed.push_str(&format!("{:0width$}", val, width = digs as usize));
4652                    i = j + 1;
4653                    continue;
4654                }
4655                let next = bytes[i + 1];
4656                // c:3445-3460 — %K / %L / %f extensions.
4657                match next {
4658                    b'K' => {
4659                        preprocessed.push_str(&format!("{}", tm_ref.tm_hour));
4660                        i += 2;
4661                        continue;
4662                    }
4663                    b'L' => {
4664                        let mut h12 = tm_ref.tm_hour % 12;
4665                        if h12 == 0 {
4666                            h12 = 12;
4667                        }
4668                        preprocessed.push_str(&format!("{}", h12));
4669                        i += 2;
4670                        continue;
4671                    }
4672                    b'f' => {
4673                        preprocessed.push_str(&format!("{}", tm_ref.tm_mday));
4674                        i += 2;
4675                        continue;
4676                    }
4677                    b'N' => {
4678                        // c:3491-3495 — `%N`: nanoseconds, always 9 digits
4679                        // (`sprintf(buf, "%09ld", nsec)`).
4680                        preprocessed.push_str(&format!("{:09}", nsec));
4681                        i += 2;
4682                        continue;
4683                    }
4684                    _ => {}
4685                }
4686            }
4687            preprocessed.push(bytes[i] as char);
4688            i += 1;
4689        }
4690
4691        let mut buf = vec![0u8; 256];
4692        let c_fmt = CString::new(preprocessed).unwrap_or_default();
4693        let len = libc::strftime(
4694            buf.as_mut_ptr() as *mut libc::c_char,
4695            buf.len(),
4696            c_fmt.as_ptr(),
4697            tm,
4698        );
4699
4700        if len > 0 {
4701            buf.truncate(len);
4702            String::from_utf8_lossy(&buf).to_string()
4703        } else {
4704            String::new()
4705        }
4706    }
4707
4708    #[cfg(not(unix))]
4709    {
4710        let _ = (fmt, secs, nsec);
4711        String::new()
4712    }
4713}
4714
4715/// Join array with delimiter (from utils.c zjoin)
4716/// Port of `zjoin(char **arr, int delim, int heap)` from `Src/utils.c:3622`.
4717/// Joins `arr` with `delim` between elements. When `delim` is itself a meta
4718/// byte (`imeta(delim)` — NUL, `Meta`, `Marker`, or a token in the
4719/// Pound..Nularg range; see `inittyptab` c:4195-4201), C writes the delimiter
4720/// in *metafied* form (`Meta` byte then `delim ^ 32`, c:3634-3637) rather than
4721/// the raw byte (c:3639). The earlier port emitted the raw char always, which
4722/// was byte-wrong for meta delimiters — notably `zjoin(array, '\0', …)` (NUL is
4723/// imeta), where C emits `0x83 0x20`, not a raw `0x00`.
4724///
4725/// WARNING: param names don't match C — Rust=(arr, delim) vs C=(arr, delim, heap).
4726/// The `heap` arg is dropped (Rust `String` owns its storage). As with
4727/// [`metafy`], the `Meta` byte cannot live in valid UTF-8, so the final
4728/// `String` boundary uses the same `from_utf8`/lossy fallback — byte-exact for
4729/// every ASCII (non-meta) delimiter, lossy only on meta delimiters. Callers
4730/// needing byte-exact meta-delimited output should join at the byte level.
4731pub fn zjoin(arr: &[String], delim: char) -> String {
4732    // c:3622
4733    // c:3629-3630 — empty array → "".
4734    if arr.is_empty() {
4735        return String::new();
4736    }
4737    let db = delim as u32;
4738    // c:3634 — `imeta(delim)`. Only byte-valued delimiters can be meta.
4739    let delim_meta = db < 0x100 && imeta_byte(db as u8);
4740    let mut out: Vec<u8> = Vec::new();
4741    for (i, s) in arr.iter().enumerate() {
4742        if i != 0 {
4743            // The separator that C writes after every element then chops off
4744            // the trailing one (c:3641) is equivalent to interposing it.
4745            if delim_meta {
4746                out.push(Meta); // c:3635
4747                out.push((db as u8) ^ 32); // c:3636
4748            } else if db < 0x100 {
4749                out.push(db as u8); // c:3639 — raw single byte
4750            } else {
4751                // Non-byte char delimiter (no C analogue): emit its UTF-8.
4752                let mut buf = [0u8; 4];
4753                out.extend_from_slice(delim.encode_utf8(&mut buf).as_bytes());
4754            }
4755        }
4756        out.extend_from_slice(s.as_bytes()); // c:3633 — strucpy(&ptr, *s)
4757    }
4758    String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
4759}
4760
4761/// Port of `char **colonsplit(char *s, int uniq)` from
4762/// `Src/utils.c:3650`. Splits `s` on `:`; when `uniq` is set,
4763/// duplicate segments are dropped (linear scan against the
4764/// already-emitted prefix).
4765/// ```c
4766/// char **
4767/// colonsplit(char *s, int uniq)
4768/// {
4769///     int ct;
4770///     char *t, **ret, **ptr, **p;
4771///     for (t = s, ct = 0; *t; t++)
4772///         if (*t == ':') ct++;
4773///     ptr = ret = zalloc(sizeof(char *) * (ct + 2));
4774///     t = s;
4775///     do {
4776///         s = t;
4777///         for (; *t && *t != ':'; t++);
4778///         if (uniq)
4779///             for (p = ret; p < ptr; p++)
4780///                 if (strlen(*p) == t - s && !strncmp(*p, s, t - s))
4781///                     goto cont;
4782///         *ptr = zalloc((t - s) + 1);
4783///         ztrncpy(*ptr++, s, t - s);
4784///       cont: ;
4785///     } while (*t++);
4786///     *ptr = NULL;
4787///     return ret;
4788/// }
4789/// ```
4790pub fn colonsplit(s: &str, uniq: bool) -> Vec<String> {
4791    // c:3650
4792    // c:3655-3657 — count colons.
4793    let ct = s.matches(':').count(); // c:3655
4794    let mut ret: Vec<String> = Vec::with_capacity(ct + 2); // c:3658 zalloc((ct+2)*sizeof(char *))
4795
4796    // c:3661-3673 — do-while loop walking segments.
4797    let bytes = s.as_bytes();
4798    let mut t: usize = 0;
4799    loop {
4800        let seg_start = t; // c:3662 s = t
4801                           // c:3664 — `for (; *t && *t != ':'; t++)`
4802        while t < bytes.len() && bytes[t] != b':' {
4803            t += 1;
4804        }
4805        let seg = &s[seg_start..t];
4806        // c:3665-3668 — uniq dedupe.
4807        if !uniq || !ret.iter().any(|p| p == seg) {
4808            // c:3665
4809            ret.push(seg.to_string()); // c:3670 zalloc + ztrncpy
4810        }
4811        // c:3673 — `while (*t++)` — break if at end, else step past colon.
4812        if t >= bytes.len() {
4813            break;
4814        } // c:3673
4815        t += 1; // c:3673 t++
4816    }
4817    // c:3674 — `*ptr = NULL;` — Rust Vec needs no sentinel.
4818    ret // c:3675
4819}
4820
4821/// Port of `skipwsep(char **s)` from `Src/utils.c:3680`.
4822///
4823/// Skip whitespace separators (`iwsep`-true bytes) at the start of
4824/// `s`. Returns `(remaining_str, count)` — `count` is the number of
4825/// bytes / chars skipped. C body:
4826///
4827/// ```c
4828/// while (*t && iwsep(*t == Meta ? t[1] ^ 32 : *t)) {
4829///     if (*t == Meta) t++;
4830///     t++;
4831///     i++;
4832/// }
4833/// ```
4834///
4835/// The C version honours Meta-encoded bytes (`Meta` followed by
4836/// `^32`-XOR'd byte). Rust port mirrors that byte-by-byte.
4837pub fn skipwsep(s: &str) -> (&str, usize) {
4838    let bytes = s.as_bytes();
4839    let mut i: usize = 0;
4840    let mut count: usize = 0;
4841    while i < bytes.len() {
4842        let b = if bytes[i] == Meta && i + 1 < bytes.len() {
4843            bytes[i + 1] ^ 32
4844        } else {
4845            bytes[i]
4846        };
4847        if !iwsep(b) {
4848            break;
4849        }
4850        if bytes[i] == Meta {
4851            i += 1;
4852        }
4853        i += 1;
4854        count += 1;
4855    }
4856    (&s[i..], count)
4857}
4858
4859/// Port of `spacesplit(char *s, int allownull, int heap, int quote)` from
4860/// `Src/utils.c:3711`. Splits `s` on `$IFS` via the `ISEP`/`IWSEP` char
4861/// classes (TYPTAB, kept in sync with `$IFS` by `inittyptab`, which
4862/// re-runs on every IFS set — params.rs:8691, c:4795), NOT on hardcoded
4863/// whitespace. zsh's word-splitting rules: runs of IFS-WHITESPACE
4864/// collapse and the leading/trailing ones yield real `""` fields (which
4865/// callers elide); each IFS-NON-WHITESPACE char delimits a field, and
4866/// consecutive ones yield `nulstring` (`Nularg`, `Src/subst.c:36`
4867/// `{Nularg,'\0'}`) empty fields, which survive and are later stripped to
4868/// `""` by remnulargs.
4869///
4870/// WARNING: param names don't match C — Rust=(s, allownull) vs C=(s,
4871/// allownull, heap, quote). The `quote` arm (backslash-escaped seps) and
4872/// `heap` C-buffer param drop: all zshrs callers pass quote=0, and Rust
4873/// owns its String storage. The previous port split only on hardcoded
4874/// `[' ','\t','\n']`, ignoring `$IFS` entirely (Bug #636).
4875pub fn spacesplit(s: &str, allownull: bool) -> Vec<String> {
4876    // c:3711
4877    use crate::ported::ztype_h::zistype;
4878    let bytes = s.as_bytes();
4879    // Meta-aware decode of the logical char value + its byte length at a
4880    // byte position (mirrors skipwsep's `*x == Meta ? x[1] ^ 32 : *x`).
4881    let char_at = |i: usize| -> (u8, usize) {
4882        // c:Src/zsh.h Meta — `\u{83}` prefix; next byte XOR 0x20.
4883        if bytes[i] == Meta && i + 1 < bytes.len() {
4884            (bytes[i + 1] ^ 32, 2)
4885        } else {
4886            (bytes[i], 1)
4887        }
4888    };
4889    // skipwsep(&s) — advance past a run of IWSEP (IFS-whitespace). c:3730
4890    let skipwsep_at = |mut i: usize| -> usize {
4891        while i < bytes.len() {
4892            let (c, l) = char_at(i);
4893            if !iwsep(c) {
4894                break;
4895            }
4896            i += l;
4897        }
4898        i
4899    };
4900    // itype_end(s, ISEP, 1) — `once` advances past exactly ONE ISEP char
4901    // if present, else leaves the index unchanged (c:4462 `if (once) break`).
4902    let isep_one = |i: usize| -> usize {
4903        if i < bytes.len() {
4904            let (c, l) = char_at(i);
4905            if zistype(c, ISEP as u32) {
4906                return i + l;
4907            }
4908        }
4909        i
4910    };
4911    // findsep(&s, NULL, 0) — advance to the next ISEP char. c:3784
4912    let findsep_at = |mut i: usize| -> usize {
4913        while i < bytes.len() {
4914            let (c, l) = char_at(i);
4915            if zistype(c, ISEP as u32) {
4916                break;
4917            }
4918            i += l;
4919        }
4920        i
4921    };
4922    let nulstring = || Nularg.to_string(); // c:subst.c:36 nulstring = {Nularg,0}
4923
4924    let mut ret: Vec<String> = Vec::new();
4925    let mut si = 0usize;
4926    let mut t = si; // c:3729 — `t = s;`
4927    si = skipwsep_at(si); // c:3730 — `skipwsep(&s);`
4928                          // c:3732-3735 — leading-field handling.
4929    if si < bytes.len() && isep_one(si) != si {
4930        // c:3733 — at an IFS-non-whitespace sep after skipping whitespace.
4931        ret.push(if allownull {
4932            String::new()
4933        } else {
4934            nulstring()
4935        });
4936    } else if !allownull && t != si {
4937        // c:3734-3735 — leading IFS-whitespace was skipped → real "".
4938        ret.push(String::new());
4939    }
4940    // c:3736-3756 — main word loop.
4941    while si < bytes.len() {
4942        let iend = isep_one(si); // c:3737 itype_end(s, ISEP, 1)
4943        let consumed_sep = iend != si;
4944        if consumed_sep {
4945            // c:3738-3741 — consume the single non-ws sep + following WS.
4946            si = iend;
4947            si = skipwsep_at(si);
4948        }
4949        // !!! POSIX-FAITHFUL GATE (no C counterpart) !!!
4950        // zsh (and `emulate sh`) emits a trailing empty field when a
4951        // non-whitespace IFS separator is the last thing in the input:
4952        // `IFS=:; set -- $v` on `a:b:` → (a, b, "") = 3. dash/ksh/bash drop
4953        // that trailing empty → (a, b) = 2. A MIDDLE empty (`a::b`) keeps
4954        // `si < len` here and is unaffected. Scoped to `!allownull` (the
4955        // unquoted-split path `set -- $v` uses); the quoted `"${=v}"` /
4956        // `${(s::)}` allownull path keeps zsh semantics. `zshrs --sh/--ksh/
4957        // --dash` sets posix_faithful(); `--... --zsh` leaves it false.
4958        if consumed_sep && si >= bytes.len() && !allownull && crate::dash_mode::posix_faithful()
4959        {
4960            break;
4961        }
4962        t = si; // c:3746 — `t = s;`
4963        si = findsep_at(si); // c:3747 — `findsep(&s, NULL, quote);`
4964        if si > t || allownull {
4965            // c:3748-3751 — emit the word `t..si`.
4966            ret.push(s[t..si].to_string());
4967        } else {
4968            // c:3753 — empty field between non-ws seps → nulstring.
4969            ret.push(nulstring());
4970        }
4971        t = si; // c:3754 — `t = s;`
4972        si = skipwsep_at(si); // c:3755 — `skipwsep(&s);`
4973    }
4974    // c:3757 — trailing IFS-whitespace → real "".
4975    if !allownull && t != si {
4976        ret.push(String::new());
4977    }
4978    ret
4979}
4980
4981/// Port of `findsep(char **s, char *sep, int quote)` from `Src/utils.c:3784`.
4982///
4983/// c:3762 — "Find a separator.  Return 0 if already at separator, 1 if
4984/// separator found later, else -1."  `pos` is C's walking `*s`: on
4985/// return it points at the separator (or end-of-string) so the caller
4986/// reads the word as `s[entry_pos..pos]`.
4987///
4988/// `sep` (c:3772):
4989///   - `None`       → split on normal `$IFS` separators (ISEP chars).
4990///   - `Some(b"")`  → no real separator: advance past one character.
4991///   - `Some(seq)`  → look for the (possibly multi-byte) literal `seq`.
4992///
4993/// `quote` (c:3776) — a `\` before a separator suppresses it, and `\\`
4994/// collapses to `\`.  This only applies when `sep` is `None` and it
4995/// strips backslashes in place, so the buffer must be modifiable
4996/// (C demands "something modifiable"; Rust takes `&mut String`).
4997///
4998/// The standalone form has no in-tree caller yet — the hot split paths
4999/// (`splitstring`, `findword`, `wordcount`) inline this logic — but it
5000/// is kept as a faithful, callable name-parity anchor.
5001/// WARNING: param names match C semantics — `pos` is C's `*s`.
5002pub fn findsep(s: &mut String, pos: &mut usize, sep: Option<&[u8]>, quote: bool) -> i32 {
5003    // c:3784
5004    use crate::ported::zsh_h::{MB_METACHARINIT, MB_METACHARLEN, MB_METACHARLENCONV};
5005    use crate::ported::ztype_h::{isep, ISEP, WC_ZISTYPE};
5006
5007    MB_METACHARINIT(); // c:3792
5008    let is_isep = |c: Option<char>| c.map(|c| WC_ZISTYPE(c, ISEP as u32)).unwrap_or(false);
5009
5010    match sep {
5011        // c:3793-3824 — default separators (ISEP), with optional quoting.
5012        None => {
5013            let start = *pos;
5014            let mut t = *pos;
5015            while t < s.len() {
5016                let b = s.as_bytes()[t];
5017                if quote && b == b'\\' {
5018                    // c:3795 — `if (quote && *t == '\\')`
5019                    if t + 1 < s.len() && s.as_bytes()[t + 1] == b'\\' {
5020                        // c:3796-3799 — `\\` → `\`: drop one backslash,
5021                        // advance past the one we keep (ilen = 1).
5022                        chuck(s, t);
5023                        t += 1;
5024                        continue;
5025                    }
5026                    // c:3801 — measure the char *after* the backslash.
5027                    let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t + 1..]);
5028                    if is_isep(c) {
5029                        // c:3802-3804 — escaped separator: strip the
5030                        // backslash, then advance over the now-bare char.
5031                        chuck(s, t);
5032                        t += ilen.max(1);
5033                        continue;
5034                    }
5035                    // c:3806-3810 — backslash is a normal byte.
5036                    if isep(b) {
5037                        break; // (never taken: '\\' is not an ISEP)
5038                    }
5039                    t += 1; // ilen = 1
5040                } else {
5041                    // c:3814-3818 — ordinary character.
5042                    let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t..]);
5043                    if is_isep(c) {
5044                        break; // c:3817
5045                    }
5046                    t += ilen.max(1);
5047                }
5048            }
5049            let i = if t > start { 1 } else { 0 }; // c:3821 `i = (t > *s)`
5050            *pos = t; // c:3822
5051            i // c:3823
5052        }
5053        // c:3825-3834 — empty separator: advance past the first char.
5054        Some(seq) if seq.is_empty() => {
5055            if *pos < s.len() {
5056                *pos += MB_METACHARLEN(&s.as_bytes()[*pos..]); // c:3831
5057                1 // c:3832
5058            } else {
5059                -1 // c:3833
5060            }
5061        }
5062        // c:3835-3845 — explicit (possibly multi-byte) literal separator.
5063        Some(seq) => {
5064            let mut i = 0i32;
5065            while *pos < s.len() {
5066                // c:3840 — `for (t=sep, tt=*s; *t && *tt && *t==*tt; ...)`
5067                let bytes = s.as_bytes();
5068                let mut k = 0usize;
5069                while k < seq.len() && *pos + k < bytes.len() && seq[k] == bytes[*pos + k] {
5070                    k += 1;
5071                }
5072                if k == seq.len() {
5073                    return if i > 0 { 1 } else { 0 }; // c:3841 `return (i > 0)`
5074                }
5075                *pos += MB_METACHARLEN(&bytes[*pos..]); // c:3842
5076                i += 1;
5077            }
5078            -1 // c:3844
5079        }
5080    }
5081}
5082
5083/// Find word at position (from utils.c findword)
5084/// Port of `findword(char **s, char *sep)` from `Src/utils.c:3849`.
5085pub fn findword<'a>(s: &'a str, sep: Option<&'a str>) -> Option<(&'a str, &'a str)> {
5086    let s = match sep {
5087        Some(_) => s,
5088        None => s.trim_start(),
5089    };
5090    if s.is_empty() {
5091        return None;
5092    }
5093    match sep {
5094        Some(sep) => {
5095            if let Some(pos) = s.find(sep) {
5096                Some((&s[..pos], &s[pos + sep.len()..]))
5097            } else {
5098                Some((s, ""))
5099            }
5100        }
5101        None => {
5102            let end = s.find(|c: char| c.is_ascii_whitespace()).unwrap_or(s.len());
5103            Some((&s[..end], &s[end..]))
5104        }
5105    }
5106}
5107
5108/// Port of `wordcount(char *s, char *sep, int mul)` from `Src/utils.c:3879`.
5109///
5110/// Returns the number of words in `s` that would result from splitting
5111/// on `sep` (or `$IFS` when `sep` is None). `mul` controls how
5112/// consecutive empty fields are counted:
5113/// - `mul == 0`: don't count leading/trailing/consecutive empties.
5114/// - `mul > 0`: count consecutive empties (each `sep` boundary
5115///   produces one word, even if the surrounding text is empty).
5116/// - `mul < 0`: count empty trailing fields (final separator after
5117///   the last non-empty field counts as one extra empty word).
5118///
5119/// C body (paraphrased):
5120/// ```c
5121/// if (sep) {
5122///     r = 1;
5123///     sl = strlen(sep);
5124///     for (; (c = findsep(&s, sep, 0)) >= 0; s += sl)
5125///         if ((c || mul) && (sl || *(s + sl)))
5126///             r++;
5127/// } else {
5128///     /* IFS-based: walk skipwsep / itype_end(s, ISEP, 1) */
5129/// }
5130/// ```
5131///
5132/// This port walks the metafied byte stream directly to mirror C's
5133/// pointer arithmetic. The `sep`-based branch is exact; the IFS
5134/// branch uses [`iwsep`] for whitespace-separator detection (C's
5135/// `ISEP` char class collapsed to whitespace, which is the common
5136/// case for default `$IFS` = `" \t\n"`).
5137pub fn wordcount(s: &str, sep: Option<&str>, mul: i32) -> i32 {
5138    // c:3879
5139    let bytes = s.as_bytes();
5140    if let Some(sep) = sep {
5141        // C: r = 1; sl = strlen(sep); for (; findsep(&s,sep,0) >= 0; s+=sl)
5142        //        if ((c || mul) && (sl || *(s+sl))) r++;
5143        let sep_bytes = sep.as_bytes();
5144        let sl = sep_bytes.len();
5145        let mut r: i32 = 1;
5146        let mut pos = 0;
5147        while pos <= bytes.len() {
5148            let rest = &bytes[pos..];
5149            let c_offset = match sep_bytes.is_empty() {
5150                true => Some(0usize),
5151                false => rest.windows(sl).position(|w| w == sep_bytes),
5152            };
5153            let Some(c) = c_offset else { break };
5154            // C `c` is the chars before the separator; `(sl || *(s+sl))`
5155            // means: if sl is zero (empty sep), only count when there's a
5156            // following char. Otherwise (sl > 0), the second clause is true
5157            // when sep is non-empty AND there are bytes after sep.
5158            let after_off = pos + c + sl;
5159            let following_nonempty = after_off < bytes.len();
5160            let cond_b = sl != 0 || following_nonempty;
5161            if (c != 0 || mul != 0) && cond_b {
5162                r += 1;
5163            }
5164            if sl == 0 {
5165                // Avoid infinite loop on empty sep — mirrors C's findsep
5166                // which advances by 1 byte when sep is empty.
5167                pos += 1;
5168            } else {
5169                pos += c + sl;
5170            }
5171        }
5172        r
5173    } else {
5174        // IFS branch (sep == NULL). C source uses itype_end(s, ISEP, 1)
5175        // to skip ISEP chars (default $IFS = " \t\n"). We use iwsep.
5176        let mut s_pos = 0usize;
5177        let t_orig = s_pos;
5178        let mut r: i32 = 0;
5179        // C: if (mul <= 0) skipwsep(&s);
5180        if mul <= 0 {
5181            while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5182                s_pos += 1;
5183            }
5184        }
5185        // C: if ((*s && itype_end(s,ISEP,1)!=s) || (mul<0 && t!=s)) r++;
5186        let has_word_now = s_pos < bytes.len() && !iwsep(bytes[s_pos]);
5187        if has_word_now || (mul < 0 && t_orig != s_pos) {
5188            r += 1;
5189        }
5190        // C: for (; *s; r++) { advance over word + maybe-skipwsep + findsep + maybe-skipwsep }
5191        while s_pos < bytes.len() {
5192            // Advance past the current word (non-ISEP chars).
5193            let word_start = s_pos;
5194            while s_pos < bytes.len() && !iwsep(bytes[s_pos]) {
5195                s_pos += 1;
5196            }
5197            if s_pos > word_start && mul <= 0 {
5198                while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5199                    s_pos += 1;
5200                }
5201            }
5202            // C: (void)findsep(&s, NULL, 0) — advance past one sep run.
5203            // Already handled above when mul<=0; for mul>0 we still need
5204            // to consume one separator byte to make progress.
5205            if s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5206                s_pos += 1;
5207            }
5208            let t_after = s_pos;
5209            if mul <= 0 {
5210                while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5211                    s_pos += 1;
5212                }
5213            }
5214            if s_pos < bytes.len() {
5215                r += 1;
5216            } else {
5217                // C: if (mul < 0 && t != s) r++;
5218                if mul < 0 && t_after != s_pos {
5219                    r += 1;
5220                }
5221                break;
5222            }
5223        }
5224        r
5225    }
5226}
5227
5228/// Join array with separator - port from zsh/Src/utils.c sepjoin() lines 3926-3958
5229///
5230/// If sep is None, uses first char of IFS (defaults to space).
5231/// Join an array with separator.
5232/// Port of `sepjoin(char **s, char *sep, int heap)` from Src/utils.c:3928.
5233/// WARNING: param names don't match C — Rust=(arr, sep) vs C=(s, sep, heap)
5234// Rust idiom replacement: `slice::join` covers the C `zalloc`+`strcpy`
5235// loop with running length pre-compute; the `heap` arg drops since
5236// String owns its own allocation.
5237/// `sepjoin` — see implementation.
5238pub fn sepjoin(arr: &[String], sep: Option<&str>) -> String {
5239    // c:3928
5240    // c:3934-3935 — if (!*s) return heap ? dupstring("") : ztrdup("");
5241    if arr.is_empty() {
5242        return String::new();
5243    }
5244    // c:3936-3945 — `if (!sep)` default-sep arm:
5245    //   if (ifs && *ifs != ' ') sep = dupstrpfx(ifs, MB_METACHARLEN(ifs));
5246    //   else sep = " ";
5247    // IFS SET-but-EMPTY takes the first branch (`*ifs` is '\0' != ' ')
5248    // and dupstrpfx("", …) yields an EMPTY separator — `"$*"` with
5249    // IFS="" concatenates. Only IFS UNSET (ifs == NULL → getsparam
5250    // None) or starting with a space falls back to " ".
5251    let ifs_storage: String;
5252    let sep_str: &str = match sep {
5253        Some(s) => s, // c:3936
5254        None => {
5255            ifs_storage = match getsparam("IFS") {
5256                // c:3938-3940 — set, first char not space (empty → "").
5257                Some(ifs) if !ifs.starts_with(' ') => ifs
5258                    .chars()
5259                    .next()
5260                    .map(|c| c.to_string())
5261                    .unwrap_or_default(),
5262                // c:3941-3944 — unset (NULL) or leading space → " ".
5263                _ => " ".to_string(),
5264            };
5265            &ifs_storage
5266        }
5267    };
5268    // c:3947-3956 — pre-compute total length, alloc, copy elements
5269    //               interleaving sep. Rust slice::join collapses all that
5270    //               into the one canonical call.
5271    arr.join(sep_str)
5272}
5273
5274/// Split string by separator - port from zsh/Src/utils.c sepsplit() lines 3961-3992
5275///
5276/// If sep is None, performs IFS-style word splitting (spacesplit).
5277/// Otherwise splits on the given separator string.
5278/// allownull: if true, allows empty strings in result
5279/// Split a string on `IFS` separators.
5280/// Port of `sepsplit(char *s, char *sep, int allownull, int heap)` from Src/utils.c:3962.
5281/// WARNING: param names don't match C — Rust=(s, sep, allownull) vs C=(s, sep, allownull, heap)
5282pub fn sepsplit(s: &str, sep: Option<&str>, allownull: bool) -> Vec<String> {
5283    // c:3962
5284    // Handle Nularg at start (zsh internal marker) - line 3968
5285    let s = if s.starts_with('\x00') && s.len() > 1 {
5286        &s[1..]
5287    } else {
5288        s
5289    };
5290
5291    match sep {
5292        None => spacesplit(s, allownull),
5293        Some("") => {
5294            // Empty separator: split into characters
5295            if allownull {
5296                s.chars().map(|c| c.to_string()).collect()
5297            } else {
5298                s.chars()
5299                    .map(|c| c.to_string())
5300                    .filter(|c| !c.is_empty())
5301                    .collect()
5302            }
5303        }
5304        Some(sep) => {
5305            let parts: Vec<String> = s.split(sep).map(|p| p.to_string()).collect();
5306            if allownull {
5307                parts
5308            } else {
5309                parts.into_iter().filter(|p| !p.is_empty()).collect()
5310            }
5311        }
5312    }
5313}
5314
5315/// Port of `getshfunc(char *nam)` from `Src/utils.c:3998`.
5316///
5317/// C body:
5318/// ```c
5319/// Shfunc getshfunc(char *nam) {
5320///     return (Shfunc) shfunctab->getnode(shfunctab, nam);
5321/// }
5322/// ```
5323///
5324/// Routes through the global `shfunctab` singleton in
5325/// hashtable.rs (hashtable::shfunctab_lock). Returns an owned
5326/// `shfunc` clone so callers can read `flags` (to detect
5327/// `PM_UNDEFINED` autoload stubs), `funcdef`, `filename`, and
5328/// `body` without holding the table lock. Owned clone vs C's
5329/// `*Shfunc` raw pointer trades one allocation for Rust
5330/// lifetime safety — `getshfunc` is a function-lookup site,
5331/// not per-statement, so the cost is irrelevant.
5332///
5333/// Returning `Option<String>` of just `body` (the old contract)
5334/// made every PM_UNDEFINED autoload stub invisible because
5335/// `body=None` collapsed via `and_then` to `None`, which callers
5336/// then read as "function doesn't exist."
5337pub fn getshfunc(nam: &str) -> Option<shfunc> {
5338    let tab = shfunctab_lock().read().expect("shfunctab poisoned");
5339    tab.get(nam).cloned()
5340}
5341
5342/// Port of `char **subst_string_by_func(Shfunc func, char *arg1, char *orig)`
5343/// from Src/utils.c:4017.
5344///
5345/// Calls the named shell function with `[func, arg1?, orig]` as
5346/// positional args under `sfcontext = SFC_SUBST` and returns the
5347/// `$reply` array on success. Routes through `callhookfunc` (the
5348/// static-linked equivalent of `doshfunc`), then reads `$reply`
5349/// from the env-var fallback because the global `paramtab` is not
5350/// yet a singleton in the Rust port (params::getaparam takes a
5351/// `&ParamTable` arg).
5352pub fn subst_string_by_func(
5353    func_name: &str,
5354    arg1: Option<&str>,
5355    orig: &str,
5356) -> Option<Vec<String>> // c:4017
5357{
5358    let osc = SFCONTEXT.load(Ordering::Relaxed); // c:4019
5359    let osm = STOPMSG.load(Ordering::Relaxed);
5360    let old_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
5361    let mut args: Vec<String> = Vec::with_capacity(3); // c:4020-4026
5362    args.push(func_name.to_string()); // c:4023
5363    if let Some(a) = arg1 {
5364        // c:4024
5365        args.push(a.to_string()); // c:4025
5366    }
5367    args.push(orig.to_string()); // c:4026
5368    SFCONTEXT // c:4027
5369        .store(SFC_SUBST, Ordering::Relaxed);
5370    INCOMPFUNC.store(0, Ordering::Relaxed); // c:4028
5371
5372    // c:4030 — `if (doshfunc(func, l, 1))`. Direct doshfunc call
5373    // mirrors C. body_runner routes through the host body-only entry
5374    // (matching the same shfunc that `callhookfunc` would resolve).
5375    let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
5376        .read()
5377        .ok()
5378        .and_then(|t| t.get(func_name).cloned());
5379    let rc = if let Some(mut shf) = shf_clone {
5380        let name_for_body = func_name.to_string();
5381        let body_args = args.clone();
5382        let body_runner = move || -> i32 {
5383            crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
5384        };
5385        crate::ported::exec::doshfunc(&mut shf, args.clone(), true, body_runner)
5386    } else {
5387        callhookfunc(func_name, Some(&args), 0, std::ptr::null_mut())
5388    };
5389    // c:4033 — `ret = getaparam("reply")` against paramtab. `reply`
5390    // is a shell-local PM_ARRAY entry, never exported to env.
5391    let ret: Option<Vec<String>> = if rc != 0 {
5392        None // c:4031
5393    } else {
5394        getaparam("reply") // c:4033
5395    };
5396
5397    SFCONTEXT.store(osc, Ordering::Relaxed); // c:4035
5398    STOPMSG.store(osm, Ordering::Relaxed); // c:4036
5399    INCOMPFUNC.store(old_incompfunc, Ordering::Relaxed); // c:4037
5400    ret // c:4038
5401}
5402
5403/// Port of `char **subst_string_by_hook(char *name, char *arg1, char *orig)`
5404/// from Src/utils.c:4049.
5405///
5406/// Looks up `name` as a shell function and calls
5407/// `subst_string_by_func` on it. If that returns no result, walks
5408/// `${name}_hook` as an array of function names, trying each in
5409/// order until one yields a `$reply`.
5410pub fn subst_string_by_hook(name: &str, arg1: Option<&str>, orig: &str) -> Option<Vec<String>> // c:4049
5411{
5412    let mut ret: Option<Vec<String>> = None;
5413    if getshfunc(name).is_some() {
5414        // c:4054
5415        ret = subst_string_by_func(name, arg1, orig); // c:4055
5416    }
5417    if ret.is_none() {
5418        // c:4058
5419        let arrnam = format!("{}_hook", name); // c:4061-4063
5420                                               // c:4065 — `arr = getaparam(arrnam)`. The previous Rust port
5421                                               // read `env::var(arrnam)` and NUL-split — wrong: the hook
5422                                               // array is a shell-local PM_ARRAY in paramtab, not env.
5423        if let Some(arr) = getaparam(&arrnam) {
5424            // c:4065
5425            for f in arr.iter() {
5426                // c:4068
5427                if f.is_empty() {
5428                    continue;
5429                }
5430                if getshfunc(f).is_some() {
5431                    // c:4069
5432                    ret = subst_string_by_func(f, arg1, orig); // c:4070
5433                    if ret.is_some() {
5434                        // c:4071
5435                        break; // c:4072
5436                    }
5437                }
5438            }
5439        }
5440    }
5441    ret // c:4094
5442}
5443
5444/// Make single-element array (from utils.c mkarray)
5445/// Port of `mkarray(char *s)` from `Src/utils.c:4083`.
5446pub fn mkarray(s: Option<&str>) -> Vec<String> {
5447    match s {
5448        Some(val) => vec![val.to_string()],
5449        None => Vec::new(),
5450    }
5451}
5452
5453/// Make single-element array on heap (from utils.c hmkarray)
5454/// Port of `hmkarray(char *s)` from `Src/utils.c:4094`.
5455pub fn hmkarray(s: &str) -> Vec<String> {
5456    if s.is_empty() {
5457        Vec::new()
5458    } else {
5459        vec![s.to_string()]
5460    }
5461}
5462
5463/// Port of `void zbeep(void)` from Src/utils.c:4105.
5464///
5465/// Honours `$ZBEEP` (a key-string sequence) when set and the BEEP
5466/// option when unset; emits the BEL char (\007) to SHTTY by
5467/// default. The Rust port writes via `write_loop` to mirror C's
5468/// raw-write semantics.
5469pub fn zbeep() {
5470    // c:4105
5471    queue_signals(); // c:4105
5472    if let Ok(zbeep) = std::env::var("ZBEEP") {
5473        // c:4109
5474        let (decoded, _) = getkeystring(&zbeep); // c:4111
5475        #[cfg(unix)]
5476        {
5477            let shtty = SHTTY.load(Ordering::Relaxed);
5478            if shtty != -1 {
5479                let _ = write_loop(shtty, decoded.as_bytes()); // c:4112
5480            } else {
5481                eprint!("{}", decoded);
5482            }
5483        }
5484        #[cfg(not(unix))]
5485        eprint!("{}", decoded);
5486    } else if isset(BEEP) {
5487        // c:4113
5488        #[cfg(unix)]
5489        {
5490            let shtty = SHTTY.load(Ordering::Relaxed);
5491            if shtty != -1 {
5492                let _ = write_loop(shtty, b"\x07"); // c:4114
5493            } else {
5494                eprint!("\x07");
5495            }
5496        }
5497        #[cfg(not(unix))]
5498        eprint!("\x07");
5499    }
5500    unqueue_signals(); // c:4115
5501}
5502
5503/// Free array (no-op in Rust, provided for API compat)
5504/// Port of `freearray(char **s)` from `Src/utils.c:4120`.
5505pub fn freearray(s: Vec<String>) {
5506    // c:4124 — DPUTS(!s, "freearray() with zero argument")
5507    // Rust takes Vec<String> by value (never null), so the C !s
5508    // check maps to no condition that can fire in Rust. Document
5509    // the gap.
5510    let _ = &s; // c:4124 (no-op; Vec is never NULL in Rust)
5511                // Rust Drop handles this
5512}
5513
5514/// Split on '=' returning (name, value) (from utils.c equalsplit)
5515/// Port of `equalsplit(char *s, char **t)` from `Src/utils.c:4133`.
5516/// WARNING: param names don't match C — Rust=(s) vs C=(s, t)
5517pub fn equalsplit(s: &str) -> Option<(String, String)> {
5518    let eq = s.find('=')?;
5519    Some((s[..eq].to_string(), s[eq + 1..].to_string()))
5520}
5521
5522// initialize the ztypes table                                              // c:4151
5523/// Port of `inittyptab()` from `Src/utils.c:4155`. Initialise the
5524/// `typtab[256]` lookup table that backs the `idigit`/`ialnum`/etc.
5525/// predicates in `ztype_h`.
5526///
5527/// C body (c:4155-4250) does:
5528///   1. Zero the table.
5529///   2. Mark 0..=31 and 128..=159 + 127 as ICNTRL.
5530///   3. Mark '0'..='9' as IDIGIT|IALNUM|IWORD|IIDENT|IUSER.
5531///   4. Mark 'a'..='z' and 'A'..='Z' as IALPHA|IALNUM|IIDENT|IUSER|IWORD.
5532///   5. Mark '_' as IIDENT|IUSER.
5533///   6. Mark '-', '.', Dash as IUSER.
5534///   7. Mark ' '/'\t' as IBLANK|INBLANK; '\n' as INBLANK.
5535///   8. Mark '\0', Meta, Marker as IMETA.
5536///   9. Mark Pound..LAST_NORMAL_TOK as ITOK|IMETA.
5537///   10. Mark Snull..Nularg as ITOK|IMETA|INULL.
5538///   11. Walk $IFS adding ISEP and IWSEP for blanks.
5539///
5540/// This first-pass port covers steps 1-7. Steps 8-11 require Meta /
5541/// Marker / Pound / Snull / Nularg constants from zsh_h/zsh.h and
5542/// the `ifs` global; the remaining Meta/IFS marks are skipped until
5543/// those land. Idempotent — safe to call multiple times.
5544pub fn inittyptab() {
5545    // utils.c:4155
5546
5547    // c:4160 — `if (!(typtab_flags & ZTF_INIT))` one-off init.
5548    {
5549        let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5550        if (flags & ZTF_INIT) == 0 {
5551            TYPTAB_FLAGS.store(ZTF_INIT, Ordering::Relaxed);
5552        }
5553    }
5554
5555    // Local scratch, flushed to the atomic table at fn end — C mutates
5556    // typtab[] in place unlocked; the atomic store-per-slot flush keeps
5557    // readers lock-free (see TYPTAB doc in ztype_h.rs).
5558    let mut t = [0u32; 256]; // c:4168 memset(typtab, 0, sizeof(typtab))
5559    // c:4169-4170 — control chars 0..32 and 128..160.
5560    for c in 0..32u32 {
5561        t[c as usize] = ICNTRL as u32;
5562        t[(c + 128) as usize] = ICNTRL as u32;
5563    }
5564    // c:4171 — `typtab[127] = ICNTRL;`
5565    t[127] = ICNTRL as u32;
5566    // c:4172-4173 — '0'..='9'.
5567    for c in (b'0' as usize)..=(b'9' as usize) {
5568        t[c] = (IDIGIT | IALNUM | IWORD | IIDENT | IUSER) as u32;
5569    }
5570    // c:4174-4175 — 'a'..='z' and matching 'A'..='Z'.
5571    for c in (b'a' as usize)..=(b'z' as usize) {
5572        let upper = c - (b'a' as usize) + (b'A' as usize);
5573        let bits = (IALPHA | IALNUM | IIDENT | IUSER | IWORD) as u32;
5574        t[c] = bits;
5575        t[upper] = bits;
5576    }
5577    // c:4190 — `typtab['_'] = IIDENT | IUSER;`
5578    t[b'_' as usize] = (IIDENT | IUSER) as u32;
5579    // c:4191 — `typtab['-'] = typtab['.'] = typtab[(unsigned char) Dash] = IUSER;`.
5580    t[b'-' as usize] = IUSER as u32;
5581    t[b'.' as usize] = IUSER as u32;
5582    // c:4191 — `Dash` token marker (0x9b per zsh.h:182, "Only in patterns").
5583    // Marking it IUSER lets pattern-side $-named-character paths
5584    // accept it as a user-name byte. Previously omitted in the port.
5585    t[crate::ported::zsh_h::Dash as usize] = IUSER as u32;
5586    // c:4192-4194 — blanks.
5587    t[b' ' as usize] |= (IBLANK | INBLANK) as u32;
5588    t[b'\t' as usize] |= (IBLANK | INBLANK) as u32;
5589    t[b'\n' as usize] |= INBLANK as u32;
5590
5591    // c:4195 — `typtab['\0'] |= IMETA;`. Previously omitted in the
5592    // Rust port — '\0' had only ICNTRL (set by the 0..32 loop at
5593    // c:4169) and was missing IMETA. C `imeta('\0')` is true (the
5594    // NUL byte must be Meta-encoded as `Meta + ('\0' ^ 32)`); the
5595    // Rust `imeta()` typtab predicate returned false, so any code
5596    // routing through `imeta(c)` (e.g. `input::shingetline`)
5597    // failed to Meta-encode NUL bytes from stdin, corrupting the
5598    // SHIN buffer when piped binary data hit a `\0`.
5599    t[0] |= IMETA as u32; // c:4195
5600                          // c:4196-4197 — Meta + Marker marked IMETA.
5601    {
5602        t[Meta as usize] |= IMETA as u32;
5603        t[Marker as usize] |= IMETA as u32;
5604    }
5605
5606    // c:4133-4134 — `for (t0 = Pound; t0 <= LAST_NORMAL_TOK; t0++)
5607    //                    typtab[t0] |= ITOK | IMETA;`
5608    // Marks all char-rewrite token markers (Pound, Stringg, Hat,
5609    // Star, ...). Without this, `itok(Stringg)` returns false and
5610    // `is_valid_assignment_target("$NAME")` wrongly accepts the
5611    // leading `$` as part of an identifier prefix → `$=cmd` lexes
5612    // as ENVSTRING instead of STRING.
5613    {
5614        let lo = Pound as usize;
5615        let hi = LAST_NORMAL_TOK as usize;
5616        for t0 in lo..=hi {
5617            t[t0] |= (ITOK | IMETA) as u32;
5618        }
5619    }
5620
5621    // c:4135-4136 — `for (t0 = Snull; t0 <= Nularg; t0++)
5622    //                    typtab[t0] |= ITOK | IMETA | INULL;`
5623    {
5624        let lo = Snull as usize;
5625        let hi = Nularg as usize;
5626        for t0 in lo..=hi {
5627            t[t0] |= (ITOK | IMETA | INULL) as u32;
5628        }
5629    }
5630
5631    // c:4202-4231 — IFS walk. Sets ISEP on every IFS char and IWSEP
5632    // on the blank (inblank) subset. Reads the current `ifs` global
5633    // (defaulting to DEFAULT_IFS), demetafies `Meta+X` pairs, and
5634    // skips a doubled blank (`s[1]==c`) so the IWSEP bit doesn't
5635    // mark "blank repeated → no-skip" IFS chars. Mirrors C exactly.
5636    {
5637        // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; ...)`.
5638        // C reads the global `ifs` variable (the same one `ifssetfn`
5639        // writes). The Rust port walks paramtab first (so the GSU
5640        // dispatch path matches C); on miss, fall through to ifs_lock
5641        // directly so a fresh `ifssetfn` update before any paramtab
5642        // entry exists is still visible to inittyptab.
5643        // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; *s; s++)`.
5644        // The default-IFS fallback is keyed on the `ifs` POINTER being
5645        // NULL (i.e. IFS unset), NOT on it pointing at an empty string.
5646        // `IFS=''` leaves a non-NULL empty `ifs`, the loop body never
5647        // runs, and typtab ends up with ZERO ISEP/IWSEP bits — which is
5648        // exactly how an empty IFS disables all word splitting
5649        // (`IFS=''; v='a b  c'; print -rl -- ${=v}` is one word). The
5650        // port used to coerce "" → DEFAULT_IFS at both the paramtab read
5651        // and the fallback, so an empty IFS still split on whitespace.
5652        let pt_ifs: Option<String> = crate::ported::params::paramtab()
5653            .read()
5654            .ok()
5655            .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)));
5656        let src: String = match pt_ifs {
5657            // IFS is SET (possibly to ""): walk its chars verbatim.
5658            Some(v) => v,
5659            // No paramtab entry yet. Fall through to the ifs_lock global so
5660            // a fresh ifssetfn update landing before the paramtab entry
5661            // exists is still visible; an empty global there means "not
5662            // initialised", which is C's NULL → CURRENT_DEFAULT_IFS.
5663            None => {
5664                let g = crate::ported::params::ifs_lock()
5665                    .lock()
5666                    .map(|g| g.clone())
5667                    .unwrap_or_default();
5668                if g.is_empty() {
5669                    DEFAULT_IFS.to_string() // c:4216 (ifs == NULL)
5670                } else {
5671                    g
5672                }
5673            }
5674        };
5675        let bytes = src.as_bytes();
5676        let mut i = 0;
5677        while i < bytes.len() {
5678            // c:4217 — `int c = (unsigned char) (*s == Meta ? *++s ^ 32 : *s)`.
5679            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
5680                i += 1;
5681                bytes[i] ^ 32
5682            } else {
5683                bytes[i]
5684            };
5685            // c:4218-4223 — MULTIBYTE non-ASCII skip. Bytes >= 0x80
5686            // (after demetafy) are not classified by typtab — they
5687            // reach wcsitype via WC_ZISTYPE instead.
5688            if c >= 0x80 {
5689                i += 1;
5690                continue;
5691            }
5692            let cu = c as usize;
5693            // c:4224-4229 — `if (inblank(c))` — for default-IFS
5694            // chars space/tab/newline, mark IWSEP unless the next
5695            // byte repeats the same char.
5696            let is_inblank = (t[cu] & (INBLANK as u32)) != 0;
5697            if is_inblank {
5698                if i + 1 < bytes.len() && bytes[i + 1] == c {
5699                    i += 1; // c:4226 — skip the dup
5700                } else {
5701                    t[cu] |= IWSEP as u32; // c:4228
5702                }
5703            }
5704            // c:4230 — `typtab[c] |= ISEP;`
5705            t[cu] |= ISEP as u32;
5706            i += 1;
5707        }
5708    }
5709
5710    // c:4232-4252 — wordchars walk. ORs IWORD onto every byte in
5711    // `$WORDCHARS` (or DEFAULT_WORDCHARS when unset). Used by every
5712    // word-class lookup in pattern matching, `${var:#word}`, etc.
5713    // Drops to ASCII-only under MULTIBYTE_SUPPORT (the non-ASCII path
5714    // routes through wordchars_wide).
5715    {
5716        // Same fallback as IFS above: paramtab first, then wordchars_lock
5717        // so wordcharssetfn updates that bypass paramtab still reach
5718        // the typtab rebuild.
5719        let wc = crate::ported::params::paramtab()
5720            .read()
5721            .ok()
5722            .and_then(|t| {
5723                t.get("WORDCHARS")
5724                    .map(|pm| crate::ported::params::wordcharsgetfn(pm))
5725            })
5726            .filter(|s| !s.is_empty())
5727            .unwrap_or_else(|| {
5728                crate::ported::params::wordchars_lock()
5729                    .lock()
5730                    .map(|g| g.clone())
5731                    .unwrap_or_default()
5732            });
5733        let src: String = if wc.is_empty() {
5734            DEFAULT_WORDCHARS.to_string()
5735        } else {
5736            wc
5737        };
5738        let bytes = src.as_bytes();
5739        let mut i = 0;
5740        while i < bytes.len() {
5741            // c:4238 — Meta+X demetafy.
5742            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
5743                i += 1;
5744                bytes[i] ^ 32
5745            } else {
5746                bytes[i]
5747            };
5748            // c:4239-4249 — MULTIBYTE non-ASCII skip.
5749            if c < 0x80 {
5750                t[c as usize] |= IWORD as u32; // c:4251
5751            }
5752            i += 1;
5753        }
5754    }
5755
5756    // c:4253-4254 — SPECCHARS walk. ORs ISPECIAL onto every member
5757    // of the hardcoded SPECCHARS string. Drives glob-special and
5758    // quote-special detection.
5759    {
5760        for &b in SPECCHARS.as_bytes() {
5761            t[b as usize] |= ISPECIAL as u32; // c:4254
5762        }
5763    }
5764
5765    // c:4255-4256 — comma special only when ZTF_SP_COMMA was set
5766    // via `makecommaspecial(1)`. KSH_GLOB / extended-glob path.
5767    {
5768        let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5769        if (flags & ZTF_SP_COMMA) != 0 {
5770            // c:4255
5771            t[b',' as usize] |= ISPECIAL as u32; // c:4256
5772        }
5773    }
5774
5775    // c:4257-4261 — bangchar special when BANGHIST + interact +
5776    // bangchar != 0. Sets ZTF_BANGCHAR flag bit then marks the
5777    // bangchar byte ISPECIAL.
5778    {
5779        let bangchar2 = bangchar.load(Ordering::SeqCst) as usize;
5780        let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5781        let interact_flag = (flags & ZTF_INTERACT) != 0;
5782        let banghist = isset(BANGHIST);
5783        if banghist && bangchar2 != 0 && bangchar2 < 256 && interact_flag {
5784            // c:4257
5785            TYPTAB_FLAGS.fetch_or(ZTF_BANGCHAR, Ordering::Relaxed); // c:4258
5786            t[bangchar2] |= ISPECIAL as u32; // c:4259
5787        } else {
5788            TYPTAB_FLAGS.fetch_and(!ZTF_BANGCHAR, Ordering::Relaxed); // c:4261
5789        }
5790    }
5791
5792    // c:4262-4263 — PATCHARS walk. ORs IPATTERN onto every member.
5793    // Used by pattern compilation to detect glob metachars.
5794    {
5795        for &b in PATCHARS.as_bytes() {
5796            t[b as usize] |= IPATTERN as u32; // c:4263
5797        }
5798    }
5799
5800    // Flush the scratch to the lock-free table (see header note).
5801    for (i, v) in t.iter().enumerate() {
5802        TYPTAB[i].store(*v, Ordering::Relaxed);
5803    }
5804}
5805
5806/// Port of `void makecommaspecial(int yesno)` from Src/utils.c:4270.
5807///
5808/// Toggles `ZTF_SP_COMMA` and the `ISPECIAL` bit on `,` in the
5809/// global typtab — used by glob/extended-glob to flag `,`
5810/// (KSH_GLOB) as a metacharacter.
5811pub fn makecommaspecial(yesno: bool) {
5812    // c:4270
5813    if yesno {
5814        // c:4272
5815        TYPTAB_FLAGS.fetch_or(ZTF_SP_COMMA, Ordering::Relaxed); // c:4273
5816        TYPTAB[b',' as usize].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4274
5817    } else {
5818        TYPTAB_FLAGS.fetch_and(!ZTF_SP_COMMA, Ordering::Relaxed); // c:4276
5819        TYPTAB[b',' as usize].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4277
5820    }
5821}
5822
5823/// Port of `void makebangspecial(int yesno)` from Src/utils.c:4283.
5824///
5825/// Toggles `ISPECIAL` on the current `bangchar`. When `yesno==0`
5826/// always clears; when nonzero, sets only if `ZTF_BANGCHAR` was
5827/// stored by `inittyptab` (i.e. BANGHIST is on).
5828pub fn makebangspecial(yesno: bool) {
5829    // c:4283
5830    let bc = bangchar.load(Ordering::SeqCst) as usize;
5831    if bc == 0 || bc >= 256 {
5832        return;
5833    }
5834    let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5835    if !yesno {
5836        // c:4289
5837        TYPTAB[bc].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4290
5838    } else if (flags & ZTF_BANGCHAR) != 0 {
5839        // c:4291
5840        TYPTAB[bc].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4292
5841    }
5842}
5843
5844/// Port of `wcsiblank(wint_t wc)` from `Src/utils.c:4302`.
5845///
5846/// ```c
5847/// mod_export int wcsiblank(wint_t wc) {
5848///     if (iswspace(wc) && wc != L'\n')
5849///         return 1;
5850///     return 0;
5851/// }
5852/// ```
5853///
5854/// "wide-character version of the iblank() macro" — true for any
5855/// whitespace EXCEPT newline. The previous Rust port included
5856/// newline (since `c.is_whitespace()` returns true for '\n') —
5857/// wrong for callers that use this to find token boundaries.
5858pub fn wcsiblank(wc: char) -> bool {
5859    wc.is_whitespace() && wc != '\n'
5860}
5861
5862/// Port of `int wcsitype(wchar_t c, int itype)` from Src/utils.c:4321.
5863///
5864/// "zistype macro extended to support wide characters. Works for
5865/// IIDENT, IWORD, IALNUM, ISEP."
5866///
5867/// The Rust port checks whether `c` falls in the typtab class
5868/// represented by `itype`. ASCII chars consult the global TYPTAB
5869/// (the same one C's `zistype()` macro indexes); non-ASCII chars
5870/// route through Unicode predicates that mirror the C `iswalnum`
5871/// fallback at line 4346.
5872pub fn wcsitype(c: char, itype: u32) -> bool {
5873    // c:4321
5874    if !isset(MULTIBYTE) {
5875        // c:4327
5876        if (c as u32) < 256 {
5877            return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
5878        }
5879        return false;
5880    }
5881    if (c as u32) < 128 {
5882        // c:4343
5883        return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
5884    }
5885    let cls = itype as u16;
5886    if cls == IIDENT {
5887        // c:4347
5888        if isset(POSIXIDENTIFIERS) {
5889            return false; // c:4348
5890        }
5891        return c.is_alphanumeric(); // c:4350
5892    }
5893    if cls == IWORD {
5894        // c:4352
5895        if c.is_alphanumeric() {
5896            return true;
5897        } // c:4353
5898          // C: IS_COMBINING(c) — no Rust crate-free combining-mark
5899          // predicate. zero-width chars (combining, zero-width-joiner,
5900          // etc.) are treated as word per c:4362.
5901        if unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0 {
5902            // c:4362
5903            return true;
5904        }
5905        // c:4364 — `wmemchr(wordchars_wide.chars, c, …)`. Reads from
5906        // the canonical `wordchars` global (writable by
5907        // `wordcharssetfn` at `Src/params.c:5143`). Previously routed
5908        // through `std::env::var("WORDCHARS")` which is the libc
5909        // process environment — never reflects runtime `WORDCHARS=:`
5910        // assignments inside the shell.
5911        let w = crate::ported::params::paramtab()
5912            .read()
5913            .ok()
5914            .and_then(|t| {
5915                t.get("WORDCHARS")
5916                    .map(|pm| crate::ported::params::wordcharsgetfn(pm))
5917            })
5918            .unwrap_or_default();
5919        return w.chars().any(|x| x == c);
5920    }
5921    if cls == ISEP {
5922        // c:4366
5923        // c:4367 — same canonical-global pattern for IFS.
5924        let ifs = crate::ported::params::paramtab()
5925            .read()
5926            .ok()
5927            .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
5928            .unwrap_or_default();
5929        return ifs.chars().any(|x| x == c);
5930    }
5931    let _ = IALNUM;
5932    c.is_alphanumeric() // c:4370
5933}
5934
5935/// Check if a character type at end of string (from utils.c itype_end)
5936/// Port of `char *itype_end(const char *ptr, int itype, int once)` from `Src/utils.c:4395`.
5937///
5938/// Walks `ptr` byte-by-byte advancing past every char whose
5939/// type-bits include `itype`. Returns the byte offset where the
5940/// walk stops (C returns the advanced pointer; the byte offset is
5941/// the Rust equivalent — `ptr + return_value` reproduces the C
5942/// pointer).
5943///
5944/// C body branches (ported line-for-line):
5945///
5946/// 1. **INAMESPC special-case** (c:4399-4413). If itype == INAMESPC
5947///    and we're not POSIXIDENTIFIERS-strict outside ksh emulation,
5948///    recurse on `ptr+(*ptr=='.')` with IIDENT to allow dotted
5949///    ksh93 namespace names. The recursion result determines
5950///    whether to advance past the dot or fall through.
5951///
5952/// 2. **MULTIBYTE branch** (c:4416-4470, gated `isset(MULTIBYTE)
5953///    && (itype != IIDENT || !isset(POSIXIDENTIFIERS))`). Walks
5954///    multibyte chars via mb_metacharlenconv (multibyte len +
5955///    wchar conversion). Per-codepoint test:
5956///      - itok byte (raw command line): test the byte via zistype
5957///      - Meta-prefixed pair: extract real byte via XOR, test via
5958///        zistype
5959///      - ASCII (< 0x80): test via zistype
5960///      - Non-ASCII codepoint: route through wcsitype (which
5961///        already handles IWORD/ISEP via WORDCHARS/IFS paramtab
5962///        lookup matching c:4364/c:4367)
5963///
5964/// 3. **Non-multibyte branch** (c:4474-4480). Simple Meta-byte +
5965///    zistype byte loop.
5966///
5967/// `once = true` stops after the first matching char (used for
5968/// "is this a valid first char" checks per c:1576 etc).
5969pub fn itype_end(s: &str, mut itype: u32, once: bool) -> usize {
5970    use crate::ported::zsh_h::{isset, Meta, EMULATE_KSH, EMULATION, MULTIBYTE, POSIXIDENTIFIERS};
5971    use crate::ported::ztype_h::{itok, zistype, IIDENT, INAMESPC};
5972
5973    // c:4399-4413 — INAMESPC handling with ksh93 namespace dot.
5974    let mut start = 0usize;
5975    if itype == INAMESPC {
5976        itype = IIDENT as u32;
5977        if !isset(POSIXIDENTIFIERS) || EMULATION(EMULATE_KSH) {
5978            // c:4403 — `t = itype_end(ptr + (*ptr == '.'), itype, 0);`
5979            let first_is_dot = s.as_bytes().first().copied() == Some(b'.');
5980            let recurse_start = if first_is_dot { 1 } else { 0 };
5981            let t = recurse_start + itype_end(&s[recurse_start..], itype, false);
5982            // c:4404-4410 — `if (t > ptr + (*ptr == '.')) {
5983            //                  if (*t == '.') ptr = t + 1;  /* Fall through */
5984            //                  else if (!once) return t;
5985            //              }`
5986            if t > recurse_start {
5987                let t_byte = s.as_bytes().get(t).copied();
5988                if t_byte == Some(b'.') {
5989                    start = t + 1; // c:4407
5990                } else if !once {
5991                    return t; // c:4409
5992                }
5993            }
5994        }
5995    }
5996
5997    let bytes = s.as_bytes();
5998    let mb = isset(MULTIBYTE) && (itype != IIDENT as u32 || !isset(POSIXIDENTIFIERS));
5999
6000    if mb {
6001        // c:4416-4470 — multibyte walk. mb_charinit() in C is a
6002        // no-op stateless reset; not needed in Rust since str::chars
6003        // is already stateless UTF-8.
6004        let mut i = start;
6005        while i < bytes.len() {
6006            let b = bytes[i];
6007            // c:4422-4425 — itok: raw token byte, test via zistype,
6008            // advance 1.
6009            if itok(b) {
6010                if !zistype(b, itype) {
6011                    break;
6012                }
6013                i += 1;
6014            } else if b == Meta {
6015                // c:4438-4440 (WEOF arm) + non-MB Meta fallback:
6016                // Meta-prefixed pair = single unescaped char via XOR.
6017                let nxt = bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20;
6018                if (nxt as u32) > 127 || !zistype(nxt, itype) {
6019                    break;
6020                }
6021                i += 2;
6022            } else if b < 0x80 {
6023                // c:4441-4443 (len == 1 && isascii) — ASCII: direct
6024                // zistype on the byte.
6025                if !zistype(b, itype) {
6026                    break;
6027                }
6028                i += 1;
6029            } else {
6030                // c:4446-4467 — non-ASCII codepoint. Decode the UTF-8
6031                // sequence starting at `i` and route through wcsitype
6032                // (which already handles IWORD via WORDCHARS paramtab
6033                // c:4364, ISEP via IFS paramtab c:4367, default via
6034                // iswalnum/is_alphanumeric c:4370).
6035                let tail = match std::str::from_utf8(&bytes[i..]) {
6036                    Ok(t) => t,
6037                    Err(_) => break, // invalid UTF-8 — stop walk
6038                };
6039                let ch = match tail.chars().next() {
6040                    Some(c) => c,
6041                    None => break,
6042                };
6043                if !wcsitype(ch, itype) {
6044                    break;
6045                }
6046                i += ch.len_utf8();
6047            }
6048            if once {
6049                break;
6050            }
6051        }
6052        i
6053    } else {
6054        // c:4474-4480 — non-multibyte arm. Simple byte loop with
6055        // Meta-prefix collapse.
6056        let mut i = start;
6057        while i < bytes.len() {
6058            let b = bytes[i];
6059            let chr = if b == Meta {
6060                bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20
6061            } else {
6062                b
6063            };
6064            if !zistype(chr, itype) {
6065                break;
6066            }
6067            i += if b == Meta { 2 } else { 1 };
6068            if once {
6069                break;
6070            }
6071        }
6072        i
6073    }
6074}
6075
6076/// Duplicate array (from utils.c arrdup)
6077/// Port of `arrdup(char **s)` from `Src/utils.c:4493`.
6078pub fn arrdup(s: &[String]) -> Vec<String> {
6079    s.to_vec()
6080}
6081
6082/// Duplicate array with max elements (from utils.c arrdup_max)
6083/// Port of `arrdup_max(char **s, unsigned max)` from `Src/utils.c:4508`.
6084pub fn arrdup_max(s: &[String], max: usize) -> Vec<String> {
6085    s.iter().take(max).cloned().collect()
6086}
6087
6088/// Duplicate array with zsh allocation (from utils.c zarrdup)
6089/// Port of `zarrdup(char **s)` from `Src/utils.c:4532`.
6090pub fn zarrdup(s: &[String]) -> Vec<String> {
6091    s.to_vec()
6092}
6093
6094/// Duplicate array of wide strings (from utils.c wcs_zarrdup) - same as zarrdup in Rust
6095/// Port of `wcs_zarrdup(wchar_t **s)` from `Src/utils.c:4547`.
6096pub fn wcs_zarrdup(s: &[String]) -> Vec<String> {
6097    s.to_vec()
6098}
6099
6100/// Spelling correction: find closest match (from utils.c spname)
6101/// Port of `spname(char *oldname)` from `Src/utils.c:4562`.
6102/// WARNING: param names don't match C — Rust=(name, dir) vs C=(oldname)
6103pub fn spname(name: &str, dir: &str) -> Option<String> {
6104    let entries = match fs::read_dir(dir) {
6105        Ok(e) => e,
6106        Err(_) => return None,
6107    };
6108
6109    let mut best = None;
6110    let mut best_dist = 4; // threshold
6111
6112    for entry in entries.flatten() {
6113        if let Some(entry_name) = entry.file_name().to_str() {
6114            let dist = spdist(name, entry_name, best_dist);
6115            if dist < best_dist {
6116                best_dist = dist;
6117                best = Some(entry_name.to_string());
6118            }
6119        }
6120    }
6121    best
6122}
6123
6124/// Spelling correction with full path (from utils.c mindist)
6125/// Port of `mindist(char *dir, char *mindistguess, char *mindistbest, int wantdir)` from `Src/utils.c:4624`.
6126/// WARNING: param names don't match C — Rust=(dir, name) vs C=(dir, mindistguess, mindistbest, wantdir)
6127pub fn mindist(dir: &str, name: &str) -> Option<(String, usize)> {
6128    let entries = match fs::read_dir(dir) {
6129        Ok(e) => e,
6130        Err(_) => return None,
6131    };
6132
6133    let mut best = None;
6134    let mut best_dist = 4;
6135
6136    for entry in entries.flatten() {
6137        if let Some(entry_name) = entry.file_name().to_str() {
6138            let dist = spdist(name, entry_name, best_dist);
6139            if dist < best_dist {
6140                best_dist = dist;
6141                best = Some(entry_name.to_string());
6142            }
6143        }
6144    }
6145    best.map(|name| (name, best_dist))
6146}
6147
6148// spellcheck a word                                                        // c:3123
6149// fix s ; if hist is nonzero, fix the history list too                     // c:3124
6150/// Compute edit distance between two strings (for spelling correction)
6151/// Direct port of `int spdist(char *s, char *t, int thresh)` from
6152/// `Src/utils.c:4675-4750`. Drives the CORRECT-option typo prompt:
6153/// returns 0 for identical, 1 for case-only mistakes, 2 for one
6154/// transposition / missing letter / QWERTY-adjacent mistype, 200 for
6155/// anything farther.
6156///
6157/// **This is NOT a Levenshtein DP — that was the previous Rust impl
6158/// dressed as a port.** The C source uses a keyboard-adjacency model
6159/// (qwertykeymap / dvorakkeymap arrays + tulower equality) which is
6160/// the actual behaviour `setopt CORRECT` depends on. Restored
6161/// faithfully.
6162pub fn spdist(s: &str, t: &str, thresh: usize) -> usize {
6163    // c:4679-4690 — qwerty keymap (rows of 14 chars each: numeric row,
6164    // QWERTY top row, home row, bottom row, then shift versions).
6165    // Embedded `\n` / `\t` mark off-grid cells.
6166    const QWERTYKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6167\t1234567890-=\t\
6168\tqwertyuiop[]\t\
6169\tasdfghjkl;'\n\t\
6170\tzxcvbnm,./\t\t\t\
6171\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6172\t!@#$%^&*()_+\t\
6173\tQWERTYUIOP{}\t\
6174\tASDFGHJKL:\"\n\t\
6175\tZXCVBNM<>?\n\n\t\
6176\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
6177    // c:4691-4702 — dvorak keymap, same shape.
6178    const DVORAKKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6179\t1234567890[]\t\
6180\t',.pyfgcrl/=\t\
6181\taoeuidhtns-\n\t\
6182\t;qjkxbmwvz\t\t\t\
6183\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6184\t!@#$%^&*(){}\t\
6185\t\"<>PYFGCRL?+\t\
6186\tAOEUIDHTNS_\n\t\
6187\t:QJKXBMWVZ\n\n\t\
6188\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
6189
6190    // c:4703-4707 — `keymap = isset(DVORAK) ? dvorakkeymap : qwertykeymap;`
6191    let keymap = if isset(DVORAK) {
6192        DVORAKKEYMAP.as_bytes()
6193    } else {
6194        QWERTYKEYMAP.as_bytes()
6195    };
6196
6197    let s_b = s.as_bytes();
6198    let t_b = t.as_bytes();
6199
6200    // c:4709-4710 — `if (!strcmp(s, t)) return 0;`
6201    if s == t {
6202        return 0;
6203    }
6204
6205    // c:4712-4714 — `for (p, q; *p && tulower(*p) == tulower(*q); p++, q++);
6206    //                  if (!*p && !*q) return 1;` — case-only mismatch.
6207    let mut p = 0usize;
6208    let mut q = 0usize;
6209    while p < s_b.len() && q < t_b.len() && tulower(s_b[p] as char) == tulower(t_b[q] as char) {
6210        p += 1;
6211        q += 1;
6212    }
6213    if p == s_b.len() && q == t_b.len() {
6214        return 1;
6215    }
6216
6217    // c:4715-4716 — `if (!thresh) return 200;`
6218    if thresh == 0 {
6219        return 200;
6220    }
6221
6222    // c:4717-4727 — first walk: detect transposition / missing letter at
6223    // first divergence.
6224    p = 0;
6225    q = 0;
6226    while p < s_b.len() && q < t_b.len() {
6227        if s_b[p] == t_b[q] {
6228            // c:4718-4719 — match: skip (don't count `aa` as transposed).
6229            p += 1;
6230            q += 1;
6231            continue;
6232        }
6233        // c:4720-4721 — `if (p[1] == q[0] && q[1] == p[0]) return
6234        //                  spdist(p+2, q+2, thresh-1) + 1;` transposition.
6235        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] {
6236            // SAFETY: bytes are ASCII for the keymap test below; from_utf8
6237            // is fine on a non-ASCII tail here because spdist takes &str.
6238            let s_tail = std::str::from_utf8(&s_b[p + 2..]).unwrap_or("");
6239            let t_tail = std::str::from_utf8(&t_b[q + 2..]).unwrap_or("");
6240            return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
6241        }
6242        // c:4722-4723 — `if (p[1] == q[0]) return spdist(p+1, q, thresh-1) + 2;`
6243        if p + 1 < s_b.len() && s_b[p + 1] == t_b[q] {
6244            let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6245            let t_tail = std::str::from_utf8(&t_b[q..]).unwrap_or("");
6246            return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6247        }
6248        // c:4724-4725 — `if (p[0] == q[1]) return spdist(p, q+1, thresh-1) + 2;`
6249        if q + 1 < t_b.len() && s_b[p] == t_b[q + 1] {
6250            let s_tail = std::str::from_utf8(&s_b[p..]).unwrap_or("");
6251            let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6252            return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6253        }
6254        // c:4726-4727 — `if (*p != *q) break;`
6255        break;
6256    }
6257    // c:4728-4729 — `if ((!*p && strlen(q) == 1) || (!*q && strlen(p) == 1))
6258    //                  return 2;` — single trailing-char insertion.
6259    if (p == s_b.len() && (t_b.len() - q) == 1) || (q == t_b.len() && (s_b.len() - p) == 1) {
6260        return 2;
6261    }
6262    // c:4730-4748 — second walk: keyboard-adjacency mistype detection.
6263    p = 0;
6264    q = 0;
6265    while p < s_b.len() && q < t_b.len() {
6266        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] {
6267            // c:4737-4738 — `if (!(z = strchr(keymap, p[0])) || *z == '\n' ||
6268            //                       *z == '\t') return spdist(p+1, q+1,
6269            //                       thresh-1) + 1;`
6270            let pos = keymap.iter().position(|&b| b == s_b[p]);
6271            let z_ok = match pos {
6272                Some(i) => keymap[i] != b'\n' && keymap[i] != b'\t',
6273                None => false,
6274            };
6275            if !z_ok {
6276                let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6277                let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6278                return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
6279            }
6280            // c:4739 — `t0 = z - keymap;`
6281            let t0 = pos.unwrap() as isize;
6282            // c:4740-4744 — eight adjacency offsets (-15,-14,-13,-1,+1,
6283            //                +13,+14,+15) → keyboard neighbours.
6284            let offsets: [isize; 8] = [-15, -14, -13, -1, 1, 13, 14, 15];
6285            let adjacent = offsets.iter().any(|&off| {
6286                let idx = t0 + off;
6287                if idx >= 0 && (idx as usize) < keymap.len() {
6288                    keymap[idx as usize] == t_b[q]
6289                } else {
6290                    false
6291                }
6292            });
6293            if adjacent {
6294                // c:4745 — `return spdist(p+1, q+1, thresh-1) + 2;`
6295                let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6296                let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6297                return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6298            }
6299            // c:4746 — `return 200;`
6300            return 200;
6301        } else if p < s_b.len() && q < t_b.len() && s_b[p] != t_b[q] {
6302            // c:4747-4748 — `else if (*p != *q) break;`
6303            break;
6304        }
6305        p += 1;
6306        q += 1;
6307    }
6308    // c:4749 — `return 200;`
6309    200
6310}
6311
6312/// Set terminal to cbreak mode (from utils.c setcbreak)
6313#[cfg(unix)]
6314/// Port of `setcbreak` from `Src/utils.c:4756`.
6315pub fn setcbreak() -> bool {
6316    if let Some(mut ti) = gettyinfo() {
6317        ti.c_lflag &= !(libc::ICANON | libc::ECHO);
6318        ti.c_cc[libc::VMIN] = 1;
6319        ti.c_cc[libc::VTIME] = 0;
6320        settyinfo(&ti)
6321    } else {
6322        false
6323    }
6324}
6325
6326#[cfg(not(unix))]
6327/// Port of `setcbreak` from `Src/utils.c:4756`.
6328pub fn setcbreak() -> bool {
6329    false
6330}
6331
6332/// Port of `void attachtty(pid_t pgrp)` from Src/utils.c:4775.
6333/// Hands the controlling terminal to `pgrp`. Gated by `jobbing &&
6334/// interact`; falls back to `mypgrp` and disables MONITOR on permanent
6335/// failure (matching the C source's recursion + `opts[MONITOR]=0` path).
6336#[cfg(unix)]
6337pub fn attachtty(pgrp: i32) {
6338    // c:4775
6339
6340    if !(jobbing() && interact()) {
6341        return; // c:4779
6342    }
6343    let shtty = SHTTY.load(Ordering::Relaxed); // c:4781
6344    if shtty == -1 {
6345        return;
6346    }
6347    let ep = ATTACHTTY_EP.load(Ordering::Relaxed);
6348    let rc = unsafe { libc::tcsetpgrp(shtty, pgrp) }; // c:4781
6349    if rc == -1 && ep == 0 {
6350        // c:4781
6351        let mypgrp_val = *crate::ported::jobs::MYPGRP // c:4792
6352            .get_or_init(|| Mutex::new(0))
6353            .lock()
6354            .unwrap();
6355        if pgrp != mypgrp_val && unsafe { libc::kill(-pgrp, 0) } == -1 {
6356            attachtty(mypgrp_val); // c:4793
6357        } else {
6358            let errno_val = io::Error::last_os_error().raw_os_error().unwrap_or(0);
6359            if errno_val != libc::ENOTTY {
6360                // c:4795
6361                zwarn(&format!(
6362                    "can't set tty pgrp: {}", // c:4797
6363                    io::Error::from_raw_os_error(errno_val)
6364                ));
6365                let _ = io::stderr().flush(); // c:4798
6366            }
6367            opt_state_set("monitor", false); // c:4815 opts[MONITOR]=0
6368            ATTACHTTY_EP.store(1, Ordering::Relaxed); // c:4815
6369        }
6370    } else if rc != -1 {
6371        // c:4815
6372        *crate::ported::jobs::LAST_ATTACHED_PGRP // c:4815
6373            .get_or_init(|| Mutex::new(0))
6374            .lock()
6375            .unwrap() = pgrp;
6376    }
6377}
6378
6379/// Port of `pid_t gettygrp(void)` from Src/utils.c:4815.
6380#[cfg(unix)]
6381pub fn gettygrp() -> i32 {
6382    // c:4815
6383    let shtty = SHTTY.load(Ordering::Relaxed);
6384    if shtty == -1 {
6385        // c:4819
6386        return -1; // c:4820
6387    }
6388    unsafe { libc::tcgetpgrp(shtty) } // c:4823
6389}
6390
6391/// Convert raw bytes (possibly containing NUL / 0x83-0x9b) to
6392/// zsh's metafied form: each `imeta(b)` byte becomes `Meta` (0x83)
6393/// followed by `b ^ 32`.
6394///
6395/// Port of `metafy(char *buf, int len, int heap)` from Src/utils.c:4856. The C source takes a
6396/// `heap` mode controlling whether the result is `zalloc`'d /
6397/// `zhalloc`'d / written into a static buffer / appended to the
6398/// existing buffer; in Rust we always return an owned `String`
6399/// since allocation strategy is uniform. The byte-level transform
6400/// is identical: walk the input, count metafy hits, allocate
6401/// `len + meta` bytes, expand each `Meta+X` pair in reverse.
6402/// Rust idiom replacement: forward byte-walk + Vec::push covers the
6403/// C two-pass (count + alloc + reverse-expand) approach; Vec grows
6404/// on demand so the pre-count is unnecessary in Rust.
6405/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len, heap)
6406pub fn metafy(buf: &str) -> String {
6407    // c:4856
6408    let bytes = buf.as_bytes();
6409    let mut out = Vec::with_capacity(bytes.len());
6410    for &b in bytes {
6411        // C: `#define imeta(c) ((c) >= Meta)` from Src/zsh.h —
6412        // every byte >= 0x83 needs escaping. The previous
6413        // narrow-range check `(0x83..=0x9b)` was a bug: bytes
6414        // 0x9c..=0xff (e.g. UTF-8 continuation bytes, high-Latin
6415        // characters) escaped C's imeta() but not the Rust
6416        // version, which then fed un-escaped bytes downstream
6417        // and corrupted Meta-aware loops.
6418        if imeta_byte(b) {
6419            out.push(Meta);
6420            out.push(b ^ 32);
6421        } else {
6422            out.push(b);
6423        }
6424    }
6425    // metafied bytes are in [0..=0x7f]∪{0x83}∪[expanded ^ 32 range];
6426    // String::from_utf8 may fail on the high bytes — fall back to
6427    // lossy. The lossy fallback inserts U+FFFD for invalid
6428    // sequences, which is fine for String-level callers (lexer /
6429    // pattern compile / display) but breaks byte-round-trip with
6430    // `unmetafy`. Byte-round-trip callers should use `metafy_bytes`
6431    // (which preserves the metafied byte sequence verbatim).
6432    String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
6433}
6434
6435/// Port of `ztrdup_metafy(const char *s)` from `Src/utils.c:4929`.
6436///
6437/// ```c
6438/// mod_export char *
6439/// ztrdup_metafy(const char *s)
6440/// {
6441///     if (!s) return NULL;
6442///     return metafy((char *)s, -1, META_DUP);
6443/// }
6444/// ```
6445pub fn ztrdup_metafy(s: &str) -> String {
6446    metafy(s)
6447}
6448
6449/// Port of `unmetafy(char *s, int *len)` from `Src/utils.c:4954`.
6450///
6451/// Take a metafied byte buffer in `s` and convert it in place to
6452/// its literal representation. C signature:
6453///
6454/// ```c
6455/// char *unmetafy(char *s, int *len);
6456/// ```
6457///
6458/// The Rust port mutates `s` in place and returns the resulting
6459/// length (mirroring C's `*len` out-parameter). C control flow:
6460///
6461/// ```c
6462/// for (p = s; *p && *p != Meta; p++);     // skip prefix with no Meta
6463/// for (t = p; (*t = *p++);)                 // walk the rest
6464///     if (*t++ == Meta && *p)
6465///         t[-1] = *p++ ^ 32;                // un-escape: XOR with 32
6466/// ```
6467///
6468/// Same algorithm here, byte-indexed against the Vec rather than
6469/// pointer-walked.
6470/// WARNING: param names don't match C — Rust=(s) vs C=(s, len)
6471pub fn unmetafy(s: &mut Vec<u8>) -> usize {
6472    // c:4954
6473    // First loop: find the first `Meta` byte. Everything before it
6474    // stays as-is, so we don't need to copy.
6475    let mut p: usize = 0;
6476    while p < s.len() && s[p] != Meta {
6477        p += 1;
6478    }
6479    // Second loop: walk from `p` onward, copying each byte into the
6480    // `t` slot (which trails `p` by one position per Meta-escape
6481    // we collapse).
6482    let mut t: usize = p;
6483    while p < s.len() {
6484        let b = s[p];
6485        s[t] = b;
6486        p += 1;
6487        if b == Meta && p < s.len() {
6488            // C: t[-1] = *p++ ^ 32; — overwrite the just-written
6489            // Meta with the un-escaped byte.
6490            s[t] = s[p] ^ 32;
6491            p += 1;
6492        }
6493        t += 1;
6494    }
6495    s.truncate(t);
6496    t
6497}
6498
6499/// Port of `int metalen(const char *s, int len)` from `Src/utils.c:4971-4983`.
6500/// ```c
6501/// int mlen = len;
6502/// while (len--) {
6503///     if (*s++ == Meta) { mlen++; s++; }
6504/// }
6505/// return mlen;
6506/// ```
6507/// Doc: "Return the character length of a metafied substring, given
6508/// the unmetafied substring length." So **input `len` is the
6509/// UNMETAFIED char count, output is the METAFIED byte count.**
6510///
6511/// Previously the Rust port had INVERTED semantics: looped while
6512/// `i < len` byte-walk, returned char count. That's the reverse
6513/// operation. Pin the correct C contract.
6514pub fn metalen(s: &str, len: usize) -> usize {
6515    // c:4972
6516    let bytes = s.as_bytes();
6517    let mut mlen = len; // c:4974
6518    let mut remaining = len;
6519    let mut i = 0;
6520    while remaining > 0 && i < bytes.len() {
6521        // c:4976 `while (len--)`
6522        if bytes[i] == Meta {
6523            // c:4977
6524            mlen += 1; // c:4978
6525            i += 2; // c:4979 s++ (already advanced past Meta)
6526        } else {
6527            i += 1;
6528        }
6529        remaining -= 1;
6530    }
6531    mlen
6532}
6533
6534/// Port of `unmeta(const char *file_name)` from `Src/utils.c:4994`.
6535///
6536/// Convert a zsh internal (metafied) string to a system-call-safe
6537/// form (e.g. for passing to `open(2)`).
6538///
6539/// C body shape (c:4994-5010):
6540/// ```c
6541/// meta = 0;
6542/// for (t = file_name; *t; t++)
6543///     if (*t == Meta) { meta = 1; break; }
6544/// if (!meta) return (char *) file_name;        // no-copy fast path
6545/// for (t = file_name, p = fn; *t; p++)
6546///     if ((*p = *t++) == Meta && *t)
6547///         *p = *t++ ^ 32;
6548/// ```
6549// Rust idiom replacement: byte-scan fast path + `unmetafy` covers
6550// the C in-place decode + alloc-on-copy dance; String owns its own
6551// allocation so no `heap` flag needed.
6552/// `unmeta` — see implementation.
6553pub fn unmeta(s: &str) -> String {
6554    // c:4994
6555    let bytes = s.as_bytes();
6556    // c:4995-4996 — Meta-byte scan; no-copy fast path.
6557    if !bytes.iter().any(|&b| b == Meta) {
6558        return s.to_string();
6559    }
6560    let mut buf = bytes.to_vec();
6561    let len = unmetafy(&mut buf); // c:4999-5001
6562    buf.truncate(len);
6563    String::from_utf8_lossy(&buf).into_owned()
6564}
6565
6566/// Port of `convchar_t unmeta_one(const char *in, int *sz)` from `Src/utils.c:5056-5086`.
6567/// Non-MULTIBYTE branch (c:5077-5083):
6568/// ```c
6569/// if (in[0] == Meta) { *sz = 2; wc = (unsigned char)(in[1] ^ 32); }
6570/// else               { *sz = 1; wc = (unsigned char) in[0]; }
6571/// ```
6572/// Returns `(decoded_char, bytes_consumed)`. The c:5070 NULL/empty
6573/// guard returns `(0, 0)`. Previously used hardcoded `0x83` for the
6574/// Meta byte — now routes through the canonical `Meta` constant
6575/// (defined as `'\u{83}'` at zsh.h:144).
6576/// WARNING: param names don't match C — Rust=(s) vs C=(in, sz)
6577pub fn unmeta_one(s: &str) -> (char, usize) {
6578    // c:5058
6579    let bytes = s.as_bytes();
6580    // c:5070 — `if (!in || !*in) return 0;`
6581    if bytes.is_empty() {
6582        return ('\0', 0);
6583    }
6584    // c:5077 — `if (in[0] == Meta)`.
6585    if bytes[0] == Meta && bytes.len() > 1 {
6586        // c:5078-5079 — `*sz = 2; wc = (unsigned char)(in[1] ^ 32);`.
6587        ((bytes[1] ^ 32) as char, 2)
6588    } else {
6589        // c:5081-5082 — `*sz = 1; wc = (unsigned char) in[0];`.
6590        (bytes[0] as char, 1)
6591    }
6592}
6593
6594/// Port of `ztrcmp(char const *s1, char const *s2)` from `Src/utils.c:5106`.
6595///
6596/// Byte-walking compare with lazy Meta resolution. C body skips
6597/// matching bytes wholesale, then on the first differing byte
6598/// un-meta-fies just that byte and compares. Faster than
6599/// `unmeta(s1).cmp(unmeta(s2))` because:
6600///   1. No allocation up front,
6601///   2. Only the first differing position pays the un-meta cost.
6602///
6603/// ```c
6604/// while(*s1 && *s1 == *s2) { s1++; s2++; }
6605/// if (!(c1 = *s1)) c1 = -1;
6606/// else if (c1 == Meta) c1 = *++s1 ^ 32;
6607/// if (!(c2 = *s2)) c2 = -1;
6608/// else if (c2 == Meta) c2 = *++s2 ^ 32;
6609/// return c1 - c2;
6610/// ```
6611pub fn ztrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
6612    // c:5106
6613    let b1 = s1.as_bytes();
6614    let b2 = s2.as_bytes();
6615    let mut i1 = 0;
6616    let mut i2 = 0;
6617    // Skip the matching prefix.
6618    while i1 < b1.len() && i2 < b2.len() && b1[i1] == b2[i2] {
6619        i1 += 1;
6620        i2 += 1;
6621    }
6622    // Resolve c1: -1 for end-of-string, else the next byte
6623    // (un-meta-fied if it's a Meta marker).
6624    let c1: i32 = if i1 >= b1.len() {
6625        -1
6626    } else if b1[i1] == Meta && i1 + 1 < b1.len() {
6627        (b1[i1 + 1] ^ 32) as i32
6628    } else {
6629        b1[i1] as i32
6630    };
6631    let c2: i32 = if i2 >= b2.len() {
6632        -1
6633    } else if b2[i2] == Meta && i2 + 1 < b2.len() {
6634        (b2[i2 + 1] ^ 32) as i32
6635    } else {
6636        b2[i2] as i32
6637    };
6638    c1.cmp(&c2)
6639}
6640
6641// pastebuf() DELETED — was a misplaced fn. The real `pastebuf()`
6642// lives in `Src/Zle/zle_misc.c:558` (a ZLE clipboard helper that
6643// pastes a Cutbuffer into the line-edit buffer). It has nothing to
6644// do with metafication, despite this file's prior body which
6645// reimplemented half of `metafy`. The real metafy lives below at
6646// `pub fn metafy()` (port of utils.c:4856).
6647
6648/// Unmetafied string length (from utils.c ztrlen lines 5135-5152)
6649pub fn ztrlen(s: &str) -> usize {
6650    // c:5136
6651    let mut len = 0;
6652    let chars: Vec<char> = s.chars().collect();
6653    let mut i = 0;
6654    while i < chars.len() {
6655        len += 1;
6656        if chars[i] as u32 == Meta as u32 && i + 1 < chars.len() {
6657            i += 2;
6658        } else {
6659            i += 1;
6660        }
6661    }
6662    len
6663}
6664
6665/// Port of `ztrlenend(char const *s, char const *eptr)` from `Src/utils.c:5162`.
6666///
6667/// ```c
6668/// for (l = 0; s < eptr; l++) {
6669///     if (*s++ == Meta) s++;  // skip past Meta-escaped pair
6670/// }
6671/// return l;
6672/// ```
6673///
6674/// Count the unmetafied character length from `s` up to `end`
6675/// bytes. Each Meta-escaped pair counts as 1 character.
6676/// Previous Rust port called `chars().count()` which counts UTF-8
6677/// codepoints, not byte-walked Meta-pairs — wrong semantics.
6678pub fn ztrlenend(s: &str, eptr: usize) -> usize {
6679    let bytes = s.as_bytes();
6680    let cap = eptr.min(bytes.len());
6681    let mut l = 0;
6682    let mut i = 0;
6683    while i < cap {
6684        if bytes[i] == Meta {
6685            // Meta sentinel + escaped byte = 1 visible char.
6686            i += 2;
6687        } else {
6688            i += 1;
6689        }
6690        l += 1;
6691    }
6692    l
6693}
6694
6695/// Port of `int ztrsub(char const *t, char const *s)` from `Src/utils.c:5185-5203`.
6696/// ```c
6697/// int l = t - s;
6698/// while (s != t) {
6699///     if (*s++ == Meta) { s++; l--; }
6700/// }
6701/// return l;
6702/// ```
6703/// "Subtract two pointers in a metafied string." `s` is the start
6704/// pointer, `t` is the end pointer; both point into the SAME buffer.
6705/// Returns the count of unmetafied chars in `[s, t)` — same as
6706/// `ztrlen` of the substring but expressed via pointer arithmetic.
6707///
6708/// Rust API: takes the full buffer and a byte-offset pair
6709/// `[start..end)` since Rust can't replicate raw-pointer subtraction
6710/// across distinct `&str` arguments without UB risk. The semantics
6711/// stay faithful: count of unmetafied chars between two offsets.
6712/// Previous Rust port took two unrelated `&str` and computed
6713/// `ztrlen(&t[..t.len()-s.len()])` — a fundamentally different
6714/// operation that worked only when `s` was the suffix of `t`.
6715/// WARNING: param names don't match C — Rust=(buf, start, end) vs C=(t, s)
6716pub fn ztrsub(buf: &str, start: usize, end: usize) -> usize {
6717    // c:5187
6718    let bytes = buf.as_bytes();
6719    let end = end.min(bytes.len());
6720    let start = start.min(end);
6721    let mut l = (end - start) as isize; // c:5189
6722    let mut i = start;
6723    while i < end {
6724        if bytes[i] == Meta {
6725            // c:5192
6726            i += 2; // c:5198
6727            l -= 1; // c:5199
6728        } else {
6729            i += 1;
6730        }
6731    }
6732    l.max(0) as usize
6733}
6734
6735/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
6736/// `Src/utils.c:5217`.
6737///
6738/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
6739/// `Src/utils.c:5217-5240`. Pulls the next entry from a DIR* the
6740/// caller owns; returns the entry's bare name (metafied String) or
6741/// `None` at EOF. The C source's `ignoredots` flag (c:5232) skips
6742/// `.` and `..` when set; callers (`spnamepat` at c:4648) that
6743/// want dot entries pass 0.
6744///
6745/// Caller pattern matches C exactly:
6746///   let mut dir = fs::read_dir(path)?;
6747///   while let Some(name) = zreaddir(&mut dir, 1) { ... }
6748///   // dir auto-closed on drop (= closedir)
6749pub fn zreaddir(dir: &mut fs::ReadDir, ignoredots: i32) -> Option<String> {
6750    // c:5217
6751    for entry in dir.by_ref() {
6752        // c:5221 readdir loop
6753        let Ok(e) = entry else { continue };
6754        let Ok(name) = e.file_name().into_string() else {
6755            continue;
6756        };
6757        // c:5232 — `if (ignoredots && de->d_name[0] == '.' &&
6758        //              (!de->d_name[1] || (de->d_name[1] == '.' && !de->d_name[2])))`.
6759        if ignoredots != 0 && (name == "." || name == "..") {
6760            continue; // c:5234
6761        }
6762        return Some(name); // c:5238 return de->d_name
6763    }
6764    None // c:5240 — fell off the end
6765}
6766
6767/// Port of `int zputs(char const *s, FILE *stream)` from `Src/utils.c:5263-5282`.
6768///
6769/// ```c
6770/// while (*s) {
6771///     if (*s == Meta)     c = *++s ^ 32;
6772///     else if (itok(*s)) { s++; continue; }
6773///     else                c = *s;
6774///     s++;
6775///     if (fputc(c, stream) < 0) return EOF;
6776/// }
6777/// return 0;
6778/// ```
6779///
6780/// Writes `s` to `stream` with Meta+X pair decoding and ITOK-byte
6781/// skipping. Returns `0` on success, `-1` on write error (mirroring
6782/// C's `EOF` sentinel for the int return).
6783pub fn zputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
6784    // c:5265
6785    let bytes = s.as_bytes(); // c:5265 *s walk
6786    let mut i = 0;
6787    while i < bytes.len() {
6788        // c:5267 while (*s)
6789        let c: u8; // c:5268 char c
6790        if bytes[i] == Meta {
6791            // c:5269 if (*s == Meta)
6792            // c:5270 — `c = *++s ^ 32;`
6793            if i + 1 < bytes.len() {
6794                c = bytes[i + 1] ^ 32;
6795                i += 1; // c:5270 ++s
6796            } else {
6797                i += 1;
6798                continue;
6799            }
6800        } else if itok(bytes[i]) {
6801            // c:5271 else if (itok(*s))
6802            // c:5272 — `s++; continue;` (skip token byte)
6803            i += 1;
6804            continue;
6805        } else {
6806            c = bytes[i]; // c:5274 else c = *s
6807        }
6808        i += 1; // c:5276 s++
6809        if stream.write_all(&[c]).is_err() {
6810            // c:5277 fputc(c, stream)
6811            return -1; // c:5278 return EOF
6812        }
6813    }
6814    0 // c:5280 return 0
6815}
6816
6817/// Port of `nicedup(char const *s, int heap)` from `Src/utils.c:5289`
6818/// (single-byte build) and `Src/utils.c:5530` (multibyte build).
6819///
6820/// C bodies:
6821/// ```c
6822/// /* Src/utils.c:5289 — !MULTIBYTE_SUPPORT */
6823/// char *retstr;
6824/// (void)sb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
6825/// return retstr;
6826///
6827/// /* Src/utils.c:5530 — MULTIBYTE_SUPPORT */
6828/// char *retstr;
6829/// (void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
6830/// return retstr;
6831/// ```
6832///
6833/// zshrs targets MULTIBYTE_SUPPORT (matches every modern zsh
6834/// build) — route through `mb_niceformat`. Previous Rust port
6835/// went through `sb_niceformat`, which is the !MULTIBYTE path —
6836/// strips wide-char handling and corrupts non-ASCII via byte-mask
6837/// `nicechar(c & 0xff)`.
6838///
6839/// C signature faithful: `(s, heap) -> String`. `heap` selects between
6840/// arena (`NICEFLAG_HEAP`) and persistent (default `ztrdup`) allocation
6841/// in C; Rust has a single allocator so both paths produce the same
6842/// owned `String`, but the `heap` parameter is preserved per Rule S1.
6843pub fn nicedup(s: &str, heap: i32) -> String {
6844    // c:5530
6845    // c:5532 — `char *retstr;`
6846    let retstr: Option<String>;
6847    // c:5534 — `(void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);`
6848    let mut slot: Option<String> = None;
6849    let _ = mb_niceformat(
6850        s,
6851        None,
6852        Some(&mut slot),
6853        if heap != 0 { NICEFLAG_HEAP } else { 0 },
6854    );
6855    retstr = slot;
6856    // c:5536 — `return retstr;`
6857    retstr.unwrap_or_default()
6858}
6859
6860/// Nice-format and duplicate string.
6861/// Port of `nicedupstring(char const *s)` from `Src/utils.c:5301`.
6862/// C body: `return nicedup(s, 1);` — heap-arena allocation form.
6863pub fn nicedupstring(s: &str) -> String {
6864    // c:5301
6865    nicedup(s, 1) // c:5303
6866}
6867
6868/// Nicely format a string
6869/// Port of `nicezputs(char const *s, FILE *stream)` from `Src/utils.c`.
6870///
6871/// Under `MULTIBYTE_SUPPORT` (the daily-driver path), C defines
6872/// `nicezputs(str, outs)` as a macro at zsh.h:3274:
6873///   `(void)mb_niceformat((str), (outs), NULL, 0)`.
6874/// Without MULTIBYTE (c:5313): `sb_niceformat(s, stream, NULL, 0)`.
6875///
6876/// The previous Rust impl was `s.chars().map(nicechar).collect()` —
6877/// wrong on every front:
6878///   1. No unmetafy step (Meta-byte pairs surfaced as `\M-…` mangled).
6879///   2. `nicechar` is byte-based (`c & 0xff`), corrupting non-ASCII
6880///      multibyte codepoints into spurious `\M-X` escapes.
6881///   3. No `itok` ZSH-token-byte handling.
6882///
6883/// Route through `mb_niceformat` to match the C macro under
6884/// MULTIBYTE — which itself unmetafies and uses `wcs_nicechar` for
6885/// proper wide-char handling.
6886pub fn nicezputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
6887    // c:5313 (non-MULTIBYTE) / zsh.h:3274 (MULTIBYTE macro:
6888    //   `(void)mb_niceformat((str), (outs), NULL, 0)`).
6889    let _ = mb_niceformat(s, Some(stream), None, 0);
6890    0 // c:5316 return 0
6891}
6892
6893/// Port of `niceztrlen(char const *s)` from `Src/utils.c:5324`.
6894///
6895/// Returns the length (in bytes) of the visible representation of
6896/// the metafied string `s`. C body walks each char via `nicechar`
6897/// and accumulates `strlen(nicechar(c))`; the Rust port mirrors
6898/// this via [`sb_niceformat`] which is the same render-then-measure
6899/// path.
6900pub fn niceztrlen(s: &str) -> usize {
6901    // c:5343-5348 — under MULTIBYTE_SUPPORT (which zshrs targets) `niceztrlen`
6902    // is the multibyte version: it walks whole characters via `mb_niceformat`,
6903    // whose per-char width comes from `wcs_nicechar_sel`'s wcwidth — so a
6904    // printable multibyte char (e.g. U+2010 `‐`, 3 bytes / 1 column) counts as
6905    // 1, not 3. The port previously called `sb_niceformat`, the single-byte
6906    // (`#else`, c:5320-5341) version that counts each byte separately, so any
6907    // description/match with a multibyte glyph over-measured its display width
6908    // by (bytes−columns). In `cd_get`'s described-list truncation
6909    // (computil.rs) that over-measure consumed the remaining-width budget too
6910    // early and clipped the tail off descriptions containing such a glyph —
6911    // e.g. `xz -<TAB>`'s "…multi‐threaded decompression" lost its final "on".
6912    mb_niceformat(s, None, None, 0)
6913}
6914
6915/// Multibyte-aware nice-format of a string.
6916/// Port of `mb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from Src/utils.c:5366. Walks the
6917/// (un-metafied) string char-by-char; for each control byte or
6918/// invalid sequence emits a `^X`/`\\xNN` representation, otherwise
6919/// passes the char through. The C source threads an
6920/// `mbstate_t` through `mbrtowc()` and falls back to single-byte
6921/// `\M-` notation on `MB_INVALID`; the Rust port uses Rust's
6922/// chars iterator which already produces valid scalar values, so
6923/// invalid-byte fallback collapses to the control-char branch.
6924/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream, outstrp, flags)
6925// Rust idiom replacement: `chars()` + `wcs_nicechar` covers the C
6926// mbrtowc loop with `MB_INVALID` fallback (Rust UTF-8 guarantees
6927// valid scalars, so the invalid-byte arm collapses).
6928/// `mb_niceformat` — see implementation.
6929pub fn mb_niceformat(
6930    s: &str,
6931    mut stream: Option<&mut dyn std::io::Write>,
6932    outstrp: Option<&mut Option<String>>,
6933    flags: i32,
6934) -> usize {
6935    // c:5366
6936    let mut l: usize = 0; // c:5368 size_t l = 0
6937    let mut newl: usize; // c:5368 size_t newl
6938                         // c:5369 — `int umlen, outalloc, outleft, eol = 0;`. outalloc/outleft
6939                         // model C's buffer-growth math; Rust String auto-grows so the realloc
6940                         // loop at c:5430-5440 collapses to push_str. umlen and eol carry.
6941    let mut umlen: usize; // c:5369
6942    let mut eol: bool = false; // c:5369 int eol = 0
6943                               // c:5370 — `wchar_t c;` (carried inside loop in Rust)
6944                               // c:5371 — `char *ums, *ptr, *fmt, *outstr, *outptr;`
6945    let mut ums: Vec<u8>; // c:5371 char *ums
6946    let mut ptr: usize; // c:5371 char *ptr
6947    let mut fmt: String; // c:5371 char *fmt
6948    let mut outstr: Option<String>; // c:5371 char *outstr
6949                                    // c:5372 — `mbstate_t mbs;` (Rust UTF-8 has no shift state; tracked
6950                                    // by the `valid_up_to` / `error_len` logic below).
6951
6952    // c:5374-5380 — `if (outstrp) outptr = outstr = zalloc(5*strlen(s));`
6953    if outstrp.is_some() {
6954        // c:5374
6955        outstr = Some(String::with_capacity(5 * s.len())); // c:5376
6956    } else {
6957        // c:5377
6958        outstr = None; // c:5379 outstr = NULL
6959    }
6960
6961    // c:5382 — `ums = ztrdup(s);`
6962    // c:5383-5387 — comment-only block carried verbatim:
6963    /*
6964     * is this necessary at this point? niceztrlen does this
6965     * but it's used in lots of places.  however, one day this may
6966     * be, too.
6967     */                                                                      // c:5383-5387
6968    // c:5388 — `untokenize(ums);` — Rust port uses the char-based
6969    // `lex::untokenize` (only fires on codepoints in the 0x84..=0xa1
6970    // ITOK range, never on UTF-8 continuation bytes); the byte-level
6971    // shape of C's untokenize is unsafe on raw UTF-8 input because a
6972    // continuation byte like 0x97 (part of `字` = 0xE5 0xAD 0x97) sits
6973    // inside the ITOK range and would be misclassified as a token.
6974    // c:5389 — `ptr = unmetafy(ums, &umlen);` — `unmeta` is the safe
6975    // UTF-8 wrapper that runs unmetafy only when Meta bytes are
6976    // present and otherwise no-ops.
6977    //
6978    // `unmetafy_str` (not `unmeta`) is what produces the raw byte buffer C
6979    // expects here. zshrs metafies at the CHARACTER level — an undecodable byte
6980    // is stored as U+0083 followed by `char::from(byte ^ 32)` — and U+0083
6981    // itself encodes as the two UTF-8 bytes C2 83. `unmeta` scans for a 0x83
6982    // BYTE, so it finds the trailing byte of that pair and mangles the string:
6983    // `$'\M-a'` came back out as `\M-^C\M-A` instead of `\M-a`.
6984    let detok = untokenize(s);
6985    ums = unmetafy_str(&detok);
6986    umlen = ums.len(); // c:5389 *umlen
6987    ptr = 0; // c:5389 ptr starts at 0 in ums
6988
6989    // c:5391 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
6990    while umlen > 0 {
6991        // c:5392
6992        // c:5393 — `cnt = eol ? MB_INVALID : mbrtowc(&c, ptr, umlen, &mbs);`
6993        // Rust equivalent: try to decode one UTF-8 scalar from ums[ptr..],
6994        // honoring `eol` (force the invalid arm when set).
6995        let cnt: usize;
6996        let decoded_c: Option<char>;
6997        if eol {
6998            // c:5396 — MB_INVALID arm via `eol = 1`.
6999            decoded_c = None;
7000            cnt = 1;
7001        } else {
7002            let remaining = &ums[ptr..ptr + umlen];
7003            match std::str::from_utf8(remaining) {
7004                Ok(s_slice) => {
7005                    // Valid UTF-8 throughout remaining; consume one char.
7006                    if let Some(ch) = s_slice.chars().next() {
7007                        decoded_c = Some(ch);
7008                        cnt = ch.len_utf8();
7009                    } else {
7010                        // Empty? Shouldn't happen given umlen > 0, but
7011                        // mirror MB_INVALID.
7012                        decoded_c = None;
7013                        cnt = 1;
7014                    }
7015                }
7016                Err(e) => {
7017                    let valid_up_to = e.valid_up_to();
7018                    if valid_up_to > 0 {
7019                        // Decode the first valid char then continue.
7020                        let valid_slice =
7021                            unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
7022                        let ch = valid_slice.chars().next().unwrap();
7023                        decoded_c = Some(ch);
7024                        cnt = ch.len_utf8();
7025                    } else if e.error_len().is_none() {
7026                        // Incomplete sequence at end of input — MB_INCOMPLETE.
7027                        // c:5394 — `case MB_INCOMPLETE: eol = 1; FALL THROUGH`
7028                        eol = true;
7029                        decoded_c = None; // MB_INVALID
7030                        cnt = 1;
7031                    } else {
7032                        // Definite invalid byte — MB_INVALID.
7033                        decoded_c = None;
7034                        cnt = 1;
7035                    }
7036                }
7037            }
7038        }
7039
7040        // c:5396-5403 / c:5404-5424 switch dispatch on cnt/decoded_c.
7041        let cnt_used: usize;
7042        match decoded_c {
7043            None => {
7044                // c:5397 — `case MB_INVALID:` (or fall-through from
7045                // MB_INCOMPLETE at c:5394).
7046                // c:5400 — `fmt = nicechar_sel(*ptr, flags & NICEFLAG_QUOTE);`
7047                fmt = nicechar_sel(ums[ptr] as char, (flags & NICEFLAG_QUOTE) != 0);
7048                newl = fmt.len(); // c:5401 newl = strlen(fmt)
7049                cnt_used = 1; // c:5402 cnt = 1
7050                              // c:5403 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless)
7051            }
7052            Some(c) if c == '\0' => {
7053                // c:5406 — `case 0:` — '\0' decodes to 0; consume 1 byte
7054                // and fall through to default.
7055                cnt_used = 1; // c:5409
7056                              // c:5411-5421 default arm (FALL THROUGH from case 0)
7057                if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
7058                    // c:5413
7059                    fmt = "\\'".to_string(); // c:5414
7060                    newl = 2; // c:5415
7061                } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
7062                    // c:5417
7063                    fmt = "\\\\".to_string(); // c:5418
7064                    newl = 2; // c:5419
7065                } else {
7066                    // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
7067                    //                                 flags & NICEFLAG_QUOTE);`
7068                    let mut width: usize = 0;
7069                    fmt =
7070                        wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
7071                    newl = width;
7072                }
7073            }
7074            Some(c) => {
7075                // c:5411 — `default:` arm (cnt > 0, valid char).
7076                cnt_used = cnt;
7077                if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
7078                    // c:5413
7079                    fmt = "\\'".to_string(); // c:5414
7080                    newl = 2; // c:5415
7081                } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
7082                    // c:5417
7083                    fmt = "\\\\".to_string(); // c:5418
7084                    newl = 2; // c:5419
7085                } else {
7086                    // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
7087                    //                                 flags & NICEFLAG_QUOTE);`
7088                    let mut width: usize = 0;
7089                    fmt =
7090                        wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
7091                    newl = width;
7092                }
7093            }
7094        }
7095
7096        umlen -= cnt_used; // c:5427 umlen -= cnt
7097        ptr += cnt_used; // c:5428 ptr += cnt
7098        l += newl; // c:5429 l += newl
7099
7100        if let Some(ref mut w) = stream {
7101            // c:5431 if (stream)
7102            // c:5432 — `zputs(fmt, stream);`
7103            let _ = w.write_all(fmt.as_bytes());
7104        }
7105        if let Some(ref mut buf) = outstr {
7106            // c:5433 if (outstr)
7107            // c:5434-5446 — append fmt to outstr, growing on demand. Rust
7108            // String auto-grows; the realloc loop collapses to push_str.
7109            buf.push_str(&fmt); // c:5446 memcpy(outptr, fmt, outlen)
7110        }
7111        let _ = fmt;
7112    }
7113
7114    // c:5451 — `free(ums);` (Rust drop at scope exit)
7115    drop(ums);
7116    if let Some(slot) = outstrp {
7117        // c:5452 if (outstrp)
7118        // c:5453 — `*outptr = '\0';` (no-op for Rust String)
7119        // c:5455-5460 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping. Rust has
7120        // a single allocator so all three paths produce identical owned
7121        // String contents; transfer ownership into caller's slot.
7122        *slot = outstr.take();
7123    }
7124
7125    l // c:5462 return l
7126}
7127
7128/// Port of `is_mb_niceformat(const char *s)` from `Src/utils.c:5474`.
7129///
7130/// Predicate: would any character in `s` need representation by
7131/// `mb_niceformat` / `nicedup`? C body:
7132/// ```c
7133/// ums = ztrdup(s);
7134/// untokenize(ums);
7135/// ptr = unmetafy(ums, &umlen);
7136/// while (umlen > 0) {
7137///     cnt = mbrtowc(&c, ptr, umlen, &mbs);
7138///     switch (cnt) {
7139///     case MB_INCOMPLETE: case MB_INVALID:
7140///         if (is_nicechar(*ptr)) { ret = 1; break; }
7141///         cnt = 1;
7142///         memset(&mbs, 0, sizeof mbs);
7143///         break;
7144///     case 0: cnt = 1;  /* FALLTHROUGH */
7145///     default:
7146///         if (is_wcs_nicechar(c)) ret = 1;
7147///         break;
7148///     }
7149///     if (ret) break;
7150///     umlen -= cnt; ptr += cnt;
7151/// }
7152/// ```
7153///
7154/// Rust port: unmetafy in place via [`unmetafy`], then walk the
7155/// resulting bytes. For valid UTF-8 sequences, check
7156/// `is_wcs_nicechar(scalar)`; for invalid bytes, check
7157/// `is_nicechar(byte)`. Either path bailing positive returns true.
7158pub fn is_mb_niceformat(s: &str) -> i32 {
7159    // c:5474
7160    // c:5476 — `int umlen, ret = 0;`
7161    let umlen: usize; // c:5476
7162    let mut ret: i32 = 0; // c:5476
7163                          // c:5477 — `char *ums, *ptr;` (eptr modelled by bytes.len())
7164    let mut ums: Vec<u8>; // c:5477
7165    let mut ptr: usize; // c:5477
7166
7167    // c:5481-5483 — `ums = ztrdup(s); untokenize(ums); ptr =
7168    //                unmetafy(ums, &umlen);`. Use BYTE-level `unmetafy_str`,
7169    // not char-level `unmeta`: `unmeta` decodes a metafied eight-bit byte
7170    // (Meta 0x83 + 0xC1 for `$'\M-a'`) into the valid Rust char U+00E1
7171    // (`á`), which the UTF-8 scan below reads as a PRINTABLE Latin-1 letter
7172    // → is_mb_niceformat=0 → `(q+)`/quotedzputs left the raw byte unquoted
7173    // (`\341`) where zsh emits `$'\M-a'`. `unmetafy_str` preserves the raw
7174    // 0xE1 byte (C's `unmetafy`), so the MB_INVALID arm's is_nicechar(0xE1)
7175    // fires (high-bit → nice) exactly as C does.
7176    let detok = untokenize(s);
7177    ums = unmetafy_str(&detok);
7178    umlen = ums.len(); // c:5483 *umlen
7179    ptr = 0; // c:5483 ptr starts at 0
7180
7181    // c:5485 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
7182    while ret == 0 && ptr < ums.len() {
7183        // c:5486 while (umlen > 0)
7184        let remaining = &ums[ptr..];
7185        match std::str::from_utf8(remaining) {
7186            Ok(s_slice) => {
7187                // c:5503-5511 — `default: if (is_wcs_nicechar(c)) ret = 1;`
7188                for ch in s_slice.chars() {
7189                    if is_wcs_nicechar(ch) {
7190                        // c:5508
7191                        ret = 1; // c:5509
7192                        break;
7193                    }
7194                }
7195                break;
7196            }
7197            Err(e) => {
7198                let valid_up_to = e.valid_up_to();
7199                if valid_up_to > 0 {
7200                    let valid = unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
7201                    for ch in valid.chars() {
7202                        if is_wcs_nicechar(ch) {
7203                            // c:5508
7204                            ret = 1; // c:5509
7205                            break;
7206                        }
7207                    }
7208                    if ret != 0 {
7209                        break;
7210                    }
7211                    ptr += valid_up_to;
7212                    continue;
7213                }
7214                // c:5493-5498 — `case MB_INVALID: if (is_nicechar(*ptr))
7215                //                ret = 1; break;`
7216                if is_nicechar(remaining[0] as char) {
7217                    // c:5494
7218                    ret = 1; // c:5495
7219                    break;
7220                }
7221                ptr += 1;
7222            }
7223        }
7224    }
7225
7226    drop(ums); // c:5519 free(ums)
7227    ret // c:5523 return ret
7228}
7229
7230/// Multibyte metachar length with conversion (from utils.c mb_metacharlenconv_r)
7231/// Port of `mb_metacharlenconv_r(const char *s, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5548`.
7232/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, wcp, mbsp)
7233// Rust idiom replacement: Rust strings are UTF-8 with valid scalar
7234// values, so `chars().next()` covers the mbrtowc state machine; no
7235// mbstate_t threading required.
7236/// `mb_metacharlenconv_r` — see implementation.
7237pub fn mb_metacharlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
7238    if let Some(c) = s[pos..].chars().next() {
7239        (c.len_utf8(), Some(c))
7240    } else {
7241        (0, None)
7242    }
7243}
7244
7245/// Multibyte-aware metafied-string char advance.
7246/// Port of `mb_metacharlenconv(const char *s, wint_t *wcp)` from Src/utils.c:5611. Returns
7247/// `(bytes_consumed, scalar_char)` for the next char in `s`. C
7248/// source dispatches to `mb_metacharlenconv_r()` for true
7249/// multibyte; we use Rust's UTF-8 char iterator which already
7250/// handles multi-byte correctly. ASCII fast-path: `Meta+X` is 2
7251/// bytes consumed → `(2, X^32)`; bare ASCII is `(1, c)`.
7252/// WARNING: param names don't match C — Rust=(s) vs C=(s, wcp)
7253pub fn mb_metacharlenconv(s: &str) -> (usize, Option<char>) {
7254    let bytes = s.as_bytes();
7255    if bytes.is_empty() {
7256        return (0, None);
7257    }
7258    if bytes[0] == 0x83 && bytes.len() >= 2 {
7259        // Meta+X pair → unescape.
7260        let raw = bytes[1] as u32 ^ 32;
7261        return (2, char::from_u32(raw));
7262    }
7263    if bytes[0] <= 0x7f {
7264        return (1, Some(bytes[0] as char));
7265    }
7266    // Multi-byte UTF-8 — let Rust decode.
7267    if let Some(c) = s.chars().next() {
7268        return (c.len_utf8(), Some(c));
7269    }
7270    (1, None)
7271}
7272
7273/// Multibyte metastring length to end (from utils.c mb_metastrlenend)
7274/// Port of `mb_metastrlenend(char *ptr, int width, char *eptr)` from `Src/utils.c:5655`.
7275///
7276/// Counts characters (`width == false`) or display columns
7277/// (`width == true`) in a metafied string. C walks the metafied
7278/// byte stream, un-metafies inline (`if (*ptr == Meta) inchar =
7279/// *++ptr ^ 32`, c:5672), and feeds one byte at a time to
7280/// `mbrtowc` to assemble each multibyte character (c:5691); a
7281/// complete character counts 1 (or its `WCWIDTH`), an invalid byte
7282/// counts as a single character (c:5712-5714).
7283///
7284/// Rust-port note: zshrs stores both metafied byte-pairs (`Meta` +
7285/// `byte ^ 32`, produced by `getkeystring` for `$'\xNN'` escapes)
7286/// AND real multibyte `char`s (the literal `$'é'` path) in one
7287/// `String`, whereas C only ever holds metafied bytes. So the port
7288/// first reduces the slice to its raw byte stream via `unmetafy_str`
7289/// (metafied pair → its byte; real `char` → its UTF-8 bytes — the
7290/// same bytes C's inline demetafy would yield), then runs C's
7291/// `mbrtowc` character-assembly loop over those bytes. Rust's UTF-8
7292/// validation is the `mbrtowc` analogue: the shortest valid prefix
7293/// is one character; a byte that begins no valid sequence counts as
7294/// one (c:5712).
7295pub fn mb_metastrlenend(ptr: &str, width: bool, eptr: usize) -> usize {
7296    // c:5672 — un-metafy the (optionally end-bounded) slice to raw bytes.
7297    let bytes = unmetafy_str(&ptr[..eptr.min(ptr.len())]);
7298    let mut num: usize = 0; // c:5658 size_t ret / counter
7299    let mut i: usize = 0;
7300    while i < bytes.len() {
7301        // c:5678-5687 — 7-bit US-ASCII subset: one character, skip mbrtowc.
7302        if bytes[i] <= 0x7f {
7303            num += 1;
7304            i += 1;
7305            continue;
7306        }
7307        // c:5691 — mbrtowc: take the shortest valid UTF-8 character.
7308        let mut num_in_char: usize = 1; // c:5701 trailing-octet span
7309        let hi = (i + 4).min(bytes.len());
7310        for end in (i + 1)..=hi {
7311            if std::str::from_utf8(&bytes[i..end]).is_ok() {
7312                num_in_char = end - i;
7313                break;
7314            }
7315        }
7316        if width {
7317            // c:5717-5723 — WCWIDTH of the assembled character (≥0).
7318            let wcw = std::str::from_utf8(&bytes[i..i + num_in_char])
7319                .ok()
7320                .and_then(|s| s.chars().next())
7321                .and_then(unicode_width::UnicodeWidthChar::width)
7322                .unwrap_or(0);
7323            num += wcw;
7324        } else {
7325            // c:5727 — one character (complete, or invalid treated as one).
7326            num += 1;
7327        }
7328        i += num_in_char;
7329    }
7330    num
7331}
7332
7333/// Multibyte char length with conversion (from utils.c mb_charlenconv_r)
7334/// Port of `mb_charlenconv_r(const char *s, int slen, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5747`.
7335/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp, mbsp)
7336pub fn mb_charlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
7337    mb_metacharlenconv_r(s, pos)
7338}
7339
7340/// Multibyte char length (from utils.c mb_charlenconv)
7341/// Port of `mb_charlenconv(const char *s, int slen, wint_t *wcp)` from `Src/utils.c:5793`.
7342/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp)
7343pub fn mb_charlenconv(s: &str, pos: usize) -> usize {
7344    s[pos..].chars().next().map(|c| c.len_utf8()).unwrap_or(0)
7345}
7346
7347/// Port of `int metacharlenconv(const char *x, int *c)` from `Src/utils.c:5810-5826`.
7348/// ```c
7349/// if (*x == Meta) {
7350///     if (c) *c = x[1] ^ 32;
7351///     return 2;
7352/// }
7353/// if (c) *c = (char)*x;
7354/// return 1;
7355/// ```
7356/// Single-byte metafied char advance — Meta+X is 2 bytes (decode
7357/// via XOR 32), plain byte is 1 byte. Previously used hardcoded
7358/// `0x83`; now routes through the canonical `Meta` const for
7359/// maintainability parity.
7360/// WARNING: param names don't match C — Rust=(s) vs C=(x, c)
7361pub fn metacharlenconv(s: &str) -> (usize, Option<char>) {
7362    // c:5811
7363    let bytes = s.as_bytes();
7364    if bytes.is_empty() {
7365        return (0, None);
7366    }
7367    if bytes[0] == Meta && bytes.len() >= 2 {
7368        // c:5818
7369        let raw = bytes[1] as u32 ^ 32; // c:5820
7370        return (2, char::from_u32(raw)); // c:5821
7371    }
7372    (1, Some(bytes[0] as char)) // c:5823-5825
7373}
7374
7375/// Plain (non-metafy) char advance.
7376/// Port of `charlenconv(const char *x, int len, int *c)` from Src/utils.c:5832 — the
7377/// non-MULTIBYTE_SUPPORT branch. Single-byte read with
7378/// `len`-bound check; matches the C source's `if (!len)` early
7379/// exit.
7380/// WARNING: param names don't match C — Rust=(s, len) vs C=(x, len, c)
7381pub fn charlenconv(s: &str, len: usize) -> (usize, Option<char>) {
7382    if len == 0 {
7383        return (0, None);
7384    }
7385    let bytes = s.as_bytes();
7386    if bytes.is_empty() {
7387        return (0, None);
7388    }
7389    (1, Some(bytes[0] as char))
7390}
7391
7392/// Port of `size_t sb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from `Src/utils.c:5849-5910`.
7393/// ```c
7394/// ums = ztrdup(s); untokenize(ums); ptr = unmetafy(ums, &umlen);
7395/// while (ptr < eptr) {
7396///     int c = (unsigned char) *ptr;
7397///     if      (c == '\''  && (flags & NICEFLAG_QUOTE)) fmt = "\\'";
7398///     else if (c == '\\'  && (flags & NICEFLAG_QUOTE)) fmt = "\\\\";
7399///     else                                              fmt = nicechar_sel(c, ...);
7400/// }
7401/// ```
7402/// Single-byte nice format. **Unmetafies the input first** (c:5872),
7403/// then calls `nicechar_sel` for EVERY byte (not just controls).
7404///
7405/// Param mapping (Rule S1, faithful to C):
7406/// - `s: &str`            ← C `const char *s`
7407/// - `stream: Option<&mut dyn Write>` ← C `FILE *stream` (`None` ≡ `NULL`)
7408/// - `outstrp: Option<&mut Option<String>>` ← C `char **outstrp`
7409///   (outer `None` ≡ `NULL`; inner `Option<String>` is the storage slot
7410///   the caller passes as `&local`, mirroring `char *retstr;
7411///   sb_niceformat(s, NULL, &retstr, ...);`).
7412/// - returns `usize` ← C `size_t l`.
7413pub fn sb_niceformat(
7414    s: &str,
7415    mut stream: Option<&mut dyn std::io::Write>,
7416    outstrp: Option<&mut Option<String>>,
7417    flags: i32,
7418) -> usize {
7419    // c:5851
7420    let mut l: usize = 0; // c:5853 size_t l = 0
7421    let mut newl: usize; // c:5853 size_t newl
7422                         // c:5854 — `int umlen, outalloc, outleft;`. outalloc/outleft model
7423                         // C's buffer-growth math; Rust String auto-grows so the realloc
7424                         // loop at c:5897-5906 collapses to push_str. umlen carries through.
7425    let umlen: usize; // c:5854
7426                      // c:5855 — `char *ums, *ptr, *eptr, *fmt, *outstr, *outptr;`
7427    let mut ums: Vec<u8>; // c:5855 char *ums
7428    let mut ptr: usize; // c:5855 char *ptr
7429    let eptr: usize; // c:5855 char *eptr
7430    let mut fmt: String; // c:5855 char *fmt
7431    let mut outstr: Option<String>; // c:5855 char *outstr
7432
7433    // c:5857-5863 — `if (outstrp) outptr = outstr = zalloc(2*strlen(s));`
7434    if outstrp.is_some() {
7435        // c:5857
7436        outstr = Some(String::with_capacity(2 * s.len())); // c:5859
7437    } else {
7438        // c:5860
7439        outstr = None; // c:5862 outstr = NULL
7440    }
7441
7442    // c:5865 — `ums = ztrdup(s);`
7443    ums = s.as_bytes().to_vec();
7444    // c:5866-5870 — comment-only block carried verbatim:
7445    /*
7446     * is this necessary at this point? niceztrlen does this
7447     * but it's used in lots of places.  however, one day this may
7448     * be, too.
7449     */                                                                      // c:5866-5870
7450    // c:5871 — `untokenize(ums);` inlined byte-level (lex::untokenize
7451    // is char-based; C does byte-walks). Mirrors `Src/exec.c:2077-2099`
7452    // exec.c untokenize().
7453    let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; // ZTOKENS — Src/lex.c:38
7454    let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
7455    for &c in &ums {
7456        // exec.c:2082
7457        if (0x84u8..=0xa1u8).contains(&c) {
7458            // exec.c:2083 itok(c)
7459            if c != 0xa1u8 {
7460                // exec.c:2086 c != Nularg
7461                let idx = (c - 0x84) as usize;
7462                if idx < ztokens_table.len() {
7463                    detok.push(ztokens_table[idx]);
7464                }
7465            }
7466        } else {
7467            detok.push(c); // exec.c:2094
7468        }
7469    }
7470    ums = detok;
7471    // c:5872 — `ptr = unmetafy(ums, &umlen);`
7472    umlen = unmetafy(&mut ums);
7473    ums.truncate(umlen);
7474    eptr = umlen; // c:5873 eptr = ptr + umlen
7475    ptr = 0; // c:5872 ptr starts at 0 in ums
7476
7477    while ptr < eptr {
7478        // c:5875
7479        let c: i32 = ums[ptr] as i32; // c:5876 int c = (unsigned char) *ptr
7480        if c == b'\'' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
7481            // c:5877
7482            fmt = "\\'".to_string(); // c:5878
7483            newl = 2; // c:5879
7484        } else if c == b'\\' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
7485            // c:5881
7486            fmt = "\\\\".to_string(); // c:5882
7487            newl = 2; // c:5883
7488        } else {
7489            // c:5885
7490            fmt = nicechar_sel(c as u8 as char, (flags & NICEFLAG_QUOTE) != 0); // c:5886
7491            newl = 1; // c:5887
7492        }
7493
7494        ptr += 1; // c:5890 ++ptr
7495        l += newl; // c:5891 l += newl
7496
7497        if let Some(ref mut w) = stream {
7498            // c:5893 if (stream)
7499            // c:5894 — `zputs(fmt, stream);`
7500            let _ = w.write_all(fmt.as_bytes());
7501        }
7502        if let Some(ref mut buf) = outstr {
7503            // c:5895 if (outstr)
7504            // c:5896-5912 — append fmt to outstr, growing on demand. Rust
7505            // String auto-grows; the realloc loop at c:5897-5906 collapses
7506            // to push_str (memcpy + outptr/outleft bookkeeping unneeded).
7507            buf.push_str(&fmt); // c:5907 memcpy(outptr, fmt, outlen)
7508        }
7509        // `fmt` is consumed at next iter assignment (C reuses pointer).
7510        let _ = fmt;
7511    }
7512
7513    // c:5915 — `free(ums);` (Rust drop at scope exit)
7514    drop(ums);
7515    if let Some(slot) = outstrp {
7516        // c:5916 if (outstrp)
7517        // c:5917 — `*outptr = '\0';` (no-op for String — push_str leaves
7518        // no embedded NUL, and Rust String is length-prefixed).
7519        // c:5919-5925 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping: in C this
7520        // selects between ztrdup (perm) / dupstring (heap arena) / direct
7521        // ownership-transfer (NODUP). Rust has a single allocator so all
7522        // three paths produce identical owned String contents; transfer
7523        // ownership of `outstr` into the caller's slot.
7524        *slot = outstr.take();
7525    }
7526
7527    l // c:5928 return l
7528}
7529
7530/// Port of `int is_sb_niceformat(const char *s)` from `Src/utils.c:5937-5959`.
7531///
7532/// Predicate: would `sb_niceformat` change the input? Walks each byte
7533/// after unmetafy, returns `1` if any byte is "nice" per `is_nicechar`,
7534/// else `0` (C `int` return faithful to Rule S1).
7535pub fn is_sb_niceformat(s: &str) -> i32 {
7536    // c:5937
7537    // c:5939 — `int umlen, ret = 0;`
7538    let umlen: usize; // c:5939
7539    let mut ret: i32 = 0; // c:5939
7540                          // c:5940 — `char *ums, *ptr, *eptr;`
7541    let mut ums: Vec<u8>; // c:5940 char *ums
7542    let mut ptr: usize; // c:5940 char *ptr
7543    let eptr: usize; // c:5940 char *eptr
7544
7545    ums = s.as_bytes().to_vec(); // c:5942 ums = ztrdup(s)
7546                                 // c:5943 — `untokenize(ums);` inlined byte-level (lex::untokenize is
7547                                 // char-based; C does byte-walks). Mirrors `Src/exec.c:2077-2099`.
7548    let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; // ZTOKENS — Src/lex.c:38
7549    let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
7550    for &c in &ums {
7551        // exec.c:2082
7552        if (0x84u8..=0xa1u8).contains(&c) {
7553            // exec.c:2083 itok(c)
7554            if c != 0xa1u8 {
7555                // exec.c:2086 c != Nularg
7556                let idx = (c - 0x84) as usize;
7557                if idx < ztokens_table.len() {
7558                    detok.push(ztokens_table[idx]);
7559                }
7560            }
7561        } else {
7562            detok.push(c); // exec.c:2094
7563        }
7564    }
7565    ums = detok;
7566    umlen = unmetafy(&mut ums); // c:5944 ptr = unmetafy(ums, &umlen)
7567    ums.truncate(umlen);
7568    eptr = umlen; // c:5945 eptr = ptr + umlen
7569    ptr = 0; // c:5944 ptr starts at 0 in ums
7570
7571    while ptr < eptr {
7572        // c:5947
7573        if is_nicechar(ums[ptr] as char) {
7574            // c:5948 is_nicechar(*ptr)
7575            ret = 1; // c:5949
7576            break; // c:5950
7577        }
7578        ptr += 1; // c:5952 ++ptr
7579    }
7580
7581    drop(ums); // c:5955 free(ums)
7582    ret // c:5957 return ret
7583}
7584
7585/// 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.
7586/// Writes `s` into `out` with TAB characters expanded to spaces against
7587/// a tabstop of `width`. `startpos` carries the cumulative emitted
7588/// column from previous calls (used by `print -X` which preserves
7589/// alignment across args). When `all_tabs` is false, only leading TABs
7590/// (those at the start of a line) are expanded; embedded TABs are
7591/// emitted verbatim and `startpos` is advanced by one tabstop. When
7592/// `all_tabs` is true, every TAB expands. Returns the new `startpos`.
7593pub(crate) fn zexpandtabs(
7594    s: &str,
7595    width: i32,
7596    startpos: i32,
7597    all_tabs: bool,
7598    out: &mut String,
7599) -> i32 {
7600    let mut startpos = startpos;
7601    let mut at_start = true;
7602    for c in s.chars() {
7603        if c == '\t' {
7604            if all_tabs || at_start {
7605                if width <= 0 || startpos % width == 0 {
7606                    out.push(' ');
7607                    startpos += 1;
7608                }
7609                if width > 0 {
7610                    while startpos % width != 0 {
7611                        out.push(' ');
7612                        startpos += 1;
7613                    }
7614                }
7615            } else {
7616                let rem = startpos % width;
7617                startpos += width - rem;
7618                out.push('\t');
7619            }
7620            continue;
7621        } else if c == '\n' || c == '\r' {
7622            out.push(c);
7623            startpos = 0;
7624            at_start = true;
7625            continue;
7626        }
7627        at_start = false;
7628        out.push(c);
7629        startpos += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as i32;
7630    }
7631    startpos
7632}
7633
7634/// Port of `int hasspecial(char const *s)` from `Src/utils.c:6072-6082`.
7635/// ```c
7636/// while (*s) {
7637///     if (ispecial(*s == Meta ? *++s ^ 32 : *s)) return 1;
7638///     s++;
7639/// }
7640/// return 0;
7641/// ```
7642/// Predicate: does `s` contain any byte that needs shell-quoting?
7643/// Routes through the canonical typtab-driven `ztype_h::ispecial`
7644/// which respects the `ZTF_SP_COMMA` (set by `makecommaspecial`)
7645/// and `ZTF_BANGCHAR` (BANGHIST + interact) flags. Previously used
7646/// a hardcoded char list — diverged from C for `,` (KSH_GLOB
7647/// extended-glob) and for the dynamic bangchar (`!` by default but
7648/// user-rewritable via `$HISTCHARS`).
7649pub fn hasspecial(s: &str) -> bool {
7650    // c:6072
7651    let bytes = s.as_bytes();
7652    let mut i = 0;
7653    while i < bytes.len() {
7654        let c = if bytes[i] == Meta && i + 1 < bytes.len() {
7655            // c:6075 — `*s == Meta ? *++s ^ 32 : *s`.
7656            let v = bytes[i + 1] ^ 32;
7657            i += 2;
7658            v
7659        } else {
7660            let v = bytes[i];
7661            i += 1;
7662            v
7663        };
7664        if crate::ported::ztype_h::ispecial(c) {
7665            // c:6075
7666            return true;
7667        }
7668    }
7669    false
7670}
7671
7672/// Port of `static char *addunprintable(char *v, const char *u, const char *uend)`
7673/// from `Src/utils.c:6082-6124`. Renders unprintable bytes using
7674/// **shell-compatible C-string escapes**:
7675///   - `\0` (NUL, with `\000` form if next byte is octal-digit)
7676///   - `\a` (0x07 BEL), `\b` (BS), `\f` (FF), `\n` (LF),
7677///     `\r` (CR), `\t` (TAB), `\v` (VT)
7678///   - `\nnn` 3-digit octal fallback for any other control byte
7679///
7680/// Previously the Rust port emitted ZLE-style caret notation (`^X`,
7681/// `\u{:04x}`) — completely different convention. C semantics target
7682/// `printf %q` / `$'...'` reuse where the output must round-trip
7683/// through `printf %b` to reconstruct the original byte. Caret
7684/// notation is what ZLE *displays* but not what `addunprintable`
7685/// emits.
7686/// WARNING: param names don't match C — Rust=(c) vs C=(v, u, uend)
7687pub fn addunprintable(c: char) -> String {
7688    // c:6082
7689    let b = c as u32 & 0xff;
7690    match b as u8 {
7691        // c:6097-6103 — `\0`. C peeks next byte and uses `\000` form
7692        // when followed by an octal digit. Rust port works on a single
7693        // char so emits just `\0`; callers needing the disambiguation
7694        // form can pass the lookahead byte separately.
7695        0x00 => "\\0".to_string(),
7696        0x07 => "\\a".to_string(), // c:6106
7697        0x08 => "\\b".to_string(), // c:6107
7698        0x0c => "\\f".to_string(), // c:6108
7699        0x0a => "\\n".to_string(), // c:6109
7700        0x0d => "\\r".to_string(), // c:6110
7701        0x09 => "\\t".to_string(), // c:6111
7702        0x0b => "\\v".to_string(), // c:6112
7703        // c:6114-6119 — `\nnn` 3-digit octal default.
7704        _ => format!("\\{:o}{:o}{:o}", (b >> 6) & 7, (b >> 3) & 7, b & 7),
7705    }
7706}
7707
7708/// One element of a metafied string, as C's `MB_METACHARLENCONV` loop sees it:
7709/// either a decoded character, or a raw byte that is not one.
7710///
7711/// A byte that cannot appear in a Rust `String` (an invalid-UTF-8 byte such as
7712/// the 0xE1 produced by `$'\M-a'`) is stored metafied — `Meta` (0x83) followed
7713/// by `byte ^ 32` — so a plain `.chars()` walk sees two innocuous characters
7714/// where the shell means one raw byte, and passes them straight through. C hits
7715/// exactly this case as `WEOF` out of `MB_METACHARLENCONV` (c:6210) and routes
7716/// it to `addunprintable()`.
7717#[derive(Clone, Copy, PartialEq)]
7718enum MetaChar {
7719    Ch(char),
7720    Raw(u8),
7721}
7722
7723/// Quote a string according to the specified type
7724/// Port from zsh/Src/utils.c quotestring() (lines 6141-6452)
7725/// Quote a string per the requested bslashquote style.
7726/// Port of `quotestring(const char *s, int instring)` from Src/utils.c — used by `print
7727/// -%q`, `${(q)var}`, completion-output escaping, history
7728/// re-emission.
7729/// WARNING: param names don't match C — Rust=(s, quote_type) vs C=(s, instring)
7730pub fn quotestring(s: &str, quote_type: i32) -> String {
7731    // c:6207-6210 — C walks the string with MB_METACHARINIT/MB_METACHARLENCONV,
7732    // which yields one decoded character at a time, or WEOF for a byte that does
7733    // not decode. These three closures are that walk, kept inline exactly as C
7734    // keeps it inline in each arm below.
7735    //
7736    // `meta_chars` is the decoder: a byte that cannot live in a Rust `String`
7737    // (an invalid-UTF-8 byte such as the 0xE1 from `$'\M-a'`) is stored metafied
7738    // — Meta (0x83) then `byte ^ 32` — so a naive `.chars()` walk sees two
7739    // innocuous characters where the shell means one raw byte and passes them
7740    // straight through. That is C's WEOF case, and it must reach addunprintable.
7741    let meta_chars = |s: &str| -> Vec<MetaChar> {
7742        let mut out = Vec::with_capacity(s.len());
7743        let mut it = s.chars().peekable();
7744        while let Some(c) = it.next() {
7745            if c as u32 == Meta as u32 {
7746                if let Some(&n) = it.peek() {
7747                    let nu = n as u32;
7748                    if (0x80..=0xff).contains(&nu) {
7749                        it.next();
7750                        out.push(MetaChar::Raw((nu as u8) ^ 32));
7751                        continue;
7752                    }
7753                }
7754            }
7755            out.push(MetaChar::Ch(c));
7756        }
7757        out
7758    };
7759    // c:6212 — `cc != WEOF && WC_ISPRINT(cc)`. A raw byte is never printable: it
7760    // IS the WEOF case.
7761    let mc_printable = |mc: MetaChar| -> bool {
7762        match mc {
7763            MetaChar::Ch(c) => !c.is_control(),
7764            MetaChar::Raw(_) => false,
7765        }
7766    };
7767    // c:6082-6124 addunprintable — emit a non-printable element BYTE by byte, so
7768    // a non-printable multibyte character becomes one escape per UTF-8 byte.
7769    // `\0` widens to `\000` when an octal digit follows so the escape cannot
7770    // swallow it (c:6097-6103). There is deliberately no `\e` case: C has none,
7771    // so ESC comes out as the octal `\033`.
7772    let push_unprintable = |out: &mut String, mc: MetaChar, next: Option<MetaChar>| {
7773        let mut buf = [0u8; 4];
7774        let bytes: &[u8] = match mc {
7775            MetaChar::Ch(c) => c.encode_utf8(&mut buf).as_bytes(),
7776            MetaChar::Raw(b) => {
7777                buf[0] = b;
7778                &buf[0..1]
7779            }
7780        };
7781        for i in 0..bytes.len() {
7782            let b = bytes[i];
7783            if b == 0 {
7784                out.push_str("\\0");
7785                let follows = if i + 1 < bytes.len() {
7786                    Some(bytes[i + 1])
7787                } else {
7788                    next.map(|n| match n {
7789                        MetaChar::Ch(c) => {
7790                            let mut nb = [0u8; 4];
7791                            c.encode_utf8(&mut nb).as_bytes()[0]
7792                        }
7793                        MetaChar::Raw(rb) => rb,
7794                    })
7795                };
7796                if matches!(follows, Some(b'0'..=b'7')) {
7797                    out.push_str("00");
7798                }
7799            } else {
7800                out.push_str(&addunprintable(b as char));
7801            }
7802        }
7803    };
7804
7805    // c:6141
7806    if s.is_empty() {
7807        return if quote_type == QT_NONE {
7808            String::new()
7809        } else if quote_type == QT_BACKSLASH {
7810            // c:6194 `if (!*s && shownull)` — shownull is 0 for plain
7811            // QT_BACKSLASH, so an empty string produces NO quotes.
7812            String::new()
7813        } else if quote_type == QT_BACKSLASH_SHOWNULL {
7814            // c:6165 sets shownull=1, so empty → '' (c:6194 adds the pair).
7815            "''".to_string()
7816        } else if quote_type == QT_SINGLE || quote_type == QT_SINGLE_OPTIONAL {
7817            "''".to_string()
7818        } else if quote_type == QT_DOUBLE {
7819            "\"\"".to_string()
7820        } else if quote_type == QT_DOLLARS {
7821            "$''".to_string()
7822        } else {
7823            String::new()
7824        };
7825    }
7826
7827    if quote_type == QT_NONE {
7828        s.to_string()
7829    } else if quote_type == QT_BACKSLASH_PATTERN {
7830        // Only bslashquote pattern characters (lines 6242-6247)
7831        let mut result = String::with_capacity(s.len() * 2);
7832        for c in s.chars() {
7833            if matches!(
7834                c,
7835                '*' | '?' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '#' | '^' | '~'
7836            ) {
7837                result.push('\\');
7838            }
7839            result.push(c);
7840        }
7841        result
7842    } else if quote_type == QT_BACKSLASH || quote_type == QT_BACKSLASH_SHOWNULL {
7843        // c:Src/utils.c:6260-6452 QT_BACKSLASH. Three sub-cases per
7844        // input char (after the special-syntax handling above):
7845        //   - `\n` → emit `$'\n'` literal 5 bytes (c:6366-6371)
7846        //   - special-and-printable → emit `\<char>` (c:6385-6395)
7847        //   - non-printable (ctrl, multibyte outside printable
7848        //     range) → emit `$'<addunprintable>'` (c:6412-6422)
7849        // Bug #144/#149 in docs/BUGS.md: previous port mapped ALL
7850        // ispecial chars to `\<char>`, so `\n`/`\t`/etc. came out
7851        // as `\<actual-byte>` instead of `$'\n'`/`$'\t'`.
7852        //
7853        // c:6301-6306 — `=` and `~` are NOT escaped unless one of:
7854        //   (B) the char is at position 0 (u == s)
7855        //   (C) MAGICEQUALSUBST is set AND the previous byte is `=`
7856        //       or `:` (covers `var==val`, `var:=val`)
7857        //   (D) the char is `~` AND EXTENDEDGLOB is set
7858        // Without these conditions, mid-word `=`/`~` falls through
7859        // to the literal-emit path (c:6418-6434).
7860        let mut result = String::with_capacity(s.len() * 2);
7861        let mut prev: char = '\0'; // would-be u[-1]
7862        let mcs = meta_chars(s);
7863        for i in 0..mcs.len() {
7864            let mc = mcs[i];
7865            let c = match mc {
7866                MetaChar::Ch(c) => c,
7867                // A raw byte is never `\n` and never ispecial — it is the
7868                // not-printable case below. Stand in with its byte value so the
7869                // `=`/`~` lookbehind still advances correctly.
7870                MetaChar::Raw(b) => b as char,
7871            };
7872            // c:6301-6306 gate for `=`/`~` (condition A/B/C/D).
7873            let eq_tilde_gate = if c == '=' || c == '~' {
7874                i == 0 // c:6303 u == s
7875                    || (isset(MAGICEQUALSUBST) && (prev == '=' || prev == ':')) // c:6304-6305
7876                    || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
7877            } else {
7878                true // c:6302 *u != '=' && *u != '~'
7879            };
7880            if mc == MetaChar::Ch('\n') {
7881                // c:6366-6371 — newline gets dedicated `$'\n'` form.
7882                result.push_str("$'\\n'");
7883            } else if matches!(mc, MetaChar::Ch(_))
7884                && ispecial(c)
7885                && eq_tilde_gate
7886                && c.is_ascii()
7887                && !c.is_ascii_control()
7888            {
7889                // c:6385-6395 — printable special → `\<char>`.
7890                result.push('\\');
7891                result.push(c);
7892            } else if !mc_printable(mc) {
7893                // c:6412-6422 — anything not printable (a control char, a
7894                // non-printable multibyte char, or a raw undecodable byte) is
7895                // wrapped one element at a time as `$'<addunprintable>'`.
7896                result.push_str("$'");
7897                push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
7898                result.push('\'');
7899            } else {
7900                result.push(c);
7901            }
7902            prev = c;
7903        }
7904        result
7905    } else if quote_type == QT_SINGLE {
7906        // c:Src/utils.c:6300-6395 QT_SINGLE — `(qq)` flag.
7907        //   The outer ispecial-gated branch at c:6301-6313 only
7908        //   triggers special handling when the QT-specific
7909        //   subcondition matches; for QT_SINGLE that's `*u == '\''`.
7910        //   Newlines do NOT match, so they fall through to the
7911        //   pass-through path at c:6394 and are emitted LITERALLY
7912        //   inside the surrounding `'…'`. Bug #199 in docs/BUGS.md:
7913        //   previous port emitted `'$'\n''` (split-quote with
7914        //   `$'\n'` escape between segments) for newline, diverging
7915        //   from zsh's literal-newline-in-single-quotes output.
7916        let mut result = String::with_capacity(s.len() + 4);
7917        result.push('\'');
7918        for c in s.chars() {
7919            if c == '\'' {
7920                // c:6364-6379 — apostrophe close-reopen sequence.
7921                result.push_str("'\\''");
7922            } else {
7923                // Newline + everything else: pass through literally.
7924                result.push(c);
7925            }
7926        }
7927        result.push('\'');
7928        result
7929    } else if quote_type == QT_SINGLE_OPTIONAL {
7930        // c:Src/utils.c:6314-6385 QT_SINGLE_OPTIONAL — minimum
7931        // quoting. Walks the string with two states:
7932        //   quotesub=1 — not currently inside a quote span. Bare
7933        //     apostrophes get `\'` (backslash form). Any OTHER
7934        //     special char triggers back-filling: insert `'` at
7935        //     `quotestart` (start of unquoted prefix), shifting
7936        //     subsequent chars right, then push char, transition
7937        //     to quotesub=2.
7938        //   quotesub=2 — currently inside a `'…'` span. Bare
7939        //     apostrophes break the span: push `'\\'`, transition
7940        //     back to quotesub=1 with quotestart=position-after.
7941        //     Other specials append in-place.
7942        // End: if quotesub=2, close with `'`.
7943        // For "hello world" this yields `'hello world'` (back-
7944        // filled at start). For "it's" it yields `it\'s` (no quote
7945        // span ever opens). The naive per-char approach without
7946        // back-filling produced `hello' 'world` — parity bug.
7947        //
7948        // c:6301-6306 — the SAME `=`/`~` gate the QT_BACKSLASH arm applies.
7949        // `ispecial` alone would open a quote span on any mid-word `=` or `~`,
7950        // so `a=b` came out as `'a=b'` where zsh leaves it bare.
7951        let chars: Vec<char> = s.chars().collect();
7952        let forces_quote = |i: usize, c: char| -> bool {
7953            if !ispecial(c) {
7954                return false; // c:6301
7955            }
7956            if c == '=' || c == '~' {
7957                i == 0 // c:6303 u == s
7958                    || (isset(MAGICEQUALSUBST) && (chars[i - 1] == '=' || chars[i - 1] == ':')) // c:6304-6305
7959                    || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
7960            } else {
7961                true // c:6302
7962            }
7963        };
7964        let needs_quoting = chars.iter().enumerate().any(|(i, &c)| forces_quote(i, c));
7965        if !needs_quoting {
7966            return s.to_string();
7967        }
7968        let mut result: Vec<char> = Vec::with_capacity(s.len() + 4);
7969        let mut quotestart: usize = 0; // index in `result` where the next `'` would go
7970        let mut quotesub: u8 = 1; // 1 = not quoting, 2 = inside `'…'`
7971        for (i, &c) in chars.iter().enumerate() {
7972            if c == '\'' {
7973                if quotesub == 2 {
7974                    // close current quote span, then `\'`, then
7975                    // mark that we may need to reopen on next special
7976                    result.push('\'');
7977                    result.push('\\');
7978                    result.push('\'');
7979                    quotesub = 1;
7980                    quotestart = result.len();
7981                } else {
7982                    result.push('\\');
7983                    result.push('\'');
7984                    quotestart = result.len();
7985                }
7986            } else if forces_quote(i, c) {
7987                if quotesub == 1 {
7988                    // Back-fill: insert `'` at quotestart, shifting
7989                    // everything after right by 1.
7990                    result.insert(quotestart, '\'');
7991                    quotesub = 2;
7992                }
7993                result.push(c);
7994            } else {
7995                result.push(c);
7996            }
7997        }
7998        if quotesub == 2 {
7999            result.push('\'');
8000        }
8001        result.into_iter().collect()
8002    } else if quote_type == QT_DOUBLE {
8003        // Double quote: "string" (lines 6272-6280, 6311-6312)
8004        let mut result = String::with_capacity(s.len() + 4);
8005        result.push('"');
8006        for c in s.chars() {
8007            if matches!(c, '$' | '`' | '"' | '\\') {
8008                result.push('\\');
8009            }
8010            result.push(c);
8011        }
8012        result.push('"');
8013        result
8014    } else if quote_type == QT_DOLLARS {
8015        // c:6203-6241 — `$'…'` quoting. Each element is either printable, in
8016        // which case it is emitted verbatim (backslashing `\`, `'`, and — under
8017        // BANGHIST — the history character), or it is NOT, in which case C hands
8018        // its raw bytes to addunprintable(): a named escape (`\t`, `\n`, `\a`, …)
8019        // or a 3-digit octal one.
8020        //
8021        // Two things this must NOT do, both of which the previous port did:
8022        //   * pass an undecodable byte through raw. `$'\M-a'` is the byte 0xE1,
8023        //     which is not valid UTF-8 and is stored metafied; walking `.chars()`
8024        //     re-emitted it verbatim instead of `\341`, so the output could not
8025        //     be re-parsed.
8026        //   * emit `\e` for ESC. C's addunprintable has no `\e` case — ESC comes
8027        //     out as the octal `\033`.
8028        let mut result = String::with_capacity(s.len() + 4);
8029        result.push_str("$'");
8030        let mcs = meta_chars(s);
8031        let bang = crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
8032        for i in 0..mcs.len() {
8033            let mc = mcs[i];
8034            if let (true, MetaChar::Ch(c)) = (mc_printable(mc), mc) {
8035                // c:6214-6224
8036                if c == '\\' || c == '\'' || (isset(BANGHIST) && bang != 0 && c as u32 == bang as u32)
8037                {
8038                    result.push('\\');
8039                }
8040                result.push(c);
8041            } else {
8042                // c:6226-6229
8043                push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
8044            }
8045        }
8046        result.push('\'');
8047        result
8048    } else if quote_type == QT_BACKTICK {
8049        // Backtick quoting (minimal - just escape backticks)
8050        s.replace('`', "\\`")
8051    } else {
8052        // Unknown quote_type — treat as no-op to match C's `default:` arm.
8053        s.to_string()
8054    }
8055}
8056
8057// ===========================================================
8058// xtrace helpers moved from src/ported/vm_helper.
8059// printprompt4 is a direct port of utils.c:1718-1735; quotedzputs
8060// is its argument-formatter companion (zsh formats `set -x` lines
8061// via the same utils.c path).
8062// ===========================================================
8063
8064/// Port of `quotedzputs(char const *s, FILE *stream)` from `Src/utils.c:6464`.
8065///
8066/// Quote a string for re-readable output (`set -x`, `typeset -p`,
8067/// `set` listing, etc.). zsh's algorithm under MULTIBYTE_SUPPORT
8068/// (c:6464-6543):
8069///   1. empty input → `''`                                     (c:6470-6475)
8070///   2. needs nice-format (controls / non-printables) → emit
8071///      `$'<mb_niceformat output>'`                            (c:6478-6492)
8072///   3. no SPECCHARS member → return string unchanged          (c:6511-6517)
8073///   4. otherwise wrap in `'…'` (Bourne) or RCQUOTES form,
8074///      with embedded `'` rewritten as `'\''` / `''`           (c:6533-6587)
8075///
8076/// Previous Rust port omitted step 2 (the `$'…'` branch).
8077/// Result: strings containing control bytes (`\n`, `\t`, escape
8078/// sequences) were single-quoted instead of `$'…'`-quoted,
8079/// breaking round-trip through `typeset -p` / `set` / `set -x`
8080/// because POSIX single-quotes are *strong* — embedded `\n` would
8081/// be re-fed as a literal newline rather than the C-escape.
8082///
8083/// The C signature is `char *quotedzputs(char const *s, FILE *stream)`
8084/// — when `stream` is non-NULL it writes there and returns NULL,
8085/// otherwise it returns the quoted string. Rust's variant covers
8086/// only the `stream==NULL` form (the `set -x` callers all want the
8087/// string back, not direct stdout writing). The `stream==Some` form
8088/// is fputs/fputc-direct in C; Rust callers that need writer-based
8089/// output should compose `print!`/`write!` with this fn's return.
8090/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream)
8091pub(crate) fn quotedzputs(s: &str) -> String {
8092    // c:6464
8093    // c:6469-6475 — `if (!*s)` empty string emits `''` literal.
8094    if s.is_empty() {
8095        return "''".to_string(); // c:6472
8096    }
8097    // c:6477-6508 — `is_mb_niceformat(s)` / `is_sb_niceformat(s)` arm:
8098    // if the string contains nice-formatted chars (controls,
8099    // non-printables), wrap in `$'…'` using sb/mb_niceformat with
8100    // NICEFLAG_QUOTE so embedded `'`/`\` get backslash-escaped.
8101    if is_mb_niceformat(s) != 0 {
8102        // c:6478-6492 (MULTIBYTE_SUPPORT branch): use mb_niceformat
8103        //   with NICEFLAG_QUOTE so multi-byte chars round-trip through
8104        //   wcs_nicechar (raw UTF-8 for printable wides, `\u`/`\U` for
8105        //   large codepoints) AND `'`/`\\` get backslash-escaped.
8106        // Under !MULTIBYTE_SUPPORT (c:6494-6508) C would use
8107        //   sb_niceformat instead. Static-link path: MULTIBYTE is
8108        //   always available in Rust.
8109        // c:6485-6492 — `mb_niceformat(s, NULL, &substr,
8110        //                              NICEFLAG_QUOTE|NICEFLAG_NODUP);`
8111        let mut substr: Option<String> = None;
8112        let _ = mb_niceformat(s, None, Some(&mut substr), NICEFLAG_QUOTE | NICEFLAG_NODUP);
8113        return format!("$'{}'", substr.unwrap_or_default()); // c:6488
8114    }
8115    // c:6511-6518 — `if (!hasspecial(s)) return dupstring(s);`.
8116    if !hasspecial(s) {
8117        // c:6511
8118        return s.to_string(); // c:6516
8119    }
8120    // c:6520-6529 — outstr buffer alloc (zhalloc). Rust uses growable
8121    // String; the C `l = strlen(s) + 2 + (per-' overhead)` size hint
8122    // is just allocation tuning and doesn't affect output.
8123    let mut out = String::with_capacity(s.len() + 2);
8124    let bytes = s.as_bytes();
8125    let csh_junkie = isset(CSHJUNKIEQUOTES); // c:6554 / c:6612
8126
8127    if isset(RCQUOTES) {
8128        // c:6533 — RCQUOTES: wrap entire string in `'…'`; each
8129        // embedded `'` becomes `''` (the rc-style doubled quote).
8130        out.push('\''); // c:6539
8131                        // c:6540-6547 — C walks METAFIED bytes; the Rust String is
8132                        // UTF-8, so the faithful transposition walks CHARS (a byte
8133                        // walk Latin-1-casts every multibyte char: em-dash E2 80 94
8134                        // became "â" + a token byte that downstream passes ate).
8135                        // Token chars (Dash U+009B, Meta U+0083) are codepoints here.
8136        let chars_v: Vec<char> = s.chars().collect();
8137        let mut i = 0;
8138        while i < chars_v.len() {
8139            let c = if chars_v[i] == Dash {
8140                // c:6541 — `if (*s == Dash) c = '-';`
8141                i += 1;
8142                '-'
8143            } else if chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len() {
8144                // c:6543 — `else if (*s == Meta) c = *++s ^ 32;`
8145                let n = chars_v[i + 1] as u32;
8146                i += 2;
8147                char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
8148            } else {
8149                // c:6546 — `else c = *s;`
8150                let dec = chars_v[i];
8151                i += 1;
8152                dec
8153            };
8154            if c == '\'' {
8155                // c:6548-6553 — `if (c == '\'') *ptr++ = '\'';` (the
8156                // rc-quote doubling).
8157                out.push('\''); // c:6553
8158            } else if c == '\n' && csh_junkie {
8159                // c:6554-6560 — `if (c == '\n' && isset(CSHJUNKIEQUOTES))
8160                //                 *ptr++ = '\\';`
8161                out.push('\\'); // c:6559
8162            }
8163            // c:6561-6570 — emit c (metafy on imeta).
8164            out.push(c); // c:6569 (non-stream branch always re-metafies;
8165                         // Rust String holds decoded chars directly)
8166        }
8167        out.push('\''); // c:6576
8168    } else {
8169        // c:6578-6637 — Bourne-style quoting, "avoiding empty quoted
8170        // strings". Tracks `inquote` so that `it's` becomes
8171        // `'it'\''s'` (no empty `''` runs).
8172        let mut inquote = false; // c:6466 (initialised at top of C fn)
8173                                 // c:6579-6586 — char-wise decode, same transposition as the
8174                                 // RCQUOTES arm above (C's byte walk is over metafied bytes).
8175        let chars_v: Vec<char> = s.chars().collect();
8176        let mut i = 0;
8177        while i < chars_v.len() {
8178            let c = if chars_v[i] == Dash {
8179                i += 1;
8180                '-' // c:6581
8181            } else if chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len() {
8182                let n = chars_v[i + 1] as u32; // c:6583
8183                i += 2;
8184                char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
8185            } else {
8186                let dec = chars_v[i]; // c:6585
8187                i += 1;
8188                dec
8189            };
8190            if c == '\'' {
8191                // c:6587-6602 — `'` closes any open inquote then emits
8192                // `\'` outside the quotes.
8193                if inquote {
8194                    out.push('\''); // c:6593
8195                    inquote = false; // c:6594
8196                }
8197                out.push('\\'); // c:6600
8198                out.push('\''); // c:6601
8199            } else {
8200                // c:6603-6629 — other chars open a quote run if not
8201                // already open, optionally backslash-escape `\n` under
8202                // CSHJUNKIEQUOTES, then emit the byte.
8203                if !inquote {
8204                    out.push('\''); // c:6609
8205                    inquote = true; // c:6610
8206                }
8207                if c == '\n' && csh_junkie {
8208                    out.push('\\'); // c:6617
8209                }
8210                out.push(c); // c:6627 (imeta-encoding handled by Rust
8211                             // String storage in the non-stream form)
8212            }
8213        }
8214        if inquote {
8215            out.push('\''); // c:6636
8216        }
8217    }
8218    // c:6639-6640 — `if (!stream) *ptr++ = '\0';` — Rust String already
8219    // NUL-terminated implicitly; no-op.
8220    out // c:6642
8221}
8222
8223/// Port of `char *dquotedztrdup(char const *s)` from `Src/utils.c:6648-6723`.
8224/// Two arms (selected by `isset(CSHJUNKIEQUOTES)` at c:6655):
8225///
8226/// **CSHJUNKIEQUOTES path** (c:6656-6686): the csh-junk-quote style
8227/// where only the non-special sections are wrapped in `"..."` and
8228/// special chars (`"`, `$`, `` ` ``) appear OUTSIDE the quotes with
8229/// backslash escape. `\n` inside the quotes gets an extra `\` so it
8230/// round-trips through history.
8231///
8232/// **Default path** (c:6687-6719): wraps the whole string in `"..."`.
8233/// `\` is doubled to `\\`. `"`, `$`, `` ` `` get backslash-escaped.
8234/// A trailing `\` gets an extra `\` appended (the `pending` quirk).
8235///
8236/// Previously the Rust port only implemented the default arm; the
8237/// CSHJUNKIEQUOTES path is now ported faithfully.
8238pub fn dquotedztrdup(s: &str) -> String {
8239    // c:6648
8240    let mut out = String::with_capacity(s.len() * 4 + 2);
8241    let bytes = s.as_bytes();
8242    // c:6655 — `if (isset(CSHJUNKIEQUOTES))`.
8243    if isset(CSHJUNKIEQUOTES) {
8244        let mut inquote = false;
8245        let mut i = 0;
8246        while i < bytes.len() {
8247            // c:6661-6662 — Meta byte decode.
8248            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
8249                i += 2;
8250                (bytes[i - 1] ^ 32) as char
8251            } else {
8252                i += 1;
8253                bytes[i - 1] as char
8254            };
8255            match c {
8256                // c:6664-6673 — `"` / `$` / `` ` `` — close quote
8257                // (if open), then `\<c>`.
8258                '"' | '$' | '`' => {
8259                    if inquote {
8260                        out.push('"');
8261                        inquote = false;
8262                    }
8263                    out.push('\\');
8264                    out.push(c);
8265                }
8266                // c:6674-6682 — default arm: open quote if needed,
8267                // backslash-escape newline, emit char.
8268                _ => {
8269                    if !inquote {
8270                        out.push('"');
8271                        inquote = true;
8272                    }
8273                    if c == '\n' {
8274                        out.push('\\');
8275                    }
8276                    out.push(c);
8277                }
8278            }
8279        }
8280        // c:6685-6686 — close trailing quote.
8281        if inquote {
8282            out.push('"');
8283        }
8284    } else {
8285        // c:6687-6718 — default (non-CSH) arm.
8286        out.push('"');
8287        let mut pending = false;
8288        let mut i = 0;
8289        while i < bytes.len() {
8290            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
8291                i += 2;
8292                (bytes[i - 1] ^ 32) as char
8293            } else {
8294                i += 1;
8295                bytes[i - 1] as char
8296            };
8297            match c {
8298                '\\' => {
8299                    if pending {
8300                        out.push('\\');
8301                    }
8302                    out.push('\\');
8303                    pending = true;
8304                }
8305                '"' | '$' | '`' => {
8306                    if pending {
8307                        out.push('\\');
8308                    }
8309                    out.push('\\');
8310                    out.push(c);
8311                    pending = false;
8312                }
8313                other => {
8314                    out.push(other);
8315                    pending = false;
8316                }
8317            }
8318        }
8319        if pending {
8320            out.push('\\');
8321        }
8322        out.push('"');
8323    }
8324    // c:6720 — `ret = metafy(buf, p - buf, META_DUP);` re-metafy result.
8325    metafy(&out)
8326}
8327
8328/// Port of `dquotedzputs(char const *s, FILE *stream)` from
8329/// Src/utils.c:6729. C body (4 lines):
8330///     `char *d = dquotedztrdup(s);
8331///      int ret = zputs(d, stream);
8332///      zsfree(d);
8333///      return ret;`
8334/// Rust returns the quoted string directly (callers compose via
8335/// `format!` rather than streaming through a FILE*), so the zputs
8336/// call drops.
8337pub fn dquotedzputs(s: &str) -> String {
8338    // c:6729
8339    dquotedztrdup(s) // c:6731
8340}
8341
8342/// Convert UCS-4 to UTF-8 (from utils.c ucs4toutf8)
8343/// Port of `ucs4toutf8(char *dest, unsigned int wval)` from `Src/utils.c:6743`.
8344///
8345/// C accepts 0..=0x7FFFFFFF (legacy UCS-4 / 6-byte UTF-8) — wider
8346/// than Unicode's 0..=0x10FFFF — to match `wctomb(3)` on Linux
8347/// with UTF-8 locale. The Rust `char::from_u32` path would reject
8348/// codepoints above 0x10FFFF (and surrogates), so this port mirrors
8349/// the C bit-pattern dispatch directly.
8350///
8351/// C body (c:6743-6779):
8352/// ```c
8353/// if (wval < 0x80) len = 1;            // ASCII
8354/// else if (wval < 0x800) len = 2;
8355/// else if (wval < 0x10000) len = 3;
8356/// else if (wval < 0x200000) len = 4;
8357/// else if (wval < 0x4000000) len = 5;
8358/// else if (wval < 0x80000000) len = 6;
8359/// else { zerr("character not in range"); return -1; }
8360///
8361/// switch (len) { /* falls through except to the last case */
8362/// case 6: dest[5] = (wval & 0x3f) | 0x80; wval >>= 6;
8363/// case 5: dest[4] = (wval & 0x3f) | 0x80; wval >>= 6;
8364/// case 4: dest[3] = (wval & 0x3f) | 0x80; wval >>= 6;
8365/// case 3: dest[2] = (wval & 0x3f) | 0x80; wval >>= 6;
8366/// case 2: dest[1] = (wval & 0x3f) | 0x80; wval >>= 6;
8367///     *dest = wval | ((0xfc << (6 - len)) & 0xfc);
8368///     break;
8369/// case 1: *dest = wval;
8370/// }
8371/// return len;
8372/// ```
8373///
8374/// Returns the encoded bytes (1..=6 long) on success, None on
8375/// out-of-range. C's `zerr("character not in range")` (c:6763) is
8376/// not emitted here — the return signals the error.
8377/// WARNING: param names don't match C — Rust=(wval) vs C=(dest, wval)
8378pub fn ucs4toutf8(wval: u32) -> Option<String> {
8379    // c:6743
8380    let len: usize = if wval < 0x80 {
8381        1
8382    }
8383    // c:6750
8384    else if wval < 0x800 {
8385        2
8386    }
8387    // c:6752
8388    else if wval < 0x10000 {
8389        3
8390    }
8391    // c:6754
8392    else if wval < 0x200000 {
8393        4
8394    }
8395    // c:6756
8396    else if wval < 0x4000000 {
8397        5
8398    }
8399    // c:6758
8400    else if wval < 0x80000000 {
8401        6
8402    }
8403    // c:6760
8404    else {
8405        // c:6762
8406        zerr("character not in range"); // c:6763
8407        return None; // c:6764
8408    };
8409    let mut buf = [0u8; 6];
8410    let mut w = wval;
8411    // c:6767-6776 — fall-through switch building trailing bytes first.
8412    match len {
8413        1 => {
8414            buf[0] = w as u8;
8415        } // c:6775
8416        n => {
8417            // Trailing (len-1) bytes: each is `(w & 0x3f) | 0x80`,
8418            // shifting w right 6 between them (c:6768-6772).
8419            for i in (1..n).rev() {
8420                buf[i] = ((w & 0x3f) as u8) | 0x80;
8421                w >>= 6;
8422            }
8423            // Leading byte: `w | ((0xfc << (6 - len)) & 0xfc)` (c:6773).
8424            buf[0] = (w as u8) | (((0xfcu32 << (6 - n)) & 0xfc) as u8);
8425        }
8426    }
8427    Some(String::from_utf8_lossy(&buf[..len]).into_owned())
8428}
8429
8430/// Port of `ucs4tomb(unsigned int wval, char *buf)` from `Src/utils.c:6788`.
8431///
8432/// Encode a UCS-4 codepoint into the buffer `buf` using the current
8433/// locale's multibyte encoding. Returns the number of bytes written,
8434/// or -1 on conversion failure. C body uses `wctomb(3)` when
8435/// `__STDC_ISO_10646__` is defined (which it is on every modern
8436/// glibc / macOS libc), falls back to UTF-8 if the codeset is
8437/// `"UTF-8"`, and uses `iconv(3)` otherwise.
8438///
8439/// This Rust port mirrors the primary `wctomb` path via libc FFI;
8440/// the iconv fallback is unused on macOS/Linux modern builds.
8441/// On conversion failure, emits `zerr("character not in range")`
8442/// to match C source line 6794.
8443///
8444/// C body shape:
8445/// ```c
8446/// int count = wctomb(buf, (wchar_t)wval);
8447/// if (count == -1) zerr("character not in range");
8448/// return count;
8449/// ```
8450pub fn ucs4tomb(wval: u32, buf: &mut [u8]) -> i32 {
8451    // libc::wctomb requires at least MB_CUR_MAX bytes (typically 4
8452    // for UTF-8, 6 for some encodings). Use a stack buffer first,
8453    // then copy into the caller's buffer.
8454    // libc crate doesn't expose wctomb on all platforms; declare
8455    // the POSIX prototype directly. wchar_t is i32 on macOS/Linux
8456    // for our supported targets.
8457    extern "C" {
8458        fn wctomb(s: *mut libc::c_char, wc: libc::wchar_t) -> libc::c_int;
8459    }
8460    // libc::c_char is i8 on most targets but u8 on aarch64-linux. Use c_char
8461    // so the wctomb arg pointer type matches per-target without a cast.
8462    let mut local = [0 as libc::c_char; 16];
8463    let count = unsafe { wctomb(local.as_mut_ptr(), wval as libc::wchar_t) };
8464    if count < 0 {
8465        zerr("character not in range");
8466        return -1;
8467    }
8468    let n = count as usize;
8469    if n > buf.len() {
8470        zerr("character not in range");
8471        return -1;
8472    }
8473    for i in 0..n {
8474        buf[i] = local[i] as u8;
8475    }
8476    count
8477}
8478
8479/// Parse getkeystring escape sequences (from utils.c getkeystring)
8480/// Handles \n \t \r \e \a \b \f \v \\ \' \" \xNN \uNNNN \UNNNNNNNN \0NNN
8481/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from `Src/utils.c:6915`.
8482/// WARNING: param names don't match C — Rust=(s) vs C=(s, len, how, misc)
8483pub fn getkeystring(s: &str) -> (String, usize) {
8484    // c:6915
8485    let mut result = String::new();
8486    let mut chars = s.chars().peekable();
8487    let mut consumed = 0;
8488
8489    while let Some(c) = chars.next() {
8490        consumed += c.len_utf8();
8491        if c != '\\' {
8492            result.push(c);
8493            continue;
8494        }
8495        match chars.next() {
8496            Some('n') => {
8497                result.push('\n');
8498                consumed += 1;
8499            }
8500            Some('t') => {
8501                result.push('\t');
8502                consumed += 1;
8503            }
8504            Some('r') => {
8505                result.push('\r');
8506                consumed += 1;
8507            }
8508            Some('e') | Some('E') => {
8509                result.push('\x1b');
8510                consumed += 1;
8511            }
8512            Some('a') => {
8513                result.push('\x07');
8514                consumed += 1;
8515            }
8516            Some('b') => {
8517                result.push('\x08');
8518                consumed += 1;
8519            }
8520            Some('f') => {
8521                result.push('\x0c');
8522                consumed += 1;
8523            }
8524            Some('v') => {
8525                result.push('\x0b');
8526                consumed += 1;
8527            }
8528            Some('\\') => {
8529                result.push('\\');
8530                consumed += 1;
8531            }
8532            Some('\'') => {
8533                result.push('\'');
8534                consumed += 1;
8535            }
8536            Some('"') => {
8537                result.push('"');
8538                consumed += 1;
8539            }
8540            Some('x') => {
8541                consumed += 1;
8542                let mut hex = String::new();
8543                for _ in 0..2 {
8544                    if let Some(&c) = chars.peek() {
8545                        if c.is_ascii_hexdigit() {
8546                            hex.push(chars.next().unwrap());
8547                            consumed += 1;
8548                        } else {
8549                            break;
8550                        }
8551                    }
8552                }
8553                // c:Src/utils.c:7169-7170 — `\x` ALWAYS runs `*t++ =
8554                // zstrtol(s+1, &s, 16)`, even with no hex digit. zstrtol
8555                // over a non-hex byte parses zero digits and returns 0, so
8556                // a NUL byte is emitted (the offending char is processed
8557                // next). `$'\xg'` → NUL + 'g'; `$'\x'` → NUL. Previously
8558                // `from_str_radix("")` errored and nothing was pushed.
8559                let val: u8 = if hex.is_empty() {
8560                    0 // c:7170 zstrtol on empty reads as 0
8561                } else {
8562                    u8::from_str_radix(&hex, 16).unwrap_or(0)
8563                };
8564                // c:Src/utils.c — `\xNN` is one raw BYTE; metafied
8565                // per c:7289-7294 (vm_helper::meta_encode_byte) so
8566                // the String stays valid UTF-8. Bug #127.
8567                {
8568                    // c:Src/utils.c metafy byte-encode step:
8569                    // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8570                    let b_ = val;
8571                    if b_ < 0x80 {
8572                        result.push(b_ as char);
8573                    } else {
8574                        result.push('\u{83}');
8575                        result.push(char::from(b_ ^ 32));
8576                    }
8577                }
8578            }
8579            Some('u') => {
8580                consumed += 1;
8581                let mut hex = String::new();
8582                for _ in 0..4 {
8583                    if let Some(&c) = chars.peek() {
8584                        if c.is_ascii_hexdigit() {
8585                            hex.push(chars.next().unwrap());
8586                            consumed += 1;
8587                        } else {
8588                            break;
8589                        }
8590                    }
8591                }
8592                if let Ok(val) = u32::from_str_radix(&hex, 16) {
8593                    if let Some(c) = char::from_u32(val) {
8594                        result.push(c);
8595                    }
8596                }
8597            }
8598            Some('U') => {
8599                consumed += 1;
8600                let mut hex = String::new();
8601                for _ in 0..8 {
8602                    if let Some(&c) = chars.peek() {
8603                        if c.is_ascii_hexdigit() {
8604                            hex.push(chars.next().unwrap());
8605                            consumed += 1;
8606                        } else {
8607                            break;
8608                        }
8609                    }
8610                }
8611                if let Ok(val) = u32::from_str_radix(&hex, 16) {
8612                    if let Some(c) = char::from_u32(val) {
8613                        result.push(c);
8614                    }
8615                }
8616            }
8617            Some(c @ '0'..='7') => {
8618                consumed += 1;
8619                let mut oct = String::new();
8620                oct.push(c);
8621                for _ in 0..2 {
8622                    if let Some(&c) = chars.peek() {
8623                        if ('0'..='7').contains(&c) {
8624                            oct.push(chars.next().unwrap());
8625                            consumed += 1;
8626                        } else {
8627                            break;
8628                        }
8629                    }
8630                }
8631                if let Ok(val) = u8::from_str_radix(&oct, 8) {
8632                    // c:Src/utils.c — octal escape is one raw BYTE;
8633                    // metafied per c:7289-7294. Bug #127.
8634                    {
8635                        // c:Src/utils.c metafy byte-encode step:
8636                        // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8637                        let b_ = val;
8638                        if b_ < 0x80 {
8639                            result.push(b_ as char);
8640                        } else {
8641                            result.push('\u{83}');
8642                            result.push(char::from(b_ ^ 32));
8643                        }
8644                    }
8645                }
8646            }
8647            Some('c') => {
8648                consumed += 1;
8649                // \cX = control character
8650                if let Some(c) = chars.next() {
8651                    consumed += 1;
8652                    result.push((c as u8 & 0x1f) as char);
8653                }
8654            }
8655            Some(mod_letter @ ('C' | 'M')) => {
8656                // c:Src/utils.c:7029-7052 + c:7265-7275 — `\C` /
8657                // `\M` set control / meta flags; optional `-`
8658                // separator; then read the next char (possibly
8659                // through additional `\C`/`\M` chains) and apply
8660                // the mask: control → `& 0x9f` (or `\C-?` → 0x7f),
8661                // meta → `| 0x80`. Bug #113 in docs/BUGS.md: the
8662                // previous Rust port matched these in the default
8663                // arm and emitted literal `\C` / `\M`. This is the
8664                // second getkeystring (utils.rs); the in-tree
8665                // `getkeystring_dollar_quote` (lex.rs) also needs
8666                // the fix and gets it in the same commit.
8667                consumed += 1;
8668                let mut control = mod_letter == 'C';
8669                let mut meta = mod_letter == 'M';
8670                // Consume optional `-` separator + chained `\C`/`\M`.
8671                loop {
8672                    if chars.peek() == Some(&'-') {
8673                        chars.next();
8674                        consumed += 1;
8675                        continue;
8676                    }
8677                    // chained `\C` / `\M`?
8678                    let mut iter_clone = chars.clone();
8679                    if iter_clone.next() == Some('\\') {
8680                        if let Some(nx) = iter_clone.next() {
8681                            if nx == 'C' || nx == 'M' {
8682                                chars.next(); // consume '\'
8683                                chars.next(); // consume C/M
8684                                consumed += 2;
8685                                if nx == 'C' {
8686                                    control = true;
8687                                } else {
8688                                    meta = true;
8689                                }
8690                                continue;
8691                            }
8692                        }
8693                    }
8694                    break;
8695                }
8696                // Read one base character (allowing nested simple
8697                // escapes for common cases).
8698                let base: Option<char> = if chars.peek() == Some(&'\\') {
8699                    chars.next();
8700                    consumed += 1;
8701                    match chars.next() {
8702                        Some('n') => {
8703                            consumed += 1;
8704                            Some('\n')
8705                        }
8706                        Some('t') => {
8707                            consumed += 1;
8708                            Some('\t')
8709                        }
8710                        Some('r') => {
8711                            consumed += 1;
8712                            Some('\r')
8713                        }
8714                        Some('a') => {
8715                            consumed += 1;
8716                            Some('\x07')
8717                        }
8718                        Some('b') => {
8719                            consumed += 1;
8720                            Some('\x08')
8721                        }
8722                        Some('e') | Some('E') => {
8723                            consumed += 1;
8724                            Some('\x1b')
8725                        }
8726                        Some('f') => {
8727                            consumed += 1;
8728                            Some('\x0c')
8729                        }
8730                        Some('v') => {
8731                            consumed += 1;
8732                            Some('\x0b')
8733                        }
8734                        Some('\\') => {
8735                            consumed += 1;
8736                            Some('\\')
8737                        }
8738                        Some('\'') => {
8739                            consumed += 1;
8740                            Some('\'')
8741                        }
8742                        Some('"') => {
8743                            consumed += 1;
8744                            Some('"')
8745                        }
8746                        Some(other) => {
8747                            consumed += 1;
8748                            Some(other)
8749                        }
8750                        None => None,
8751                    }
8752                } else {
8753                    chars.next().inspect(|c| {
8754                        consumed += c.len_utf8();
8755                    })
8756                };
8757                if let Some(ch) = base {
8758                    let mut byte = ch as u32;
8759                    if control {
8760                        if byte == '?' as u32 {
8761                            byte = 0x7f;
8762                        } else {
8763                            byte &= 0x9f;
8764                        }
8765                    }
8766                    if meta {
8767                        byte |= 0x80;
8768                    }
8769                    // c:Src/utils.c:7265-7275 — masked result is one
8770                    // raw BYTE (`$'\M-i'` = 0xe9), metafied per
8771                    // c:7289-7294. Multibyte base chars (> 0xff)
8772                    // keep the codepoint form. Bug #127.
8773                    if byte <= 0xff {
8774                        {
8775                            // c:Src/utils.c metafy byte-encode step:
8776                            // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8777                            let b_ = byte as u8;
8778                            if b_ < 0x80 {
8779                                result.push(b_ as char);
8780                            } else {
8781                                result.push('\u{83}');
8782                                result.push(char::from(b_ ^ 32));
8783                            }
8784                        }
8785                    } else if let Some(c) = char::from_u32(byte) {
8786                        result.push(c);
8787                    }
8788                }
8789            }
8790            Some(c) if isset(BANGHIST)
8791                && crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst) != 0
8792                && c as u32
8793                    == crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst) as u32 =>
8794            {
8795                // The reverse of quotestring's BANGHIST escape (c:Src/utils.c:6230
8796                // — `\!` is emitted for the history char): unquoting `$'a\!b'`
8797                // must strip the backslash so `${(Q)${(qqqq)v}}` round-trips.
8798                // In zsh this happens because `(Q)` re-lexes the string and
8799                // history expansion consumes the `\<bangchar>`; here the decoder
8800                // consumes it directly.
8801                consumed += 1;
8802                result.push(c);
8803            }
8804            Some(c) => {
8805                consumed += 1;
8806                result.push('\\');
8807                result.push(c);
8808            }
8809            None => {
8810                result.push('\\');
8811            }
8812        }
8813    }
8814    (result, consumed)
8815}
8816
8817/// Check if s is a prefix of t (from utils.c strpfx)
8818// Return non-zero if s is a prefix of t.                                  // c:7345
8819/// `strpfx` — see implementation.
8820pub fn strpfx(s: &str, t: &str) -> bool {
8821    t.starts_with(s)
8822}
8823
8824/// Check if s is a suffix of t (from utils.c strsfx)
8825// Return non-zero if s is a suffix of t.                                  // c:7345
8826/// `strsfx` — see implementation.
8827pub fn strsfx(s: &str, t: &str) -> bool {
8828    t.ends_with(s)
8829}
8830
8831/// Go up n directories (from utils.c upchdir)
8832/// Port of `upchdir(int n)` from `Src/utils.c:7356`.
8833pub fn upchdir(n: usize) -> io::Result<()> {
8834    let mut path = String::new();
8835    for i in 0..n {
8836        if i > 0 {
8837            path.push('/');
8838        }
8839        path.push_str("..");
8840    }
8841    std::env::set_current_dir(&path)?;
8842    Ok(())
8843}
8844
8845/// Port of `struct dirsav` from `Src/zsh.h:1159`.
8846///
8847/// ```c
8848/// struct dirsav {
8849///     int dirfd, level;
8850///     char *dirname;
8851///     dev_t dev;
8852///     ino_t ino;
8853/// };
8854/// ```
8855///
8856/// The previous Rust port omitted `dev` and `ino` which the
8857/// `restoredir` integrity check (utils.c:7592) reads. Adding them
8858/// so callers can verify the saved-and-restored cwd matches the
8859/// captured device + inode.
8860// `struct dirsav` lives in `dirsav` per Rule C
8861// (its C definition is `Src/zsh.h:1159`, not utils.c). The previous
8862// Rust port had a `pub struct DirSav` PascalCase duplicate of the
8863// canonical lowercase struct; deleted in favour of routing through
8864// `zsh_h::dirsav` directly.
8865
8866/// Port of `init_dirsav(Dirsav d)` from `Src/utils.c:7381`. Initialize a
8867/// `dirsav` struct to its empty/default state. C body memset's the
8868/// fields to 0 (dirfd to -1).
8869///
8870/// C signature: `void init_dirsav(Dirsav d)` where
8871/// `Dirsav = struct dirsav *`. Rust port returns the initialised
8872/// struct since callers always pair-with a fresh allocation.
8873/// WARNING: param names don't match C — Rust=() vs C=(path)
8874/// C body (3 lines):
8875///   `d->ino = d->dev = 0; d->dirname = NULL; d->dirfd = d->level = -1;`
8876/// The C `dirname = NULL` becomes `dirname: None`; Rust port prefills
8877/// dirname with current_dir for legacy callers that immediately read
8878/// it (mirrors what `setpwd()` does in C right after `init_dirsav`).
8879pub fn init_dirsav() -> dirsav {
8880    // c:7381
8881    dirsav {
8882        dirfd: -1,
8883        level: 0,
8884        dev: 0,
8885        ino: 0, // c:7383-7385
8886        dirname: std::env::current_dir()
8887            .ok()
8888            .map(|p| p.to_string_lossy().to_string()),
8889    }
8890}
8891
8892/// Port of `lchdir(char const *path, struct dirsav *d, int hard)` from `Src/utils.c:7400`.
8893///
8894/// c:7388 — "Change directory, without following symlinks."  With `hard`
8895/// set, descends `path` one component at a time: `lstat`s each component
8896/// (so a symlink is never followed), `chdir`s into it, then re-`lstat`s
8897/// `.` and compares dev/ino against the component it just stat'd. A
8898/// symlink swapped under us between the `lstat` and the `chdir` is caught
8899/// by the mismatch and the saved directory is restored via `restoredir`.
8900/// `d` (when non-NULL) is filled so the caller can later restore the cwd.
8901///
8902/// Returns 0 on success, -1 on a normal descent failure, -2 if the cwd
8903/// could not even be restored afterwards.
8904///
8905/// zshrs targets macOS/Linux, so the live C arms are HAVE_LSTAT +
8906/// HAVE_FCHDIR; the no-lstat / no-fchdir fallbacks (which never compile
8907/// on our platforms) are elided. C restores `errno = err` before each
8908/// non-zero return so the caller can read the break-reason; that errno
8909/// propagation is elided here (no current caller inspects errno — the
8910/// -1/-2/0 return is the contract).
8911/// WARNING: param names match C — (path, d, hard).
8912#[cfg(unix)]
8913pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
8914    // c:7400
8915    use std::ffi::CString;
8916
8917    let mut ds = init_dirsav(); // c:7405 — local fallback `struct dirsav ds`
8918    let used_local = d.is_none(); // tracks C's `d == &ds`
8919                                  // c:7415-7418 — `if (!d) { init_dirsav(&ds); d = &ds; }`
8920    let d: &mut dirsav = d.unwrap_or(&mut ds);
8921
8922    let bytes = path.as_bytes();
8923    let mut level: i32;
8924
8925    // c:7419-7424 (HAVE_LSTAT arm) —
8926    //   `if ((*path == '/' || !hard) && (d != &ds || hard))`
8927    if (bytes.first() == Some(&b'/') || hard == 0) && (!used_local || hard != 0) {
8928        level = -1; // c:7425
8929    } else {
8930        level = 0; // c:7431
8931                   // c:7432-7435 — record dev/ino of `.` if not already captured.
8932        if d.dev == 0 && d.ino == 0 {
8933            if let Ok(meta) = fs::metadata(".") {
8934                d.dev = meta.dev(); // c:7433
8935                d.ino = meta.ino(); // c:7434
8936            }
8937        }
8938    }
8939
8940    // c:7439-7451 — soft (`!hard`) path: count components, set d->level,
8941    // and delegate to zchdir.
8942    if hard == 0 {
8943        if !used_local {
8944            // c:7443-7447 — count '/'-separated components into `level`.
8945            let mut p = 0usize;
8946            while p < bytes.len() {
8947                while p < bytes.len() && bytes[p] != b'/' {
8948                    p += 1;
8949                }
8950                while p < bytes.len() && bytes[p] == b'/' {
8951                    p += 1;
8952                }
8953                level += 1;
8954            }
8955            d.level = level; // c:7448
8956        }
8957        return zchdir(path); // c:7450
8958    }
8959
8960    // c:7453+ — hard path (HAVE_LSTAT + HAVE_FCHDIR).
8961    let mut close_dir = false; // c:7412
8962                               // c:7455-7460 — save the starting dir via an fd if we don't have one.
8963    if d.dirfd < 0 {
8964        close_dir = true; // c:7456
8965        let dot = CString::new(".").unwrap();
8966        d.dirfd = unsafe { libc::open(dot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:7457
8967        if d.dirfd < 0
8968            && zgetdir(Some(&mut *d)).is_some()
8969            && d.dirname
8970                .as_deref()
8971                .map(|s| !s.starts_with('/'))
8972                .unwrap_or(false)
8973        {
8974            // c:7458-7459 — cwd is relative; fall back to opening "..".
8975            let dotdot = CString::new("..").unwrap();
8976            d.dirfd = unsafe { libc::open(dotdot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
8977        }
8978    }
8979
8980    // c:7462-7464 — absolute path: start the descent from root.
8981    if bytes.first() == Some(&b'/') {
8982        let root = CString::new("/").unwrap();
8983        if unsafe { libc::chdir(root.as_ptr()) } < 0 {
8984            zwarn(&format!(
8985                "failed to chdir(/): {}",
8986                io::Error::last_os_error()
8987            )); // c:7464
8988        }
8989    }
8990
8991    let dot = CString::new(".").unwrap();
8992    let mut pos = 0usize; // index into `path`, replaces C `path`/`pptr`
8993    let mut err: i32 = 0; // c:7408
8994    loop {
8995        // c:7466-7467 — skip leading slashes.
8996        while pos < bytes.len() && bytes[pos] == b'/' {
8997            pos += 1;
8998        }
8999        // c:7468-7480 — end of path reached: success.
9000        if pos >= bytes.len() {
9001            if !used_local {
9002                d.level = level; // c:7472
9003            }
9004            // c:7474-7477 — close the saved-dir fd if we opened it.
9005            if d.dirfd >= 0 && close_dir {
9006                unsafe { libc::close(d.dirfd) };
9007                d.dirfd = -1;
9008            }
9009            return 0; // c:7479
9010        }
9011        // c:7481 — find the end of this path component.
9012        let start = pos;
9013        let mut end = start + 1;
9014        while end < bytes.len() && bytes[end] != b'/' {
9015            end += 1;
9016        }
9017        // c:7482-7485 — component too long.
9018        if end - start > libc::PATH_MAX as usize {
9019            err = libc::ENAMETOOLONG;
9020            break;
9021        }
9022        // c:7486-7488 — copy the component into `buf`.
9023        let comp = &bytes[start..end];
9024        pos = end;
9025        let cbuf = match CString::new(comp) {
9026            Ok(c) => c,
9027            Err(_) => {
9028                err = libc::ENOENT; // embedded NUL — lstat would fail anyway.
9029                break;
9030            }
9031        };
9032        // c:7489-7492 — lstat the component (does NOT follow a symlink).
9033        let mut st1: libc::stat = unsafe { std::mem::zeroed() };
9034        if unsafe { libc::lstat(cbuf.as_ptr(), &mut st1) } != 0 {
9035            err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9036            break;
9037        }
9038        // c:7493-7496 — must be a real directory, not a symlink/other.
9039        if st1.st_mode as u32 & libc::S_IFMT as u32 != libc::S_IFDIR as u32 {
9040            err = libc::ENOTDIR;
9041            break;
9042        }
9043        // c:7497-7500 — chdir into it.
9044        if unsafe { libc::chdir(cbuf.as_ptr()) } != 0 {
9045            err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9046            break;
9047        }
9048        if level >= 0 {
9049            level += 1; // c:7501-7502
9050        }
9051        // c:7503-7510 — re-lstat `.` and verify the dir we landed in is
9052        // the same dev/ino we lstat'd, catching a symlink swap race.
9053        let mut st2: libc::stat = unsafe { std::mem::zeroed() };
9054        if unsafe { libc::lstat(dot.as_ptr(), &mut st2) } != 0 {
9055            err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9056            break;
9057        }
9058        if st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino {
9059            err = libc::ENOTDIR; // c:7508
9060            break;
9061        }
9062    }
9063
9064    // c:7512-7548 — descent failed; restore the saved directory.
9065    if restoredir(&mut *d) != 0 {
9066        let restoreerr = io::Error::last_os_error(); // c:7513
9067                                                     // c:7519-7532 — restore failed too; force cwd to $HOME then "/".
9068        let mut reached = false;
9069        for i in 0..2 {
9070            // c:7521-7527 — destination: $HOME on pass 0, "/" on pass 1.
9071            let cdest = if i != 0 {
9072                "/".to_string()
9073            } else {
9074                match getsparam("HOME") {
9075                    Some(h) => h,     // c:7526
9076                    None => continue, // c:7525 `if (!home) continue;`
9077                }
9078            };
9079            setsparam("PWD", &cdest); // c:7528-7529 `zsfree(pwd); pwd = ztrdup(cdest);`
9080            if let Ok(cd) = CString::new(cdest.as_bytes()) {
9081                if unsafe { libc::chdir(cd.as_ptr()) } == 0 {
9082                    // c:7530
9083                    reached = true;
9084                    break;
9085                }
9086            }
9087        }
9088        if !reached {
9089            // c:7533-7534 — couldn't even reach "/".
9090            zerr(&format!(
9091                "lost current directory, failed to cd to /: {}",
9092                io::Error::last_os_error()
9093            ));
9094        } else {
9095            // c:7535-7537
9096            let pwd = getsparam("PWD").unwrap_or_default();
9097            zerr(&format!(
9098                "lost current directory: {}: changed to `{}'",
9099                restoreerr, pwd
9100            ));
9101        }
9102        // c:7540-7545 — close the saved-dir fd if we opened it.
9103        if d.dirfd >= 0 && close_dir {
9104            unsafe { libc::close(d.dirfd) };
9105            d.dirfd = -1;
9106        }
9107        let _ = err; // c:7546 `errno = err;` propagation elided (see doc).
9108        return -2; // c:7547
9109    }
9110    // c:7549-7558 — descent failed but the saved directory was restored.
9111    if d.dirfd >= 0 && close_dir {
9112        unsafe { libc::close(d.dirfd) };
9113        d.dirfd = -1;
9114    }
9115    let _ = err; // c:7557 `errno = err;` propagation elided (see doc).
9116    -1 // c:7558
9117}
9118
9119/// Non-unix stub: lchdir's symlink-safe descent is built on POSIX
9120/// `lstat`/`fchdir`/`chdir`, which have no Windows equivalent here.
9121#[cfg(not(unix))]
9122pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
9123    let _ = (path, d, hard);
9124    -1
9125}
9126
9127/// Port of `restoredir(struct dirsav *d)` from `Src/utils.c:7565`.
9128///
9129/// ```c
9130/// int restoredir(struct dirsav *d) {
9131///     if (d->dirname && *d->dirname == '/')
9132///         return chdir(d->dirname);
9133///     if (d->dirfd >= 0) {
9134///         if (!fchdir(d->dirfd)) {
9135///             if (!d->dirname) return 0;
9136///             else if (chdir(d->dirname)) {
9137///                 close(d->dirfd); d->dirfd = -1; err = -2;
9138///             }
9139///         } else {
9140///             close(d->dirfd); d->dirfd = err = -1;
9141///         }
9142///     } else if (d->level > 0)
9143///         err = upchdir(d->level);
9144///     else if (d->level < 0) err = -1;
9145///     // dev/ino integrity check ...
9146/// }
9147/// ```
9148///
9149/// Restore the cwd captured in `d`. Absolute `dirname` short-
9150/// circuits to `chdir`. Otherwise tries `fchdir(dirfd)` (when
9151/// supported) then falls through to `upchdir(level)` for the
9152/// nested-fn-exit case. Returns 0 on success, non-zero on failure
9153/// (matching C's int return).
9154///
9155/// Signature change: previous Rust port took `saved: &str` and
9156/// returned `bool` — different shape from C, missed the dirfd /
9157/// level / dev / ino fields entirely.
9158pub fn restoredir(d: &mut dirsav) -> i32 {
9159    // C: if (d->dirname && *d->dirname == '/') return chdir(d->dirname);
9160    if let Some(name) = d.dirname.as_ref() {
9161        if name.starts_with('/') {
9162            return match std::env::set_current_dir(name) {
9163                Ok(_) => 0,
9164                Err(_) => -1,
9165            };
9166        }
9167    }
9168    let mut err: i32 = 0;
9169    // C: HAVE_FCHDIR path — try fchdir(dirfd) first.
9170    #[cfg(unix)]
9171    if d.dirfd >= 0 {
9172        let rc = unsafe { libc::fchdir(d.dirfd) };
9173        if rc == 0 {
9174            if d.dirname.is_none() {
9175                return 0;
9176            }
9177            let name = d.dirname.as_ref().unwrap();
9178            if std::env::set_current_dir(name).is_err() {
9179                unsafe { libc::close(d.dirfd) };
9180                d.dirfd = -1;
9181                err = -2;
9182            }
9183        } else {
9184            unsafe { libc::close(d.dirfd) };
9185            d.dirfd = -1;
9186            err = -1;
9187        }
9188    } else if d.level > 0 {
9189        // C: err = upchdir(d->level);
9190        let _ = upchdir(d.level as usize);
9191    } else if d.level < 0 {
9192        err = -1;
9193    }
9194    // C: dev/ino integrity check after the chdir/fchdir.
9195    if (d.dev != 0 || d.ino != 0) && err == 0 {
9196        if let Ok(meta) = fs::metadata(".") {
9197            if meta.ino() != d.ino || meta.dev() != d.dev {
9198                err = -1;
9199            }
9200        } else {
9201            err = -1;
9202        }
9203    }
9204    err
9205}
9206
9207/// Port of `privasserted()` from `Src/utils.c:7607`.
9208///
9209/// "Check whether the shell is running with privileges in effect.
9210/// This is the case if EITHER the euid is zero, OR (if the system
9211/// supports POSIX.1e (POSIX.6) capability sets) the process'
9212/// Effective or Inheritable capability sets are non-empty."
9213///
9214/// ```c
9215/// if (!geteuid()) return 1;
9216/// #ifdef HAVE_CAP_GET_PROC
9217///     cap_t caps = cap_get_proc();
9218///     if (caps) {
9219///         cap_flag_value_t val;
9220///         for (cap_value_t cap = 0;
9221///              !cap_get_flag(caps, cap, CAP_EFFECTIVE, &val); cap++)
9222///             if (val && cap != CAP_WAKE_ALARM) {
9223///                 cap_free(caps);
9224///                 return 1;
9225///             }
9226///     }
9227///     cap_free(caps);
9228/// #endif
9229///     return 0;
9230/// ```
9231///
9232/// The previous Rust port checked `getuid() != geteuid()` which is
9233/// the SUID-binary detection, not the "running with privileges"
9234/// check. The capability-set inspection requires libcap (gated
9235/// behind the `libcap` feature in `crate::ported::modules::cap`);
9236/// without it, only the euid==0 path is exercised — same as the
9237/// C `#else` arm when HAVE_CAP_GET_PROC isn't defined.
9238pub fn privasserted() -> bool {
9239    #[cfg(unix)]
9240    {
9241        if unsafe { libc::geteuid() } == 0 {
9242            return true;
9243        }
9244    }
9245    // POSIX.1e capabilities check (HAVE_CAP_GET_PROC) — only
9246    // active on Linux when zshrs is built with `--features
9247    // libcap`. The cap module's `cap_get_proc` returns Ok(text)
9248    // when the process has any capability set; we treat any
9249    // non-default-empty result as "privileges asserted".
9250    #[cfg(all(target_os = "linux", feature = "libcap"))]
9251    {
9252        // Pending: walk the capability set with cap_get_flag and
9253        // skip CAP_WAKE_ALARM as the C source does. The cap.rs
9254        // port doesn't yet expose the flag-iteration FFI; until
9255        // it does, conservative-true on any non-empty cap text.
9256        if let Ok(text) = crate::ported::modules::cap::cap_get_proc() {
9257            // Empty / default-empty cap text = no privileges.
9258            if !text.is_empty() && text != "=" {
9259                return true;
9260            }
9261        }
9262    }
9263    false
9264}
9265
9266/// Port of `mode_to_octal(mode_t mode)` from `Src/utils.c:7634`.
9267///
9268/// Convert a `mode_t` into the equivalent canonical octal value
9269/// by testing each `S_I*` flag explicitly and OR-ing the matching
9270/// octal bit. This is NOT just `mode & 07777` — on systems where
9271/// the libc `S_IRUSR`/etc. constants don't match the canonical
9272/// values (e.g. when zsh runs against a non-POSIX libc),
9273/// `mode & 07777` returns the libc representation. The C version
9274/// translates to canonical bits explicitly so callers get a stable
9275/// portable result.
9276///
9277/// ```c
9278/// int mode_to_octal(mode_t mode)
9279/// {
9280///     int m = 0;
9281///     if (mode & S_ISUID) m |= 04000;
9282///     ... (12 bit-by-bit mappings)
9283///     return m;
9284/// }
9285/// ```
9286pub fn mode_to_octal(mode: u32) -> i32 {
9287    // c:7634
9288    #[cfg(not(unix))]
9289    {
9290        // No POSIX permission bits on non-Unix; fall back to canonical
9291        // octal-bit layout via the same mask values C uses on POSIX.
9292        let mut o: i32 = 0;
9293        if mode & 0o4000 != 0 {
9294            o |= 0o4000;
9295        } // c:7638-7639
9296        if mode & 0o2000 != 0 {
9297            o |= 0o2000;
9298        } // c:7640-7641
9299        if mode & 0o1000 != 0 {
9300            o |= 0o1000;
9301        } // c:7642-7643
9302        if mode & 0o0400 != 0 {
9303            o |= 0o0400;
9304        } // c:7644-7645
9305        if mode & 0o0200 != 0 {
9306            o |= 0o0200;
9307        } // c:7646-7647
9308        if mode & 0o0100 != 0 {
9309            o |= 0o0100;
9310        } // c:7648-7649
9311        if mode & 0o0040 != 0 {
9312            o |= 0o0040;
9313        } // c:7650-7651
9314        if mode & 0o0020 != 0 {
9315            o |= 0o0020;
9316        } // c:7652-7653
9317        if mode & 0o0010 != 0 {
9318            o |= 0o0010;
9319        } // c:7654-7655
9320        if mode & 0o0004 != 0 {
9321            o |= 0o0004;
9322        } // c:7656-7657
9323        if mode & 0o0002 != 0 {
9324            o |= 0o0002;
9325        } // c:7658-7659
9326        if mode & 0o0001 != 0 {
9327            o |= 0o0001;
9328        } // c:7660-7661
9329        return o; // c:7662
9330    }
9331    #[cfg(unix)]
9332    {
9333        // c:7636 — int m = 0;
9334        let mut m: i32 = 0;
9335        // c:7638-7661 — 12 bit-by-bit mappings from libc S_I* → canonical octal.
9336        if mode & S_ISUID as u32 != 0 {
9337            m |= 0o4000;
9338        } // c:7638-7639
9339        if mode & S_ISGID as u32 != 0 {
9340            m |= 0o2000;
9341        } // c:7640-7641
9342        if mode & S_ISVTX as u32 != 0 {
9343            m |= 0o1000;
9344        } // c:7642-7643
9345        if mode & S_IRUSR as u32 != 0 {
9346            m |= 0o0400;
9347        } // c:7644-7645
9348        if mode & S_IWUSR as u32 != 0 {
9349            m |= 0o0200;
9350        } // c:7646-7647
9351        if mode & S_IXUSR as u32 != 0 {
9352            m |= 0o0100;
9353        } // c:7648-7649
9354        if mode & S_IRGRP as u32 != 0 {
9355            m |= 0o0040;
9356        } // c:7650-7651
9357        if mode & S_IWGRP as u32 != 0 {
9358            m |= 0o0020;
9359        } // c:7652-7653
9360        if mode & S_IXGRP as u32 != 0 {
9361            m |= 0o0010;
9362        } // c:7654-7655
9363        if mode & S_IROTH as u32 != 0 {
9364            m |= 0o0004;
9365        } // c:7656-7657
9366        if mode & S_IWOTH as u32 != 0 {
9367            m |= 0o0002;
9368        } // c:7658-7659
9369        if mode & S_IXOTH as u32 != 0 {
9370            m |= 0o0001;
9371        } // c:7660-7661
9372        m // c:7662
9373    }
9374}
9375
9376/// Port of `mailstat(char *path, struct stat *st)` from `Src/utils.c:7685`.
9377///
9378/// C signature: `int mailstat(char *path, struct stat *st)`.
9379/// Writes maildir aggregate stats into `*st` (or the native stat for
9380/// non-directory paths) and returns the underlying `stat(2)` return
9381/// (0 on success, -1 on error — matches C's `i = stat(path, st);
9382/// return i;`).
9383///
9384/// When `path` is a maildir directory (containing `cur/`, `tmp/`,
9385/// `new/` subdirs), walks `new/` + `cur/` and aggregates into `*st`:
9386/// nlink=1, S_IFDIR→S_IFREG, size=Σ message bytes,
9387/// blocks=Σ messages, atime=newest, mtime=newest. When the path is
9388/// a plain file, leaves the native `stat(2)` result in `*st`.
9389pub fn mailstat(path: &str, st: &mut libc::stat) -> i32 {
9390    // c:7685
9391    let c_path = match CString::new(path) {
9392        Ok(c) => c,
9393        Err(_) => return -1,
9394    };
9395    // C: if ((i = stat(path, st)) != 0 || !S_ISDIR(st->st_mode)) return i;
9396    let i = unsafe { libc::stat(c_path.as_ptr(), st as *mut _) }; // c:7693
9397    if i != 0 || (st.st_mode & libc::S_IFMT) != libc::S_IFDIR {
9398        // c:7693
9399        return i; // c:7693
9400    }
9401    // C 7700-7706: nlink=1, S_IFDIR → S_IFREG, zero size/blocks.
9402    st.st_nlink = 1; // c:7701
9403    st.st_mode &= !libc::S_IFDIR; // c:7702
9404    st.st_mode |= libc::S_IFREG; // c:7703
9405    st.st_size = 0; // c:7704
9406    st.st_blocks = 0; // c:7705
9407                      // C 7707-7712: stat(path/cur). If absent or not a dir, return 0
9408                      // with the partial out (just the IFREG-coerced root).
9409    let cur_path = match CString::new(format!("{}/cur", path)) {
9410        Ok(c) => c,
9411        Err(_) => return 0,
9412    };
9413    let mut sub: libc::stat = unsafe { std::mem::zeroed() };
9414    if unsafe { libc::stat(cur_path.as_ptr(), &mut sub) } != 0               // c:7708
9415        || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9416    // c:7708
9417    {
9418        return 0; // c:7710
9419    }
9420    st.st_atime = sub.st_atime; // c:7712
9421                                // C 7715-7722: stat(path/tmp).
9422    let tmp_path = match CString::new(format!("{}/tmp", path)) {
9423        Ok(c) => c,
9424        Err(_) => return 0,
9425    };
9426    if unsafe { libc::stat(tmp_path.as_ptr(), &mut sub) } != 0               // c:7716
9427        || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9428    // c:7716
9429    {
9430        return 0; // c:7718
9431    }
9432    st.st_mtime = sub.st_mtime; // c:7720
9433                                // C 7724-7730: stat(path/new). C overwrites mtime with new/'s mtime.
9434    let new_path = match CString::new(format!("{}/new", path)) {
9435        Ok(c) => c,
9436        Err(_) => return 0,
9437    };
9438    if unsafe { libc::stat(new_path.as_ptr(), &mut sub) } != 0               // c:7724
9439        || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9440    // c:7724
9441    {
9442        return 0; // c:7726
9443    }
9444    st.st_mtime = sub.st_mtime; // c:7728
9445                                // C 7749-7778: walk new/ and cur/, sum size + blocks, track newest
9446                                // atime / mtime.
9447    let mut atime: libc::time_t = 0; // c:7748
9448    let mut mtime: libc::time_t = 0; // c:7748
9449    for sub_name in ["new", "cur"] {
9450        let dir = format!("{}/{}", path, sub_name);
9451        let entries = match fs::read_dir(&dir) {
9452            Ok(e) => e,
9453            Err(_) => return 0,
9454        };
9455        for entry in entries.flatten() {
9456            let name = entry.file_name();
9457            let name_bytes = name.as_encoded_bytes();
9458            // C: if (fn->d_name[0] == '.') continue;
9459            if name_bytes.first() == Some(&b'.') {
9460                // c:7758
9461                continue;
9462            }
9463            let entry_path = match CString::new(entry.path().to_string_lossy().as_bytes()) {
9464                Ok(c) => c,
9465                Err(_) => continue,
9466            };
9467            let mut entry_st: libc::stat = unsafe { std::mem::zeroed() };
9468            if unsafe { libc::stat(entry_path.as_ptr(), &mut entry_st) } != 0 {
9469                // c:7762
9470                continue;
9471            }
9472            st.st_size += entry_st.st_size; // c:7766
9473            st.st_blocks += 1; // c:7767
9474                               // C: if (atime != mtime && atime > atime_max) atime_max = atime;
9475            if entry_st.st_atime != entry_st.st_mtime && entry_st.st_atime > atime {
9476                // c:7769
9477                atime = entry_st.st_atime;
9478            }
9479            if entry_st.st_mtime > mtime {
9480                // c:7771
9481                mtime = entry_st.st_mtime;
9482            }
9483        }
9484    }
9485    // C 7783-7784: if (atime) st_ret.st_atime = atime;
9486    if atime != 0 {
9487        // c:7783
9488        st.st_atime = atime;
9489    }
9490    if mtime != 0 {
9491        // c:7785
9492        st.st_mtime = mtime;
9493    }
9494    0 // c:7787
9495}
9496
9497/// Script name for error messages
9498pub static mut SCRIPT_NAME: Option<String> = None;
9499/// Script filename
9500pub static mut SCRIPT_FILENAME: Option<String> = None;
9501
9502/// Print a fatal error to stderr.
9503/// Port of `zerr(VA_ALIST1(const char *fmt))` from Src/utils.c:172. C source sets `errflag`
9504/// after emitting `<prefix>: <msg>\n` so the running script aborts
9505/// at the next safe point. The Rust port currently just prints —
9506// =====================================================================
9507// Module-static state for the zerr/zwarn/zerrnam/zwarnnam family —
9508// port of the file-statics in Src/init.c + Src/exec.c that
9509// `zwarning()` (utils.c:142) reads to build the error prefix.
9510// =====================================================================
9511
9512// name of script being sourced                                             // c:33
9513/// Port of `char *scriptname` from `Src/init.c`. Set when `source`
9514/// is reading a script; cleared on return. Used by `zwarning()`
9515/// (utils.c:147) as the diagnostic prefix.
9516static SCRIPTNAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9517
9518/// Port of `char *argzero` from `Src/init.c`. The shell's argv[0].
9519/// Used by `zwarning()` (utils.c:147) as the fallback diagnostic
9520/// prefix when scriptname is unset.
9521static ARGZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9522
9523/// Port of `char *scriptfilename` from `Src/init.c` (listed in
9524/// `zsh.export:356`). The FILE that the current code was PARSED
9525/// from. Distinct from `scriptname` (which doshfunc overrides on
9526/// function entry at c:5903). `scriptfilename` stays at the outer
9527/// file across function calls; PS4's `%x` reads it at
9528/// `Src/prompt.c:937`.
9529///
9530/// Set at init.c:479 (`-c` mode → `scriptname = scriptfilename
9531/// = "zsh"`), init.c:1367 (`source` enters the named file),
9532/// init.c:1558+1592+1667 (save/install/restore around `.` /
9533/// `source` bin_dot dispatch).
9534static SCRIPTFILENAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9535
9536/// Port of `char *posixzero` from `Src/params.c:76`. The original
9537/// argv[0] preserved unchanged by later mutations. Used by
9538/// `argzerogetfn` for `$0` under `isset(POSIXARGZERO)`.
9539static POSIXZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9540
9541// error flag: bits from enum errflag_bits                                 // c:124
9542/// Port of `int errflag` from `Src/init.c`. Tracks whether an
9543/// error has been raised (`ERRFLAG_ERROR = 1`) or break/return
9544/// is in flight (`ERRFLAG_INT = 2`).
9545///
9546/// Direct global access: `errflag.load(Ordering::Relaxed)` reads
9547/// the current value, `errflag.fetch_or(ERRFLAG_ERROR, …)` matches
9548/// C's `errflag |= ERRFLAG_ERROR`, `errflag.store(0, …)` matches
9549/// C's `errflag = 0`.
9550#[allow(non_upper_case_globals)]
9551pub static errflag: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9552
9553/// Port of `int noerrs` from `Src/init.c`. Counter — when `> 0`,
9554/// suppresses error printing. `noerrs >= 2` also suppresses the
9555/// `errflag` set inside `zerr`/`zerrnam`.
9556static NOERRS: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
9557
9558/// Port of `int locallevel` from `Src/init.c`. Function-call depth
9559/// (0 = top-level, 1+ = inside a fn). `zwarning()` checks this in
9560/// the script-prefix path (utils.c:150).
9561// `LOCALLEVEL` removed — see `locallevel_lock` removal comment
9562// below. Canonical static lives in `LOCALLEVEL`
9563// (port of params.c:54).
9564
9565/// Port of `int lineno` from `Src/init.c`. Current line number;
9566/// `zerrmsg()` includes it in the diagnostic when locallevel > 0
9567/// or shinstdin is unset (utils.c:301).
9568static LINENO: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
9569
9570/// Port of the `isset(SHINSTDIN)` check from utils.c:150. C reads
9571/// the SHINSTDIN option directly; the Rust port caches it here so
9572/// callers don't pull in the whole option-table for every error.
9573static SHINSTDIN_OPT: std::sync::OnceLock<Mutex<bool>> = std::sync::OnceLock::new();
9574
9575/// `ERRFLAG_ERROR` from `Src/zsh.h`. Set on `zerr`/`zerrnam` to
9576/// signal a fatal error has occurred.
9577pub const ERRFLAG_ERROR: i32 = 1;
9578
9579/// Port of `mod_export int incompfunc` from `Src/utils.c:46`.
9580/// Set non-zero while a `comp*` builtin is dispatching from inside
9581/// a user-defined completion function — guards `comparguments` /
9582/// `compset` / `compadd` / `compdescribe` / `comptags` / `compvalues`
9583/// / `compfiles` / `compgroups` / `compquote` against being called
9584/// outside the `compfunc` shfunc context (each builtin checks
9585/// `INCOMPFUNC` early and emits "can only be called from completion
9586/// function" when zero).
9587pub static INCOMPFUNC: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:46
9588
9589/// Port of `mod_export int resetneeded` from `Src/utils.c:1821`.
9590/// Set when the editor needs a redraw — incremented by widgets +
9591/// signal handlers (e.g. SIGWINCH); the next prompt loop tick clears
9592/// it after running zrefresh.
9593pub static RESETNEEDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1821
9594
9595/// Port of `mod_export int winchanged` from `Src/utils.c:1827`.
9596/// Set by the SIGWINCH handler — the next refresh re-queries the
9597/// terminal size and re-renders, then clears the flag.
9598pub static WINCHANGED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1827
9599
9600/// Port of C's `GETKEY_*` flag enumeration from `Src/zsh.h:3143`.
9601/// Passed to `getkeystring_with` to alter escape interpretation:
9602///   - GETKEY_OCTAL_ESC: `\NNN` octal escapes are interpreted (1<<0)
9603///   - GETKEY_EMACS: unknown `\<char>` drops the backslash (1<<1)
9604///   - GETKEY_BACKSLASH_C: `\c` truncates the result (1<<3)
9605pub const GETKEY_OCTAL_ESC: u32 = 1 << 0; // c:zsh.h:3143
9606/// `GETKEY_EMACS` constant.
9607pub const GETKEY_EMACS: u32 = 1 << 1; // c:zsh.h:3150
9608/// `GETKEY_CTRL` constant.
9609pub const GETKEY_CTRL: u32 = 1 << 2; // c:zsh.h:3152
9610/// `GETKEY_BACKSLASH_C` constant.
9611pub const GETKEY_BACKSLASH_C: u32 = 1 << 3; // c:zsh.h:3154
9612
9613/// `GETKEYS_PRINT = GETKEY_OCTAL_ESC | GETKEY_BACKSLASH_C |
9614/// GETKEY_EMACS` per Src/zsh.h:3185 — the flag set `bin_print` uses
9615/// when interpreting escapes (with EMACS: unknown `\<c>` → `<c>`).
9616pub const GETKEYS_PRINT: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_BACKSLASH_C; // c:zsh.h:3185
9617
9618/// `GETKEYS_ECHO = GETKEY_BACKSLASH_C` per Src/zsh.h:3178 — the flag
9619/// set `echo` uses (and `print -e`). NOT GETKEY_EMACS: unknown
9620/// `\<c>` is preserved as literal `\<c>`. NOT GETKEY_OCTAL_ESC:
9621/// `\NNN` is NOT interpreted as octal. Only `\a \b \e \E \f \n \r
9622/// \t \v \\` (plus `\c` truncation) are handled.
9623///
9624/// Per builtin.c:4754-4760 — `bin_print` selects this for BIN_ECHO
9625/// or when `-e` is set; otherwise GETKEYS_PRINT.
9626pub const GETKEYS_ECHO: u32 = GETKEY_BACKSLASH_C; // c:zsh.h:3178
9627
9628/// `GETKEYS_BINDKEY = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL`
9629/// per Src/zsh.h:3187 — the flag set `print -b` uses. Like GETKEYS_PRINT
9630/// it has EMACS (so `\C-`/`\M-` backslash escapes are processed and
9631/// unknown `\<c>` collapses to `<c>`) but adds GETKEY_CTRL (enabling the
9632/// `^X` caret notation) and drops GETKEY_BACKSLASH_C (no `\c` truncation).
9633pub const GETKEYS_BINDKEY: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL; // c:zsh.h:3187
9634
9635/// Static `int ep` from Src/utils.c:4775 — sticky flag suppressing the
9636/// `can't set tty pgrp` warning after the first failure.
9637static ATTACHTTY_EP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:4775
9638
9639// =====================================================================
9640// !!! WARNING: RUST-ONLY HELPERS — NO DIRECT C COUNTERPART !!!
9641// =====================================================================
9642//
9643// These three ported (`fdtable_lock`, `fdtable_get`, `fdtable_set`) DO
9644// NOT EXIST as functions in `Src/utils.c`. The C source declares
9645// `fdtable` as a bare `unsigned char *` global (Src/utils.c:~63) and
9646// every call site reads/writes it as a direct array index:
9647//
9648//     if (fdtable[fd] != FDT_UNUSED) ...
9649//     fdtable[fd] = FDT_EXTERNAL;
9650//
9651// Rust can't hand out raw mutable indexes through a `Mutex<Vec<u8>>`
9652// safely without a borrow scope, so the Rust port wraps the same
9653// slot access in three `pub fn`s. Each call site to these helpers
9654// corresponds 1:1 to a `fdtable[fd] = X` or `fdtable[fd] != X`
9655// statement in the C source — they are NOT new policy, they only
9656// adapt the storage shape from `unsigned char *` to a growable
9657// `Mutex<Vec<i32>>`.
9658//
9659// Also adapts `growfdtable` (Src/utils.c:1965) by lazily growing the
9660// Vec inside `fdtable_set` instead of a separate `growfdtable`
9661// call — the C source calls `growfdtable(fd)` immediately before
9662// every `fdtable[fd] = X` write anyway.
9663//
9664// !!! Do NOT use these for any state that the C source does not
9665// already store in `fdtable[]`. Adding a new "fd kind" here is a
9666// scope expansion, not a port. !!!
9667// =====================================================================
9668
9669static FDTABLE: std::sync::OnceLock<Mutex<Vec<i32>>> = std::sync::OnceLock::new();
9670
9671/// Port of `int max_zsh_fd` global from `Src/exec.c:201`.
9672/// "The highest fd we know zsh is using" — bumped by `fdtable_set`
9673/// callers, shrunk by `zclose` when trailing slots fall to UNUSED.
9674pub static MAX_ZSH_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
9675
9676/// Port of `int fdtable_flocks` global from `Src/exec.c:204`.
9677/// Count of `FDT_FLOCK`-tagged fds; consulted by `closem()` to
9678/// decide whether to skip the flock-fd sweep.
9679pub static FDTABLE_FLOCKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9680
9681/// Setter for `scriptname`. Called from `bin_dot` / `source`
9682/// when entering a script.
9683pub fn set_scriptname(name: Option<String>) {
9684    *scriptname_lock().lock().unwrap() = name;
9685}
9686
9687/// Read `scriptname` — direct mirror of the C file-scope
9688/// `char *scriptname;` at `Src/utils.c:36`. Exposed for the
9689/// prompt expander (`%N`), `bin_dot`, and ZLE source tracking.
9690pub fn scriptname_get() -> Option<String> {
9691    scriptname_lock().lock().unwrap().clone()
9692}
9693
9694/// Setter for `scriptfilename` — direct mirror of the C
9695/// `scriptfilename` global at `Src/init.c` (zsh.export:356).
9696/// Called from `bin_dot` at init.c:1592, restored at c:1667.
9697pub fn set_scriptfilename(name: Option<String>) {
9698    *scriptfilename_lock().lock().unwrap() = name;
9699}
9700
9701/// Read `scriptfilename` — direct mirror of the C global.
9702/// Used by PS4's `%x` (`Src/prompt.c:937`) and the function-call
9703/// frame at `Src/exec.c:1613` (`fstack.filename = scriptfilename`).
9704pub fn scriptfilename_get() -> Option<String> {
9705    scriptfilename_lock().lock().unwrap().clone()
9706}
9707
9708/// Read `locallevel` — function nesting depth. Routes through the
9709/// canonical `LOCALLEVEL` static (port of
9710/// `Src/params.c:54`). The prior Rust port stored a duplicate
9711/// `Mutex<i32>` here that split state from params.rs; both copies
9712/// were never kept in sync.
9713pub fn locallevel() -> i32 {
9714    LOCALLEVEL.load(Ordering::Relaxed)
9715}
9716
9717/// Bump `locallevel` (called by `startparamscope`).
9718pub fn inc_locallevel() {
9719    LOCALLEVEL.fetch_add(1, Ordering::Relaxed);
9720}
9721
9722/// Decrement `locallevel` (called by `endparamscope`).
9723pub fn dec_locallevel() {
9724    LOCALLEVEL.fetch_sub(1, Ordering::Relaxed);
9725}
9726
9727/// Setter for `argzero`. Called once at shell init from `parseargs`.
9728pub fn set_argzero(name: Option<String>) {
9729    // c:271 (init.c) — `argv0 = argzero = posixzero = *argv++;`. At
9730    // shell init both argzero and posixzero share the same source.
9731    // posixzero is preserved unchanged after later mutations (exec -a,
9732    // function frames) — argzero is what gets rewritten. If posixzero
9733    // hasn't been independently set yet, mirror argzero so the
9734    // POSIXARGZERO branch in `argzerogetfn` returns something useful.
9735    let mut posix_lock = posixzero_lock().lock().unwrap();
9736    if posix_lock.is_none() {
9737        *posix_lock = name.clone();
9738    }
9739    drop(posix_lock);
9740    *argzero_lock().lock().unwrap() = name;
9741}
9742
9743/// Read `argzero`. Used by `argzerogetfn` for `$0`.
9744pub fn argzero() -> Option<String> {
9745    argzero_lock().lock().unwrap().clone()
9746}
9747
9748/// Port of `char *posixzero` from `Src/params.c:76`. The original
9749/// `argv[0]` preserved from shell startup, unchanged by later `exec -a`
9750/// or function-call rewrites. C's `argzerogetfn` (params.c:4958)
9751/// returns this instead of `argzero` when `isset(POSIXARGZERO)`.
9752///
9753/// Set once at init via `set_posixzero` (called from the init path
9754/// in C at init.c:271/297/321). `set_argzero` mirrors the value here
9755/// on first call so the POSIXARGZERO branch has something to read
9756/// even if the init path hasn't run yet.
9757pub fn set_posixzero(name: Option<String>) {
9758    *posixzero_lock().lock().unwrap() = name;
9759}
9760
9761/// Read `posixzero`. C: `posixzero` (params.c:76 global). Used by
9762/// `argzerogetfn` when `isset(POSIXARGZERO)`.
9763pub fn posixzero() -> Option<String> {
9764    posixzero_lock().lock().unwrap().clone()
9765}
9766
9767/// Setter for `noerrs`. Increment to suppress error output;
9768/// decrement to restore.
9769pub fn set_noerrs(v: i32) {
9770    *noerrs_lock().lock().unwrap() = v;
9771}
9772
9773/// Setter for `locallevel`. Called by function-call entry/exit.
9774pub fn set_locallevel(v: i32) {
9775    LOCALLEVEL.store(v, Ordering::Relaxed);
9776}
9777
9778/// Setter for `lineno`. Called by the parser as it advances.
9779pub fn set_lineno(v: i32) {
9780    *lineno_lock().lock().unwrap() = v;
9781}
9782
9783/// Setter for the cached SHINSTDIN flag. Called by the option
9784/// machinery whenever `setopt shinstdin` / `unsetopt shinstdin`
9785/// fires.
9786pub fn set_shinstdin(v: bool) {
9787    *shinstdin_lock().lock().unwrap() = v;
9788}
9789
9790/// Check if string is a valid identifier
9791// `isident` DELETED — fake duplicate (cited `c:params.c:1288` from
9792// utils.rs, location mismatch). Canonical port is
9793// `isident` at `params.rs:2056`, which
9794// correctly handles namespace prefix (`ns.var`) per the real C
9795// body — the utils.rs copy dropped that arm. Callers now route
9796// through `isident` directly.
9797
9798// QuoteType enum + impl deleted — the canonical quote-type values are
9799// the bare `QT_*: i32` constants at `zsh_h.rs:175` (port of anonymous
9800// `enum { QT_NONE, QT_BACKSLASH, … }` from `Src/zsh.h:253-298`).
9801// `quotestring()` takes `quote_type: i32` matching C's signature.
9802
9803/// Map a `(q)` flag count to a `QT_*` value.
9804/// Port of the q-flag dispatch in `Src/subst.c` `paramsubst()` —
9805/// `(q)`=`QT_BACKSLASH`, `(qq)`=`QT_SINGLE`, `(qqq)`=`QT_DOUBLE`,
9806/// `(qqqq+)`=`QT_DOLLARS`.
9807pub fn qflag_quotetype(count: u32) -> i32 {
9808    match count {
9809        0 => QT_NONE,
9810        1 => QT_BACKSLASH,
9811        2 => QT_SINGLE,
9812        3 => QT_DOUBLE,
9813        _ => QT_DOLLARS,
9814    }
9815}
9816
9817/// Port of `ispecial()` macro from `Src/ztype.h:59` — `zistype(X,
9818/// ISPECIAL)`. C populates the ISPECIAL typtab bit at
9819/// `Src/utils.c:4253-4262`:
9820///   - Every byte in `SPECCHARS` (`Src/zsh.h:228` =
9821///     `"#$^*()=|{}[]\`<>?~;&\\n\\t \\\\\\'\\""`)
9822///   - `,` when `typtab_flags & ZTF_SP_COMMA` (set by
9823///     `makecommaspecial(1)` per c:4271)
9824///   - `bangchar` (default `!`) when `BANGHIST` is set AND
9825///     `ZTF_INTERACT` is set (interactive shell)
9826///
9827/// The Rust port enumerates `SPECCHARS` directly because the
9828/// typtab is not lazily initialised — production `ispecial` calls
9829/// happen after `init_typtab` runs at shell startup, but
9830/// `quotestring` is unit-tested in isolation where the typtab is
9831/// all-zero. Match the C SPECCHARS set byte-for-byte; the option-
9832/// driven augments (`,` and bangchar) are still NOT wired through
9833/// (a known gap — see the typtab-routing TODO at the param level).
9834fn ispecial(c: char) -> bool {
9835    // c:59 (Src/ztype.h)
9836    // c:228 (Src/zsh.h) `SPECCHARS` — exact byte set from the C
9837    // string literal. `^` and `{`/`}` were already present; `!`
9838    // is INTENTIONALLY OMITTED here because C only ISPECIAL-tags
9839    // bangchar under BANGHIST + interactive. Including `!`
9840    // unconditionally diverges from C for non-interactive scripts
9841    // and unbang'd input.
9842    matches!(
9843        c,
9844        '#' | '$' | '^' | '*' | '(' | ')' | '='  // c:228 first half
9845            | '|' | '{' | '}' | '[' | ']' | '`'   // c:228 mid
9846            | '<' | '>' | '?' | '~' | ';' | '&'   // c:228 specials
9847            | '\n' | '\t' | ' ' | '\\' | '\'' | '"' // c:228 whitespace/backslash/quotes
9848    )
9849}
9850
9851/// Convert integer to string with specified base — re-export of
9852/// the canonical port at `convbase_param`.
9853///
9854/// The previous Rust port at this file was a SECOND, divergent
9855/// implementation of `convbase`:
9856///   - Used LOWERCASE digit chars (`"0123456789abc..."`) for
9857///     bases 11..36, while C uses UPPERCASE (`(dig - 10) + 'A'`
9858///     at Src/params.c:5621).
9859///   - Skipped CBASES + OCTALZEROES prefix handling at c:5598-5604
9860///     (`0x`/`0` prefixes when option-gated).
9861///   - Returned wrong format for bases not handled by the explicit
9862///     match arms.
9863///
9864/// The canonical port lives at `params.rs::convbase_ptr` (c:5588
9865/// faithful body) + `params.rs::convbase` (c:5634 wrapper). Route
9866/// through there so utils.rs / params.rs agree byte-for-byte and
9867/// no divergent duplicate stays alive.
9868pub fn convbase(val: i64, base: u32) -> String {
9869    // c:5634 (Src/params.c)
9870    convbase_param(val, base)
9871}
9872
9873// `checkglobqual` DELETED — fake bracket-depth scanner cited
9874// `c:glob.c:1158` from utils.rs. Canonical port is
9875// `crate::ported::glob::checkglobqual(str, sl, nobareglob, sp)` at
9876// `glob.rs:813`, which matches the real C signature
9877// `(char *str, int sl, int nobareglob, char **sp)`. The utils.rs
9878// fake reduced the signature to a single bool and discarded the
9879// `sp` glob-qualifier-start out-pointer that callers need. Zero
9880// callers used the utils.rs version.
9881
9882// `dupstrpfx` DELETED — fake duplicate cited `c:string.c:161` from
9883// utils.rs (wrong location). Canonical port is
9884// `dupstrpfx` at `string.rs:161`. Zero
9885// callers used the utils.rs version.
9886
9887/// Get username from UID (from utils.c getpwuid handling)
9888pub fn statuidprint(uid: u32) -> Option<String> {
9889    #[cfg(unix)]
9890    {
9891        let pwd = unsafe { libc::getpwuid(uid) };
9892        if pwd.is_null() {
9893            return None;
9894        }
9895        let name = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_name) };
9896        name.to_str().ok().map(|s| s.to_string())
9897    }
9898    #[cfg(not(unix))]
9899    {
9900        let _ = uid;
9901        None
9902    }
9903}
9904
9905// `ztrdup` / `dyncat` / `tricat` / `bicat` DELETED — these were
9906// fake duplicates of the canonical ports in `src/ported/string.rs`
9907// (`Src/string.c:62`, `:131`, `:98`, `:145`). The utils.rs copies
9908// admitted they were not in utils.c via `c:string.c:NNN`
9909// annotations; PORT.md Rule 1 disallows the duplicate location.
9910// Callers now route through `crate::ported::string::{ztrdup,
9911// dyncat, tricat, bicat}` directly.
9912
9913// `iwsep` / `iwsep_byte` DELETED — these were fake hardcoded
9914// `c==' '||c=='\t'||c=='\n'` checks claiming to port `iwsep` from
9915// "Src/zsh.h", but the real macro lives at `Src/ztype.h:61`
9916// (`#define iwsep(X) zistype(X, IWSEP)`) and consults the `typtab[]`
9917// lookup — which mutates when IFS is reassigned. The canonical port
9918// is `iwsep(u8) -> bool` at `ztype_h.rs:133`.
9919// Internal utils.rs callers (skipwsep / spacesplit / findword) now
9920// route through it directly.
9921
9922// `imeta(c: char)` DELETED — fake `char`-arg wrapper cited
9923// `c:ztype.h:60` from utils.rs (wrong location, wrong signature).
9924// Canonical port is `crate::ported::ztype_h::imeta(u8) -> bool` at
9925// `ztype_h.rs:130`. C `imeta(X)` takes a byte; wrapping with `char`
9926// invented an `> 0xff` early-out that C doesn't have. Migrated 1
9927// caller (`zle/computil.rs:6194`).
9928
9929/// Get hostname
9930pub fn gethostname() -> String {
9931    #[cfg(unix)]
9932    {
9933        let mut buf = vec![0u8; 256];
9934        unsafe {
9935            if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) == 0 {
9936                let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
9937                return String::from_utf8_lossy(&buf[..len]).to_string();
9938            }
9939        }
9940    }
9941    // gethostname(2) failure fallback. C consults `$HOST` /
9942    // `cached_hostname`. Read paramtab; fall back to "localhost".
9943    getsparam("HOST")
9944        .or_else(|| getsparam("HOSTNAME"))
9945        .unwrap_or_else(|| "localhost".to_string())
9946}
9947
9948/// Get current working directory — re-export of the canonical
9949/// port at `compat::zgetcwd` (Src/compat.c:559).
9950// `zgetcwd` DELETED — fake `Option<String>` wrapper around the
9951// canonical `compat::zgetcwd() -> String` port (`Src/compat.c:559`).
9952// C `zgetcwd` never returns NULL (falls back to `"."` per the
9953// `dupstring(".")` arm), so the `Option` was caller-API churn, not
9954// a port. Callers route through `crate::ported::compat::zgetcwd()`
9955// directly.
9956
9957// `zchdir` duplicate deleted — canonical port at compat.rs:253
9958// matches C's `int zchdir(char *dir)` signature (returns i32, not
9959// bool). Zero callers used the utils.rs bool variant.
9960
9961// `realpath` / `mkdir` / `symlink` / `readlink` / `getenv` DELETED —
9962// five Rust-only convenience wrappers around std::fs / std::env /
9963// std::os::unix::fs with ZERO callers across the tree. Each was a
9964// thin std-lib re-export with no zsh C counterpart:
9965//   - realpath  → std::fs::canonicalize (libc realpath(3) — not in
9966//                 utils.c; the zsh source uses chrealpath at hist.c:1971)
9967//   - mkdir     → std::fs::create_dir (libc mkdir(2) — zsh's analogue
9968//                 is Modules/files.c::bin_mkdir, not a utils.c helper)
9969//   - symlink   → std::os::unix::fs::symlink (libc symlink(2))
9970//   - readlink  → std::fs::read_link (libc readlink(2))
9971//   - getenv    → std::env::var (libc getenv(3) — zsh's analogue is
9972//                 zgetenv in compat.c)
9973// PORT.md Rule 0/A: names must exist in upstream zsh C source as a
9974// `<name>(` function. None of these qualify; with zero callers they
9975// were dead code by definition. Callers needing the same semantics
9976// route through the canonical port (chrealpath / bin_mkdir / etc.)
9977// or std::fs directly.
9978
9979/// Per-prompt callback registry.
9980/// Port of the static `prepromptfns` LinkList in Src/utils.c:1319.
9981/// Holds the bare-fn pointers `addprepromptfn`/`delprepromptfn`
9982/// register and `preprompt()` walks.
9983static PREPROMPT_FNS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
9984
9985/// Set environment variable
9986pub fn setenv(name: &str, value: &str) {
9987    std::env::set_var(name, value);
9988}
9989
9990/// Unset environment variable
9991pub fn unsetenv(name: &str) {
9992    std::env::remove_var(name);
9993}
9994
9995/// Time-ordered timed-function registry.
9996/// Port of the `timedfns` LinkList Src/utils.c:1371 keeps for the
9997/// `sched` builtin. Sorted ascending by `when` (epoch seconds);
9998/// `addtimedfn` does an insertion sort (matches the C source's
9999/// `for (;;)` walk at lines 1394-1411).
10000pub static TIMED_FNS: Mutex<Vec<(i64, fn())>> = Mutex::new(Vec::new()); // c:1371 timedfns (mod_export in C)
10001
10002/// Get current user ID
10003pub fn getuid() -> u32 {
10004    #[cfg(unix)]
10005    unsafe {
10006        libc::getuid()
10007    }
10008    #[cfg(not(unix))]
10009    0
10010}
10011
10012/// Get effective user ID
10013pub fn geteuid() -> u32 {
10014    #[cfg(unix)]
10015    unsafe {
10016        libc::geteuid()
10017    }
10018    #[cfg(not(unix))]
10019    0
10020}
10021
10022/// Get current group ID
10023pub fn getgid() -> u32 {
10024    #[cfg(unix)]
10025    unsafe {
10026        libc::getgid()
10027    }
10028    #[cfg(not(unix))]
10029    0
10030}
10031
10032/// Get effective group ID
10033pub fn getegid() -> u32 {
10034    #[cfg(unix)]
10035    unsafe {
10036        libc::getegid()
10037    }
10038    #[cfg(not(unix))]
10039    0
10040}
10041
10042/// Get process ID
10043pub fn getpid() -> i32 {
10044    std::process::id() as i32
10045}
10046
10047/// Get parent process ID
10048pub fn getppid() -> i32 {
10049    #[cfg(unix)]
10050    unsafe {
10051        libc::getppid()
10052    }
10053    #[cfg(not(unix))]
10054    0
10055}
10056
10057/// `getkeystring` with the C `how` parameter (Src/utils.c:6915).
10058/// The plain `getkeystring(s)` shim above defaults `how=0` for
10059/// non-EMACS callers (zbeep, dollar-quote — they keep unknown
10060/// `\<char>` as literal `\<char>`). Pass `GETKEYS_PRINT` to get
10061/// the print/echo behavior (drop backslash on unknown).
10062/// `getkeystring` with the C `how` parameter AND the
10063/// `GETKEY_UPDATE_OFFSET` cursor-offset out-param (Src/utils.c:6915).
10064///
10065/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from
10066/// `Src/utils.c:6915`, specifically its `GETKEY_UPDATE_OFFSET` path.
10067/// When `how` sets `GETKEY_UPDATE_OFFSET` and `misc` is `Some`, `*misc`
10068/// is the C `*misc` completion offset — a byte index into `s` — and is
10069/// updated as escapes positioned before the cursor collapse:
10070///   - `-1` per collapsed `\<char>` escape whose backslash precedes the
10071///     cursor, `s - sstart < *misc` (c:6987); undone if the backslash is
10072///     kept literal in the default (non-EMACS) arm (c:7181-7183);
10073///   - additional `-6` for `\u`, `-10` (`-4`+`-6`) for `\U` (c:7073-7084),
10074///     then `+N` for the N output bytes emitted (c:7123-7124).
10075/// Octal/hex escapes get only the base `-1`, matching C's own incomplete
10076/// `/* HERE: GETKEY_UPDATE_OFFSET? */` handling (c:7155).
10077///
10078/// The offset is a byte index. For ASCII `$'...'` content (one byte per
10079/// char) it matches the C metafied-byte semantics exactly; the byte-vs-
10080/// char divergence for non-ASCII content is the pre-existing shared-cursor
10081/// limitation documented on `set_comp_sep` (compcore.rs), not new here.
10082///
10083/// Callers with no offset to track pass `misc = None`.
10084pub fn getkeystring_with(
10085    s: &str,
10086    how: u32,
10087    mut misc: Option<&mut i32>,
10088) -> (String, usize) {
10089    // c:utils.c:6915
10090    let update_off = (how & crate::ported::zsh_h::GETKEY_UPDATE_OFFSET as u32) != 0;
10091    let mut result = String::new();
10092    let mut chars = s.chars().peekable();
10093    let mut consumed = 0;
10094    // c:7194 — C's `^` handler is `control = 1; continue;`, so the control bit
10095    // stays PENDING across loop iterations and a following `\M`/`\C` escape
10096    // chains onto it (`^\M-a` → c:7034 `meta = 1 + control` → meta==2 → 0x81).
10097    // This port's `^` arm consumes its base character inline, which cannot
10098    // express that, so the flag carries the state to the modifier arm instead.
10099    let mut pending_control = false;
10100    while let Some(c) = chars.next() {
10101        consumed += c.len_utf8();
10102        // c:utils.c:7194 — `^X` caret notation. A bare `^` (not a backslash
10103        // escape) followed by any char applies the control mask to it, but
10104        // ONLY under GETKEY_CTRL — i.e. `print -b` / bindkey. Plain print
10105        // (GETKEYS_PRINT, no GETKEY_CTRL) keeps `^A` literal.
10106        if c == '^' && !pending_control && (how & GETKEY_CTRL) != 0 {
10107            // A `\M`/`\C` escape after the caret must be handled by the
10108            // modifier arm with control already pending — C reaches it via
10109            // `continue` rather than consuming the base char here. Gated on
10110            // GETKEY_EMACS because that is what enables the modifier arm at
10111            // all (c:7031/7043); without it C's `case 'M'` emits a literal
10112            // backslash instead.
10113            let mut la = chars.clone();
10114            if (how & GETKEY_EMACS) != 0
10115                && la.next() == Some('\\')
10116                && matches!(la.next(), Some('M') | Some('C'))
10117            {
10118                pending_control = true;
10119                continue;
10120            }
10121            if let Some(base) = chars.next() {
10122                consumed += base.len_utf8();
10123                let mut byte = base as u32;
10124                // c:7261-7267 — `^?` → 0x7f (DEL), else clear bits 5-6.
10125                if byte == '?' as u32 {
10126                    byte = 0x7f;
10127                } else if byte <= 0xff {
10128                    byte &= 0x9f;
10129                }
10130                if byte <= 0xff {
10131                    let b_ = byte as u8;
10132                    if b_ < 0x80 {
10133                        result.push(b_ as char);
10134                    } else {
10135                        result.push('\u{83}');
10136                        result.push(char::from(b_ ^ 32));
10137                    }
10138                } else if let Some(ch) = char::from_u32(byte) {
10139                    result.push(ch);
10140                }
10141                continue;
10142            }
10143            // c:7194 `s[1]` guard — a trailing `^` with no char is literal.
10144            result.push(c);
10145            continue;
10146        }
10147        if c != '\\' {
10148            result.push(c);
10149            continue;
10150        }
10151        // c is a backslash; record its byte start offset for
10152        // GETKEY_UPDATE_OFFSET (`s - sstart` in C).
10153        let bs_off = consumed - c.len_utf8();
10154        // c:utils.c:6987 — base offset decrement for a collapsed `\<char>`
10155        // escape whose backslash precedes the cursor. C gates on
10156        // `*s == '\\' && s[1]`, so a following char must exist (peek).
10157        let mut miscadded = false;
10158        if update_off && chars.peek().is_some() {
10159            if let Some(m) = misc.as_deref_mut() {
10160                if (bs_off as i32) < *m {
10161                    *m -= 1;
10162                    miscadded = true;
10163                }
10164            }
10165        }
10166        match chars.next() {
10167            Some('n') => {
10168                result.push('\n');
10169                consumed += 1;
10170            }
10171            Some('t') => {
10172                result.push('\t');
10173                consumed += 1;
10174            }
10175            Some('r') => {
10176                result.push('\r');
10177                consumed += 1;
10178            }
10179            // c:utils.c:7018-7024 — `\E` is EMACS-gated:
10180            //     case 'E':
10181            //         if (!(how & GETKEY_EMACS)) {
10182            //             *t++ = '\\', s--;
10183            //             if (miscadded) (*misc)++;
10184            //             continue;
10185            //         }
10186            //         /* FALL THROUGH */
10187            //     case 'e':
10188            //         *t++ = '\033';
10189            // Without EMACS `\E` stays a literal backslash + `E`; only `\e`
10190            // is unconditional. Folding `E` in with `e` made `${(g::)v}` on
10191            // `\E` yield ESC where zsh yields `\E`. Unlike the octal
10192            // `continue` arm (c:7159), C restores the miscadded decrement
10193            // here, since the backslash is kept.
10194            Some('E') if (how & GETKEY_EMACS) == 0 => {
10195                consumed += 1;
10196                if miscadded {
10197                    if let Some(m) = misc.as_deref_mut() {
10198                        *m += 1;
10199                    }
10200                }
10201                result.push('\\');
10202                result.push('E');
10203            }
10204            Some('e') | Some('E') => {
10205                result.push('\x1b');
10206                consumed += 1;
10207            }
10208            Some('a') => {
10209                result.push('\x07');
10210                consumed += 1;
10211            }
10212            Some('b') => {
10213                result.push('\x08');
10214                consumed += 1;
10215            }
10216            Some('f') => {
10217                result.push('\x0c');
10218                consumed += 1;
10219            }
10220            Some('v') => {
10221                result.push('\x0b');
10222                consumed += 1;
10223            }
10224            // c:utils.c:7140-7152 — `\\` (and `\'` under
10225            // GETKEY_DOLLAR_QUOTE) explicitly emit the trailing byte
10226            // bare. Outside DOLLAR_QUOTE, `\'` FALLTHROUGHs to default.
10227            // `\\` stays special because the default arm (c:7180-7184)
10228            // skips the leading-backslash emission iff `*s == '\\'`,
10229            // so `\\` reduces to `\` regardless of GETKEY_EMACS.
10230            //
10231            // Previous Rust port also had `Some('\'')` and `Some('"')`
10232            // arms that unconditionally dropped the backslash. That
10233            // contradicted C: under GETKEYS_ECHO (no GETKEY_EMACS),
10234            // `\'` must remain `\'` because echo doesn't strip
10235            // unknown escapes. Removed those arms — `\'` and `\"`
10236            // now flow through the default arm and honour GETKEY_EMACS.
10237            //
10238            // Root cause of `echo "${(qq)s}"` for `s="a'b"` emitting
10239            // `'a'''b'` instead of zsh's `'a'\''b'`.
10240            Some('\\') => {
10241                result.push('\\');
10242                consumed += 1;
10243            }
10244            // c:utils.c:7072-7138 — `\u` (4-hex) / `\U` (8-hex)
10245            // Unicode codepoint escapes. Always interpreted; the C
10246            // source's `case 'U':` / `case 'u':` arms have no flag
10247            // gating (only the GETKEY_UPDATE_OFFSET bookkeeping). C
10248            // calls `ucs4tomb(wval, t)` which writes UTF-8 bytes.
10249            // Previously these were absent from `getkeystring_with`,
10250            // so `echo -e "\U00000041"` emitted literal `\U00000041`
10251            // instead of 'A'.
10252            Some('u') => {
10253                consumed += 1;
10254                // c:utils.c:7077-7084 — extra `-6` when the escape precedes
10255                // the cursor (checked at the 'u', i.e. offset bs_off+1).
10256                if update_off {
10257                    if let Some(m) = misc.as_deref_mut() {
10258                        if ((bs_off + 1) as i32) < *m {
10259                            *m -= 6;
10260                        }
10261                    }
10262                }
10263                let mut hex = String::new();
10264                for _ in 0..4 {
10265                    if let Some(&c) = chars.peek() {
10266                        if c.is_ascii_hexdigit() {
10267                            hex.push(chars.next().unwrap());
10268                            consumed += 1;
10269                        } else {
10270                            break;
10271                        }
10272                    }
10273                }
10274                // c:utils.c:7085 `wval = 0;` BEFORE the digit loop, and
10275                // c:7087-7095 leaves it untouched when the first char is not
10276                // a hex digit (`s--; break;`). So ZERO digits is not "not an
10277                // escape" — it emits codepoint 0 via `ucs4tomb(wval, t)`
10278                // (c:7101). `\u` → one NUL byte, `\uzz` → NUL + "zz". An Err
10279                // on the empty parse pushed nothing at all.
10280                let val = u32::from_str_radix(&hex, 16).unwrap_or(0);
10281                if let Some(ch) = char::from_u32(val) {
10282                    result.push(ch);
10283                    // c:utils.c:7123-7124 — add one per output byte,
10284                    // checked at the last consumed input byte.
10285                    if update_off {
10286                        if let Some(m) = misc.as_deref_mut() {
10287                            if ((consumed - 1) as i32) < *m {
10288                                *m += ch.len_utf8() as i32;
10289                            }
10290                        }
10291                    }
10292                }
10293            }
10294            Some('U') => {
10295                consumed += 1;
10296                // c:utils.c:7073-7084 — `\U` extra `-4` (c:7073) then the
10297                // shared `\u` `-6` (c:7077), both gated at offset bs_off+1.
10298                if update_off {
10299                    if let Some(m) = misc.as_deref_mut() {
10300                        if ((bs_off + 1) as i32) < *m {
10301                            *m -= 10;
10302                        }
10303                    }
10304                }
10305                let mut hex = String::new();
10306                for _ in 0..8 {
10307                    if let Some(&c) = chars.peek() {
10308                        if c.is_ascii_hexdigit() {
10309                            hex.push(chars.next().unwrap());
10310                            consumed += 1;
10311                        } else {
10312                            break;
10313                        }
10314                    }
10315                }
10316                // c:utils.c:7085 — same `wval = 0` default as the `\u` arm
10317                // above: `\U` with no hex digits emits codepoint 0, not
10318                // nothing.
10319                let val = u32::from_str_radix(&hex, 16).unwrap_or(0);
10320                if let Some(ch) = char::from_u32(val) {
10321                    result.push(ch);
10322                    // c:utils.c:7123-7124 — add one per output byte.
10323                    if update_off {
10324                        if let Some(m) = misc.as_deref_mut() {
10325                            if ((consumed - 1) as i32) < *m {
10326                                *m += ch.len_utf8() as i32;
10327                            }
10328                        }
10329                    }
10330                }
10331            }
10332            // c:utils.c:7156-7178 — the NUMERIC-ESCAPE branch. C handles
10333            // `\x`, `\NNN` and `\0NNN` in ONE arm, and the shared tail is
10334            // what makes them agree, so this port keeps them together too:
10335            //
10336            //     if ((idigit(*s) && *s < '8') || *s == 'x') {
10337            //         if (!(how & GETKEY_OCTAL_ESC)) {
10338            //             if (*s == '0')
10339            //                 s++;
10340            //             else if (*s != 'x') {
10341            //                 *t++ = '\\', s--;
10342            //                 continue;
10343            //             }
10344            //         }
10345            //         if (s[1] && s[2] && s[3]) {
10346            //             svchar = s[3]; s[3] = '\0'; u = s;
10347            //         }
10348            //         *t++ = zstrtol(s + (*s == 'x'), &s,
10349            //                        (*s == 'x') ? 16 : 8);
10350            //         ...
10351            //         s--;
10352            //     }
10353            //
10354            // Three behaviours here were each missing when these were three
10355            // separate arms:
10356            //
10357            //  1. Without GETKEY_OCTAL_ESC a NON-ZERO digit is not an escape
10358            //     at all (c:7159): C emits a literal backslash and backs up so
10359            //     the digit is re-read as an ordinary char. This is INSIDE the
10360            //     numeric branch, so — unlike the default arm — GETKEY_EMACS
10361            //     does not suppress the backslash: `${(g:e:)v}` on `\101` is
10362            //     `\101`, not `101`.
10363            //  2. The value goes through `zstrtol`, which SKIPS leading blanks
10364            //     and accepts a `+`/`-` sign (c:2444-2450), and whose end
10365            //     pointer lands past everything it consumed — blanks included.
10366            //     So `\0 1` is byte 0o1, `\x 41` is 0x04 then `1`, `\x y` is
10367            //     NUL then `y`, and `\0-1` is 0xff (a NEGATIVE parse truncated
10368            //     to a byte). Peeking for "is the next char a digit" stopped at
10369            //     the blank and left it in the output.
10370            //  3. The base is chosen from the char AT `s` AFTER the `\0`
10371            //     introducer was skipped, so `\0x41` is HEX (`A`), not NUL
10372            //     followed by `x41`.
10373            //
10374            // c:7166-7168 windows the parse by NUL-ing `s[3]` when three chars
10375            // follow `s`, capping it at 3 significant chars (2 after an `x`),
10376            // then restores the byte — so `\1234` is 0o123 then `4`.
10377            //
10378            // `*t++ = zstrtol(...)` assigns a zlong to a char, truncating to
10379            // the low byte: `\777` (511) is 0xff, `\400` (256) is NUL. Parsing
10380            // straight into u8 made those an Err and emitted nothing at all.
10381            Some(d) if d.is_digit(8) || d == 'x' => {
10382                consumed += 1;
10383                // c:7157-7161 — the pre-checks that run only without
10384                // GETKEY_OCTAL_ESC. `\0` is the octal introducer (`s++`);
10385                // any other bare digit is not an escape.
10386                let zero_intro = (how & GETKEY_OCTAL_ESC) == 0 && d == '0';
10387                if (how & GETKEY_OCTAL_ESC) == 0 && d != '0' && d != 'x' {
10388                    // c:7159 `*t++ = '\\', s--; continue;`
10389                    result.push('\\');
10390                    result.push(d);
10391                    continue;
10392                }
10393                // C's `s` after the pre-checks: the char AFTER the `\0`
10394                // introducer, else the matched char itself.
10395                //
10396                // Four chars of lookahead is all C can read: the window test
10397                // reaches `s[3]`, and the parse never sees past it. Bounding
10398                // the clone keeps this O(1) per escape — collecting the whole
10399                // tail here would make decoding an escape-dense string O(n²).
10400                let rest: Vec<char> = chars.clone().take(4).collect();
10401                let (p, after): (Option<char>, &[char]) = if zero_intro {
10402                    (rest.first().copied(), rest.get(1..).unwrap_or(&[]))
10403                } else {
10404                    (Some(d), &rest[..])
10405                };
10406                // c:7166 `if (s[1] && s[2] && s[3])` — needs three chars after
10407                // `s`; the window then spans `s` plus two more.
10408                let win: &[char] = if after.len() >= 3 { &after[..2] } else { after };
10409                let mut vis: Vec<char> = Vec::with_capacity(3);
10410                if let Some(pc) = p {
10411                    vis.push(pc);
10412                }
10413                vis.extend_from_slice(win);
10414                // c:7170 `zstrtol(s + (*s == 'x'), &s, (*s == 'x') ? 16 : 8)`.
10415                let (base, skip_p) = if p == Some('x') { (16, 1) } else { (8, 0) };
10416                let parse_src: String = vis[skip_p.min(vis.len())..].iter().collect();
10417                let (num, tail) = zstrtol(&parse_src, base);
10418                let used_chars = parse_src[..parse_src.len() - tail.len()].chars().count();
10419                // Advance past what C consumed. `vis[0]` is the already-taken
10420                // match char unless the `\0` introducer moved `s` forward.
10421                let vis_used = skip_p + used_chars;
10422                let advance = if zero_intro { vis_used } else { vis_used.saturating_sub(1) };
10423                for _ in 0..advance {
10424                    chars.next();
10425                    consumed += 1;
10426                }
10427                let val = (num as u64 & 0xff) as u8;
10428                // c:Src/utils.c — the escape is one raw BYTE; metafy high
10429                // bytes (c:7289-7294) so `\377` unmetafies to a single 0xff,
10430                // not the UTF-8 pair c3 bf.
10431                if val < 0x80 {
10432                    result.push(val as char);
10433                } else {
10434                    result.push('\u{83}');
10435                    result.push(char::from(val ^ 32));
10436                }
10437                // c:7172-7173 — under GETKEY_PRINTF_PERCENT a numeric escape
10438                // producing `%` gets a second `%`.
10439                if (how & crate::ported::zsh_h::GETKEY_PRINTF_PERCENT as u32) != 0 && val == b'%' {
10440                    result.push('%');
10441                }
10442            }
10443            // c:utils.c:7029-7052 + c:7255-7275 — `\C` / `\M` set the
10444            // control / meta modifiers (bindkey-style key escapes), an
10445            // optional `-` separator, then the next base char (possibly
10446            // through chained `\C`/`\M`) gets the mask applied:
10447            // control → `& 0x9f` (or `\C-?` → 0x7f), meta → `| 0x80`.
10448            // Gated on GETKEY_EMACS (c:7031/7043 `if (how & GETKEY_EMACS)`):
10449            // print / print -b / $'…' have it, echo does not (so echo keeps
10450            // `\C-a` literal via the default arm). Mirrors the same handler
10451            // already present in `getkeystring` (utils.rs) and
10452            // `getkeystring_dollar_quote` (lex.rs); print's `getkeystring_with`
10453            // path lacked it, so `print "\C-a"` emitted literal `C-a`.
10454            Some(mod_letter @ ('C' | 'M')) if (how & GETKEY_EMACS) != 0 => {
10455                consumed += 1;
10456                // c:7034 `meta = 1 + control;  /* preserve the order of ^ and
10457                // meta */` — meta is a COUNT, not a flag: 2 when `\M` was seen
10458                // while control was already pending, which makes c:7261-7264
10459                // apply `|0x80` BEFORE the control mask instead of after. The
10460                // two orders differ for `?`: `\M-\C-?` is 0xff, `\C-\M-?` is
10461                // 0x9f. Booleans could not express that.
10462                // A `^` already seen (c:7194 `control = 1; continue;`) arrives
10463                // here as pending state, so `^\M-a` gives meta = 1 + 1 = 2.
10464                let mut control: u32 = if pending_control {
10465                    pending_control = false;
10466                    1
10467                } else {
10468                    0
10469                };
10470                let mut meta: u32 = 0;
10471                if mod_letter == 'C' {
10472                    control = 1; // c:7046
10473                } else {
10474                    meta = 1 + control; // c:7034
10475                }
10476                // c:7032-7033 / 7044-7045 — `if (s[1] == '-') s++;` consumes at
10477                // most ONE separator per modifier. Looping over a RUN of dashes
10478                // ate the base character of `\M--` (meta applied to `-`, 0xad),
10479                // leaving an empty binding.
10480                if chars.peek() == Some(&'-') {
10481                    chars.next();
10482                    consumed += 1;
10483                }
10484                // Chained `\C`/`\M`, and the caret form after a modifier.
10485                loop {
10486                    let mut iter_clone = chars.clone();
10487                    if iter_clone.next() == Some('\\') {
10488                        if let Some(nx) = iter_clone.next() {
10489                            if nx == 'C' || nx == 'M' {
10490                                chars.next(); // consume '\'
10491                                chars.next(); // consume C/M
10492                                consumed += 2;
10493                                if nx == 'C' {
10494                                    control = 1; // c:7046
10495                                } else {
10496                                    meta = 1 + control; // c:7034
10497                                }
10498                                if chars.peek() == Some(&'-') {
10499                                    chars.next();
10500                                    consumed += 1;
10501                                }
10502                                continue;
10503                            }
10504                        }
10505                    }
10506                    // c:7194 — `else if (*s == '^' && !control && (how &
10507                    // GETKEY_CTRL) && s[1]) { control = 1; continue; }`. C's
10508                    // `case 'M'` only sets `meta` and `continue`s, so the MAIN
10509                    // loop reaches this `^` handler with meta still pending:
10510                    // `\M-^?` is control+meta applied to `?` (0xff). This arm
10511                    // consumed the base char itself, so `^` became the base and
10512                    // `\M-^?` bound TWO bytes (0xde 0x3f) — breaking the very
10513                    // common `bindkey '\M-^?' backward-kill-word`.
10514                    if control == 0 && (how & GETKEY_CTRL) != 0 && chars.peek() == Some(&'^') {
10515                        let mut it2 = chars.clone();
10516                        it2.next();
10517                        if it2.next().is_some() {
10518                            // c:7194 `s[1]` guard
10519                            chars.next();
10520                            consumed += 1;
10521                            control = 1;
10522                            continue;
10523                        }
10524                    }
10525                    break;
10526                }
10527                // Read one base character (allowing nested simple escapes).
10528                let base: Option<char> = if chars.peek() == Some(&'\\') {
10529                    chars.next();
10530                    consumed += 1;
10531                    match chars.next() {
10532                        Some('n') => {
10533                            consumed += 1;
10534                            Some('\n')
10535                        }
10536                        Some('t') => {
10537                            consumed += 1;
10538                            Some('\t')
10539                        }
10540                        Some('r') => {
10541                            consumed += 1;
10542                            Some('\r')
10543                        }
10544                        Some('a') => {
10545                            consumed += 1;
10546                            Some('\x07')
10547                        }
10548                        Some('b') => {
10549                            consumed += 1;
10550                            Some('\x08')
10551                        }
10552                        Some('e') | Some('E') => {
10553                            consumed += 1;
10554                            Some('\x1b')
10555                        }
10556                        Some('f') => {
10557                            consumed += 1;
10558                            Some('\x0c')
10559                        }
10560                        Some('v') => {
10561                            consumed += 1;
10562                            Some('\x0b')
10563                        }
10564                        Some('\\') => {
10565                            consumed += 1;
10566                            Some('\\')
10567                        }
10568                        Some('\'') => {
10569                            consumed += 1;
10570                            Some('\'')
10571                        }
10572                        Some('"') => {
10573                            consumed += 1;
10574                            Some('"')
10575                        }
10576                        Some(other) => {
10577                            consumed += 1;
10578                            Some(other)
10579                        }
10580                        None => None,
10581                    }
10582                } else {
10583                    chars.next().inspect(|c| {
10584                        consumed += c.len_utf8();
10585                    })
10586                };
10587                if let Some(ch) = base {
10588                    let mut byte = ch as u32;
10589                    // c:7261-7264 — `if (meta == 2) { t[-1] |= 0x80; meta = 0; }`
10590                    // runs BEFORE the control mask: `\M` seen while control was
10591                    // already pending sets the high bit first.
10592                    if meta == 2 {
10593                        byte |= 0x80;
10594                    }
10595                    // c:7265-7271 — control mask (`\C-?` → 0x7f, else & 0x9f).
10596                    if control == 1 {
10597                        if byte == '?' as u32 {
10598                            byte = 0x7f;
10599                        } else {
10600                            byte &= 0x9f;
10601                        }
10602                    }
10603                    // c:7272-7275 — `if (meta) { t[-1] |= 0x80; }` — the
10604                    // meta==1 order, applied AFTER the mask.
10605                    if meta == 1 {
10606                        byte |= 0x80;
10607                    }
10608                    // c:7289-7294 — a masked byte >= 0x80 is metafied so it
10609                    // unmetafies back to the single raw byte on output.
10610                    if byte <= 0xff {
10611                        let b_ = byte as u8;
10612                        if b_ < 0x80 {
10613                            result.push(b_ as char);
10614                        } else {
10615                            result.push('\u{83}');
10616                            result.push(char::from(b_ ^ 32));
10617                        }
10618                    } else if let Some(c) = char::from_u32(byte) {
10619                        result.push(c);
10620                    }
10621                }
10622            }
10623            // c:utils.c:7180-7184 — default arm. With GETKEY_EMACS
10624            // set, drop the backslash; otherwise keep `\<char>`.
10625            Some(c) => {
10626                consumed += 1;
10627                // c:utils.c:7045 — `\c` under GETKEY_BACKSLASH_C
10628                // means TRUNCATE: drop the rest of the input,
10629                // suppress the trailing newline. Used by `echo`
10630                // (and `print` without -r). Set TLS flag so the
10631                // caller can detect + suppress the newline.
10632                if c == 'c' && (how & GETKEY_BACKSLASH_C) != 0 {
10633                    GETKEY_TRUNCATED.with(|cell| cell.set(true));
10634                    break;
10635                }
10636                if (how & GETKEY_EMACS) == 0 {
10637                    // c:utils.c:7181-7183 — backslash kept literal (no EMACS),
10638                    // so the string does not collapse here: undo the base
10639                    // GETKEY_UPDATE_OFFSET decrement applied above.
10640                    if miscadded {
10641                        if let Some(m) = misc.as_deref_mut() {
10642                            *m += 1;
10643                        }
10644                    }
10645                    result.push('\\');
10646                }
10647                result.push(c);
10648            }
10649            None => {
10650                result.push('\\');
10651            }
10652        }
10653    }
10654    (result, consumed)
10655}
10656
10657thread_local! {
10658    /// Sticky flag set by `getkeystring_with` when a `\c` escape
10659    /// truncates the input under `GETKEY_BACKSLASH_C`. The caller
10660    /// (bin_print / bin_echo) reads + clears via `getkey_truncated_take()`
10661    /// to suppress the trailing newline and skip any subsequent args
10662    /// in the print pass.
10663    pub static GETKEY_TRUNCATED: std::cell::Cell<bool> =
10664        const { std::cell::Cell::new(false) };
10665}
10666
10667/// Read + clear the `\c`-truncated flag set by the previous
10668/// `getkeystring_with` call. Returns true once after a truncation
10669/// fires, false otherwise.
10670pub fn getkey_truncated_take() -> bool {
10671    GETKEY_TRUNCATED.with(|c| {
10672        let v = c.get();
10673        c.set(false);
10674        v
10675    })
10676}
10677
10678/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
10679/// the C expression `fdtable[fd]` (read). Returns `FDT_UNUSED` for
10680/// any fd that has not been explicitly set, matching the C source's
10681/// post-`growfdtable` zero-fill behaviour at Src/utils.c:1979.
10682pub fn fdtable_get(fd: i32) -> i32 {
10683    // c:utils.c:fdtable[fd]
10684    if fd < 0 {
10685        return FDT_UNUSED;
10686    }
10687    let g = fdtable_lock().lock().unwrap();
10688    g.get(fd as usize).copied().unwrap_or(FDT_UNUSED)
10689}
10690
10691/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
10692/// the C statement `fdtable[fd] = kind;`. Inlines the `growfdtable`
10693/// call from Src/utils.c:1965 since C always invokes it immediately
10694/// before the assignment anyway. Also updates `MAX_ZSH_FD` to track
10695/// the highest assigned slot — C does this inside `check_fd_table`
10696/// at `Src/utils.c:1982` (`max_zsh_fd = fd;`). Previously the Rust
10697/// `fdtable_set` skipped this, leaving `max_zsh_fd = 0` after any
10698/// `addlockfd` / `addmodulefd` call, which broke `zcloselockfd`'s
10699/// `if (fd > max_zsh_fd)` guard.
10700pub fn fdtable_set(fd: i32, kind: i32) {
10701    // c:utils.c:fdtable[fd]
10702    if fd < 0 {
10703        return;
10704    }
10705    let mut g = fdtable_lock().lock().unwrap();
10706    if (fd as usize) >= g.len() {
10707        g.resize((fd as usize) + 1, FDT_UNUSED);
10708    }
10709    g[fd as usize] = kind;
10710    // c:1982 — `max_zsh_fd = fd;` (with `if (fd <= max_zsh_fd) return;`
10711    // guard at c:1971). Inline equivalent: bump only when this fd
10712    // exceeds the current max.
10713    let cur = MAX_ZSH_FD.load(Ordering::Relaxed);
10714    if fd > cur {
10715        MAX_ZSH_FD.store(fd, Ordering::Relaxed);
10716    }
10717}
10718
10719/// Port of `imeta()` macro from `Src/ztype.h:60` — `zistype(X, IMETA)`.
10720///
10721/// IMETA is set in the typtab at `Src/utils.c:4195-4201` for:
10722///   - `'\0'` (c:4195)
10723///   - `Meta` (0x83) (c:4196)
10724///   - `Marker` (0xa2) (c:4197)
10725///   - `Pound..=LAST_NORMAL_TOK` = `0x84..=0x9c` (c:4198)
10726///   - `Snull..=Nularg` = `0x9d..=0xa1` (c:4200)
10727///
10728/// The CANONICAL set is `{0x00, 0x83..=0xa2}`. The previous Rust
10729/// port used `b >= 0x83` (every byte 0x83..=0xff) which was WRONG
10730/// for bytes `0xa3..=0xff`: C `imeta()` returns false for those,
10731/// so metafy passes them through unchanged. Rust's broader range
10732/// caused `metafy` to corrupt UTF-8 multibyte content (e.g.,
10733/// `é` = `0xc3 0xa9`: both bytes are NOT IMETA in C, but Rust
10734/// escaped both as `Meta + (byte ^ 32)`, mangling the encoding
10735/// and breaking every downstream consumer that expects the raw
10736/// UTF-8 bytes to round-trip).
10737///
10738/// Use the closed-range form rather than the typtab lookup so
10739/// `metafy` / `imeta_byte` work in test contexts where the typtab
10740/// isn't initialised, AND so the fast-path doesn't go through a
10741/// Mutex lock per byte (`metafy` is called per-character on every
10742/// shell input line).
10743#[inline]
10744pub fn imeta_byte(b: u8) -> bool {
10745    // c:4195-4201 — canonical IMETA range.
10746    b == 0 || (0x83..=0xa2).contains(&b)
10747}
10748
10749// `pub fn convfloat` and `pub fn convfloat_underscore` re-export
10750// wrappers DELETED per PORT.md Rule C. C defines both in
10751// `Src/params.c:5690` and `:5765` — the canonical Rust home is
10752// `src/ported/params.rs` (`convfloat` at line 7356,
10753// `convfloat_underscore` matching). Caller convenience cost is
10754// `use crate::ported::params::convfloat;` instead of `utils::`;
10755// no Rust-side re-export needed.
10756
10757// ===========================================================
10758// Methods moved verbatim from src/ported/vm_helper because their
10759// C counterpart's source file maps 1:1 to this Rust module.
10760// Phase: drift
10761// ===========================================================
10762
10763// BEGIN moved-from-exec-rs
10764// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
10765
10766// END moved-from-exec-rs
10767
10768// ===========================================================
10769// Free ported moved verbatim from src/ported/vm_helper.
10770// ===========================================================
10771// BEGIN moved-from-exec-rs (free ported)
10772pub(crate) fn base64_decode(s: &str) -> Vec<u8> {
10773    let decode_char = |c: u8| -> Option<u8> {
10774        match c {
10775            b'A'..=b'Z' => Some(c - b'A'),
10776            b'a'..=b'z' => Some(c - b'a' + 26),
10777            b'0'..=b'9' => Some(c - b'0' + 52),
10778            b'+' => Some(62),
10779            b'/' => Some(63),
10780            _ => None,
10781        }
10782    };
10783    let bytes = s.as_bytes();
10784    let mut out = Vec::with_capacity(s.len() / 4 * 3);
10785    let mut i = 0;
10786    while i + 4 <= bytes.len() {
10787        let chunk = &bytes[i..i + 4];
10788        let pad = chunk.iter().filter(|&&c| c == b'=').count();
10789        let v0 = decode_char(chunk[0]).unwrap_or(0) as u32;
10790        let v1 = decode_char(chunk[1]).unwrap_or(0) as u32;
10791        let v2 = decode_char(chunk[2]).unwrap_or(0) as u32;
10792        let v3 = decode_char(chunk[3]).unwrap_or(0) as u32;
10793        let n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
10794        out.push(((n >> 16) & 0xff) as u8);
10795        if pad < 2 {
10796            out.push(((n >> 8) & 0xff) as u8);
10797        }
10798        if pad < 1 {
10799            out.push((n & 0xff) as u8);
10800        }
10801        i += 4;
10802    }
10803    out
10804}
10805// END moved-from-exec-rs (free ported)
10806
10807// ===========================================================
10808// Utility helpers moved from src/ported/vm_helper.
10809// All correspond to Src/utils.c logic (path/string/bslashquote helpers).
10810// ===========================================================
10811
10812// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10813// ─── RUST-ONLY ACCESSORS ───
10814//
10815// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10816// RwLock<T>>` globals declared above. C zsh uses direct global
10817// access; Rust needs these wrappers because `OnceLock::get_or_init`
10818// is the only way to lazily construct shared state. These ported sit
10819// here so the body of this file reads in C source order without
10820// the accessor wrappers interleaved between real port ported.
10821// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10822
10823// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10824// ─── RUST-ONLY ACCESSORS ───
10825//
10826// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10827// RwLock<T>>` globals declared above. C zsh uses direct global
10828// access; Rust needs these wrappers because `OnceLock::get_or_init`
10829// is the only way to lazily construct shared state. These ported sit
10830// here so the body of this file reads in C source order without
10831// the accessor wrappers interleaved between real port ported.
10832// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10833
10834// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10835// ─── RUST-ONLY ACCESSORS ───
10836//
10837// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10838// RwLock<T>>` globals declared above. C zsh uses direct global
10839// access; Rust needs these wrappers because `OnceLock::get_or_init`
10840// is the only way to lazily construct shared state. These ported sit
10841// here so the body of this file reads in C source order without
10842// the accessor wrappers interleaved between real port ported.
10843// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10844
10845// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10846// ─── RUST-ONLY ACCESSORS ───
10847//
10848// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10849// RwLock<T>>` globals declared above. C zsh uses direct global
10850// access; Rust needs these wrappers because `OnceLock::get_or_init`
10851// is the only way to lazily construct shared state. These ported sit
10852// here so the body of this file reads in C source order without
10853// the accessor wrappers interleaved between real port ported.
10854// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10855
10856// WARNING: NOT IN UTILS.C — Rust-only OnceLock get-or-init
10857// helpers. C dereferences each global directly.
10858fn scriptname_lock() -> &'static Mutex<Option<String>> {
10859    SCRIPTNAME.get_or_init(|| Mutex::new(None))
10860}
10861
10862// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10863fn argzero_lock() -> &'static Mutex<Option<String>> {
10864    ARGZERO.get_or_init(|| Mutex::new(None))
10865}
10866
10867// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10868fn scriptfilename_lock() -> &'static Mutex<Option<String>> {
10869    SCRIPTFILENAME.get_or_init(|| Mutex::new(None))
10870}
10871
10872// WARNING: NOT IN UTILS.C — Rust-only OnceLock accessor for `posixzero`.
10873// C's `posixzero` lives in params.c:76; the canonical storage lives
10874// here for OnceLock initialisation parity with `argzero`.
10875fn posixzero_lock() -> &'static Mutex<Option<String>> {
10876    POSIXZERO.get_or_init(|| Mutex::new(None))
10877}
10878
10879// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10880pub fn noerrs_lock() -> &'static Mutex<i32> {
10881    NOERRS.get_or_init(|| Mutex::new(0))
10882}
10883
10884// `locallevel_lock` removed — duplicate of canonical
10885// `LOCALLEVEL` (port of params.c:54). All
10886// accessors here now route through the canonical AtomicI32.
10887// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10888fn lineno_lock() -> &'static Mutex<i32> {
10889    LINENO.get_or_init(|| Mutex::new(0))
10890}
10891
10892// WARNING: NOT IN UTILS.C — Rust-only cache for the SHINSTDIN
10893// option flag so error-emission doesn't pull in the option-table.
10894fn shinstdin_lock() -> &'static Mutex<bool> {
10895    SHINSTDIN_OPT.get_or_init(|| Mutex::new(false))
10896}
10897
10898/// !!! RUST-ONLY HELPER — see WARNING block above. C source uses bare
10899/// `unsigned char *fdtable` global from Src/utils.c:~63.
10900fn fdtable_lock() -> &'static Mutex<Vec<i32>> {
10901    FDTABLE.get_or_init(|| Mutex::new(Vec::new()))
10902}
10903
10904#[cfg(test)]
10905mod tests {
10906    use super::*;
10907
10908    /// checkmailpath port (utils.c:1620). Drives mtime/atime/size through
10909    /// `libc::utimes` so the new-mail condition is deterministic in headless CI.
10910    #[test]
10911    fn checkmailpath_new_mail_conditions() {
10912        use std::io::Write as _;
10913        let _g = crate::test_util::global_state_lock();
10914
10915        // Unique temp file with content (size != 0).
10916        let dir = std::env::temp_dir();
10917        let path = dir.join(format!("zshrs_mailtest_{}", std::process::id()));
10918        {
10919            let mut f = std::fs::File::create(&path).unwrap();
10920            f.write_all(b"new message\n").unwrap();
10921        }
10922        let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
10923        // atime (times[0]) <= mtime (times[1]); both well past 0.
10924        let set_times = |atime: i64, mtime: i64| {
10925            let tv = [
10926                libc::timeval {
10927                    tv_sec: atime as libc::time_t,
10928                    tv_usec: 0,
10929                },
10930                libc::timeval {
10931                    tv_sec: mtime as libc::time_t,
10932                    tv_usec: 0,
10933                },
10934            ];
10935            assert_eq!(unsafe { libc::utimes(cpath.as_ptr(), tv.as_ptr()) }, 0);
10936        };
10937
10938        let saved_shout = *crate::ported::init::shout.lock().unwrap();
10939        let saved_lmc = LAST_MAILCHECK.load(Ordering::Relaxed);
10940        *crate::ported::init::shout.lock().unwrap() = 1; // interactive
10941        LAST_MAILCHECK.store(0, Ordering::Relaxed);
10942
10943        let p = path.to_str().unwrap().to_string();
10944
10945        // c:1671-1675 — unread + newer than last check → "You have new mail."
10946        set_times(1_000_000, 2_000_000);
10947        assert_eq!(
10948            checkmailpath(&[p.clone()]),
10949            vec!["You have new mail.".to_string()]
10950        );
10951
10952        // c:1676-1698 — `PATH?message`: the message (here a literal) is emitted.
10953        assert_eq!(
10954            checkmailpath(&[format!("{}?custom note", p)]),
10955            vec!["custom note".to_string()]
10956        );
10957
10958        // c:1672 — mtime older than the last check → nothing.
10959        LAST_MAILCHECK.store(9_000_000, Ordering::Relaxed);
10960        assert!(checkmailpath(&[p.clone()]).is_empty());
10961
10962        // c:1670 — non-interactive (no shout) → nothing, even when fresh.
10963        LAST_MAILCHECK.store(0, Ordering::Relaxed);
10964        *crate::ported::init::shout.lock().unwrap() = 0;
10965        assert!(checkmailpath(&[p.clone()]).is_empty());
10966
10967        // c:1634-1636 — empty component is reported (stderr), yields no message.
10968        *crate::ported::init::shout.lock().unwrap() = 1;
10969        assert!(checkmailpath(&["?orphan".to_string()]).is_empty());
10970
10971        *crate::ported::init::shout.lock().unwrap() = saved_shout;
10972        LAST_MAILCHECK.store(saved_lmc, Ordering::Relaxed);
10973        let _ = std::fs::remove_file(&path);
10974    }
10975
10976    /// zsh_errno_msg / zerrmsg `%e` rendering (utils.c:348-365). EINTR is the
10977    /// literal "interrupt"; EIO is verbatim strerror (keeps its capital); every
10978    /// other errno lowercases the first letter (`tulower`). The old
10979    /// io::Error-based path kept the capital and appended " (os error N)".
10980    #[test]
10981    fn zsh_errno_msg_matches_c_percent_e() {
10982        // c:355-358 — EINTR → "interrupt".
10983        assert_eq!(zsh_errno_msg(libc::EINTR), "interrupt");
10984
10985        // c:366 — non-EIO errno lowercases the first letter and never carries
10986        // the Rust "(os error N)" suffix.
10987        let acc = zsh_errno_msg(libc::EACCES);
10988        assert!(
10989            acc.chars().next().is_some_and(|c| c.is_lowercase()),
10990            "EACCES first letter must be lowercased: {acc:?}"
10991        );
10992        assert!(
10993            !acc.contains("(os error"),
10994            "must be strerror, not io::Error: {acc:?}"
10995        );
10996
10997        // c:359-364 — EIO is emitted verbatim (its message reads wrong
10998        // lowercased), so its first letter stays capitalized.
10999        let eio = zsh_errno_msg(libc::EIO);
11000        assert!(
11001            eio.chars().next().is_some_and(|c| c.is_uppercase()),
11002            "EIO must be verbatim strerror (capitalized): {eio:?}"
11003        );
11004    }
11005
11006    /// c:351-354 — zerrmsg's `%e` arm sets errflag |= ERRFLAG_ERROR on EINTR.
11007    #[test]
11008    fn zerrmsg_eintr_sets_errflag() {
11009        let _g = crate::test_util::global_state_lock();
11010        let saved = errflag.load(Ordering::Relaxed);
11011        errflag.store(0, Ordering::Relaxed);
11012        zerrmsg("test", Some(libc::EINTR)); // writes to stderr; side-effect is the flag
11013        assert_ne!(
11014            errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
11015            0,
11016            "EINTR must set ERRFLAG_ERROR"
11017        );
11018        // A non-EINTR errno must NOT set the flag.
11019        errflag.store(0, Ordering::Relaxed);
11020        zerrmsg("test", Some(libc::EACCES));
11021        assert_eq!(errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR, 0);
11022        errflag.store(saved, Ordering::Relaxed);
11023    }
11024
11025    /// zjoin port (utils.c:3622). ASCII delimiters join byte-for-byte; a meta
11026    /// delimiter (NUL is `imeta` per inittyptab c:4195) must be written in
11027    /// metafied form (`Meta` + `delim^32`), never as a raw byte.
11028    #[test]
11029    fn zjoin_ascii_and_meta_delims() {
11030        let v = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
11031
11032        // c:3632-3641 — ordinary (non-meta) delimiter: plain interposition.
11033        assert_eq!(zjoin(&v(&["a", "b", "c"]), ' '), "a b c");
11034        assert_eq!(zjoin(&v(&["foo"]), ':'), "foo"); // single elem, no delim
11035                                                     // c:3629-3630 — empty array → "".
11036        assert_eq!(zjoin(&v(&[]), ' '), "");
11037
11038        // c:3634-3637 — NUL is imeta, so the separator is metafied, NOT a raw
11039        // NUL byte (the prior `arr.join("\0")` bug). The Meta byte (0x83) can't
11040        // survive the UTF-8 String boundary, but the raw NUL must be gone.
11041        let joined = zjoin(&v(&["a", "b"]), '\0');
11042        assert!(
11043            !joined.as_bytes().contains(&0u8),
11044            "NUL delim must be metafied, not emitted as a raw NUL byte: {joined:?}"
11045        );
11046        assert_ne!(joined, "a\u{0}b", "must not raw-join on a meta delimiter");
11047        assert!(joined.starts_with('a') && joined.ends_with('b'));
11048    }
11049
11050    /// findsep port (utils.c:3784). Pure function — no global/cwd state.
11051    #[test]
11052    fn findsep_default_ifs_separators() {
11053        inittyptab(); // ISEP bits are set by the type table (c:4155)
11054                      // c:3814-3817 — advance to the first ISEP char, return 1.
11055        let mut s = "foo bar".to_string();
11056        let mut pos = 0usize;
11057        assert_eq!(findsep(&mut s, &mut pos, None, false), 1);
11058        assert_eq!(pos, 3); // points at the space
11059        assert_eq!(&s[..pos], "foo");
11060
11061        // c:3821 — already sitting on a separator: return 0, no advance.
11062        let mut s = " foo".to_string();
11063        let mut pos = 0usize;
11064        assert_eq!(findsep(&mut s, &mut pos, None, false), 0);
11065        assert_eq!(pos, 0);
11066    }
11067
11068    #[test]
11069    fn findsep_quote_strips_escaped_separator() {
11070        inittyptab(); // ISEP bits are set by the type table (c:4155)
11071                      // c:3795-3804 — `\<sep>` is not a separator; the backslash is
11072                      // stripped in place and the bare char is consumed into the word.
11073        let mut s = "foo\\ bar".to_string(); // foo<bslash><space>bar
11074        let mut pos = 0usize;
11075        let r = findsep(&mut s, &mut pos, None, true);
11076        assert_eq!(r, 1);
11077        assert_eq!(s, "foo bar"); // backslash removed
11078        assert_eq!(pos, s.len()); // whole thing is one word, no real sep
11079    }
11080
11081    #[test]
11082    fn findsep_quote_collapses_double_backslash() {
11083        // c:3796-3799 — `\\` collapses to a single literal backslash.
11084        let mut s = "a\\\\b".to_string(); // a <bslash><bslash> b
11085        let mut pos = 0usize;
11086        let r = findsep(&mut s, &mut pos, None, true);
11087        assert_eq!(r, 1);
11088        assert_eq!(s, "a\\b"); // one backslash left
11089        assert_eq!(pos, s.len()); // no separator present
11090    }
11091
11092    #[test]
11093    fn findsep_literal_multichar_separator() {
11094        // c:3836-3841 — explicit multi-byte literal separator.
11095        let mut s = "a::b".to_string();
11096        let mut pos = 0usize;
11097        assert_eq!(findsep(&mut s, &mut pos, Some(b"::"), false), 1);
11098        assert_eq!(pos, 1); // points at the "::"
11099        assert_eq!(&s[..pos], "a");
11100
11101        // c:3844 — separator absent → -1.
11102        let mut s = "abc".to_string();
11103        let mut pos = 0usize;
11104        assert_eq!(findsep(&mut s, &mut pos, Some(b"x"), false), -1);
11105    }
11106
11107    #[test]
11108    fn findsep_empty_separator_advances_one_char() {
11109        // c:3825-3834 — empty sep just steps past one character.
11110        let mut s = "ab".to_string();
11111        let mut pos = 0usize;
11112        assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), 1);
11113        assert_eq!(pos, 1);
11114
11115        let mut s = String::new();
11116        let mut pos = 0usize;
11117        assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), -1); // c:3833
11118    }
11119
11120    /// c:4033 — `subst_string_by_func` returns `getaparam("reply")`
11121    /// after the hook function finishes. The previous Rust port read
11122    /// `env::var("reply")` and split on NUL — wrong because `reply`
11123    /// is a shell-local PM_ARRAY in paramtab, never exported. This
11124    /// pin sets `reply` via `setaparam` (the paramtab write path)
11125    /// and exercises `subst_string_by_hook` end-to-end isn't viable
11126    /// in unit tests without a function-name hook, so we exercise
11127    /// just the getaparam plumbing.
11128    #[test]
11129    fn getaparam_reads_reply_from_paramtab_not_env() {
11130        let _g = crate::test_util::global_state_lock();
11131        // Stash any prior `reply` value.
11132        let saved = getaparam("reply");
11133
11134        // Write a known reply array via the canonical setaparam path.
11135        let payload = vec!["abbreviated".to_string(), "11".to_string()];
11136        let _ = setaparam("reply", payload.clone());
11137
11138        // The reply array must be reachable via getaparam — not env.
11139        // (env::var would return Err because setaparam never exports
11140        //  an un-flagged array to env.)
11141        assert_eq!(
11142            getaparam("reply"),
11143            Some(payload),
11144            "getaparam(\"reply\") must return paramtab array"
11145        );
11146
11147        // Restore.
11148        let _ = setaparam("reply", saved.unwrap_or_default());
11149    }
11150
11151    /// c:1133-1134 — `finddir` reads the global `home` variable (the
11152    /// canonical `$HOME` storage, not `getenv("HOME")`). The global
11153    /// is updated by `homesetfn` (Src/params.c:5118) whenever the user
11154    /// assigns `HOME=...` inside the shell.
11155    /// Regression target: a previous Rust port read `env::var("HOME")`
11156    /// so an in-shell `HOME=...` assignment would not retarget
11157    /// `~`-abbreviation until the user re-exported HOME.
11158    #[test]
11159    fn finddir_uses_paramtab_home_not_env() {
11160        let _g = crate::test_util::global_state_lock();
11161        // C's `homesetfn` (params.c:5118) UNUSED(Param pm) — the
11162        // function ignores its param-pointer argument and writes the
11163        // canonical `char *home` global directly. zshrs's port mirrors
11164        // that: `params::homesetfn(_pm, x)` ignores _pm and updates
11165        // `home_lock()`. So we pass a stack-default param; the paramtab
11166        // wiring (PM_SPECIAL dispatch) is not on the test path.
11167        let mut pm = crate::ported::zsh_h::param::default();
11168        let saved = crate::ported::params::homegetfn(&pm);
11169        let sentinel = "/tmp/zshrs-finddir-pin".to_string();
11170        homesetfn(&mut pm, sentinel.clone());
11171
11172        // `/tmp/zshrs-finddir-pin/x` must abbreviate to `~/x`.
11173        let abbrev = finddir(&format!("{}/x", sentinel));
11174        assert_eq!(
11175            abbrev.as_deref(),
11176            Some("~/x"),
11177            "finddir must consult canonical HOME (got {:?})",
11178            abbrev
11179        );
11180
11181        // Restore.
11182        homesetfn(&mut pm, saved);
11183    }
11184
11185    #[test]
11186    fn test_sepsplit() {
11187        let _g = crate::test_util::global_state_lock();
11188        assert_eq!(sepsplit("a:b:c", Some(":"), false), vec!["a", "b", "c"]);
11189        assert_eq!(sepsplit("a::b", Some(":"), false), vec!["a", "b"]);
11190        assert_eq!(sepsplit("a::b", Some(":"), true), vec!["a", "", "b"]);
11191    }
11192
11193    #[test]
11194    fn test_unmetafy_no_meta_byte_passes_through() {
11195        let _g = crate::test_util::global_state_lock();
11196        // No Meta byte → buffer unchanged, length unchanged.
11197        let mut buf = b"hello".to_vec();
11198        let n = unmetafy(&mut buf);
11199        assert_eq!(n, 5);
11200        assert_eq!(&buf, b"hello");
11201    }
11202
11203    #[test]
11204    fn test_unmetafy_collapses_meta_escapes() {
11205        let _g = crate::test_util::global_state_lock();
11206        // C: Meta byte (0x83) followed by `'a' ^ 32` (0x41 = 'A')
11207        // unmetafies to a single byte 'a' (0x61).
11208        // i.e. {0x83, 'a' ^ 32} → {'a'}.
11209        let mut buf = vec![0x83, b'a' ^ 32];
11210        let n = unmetafy(&mut buf);
11211        assert_eq!(n, 1);
11212        assert_eq!(buf, vec![b'a']);
11213    }
11214
11215    #[test]
11216    fn test_unmetafy_mixed_prefix_then_meta() {
11217        let _g = crate::test_util::global_state_lock();
11218        // Plain prefix, then Meta-escaped 0xFF (0x83, 0xFF ^ 32 = 0xDF).
11219        let mut buf = vec![b'X', b'Y', 0x83, 0xFF ^ 32, b'Z'];
11220        let n = unmetafy(&mut buf);
11221        assert_eq!(n, 4);
11222        assert_eq!(buf, vec![b'X', b'Y', 0xFF, b'Z']);
11223    }
11224
11225    #[test]
11226    fn test_unmetafy_returns_self_value() {
11227        let _g = crate::test_util::global_state_lock();
11228        // C returns `s` (the buffer) for chaining; Rust returns
11229        // the new length. Verify length matches a call that
11230        // collapses two Meta-escapes.
11231        let mut buf = vec![
11232            b'A',
11233            0x83,
11234            b'B' ^ 32, // → 'B'
11235            0x83,
11236            b'C' ^ 32, // → 'C'
11237            b'D',
11238        ];
11239        let n = unmetafy(&mut buf);
11240        assert_eq!(n, 4);
11241        assert_eq!(buf, b"ABCD".to_vec());
11242    }
11243
11244    #[test]
11245    fn test_imeta_byte_threshold() {
11246        let _g = crate::test_util::global_state_lock();
11247        // Canonical IMETA per Src/utils.c:4195-4201:
11248        //   - 0x00 (c:4195)
11249        //   - 0x83..=0xa2 (Meta through Marker — c:4196-4200)
11250        //
11251        // The previous version of this test asserted `imeta_byte(0xFF) == true`
11252        // based on the WRONG `b >= Meta` predicate the Rust port
11253        // originally used. C `imeta()` reads the typtab; 0xa3..=0xff
11254        // have NO typtab assignment so they're NOT IMETA, and
11255        // `metafy` must NOT escape them.
11256        assert!(imeta_byte(0x00), "c:4195 — NUL is IMETA");
11257        assert!(!imeta_byte(0x82), "0x82 is NOT IMETA (below Meta)");
11258        assert!(imeta_byte(Meta), "c:4196 — Meta (0x83) is IMETA");
11259        assert!(imeta_byte(0xa2), "c:4197 — Marker (0xa2) is IMETA");
11260        // c:4198-4200 upper bound — Nularg is the last IMETA byte.
11261        assert!(imeta_byte(0xa1), "c:4200 — Nularg (0xa1) is IMETA");
11262        // 0xa3..=0xff are NOT IMETA — UTF-8 multi-byte content
11263        // must pass through `metafy` unchanged.
11264        assert!(!imeta_byte(0xa3), "0xa3 NOT IMETA (above Marker)");
11265        assert!(!imeta_byte(0xc3), "0xc3 NOT IMETA (UTF-8 'é' lead)");
11266        assert!(!imeta_byte(0xFF), "0xFF NOT IMETA");
11267    }
11268
11269    #[test]
11270    fn test_meta_constant_value() {
11271        let _g = crate::test_util::global_state_lock();
11272        // Locked at 0x83 by Src/zsh.h. If this test fails, zsh
11273        // bumped the Meta sentinel and the encoding mapping needs
11274        // a full audit.
11275        assert_eq!(Meta, 0x83);
11276    }
11277
11278    #[test]
11279    #[cfg(unix)]
11280    fn test_mode_to_octal_canonical_bits() {
11281        let _g = crate::test_util::global_state_lock();
11282        // rwx for owner = 0o700.
11283        let mode = (S_IRUSR | S_IWUSR | S_IXUSR) as u32;
11284        assert_eq!(mode_to_octal(mode), 0o700);
11285        // rwx all = 0o777.
11286        let all = (S_IRUSR | S_IWUSR | S_IXUSR) as u32 * (1 + 8 + 64);
11287        // Use libc constants individually for portability.
11288        let m = (S_IRUSR
11289            | S_IWUSR
11290            | S_IXUSR
11291            | S_IRGRP
11292            | S_IWGRP
11293            | S_IXGRP
11294            | S_IROTH
11295            | S_IWOTH
11296            | S_IXOTH) as u32;
11297        assert_eq!(mode_to_octal(m), 0o777);
11298        let _ = all;
11299    }
11300
11301    #[test]
11302    #[cfg(unix)]
11303    fn test_mode_to_octal_setuid_setgid_sticky() {
11304        let _g = crate::test_util::global_state_lock();
11305        assert_eq!(mode_to_octal(S_ISUID as u32), 0o4000);
11306        assert_eq!(mode_to_octal(S_ISGID as u32), 0o2000);
11307        assert_eq!(mode_to_octal(S_ISVTX as u32), 0o1000);
11308        // All three: 0o7000.
11309        let all = (S_ISUID | S_ISGID | S_ISVTX) as u32;
11310        assert_eq!(mode_to_octal(all), 0o7000);
11311    }
11312
11313    #[test]
11314    #[cfg(unix)]
11315    fn test_mailstat_plain_file_returns_native_stat() {
11316        let _g = crate::test_util::global_state_lock();
11317        // Plain file path → *st fields mirror native stat,
11318        // not the maildir aggregation.
11319        let mut st: libc::stat = unsafe { std::mem::zeroed() };
11320        let rc = mailstat("/etc/hosts", &mut st);
11321        if rc == 0 {
11322            assert_eq!(
11323                st.st_nlink as u64,
11324                fs::metadata("/etc/hosts").unwrap().nlink()
11325            );
11326        }
11327    }
11328
11329    #[test]
11330    fn test_mailstat_nonexistent_returns_neg1() {
11331        let _g = crate::test_util::global_state_lock();
11332        let mut st: libc::stat = unsafe { std::mem::zeroed() };
11333        assert_eq!(mailstat("/nonexistent/path/does/not/exist", &mut st), -1);
11334    }
11335
11336    #[test]
11337    fn test_mailstat_directory_without_maildir_subdirs() {
11338        let _g = crate::test_util::global_state_lock();
11339        // /tmp is a directory but not a maildir (no cur/tmp/new) —
11340        // returns the partial aggregate (top dir's atime/mtime,
11341        // size=0 since cur/ wasn't found before we'd start summing).
11342        let mut st: libc::stat = unsafe { std::mem::zeroed() };
11343        let rc = mailstat("/tmp", &mut st);
11344        assert_eq!(rc, 0);
11345        assert_eq!(st.st_nlink, 1);
11346        assert_eq!(st.st_size, 0);
11347        assert_eq!(st.st_blocks, 0);
11348        // S_IFDIR bit should be cleared, S_IFREG set.
11349        #[cfg(unix)]
11350        {
11351            assert_eq!(st.st_mode & libc::S_IFDIR, 0);
11352            assert_ne!(st.st_mode & libc::S_IFREG, 0);
11353        }
11354    }
11355
11356    #[test]
11357    fn test_dupstrpfx_byte_counted() {
11358        let _g = crate::test_util::global_state_lock(); // c:161
11359                                                        // 5 bytes of ASCII = 5 chars, identical. Canonical
11360                                                        // port lives at `string.rs:161`; pin from utils.rs's test
11361                                                        // module via the qualified path.
11362        assert_eq!(dupstrpfx("hello", 3), "hel"); // c:161
11363        assert_eq!(dupstrpfx("hi", 10), "hi"); // c:161
11364        assert_eq!(dupstrpfx("anything", 0), ""); // c:161
11365    }
11366
11367    #[test]
11368    fn test_metafy_passes_through_ascii() {
11369        let _g = crate::test_util::global_state_lock();
11370        // ASCII bytes (< 0x83) stay untouched.
11371        assert_eq!(metafy("hello"), "hello");
11372        assert_eq!(metafy(""), "");
11373    }
11374
11375    #[test]
11376    fn test_metafy_imeta_predicate_matches_c_macro() {
11377        let _g = crate::test_util::global_state_lock();
11378        // Canonical C IMETA per Src/utils.c:4195-4201:
11379        //   - 0x00 (c:4195)
11380        //   - 0x83 (Meta, c:4196)
11381        //   - 0x84..=0x9c (Pound..LAST_NORMAL_TOK=Bang, c:4198)
11382        //   - 0x9d..=0xa1 (Snull..Nularg, c:4200)
11383        //   - 0xa2 (Marker, c:4197)
11384        //
11385        // The closed set is {0x00, 0x83..=0xa2}. Every other byte
11386        // (0x01..=0x82, 0xa3..=0xff) is NOT IMETA in C and so must
11387        // NOT be Meta-encoded by `metafy`. The previous Rust
11388        // hardcoded predicate `b >= 0x83` falsely marked
11389        // 0xa3..=0xff as IMETA, corrupting UTF-8 multibyte
11390        // content.
11391
11392        // NUL is IMETA (c:4195).
11393        assert!(imeta_byte(0x00), "c:4195 — '\\0' IS imeta");
11394
11395        // 0x01..=0x82 are NOT IMETA (no typtab assignment for them).
11396        for b in 0x01u8..=0x82 {
11397            assert!(!imeta_byte(b), "byte {:#x} should NOT be imeta", b);
11398        }
11399
11400        // 0x83..=0xa2 ARE IMETA (the canonical full range).
11401        for b in 0x83u8..=0xa2 {
11402            assert!(imeta_byte(b), "c:4196-4200 — byte {:#x} IS imeta", b);
11403        }
11404
11405        // 0xa3..=0xff are NOT IMETA. C `imeta()` returns false
11406        // for these so `metafy` must pass them through unchanged.
11407        // UTF-8 continuation bytes (0x80..=0xbf) and multi-byte
11408        // leads (0xc0..=0xff) live here and must round-trip.
11409        for b in 0xa3u8..=0xff {
11410            assert!(
11411                !imeta_byte(b),
11412                "byte {:#x} should NOT be imeta (no c:4195-4201 assignment)",
11413                b
11414            );
11415        }
11416    }
11417
11418    /// Pin: `metafy` passes UTF-8 multibyte bytes through unchanged
11419    /// when they fall outside the canonical IMETA range. Previously
11420    /// the broader `b >= 0x83` predicate corrupted every UTF-8
11421    /// continuation byte and lead byte that wasn't a token marker.
11422    #[test]
11423    fn metafy_preserves_utf8_high_bytes_outside_imeta_range() {
11424        let _g = crate::test_util::global_state_lock();
11425        // 'é' = U+00E9 = UTF-8 0xc3 0xa9. Both bytes are >= 0x83 BUT
11426        // both are also > 0xa2, so they're NOT IMETA per the typtab.
11427        // C `metafy` passes them through unchanged.
11428        let input = std::str::from_utf8(&[0xC3, 0xA9]).unwrap();
11429        let out = metafy(input);
11430        // Should round-trip the two bytes exactly (no Meta escape).
11431        let out_bytes = out.as_bytes();
11432        assert_eq!(
11433            out_bytes,
11434            &[0xC3, 0xA9],
11435            "c:4196-4200 — UTF-8 bytes 0xc3/0xa9 outside IMETA range must pass through"
11436        );
11437    }
11438
11439    #[test]
11440    fn test_ztrcmp_meta_aware() {
11441        let _g = crate::test_util::global_state_lock();
11442        // Two identical metafied strings → Equal.
11443        assert_eq!(ztrcmp("foo", "foo"), std::cmp::Ordering::Equal);
11444        // "foo" < "foz".
11445        assert_eq!(ztrcmp("foo", "foz"), std::cmp::Ordering::Less);
11446        // Prefix comparison: shorter < longer.
11447        assert_eq!(ztrcmp("foo", "foobar"), std::cmp::Ordering::Less);
11448        // Meta-encoded comparison: {0x83, 'a'^32} should compare as 'a'.
11449        let s_meta = unsafe { std::str::from_utf8_unchecked(&[0x83, b'a' ^ 32]) };
11450        let s_plain = "a";
11451        // Meta-encoded "a" should compare equal to plain "a".
11452        assert_eq!(ztrcmp(s_meta, s_plain), std::cmp::Ordering::Equal);
11453    }
11454
11455    #[test]
11456    fn test_skipwsep_skips_runs() {
11457        let _g = crate::test_util::global_state_lock();
11458        // 3 spaces + 'x' → returns ("x", 3).
11459        let (rest, n) = skipwsep("   x");
11460        assert_eq!(rest, "x");
11461        assert_eq!(n, 3);
11462        // No leading whitespace → 0 skipped.
11463        let (rest, n) = skipwsep("foo");
11464        assert_eq!(rest, "foo");
11465        assert_eq!(n, 0);
11466        // Mix of space/tab/newline.
11467        let (rest, n) = skipwsep(" \t\nbar");
11468        assert_eq!(rest, "bar");
11469        assert_eq!(n, 3);
11470    }
11471
11472    #[test]
11473    fn test_imeta_macro_threshold() {
11474        let _g = crate::test_util::global_state_lock(); // c:60
11475                                                        // `Src/ztype.h:60` `imeta(X) zistype(X, IMETA)` — typtab-driven
11476                                                        // predicate. Per `Src/utils.c:4195-4201`, IMETA is set on:
11477                                                        // NUL, Meta=0x83, Marker=0xa2, and the Pound..Nularg ITOK range
11478                                                        // (0x84..=0xa2). Routes through canonical
11479                                                        // `ztype_h::imeta(u8)`; init the typtab first since the
11480                                                        // canonical port reads through it.
11481        inittyptab(); // c:4148
11482        assert!(imeta(0x00), "c:4195 — NUL is IMETA"); // c:4195
11483        assert!(imeta(Meta), "c:4196 — Meta (0x83) is IMETA"); // c:4196
11484        assert!(imeta(0xa2), "c:4197 — Marker (0xa2) is IMETA"); // c:4197
11485        assert!(
11486            imeta(0x84),
11487            "c:4199-4201 — Pound (0x84) is IMETA via ITOK range"
11488        );
11489        assert!(
11490            imeta(0x9b),
11491            "c:4199-4201 — Dash sentinel within ITOK range is IMETA"
11492        );
11493        assert!(!imeta(0x82), "c:4170 — 0x82 is ICNTRL, not IMETA"); // c:4170
11494        assert!(!imeta(0xa3), "byte 0xa3 is past Marker — NOT IMETA");
11495        assert!(
11496            !imeta(0xff),
11497            "byte 0xff is NOT IMETA (the prior `>= Meta` over-report)"
11498        );
11499        assert!(!imeta(b' '), "space is not IMETA");
11500        assert!(!imeta(b'A'), "'A' is not IMETA");
11501    }
11502
11503    #[test]
11504    fn test_unmeta_routes_through_unmetafy() {
11505        let _g = crate::test_util::global_state_lock();
11506        // unmeta wraps the in-place unmetafy via a byte-vector
11507        // copy; the no-Meta fast path returns the source as-is.
11508        assert_eq!(unmeta("plain"), "plain");
11509    }
11510
11511    #[test]
11512    fn test_iwsep_includes_newline() {
11513        let _g = crate::test_util::global_state_lock(); // c:61
11514                                                        // The previous port omitted '\n' which broke wordcount on
11515                                                        // multi-line input. Routes through canonical
11516                                                        // `ztype_h::iwsep` (`Src/ztype.h:61`).
11517        assert!(iwsep(b'\n')); // c:61
11518        assert!(iwsep(b'\t')); // c:61
11519        assert!(iwsep(b' ')); // c:61
11520        assert!(!iwsep(b'a')); // c:61
11521    }
11522
11523    #[test]
11524    fn test_mailstat_aggregates_maildir() {
11525        let _g = crate::test_util::global_state_lock();
11526        // Create a temp maildir layout with 2 messages in new/ and 1
11527        // in cur/, verify the aggregate.
11528        let tmp = std::env::temp_dir().join(format!("zshrs_mailstat_test_{}", std::process::id()));
11529        let _ = fs::remove_dir_all(&tmp);
11530        fs::create_dir_all(tmp.join("cur")).unwrap();
11531        fs::create_dir_all(tmp.join("new")).unwrap();
11532        fs::create_dir_all(tmp.join("tmp")).unwrap();
11533        let mut f = fs::File::create(tmp.join("new").join("msg1")).unwrap();
11534        f.write_all(b"hello").unwrap();
11535        let mut f = fs::File::create(tmp.join("new").join("msg2")).unwrap();
11536        f.write_all(b"world!").unwrap();
11537        let mut f = fs::File::create(tmp.join("cur").join("msg3")).unwrap();
11538        f.write_all(b"third").unwrap();
11539        let mut st: libc::stat = unsafe { std::mem::zeroed() };
11540        let rc = mailstat(tmp.to_str().unwrap(), &mut st);
11541        assert_eq!(rc, 0, "maildir should stat");
11542        assert_eq!(st.st_blocks, 3, "3 messages total across new/ + cur/");
11543        assert_eq!(st.st_size, 5 + 6 + 5, "5+6+5 bytes total");
11544        let _ = fs::remove_dir_all(&tmp);
11545    }
11546
11547    #[test]
11548    fn test_spacesplit() {
11549        let _g = crate::test_util::global_state_lock();
11550        assert_eq!(spacesplit("a b c", false), vec!["a", "b", "c"]);
11551        assert_eq!(spacesplit("a  b", false), vec!["a", "b"]);
11552    }
11553
11554    #[test]
11555    fn test_sepjoin() {
11556        let _g = crate::test_util::global_state_lock();
11557        assert_eq!(
11558            sepjoin(&["a".into(), "b".into(), "c".into()], Some(":")),
11559            "a:b:c"
11560        );
11561        assert_eq!(sepjoin(&["a".into(), "b".into()], None), "a b");
11562    }
11563
11564    #[test]
11565    fn test_isident() {
11566        let _g = crate::test_util::global_state_lock(); // c:1288
11567                                                        // Canonical port lives at `params.rs:2056` (`Src/params.c:1288`).
11568        assert!(isident("foo")); // c:1288
11569        assert!(isident("_bar")); // c:1288
11570        assert!(isident("baz123")); // c:1288
11571        assert!(!isident("123abc")); // c:1288
11572        assert!(!isident("foo-bar")); // c:1288
11573    }
11574
11575    #[test]
11576    fn test_nicechar() {
11577        let _g = crate::test_util::global_state_lock();
11578        assert_eq!(nicechar('\n'), "\\n");
11579        assert_eq!(nicechar('\t'), "\\t");
11580        assert_eq!(nicechar('a'), "a");
11581    }
11582
11583    #[test]
11584    fn test_quotedzputs_single_quote_wrap() {
11585        let _g = crate::test_util::global_state_lock();
11586        assert_eq!(quotedzputs("simple"), "simple");
11587        assert_eq!(quotedzputs("has space"), "'has space'");
11588        assert_eq!(quotedzputs("it's"), "'it'\\''s'");
11589    }
11590
11591    #[test]
11592    fn test_quotestring_backslash() {
11593        let _g = crate::test_util::global_state_lock();
11594        assert_eq!(quotestring("hello", QT_BACKSLASH), "hello");
11595        assert_eq!(quotestring("has space", QT_BACKSLASH), "has\\ space");
11596        assert_eq!(quotestring("$var", QT_BACKSLASH), "\\$var");
11597    }
11598
11599    /// Pin: `ispecial(c)` matches the canonical SPECCHARS set at
11600    /// `Src/zsh.h:228` exactly: `"#$^*()=|{}[]\`<>?~;&\n\t \\'\""`.
11601    /// Previously the Rust local `ispecial` included `!` unconditionally
11602    /// which diverged from C — C only ISPECIAL-tags bangchar (default
11603    /// `!`) under BANGHIST + interactive (per `Src/utils.c:4257-4261`).
11604    ///
11605    /// This test exercises the path indirectly via `quotestring` with
11606    /// `QT_BACKSLASH` (which prepends `\` before every ispecial char).
11607    #[test]
11608    fn quotestring_backslash_only_specchars_no_bang_in_default() {
11609        let _g = crate::test_util::global_state_lock();
11610        // `!` is NOT in canonical SPECCHARS — should NOT be backslashed
11611        // in default non-interactive mode (matches C).
11612        assert_eq!(
11613            quotestring("a!b", QT_BACKSLASH),
11614            "a!b",
11615            "c:228 — `!` is not in SPECCHARS; only bangchar+BANGHIST adds it"
11616        );
11617        // `,` is NOT in canonical SPECCHARS until makecommaspecial(1).
11618        assert_eq!(
11619            quotestring("a,b", QT_BACKSLASH),
11620            "a,b",
11621            "c:228 — `,` not in SPECCHARS until makecommaspecial(1)"
11622        );
11623        // `^` IS in canonical SPECCHARS (per c:228 `"#$^..."`).
11624        assert_eq!(
11625            quotestring("a^b", QT_BACKSLASH),
11626            "a\\^b",
11627            "c:228 — `^` is in SPECCHARS"
11628        );
11629        // Open and close braces are in canonical SPECCHARS.
11630        assert_eq!(
11631            quotestring("a{b", QT_BACKSLASH),
11632            "a\\{b",
11633            "c:228 — open-brace is in SPECCHARS"
11634        );
11635        assert_eq!(
11636            quotestring("a}b", QT_BACKSLASH),
11637            "a\\}b",
11638            "c:228 — close-brace is in SPECCHARS"
11639        );
11640        // `#` is in canonical SPECCHARS (first char of c:228).
11641        assert_eq!(
11642            quotestring("a#b", QT_BACKSLASH),
11643            "a\\#b",
11644            "c:228 — `#` is the first char of SPECCHARS"
11645        );
11646        // `\\` (literal backslash) is in canonical SPECCHARS.
11647        assert_eq!(
11648            quotestring("a\\b", QT_BACKSLASH),
11649            "a\\\\b",
11650            "c:228 — `\\\\` in SPECCHARS"
11651        );
11652    }
11653
11654    #[test]
11655    fn test_quotestring_single() {
11656        let _g = crate::test_util::global_state_lock();
11657        assert_eq!(quotestring("hello", QT_SINGLE), "'hello'");
11658        assert_eq!(quotestring("it's", QT_SINGLE), "'it'\\''s'");
11659    }
11660
11661    #[test]
11662    fn test_quotestring_double() {
11663        let _g = crate::test_util::global_state_lock();
11664        assert_eq!(quotestring("hello", QT_DOUBLE), "\"hello\"");
11665        assert_eq!(quotestring("say \"hi\"", QT_DOUBLE), "\"say \\\"hi\\\"\"");
11666    }
11667
11668    #[test]
11669    fn test_quotestring_dollars() {
11670        let _g = crate::test_util::global_state_lock();
11671        assert_eq!(quotestring("hello", QT_DOLLARS), "$'hello'");
11672        assert_eq!(quotestring("line\nbreak", QT_DOLLARS), "$'line\\nbreak'");
11673        assert_eq!(quotestring("tab\there", QT_DOLLARS), "$'tab\\there'");
11674    }
11675
11676    #[test]
11677    fn test_quotestring_pattern() {
11678        let _g = crate::test_util::global_state_lock();
11679        assert_eq!(quotestring("*.txt", QT_BACKSLASH_PATTERN), "\\*.txt");
11680        assert_eq!(quotestring("file[1]", QT_BACKSLASH_PATTERN), "file\\[1\\]");
11681    }
11682
11683    #[test]
11684    fn test_quotetype_from_q_count() {
11685        let _g = crate::test_util::global_state_lock();
11686        assert_eq!(qflag_quotetype(1), QT_BACKSLASH);
11687        assert_eq!(qflag_quotetype(2), QT_SINGLE);
11688        assert_eq!(qflag_quotetype(3), QT_DOUBLE);
11689        assert_eq!(qflag_quotetype(4), QT_DOLLARS);
11690    }
11691
11692    #[test]
11693    fn test_tulower_tuupper() {
11694        let _g = crate::test_util::global_state_lock();
11695        assert_eq!(tulower('A'), 'a');
11696        assert_eq!(tuupper('a'), 'A');
11697        assert_eq!(tulower('1'), '1');
11698    }
11699
11700    #[test]
11701    fn test_wordcount_ifs_default() {
11702        let _g = crate::test_util::global_state_lock();
11703        // C: wordcount("a b c", NULL, 0) -> 3
11704        assert_eq!(wordcount("a b c", None, 0), 3);
11705        // Leading/trailing whitespace coalesced when mul <= 0.
11706        assert_eq!(wordcount("  a b  ", None, 0), 2);
11707        // Empty string with mul == 0 -> 0 words.
11708        assert_eq!(wordcount("", None, 0), 0);
11709        // Single word, no separators.
11710        assert_eq!(wordcount("foo", None, 0), 1);
11711    }
11712
11713    #[test]
11714    fn test_wordcount_with_explicit_sep() {
11715        let _g = crate::test_util::global_state_lock();
11716        // C: wordcount("a:b:c", ":", 0) -> 3 (3 fields, 2 separators)
11717        assert_eq!(wordcount("a:b:c", Some(":"), 0), 3);
11718        // Empty fields counted when mul != 0.
11719        assert_eq!(wordcount("a::b", Some(":"), 1), 3);
11720        // Without mul, consecutive empties collapse: a, b => 2... but
11721        // C's "if ((c || mul) && (sl || *(s+sl)))" — second `:` has
11722        // c=0 and mul=0 so doesn't increment. Result: a, b => 2.
11723        assert_eq!(wordcount("a::b", Some(":"), 0), 2);
11724    }
11725
11726    #[test]
11727    fn test_ucs4tomb_ascii() {
11728        let _g = crate::test_util::global_state_lock();
11729        let mut buf = [0u8; 8];
11730        // 'A' = 0x41, ASCII, single byte in any locale.
11731        let n = ucs4tomb('A' as u32, &mut buf);
11732        // wctomb may return 1 in C/POSIX locale; in UTF-8 locale also 1.
11733        assert_eq!(n, 1);
11734        assert_eq!(buf[0], b'A');
11735    }
11736
11737    #[test]
11738    fn test_is_mb_niceformat_plain_ascii() {
11739        let _g = crate::test_util::global_state_lock();
11740        // Plain printable ASCII — nothing needs nicechar escaping.
11741        assert_eq!(is_mb_niceformat("hello world"), 0);
11742    }
11743
11744    #[test]
11745    fn test_is_mb_niceformat_with_control_char() {
11746        let _g = crate::test_util::global_state_lock();
11747        // Tab is control (< 0x20) — needs nice escaping.
11748        assert_eq!(is_mb_niceformat("a\tb"), 1);
11749        // Bell character.
11750        assert_eq!(is_mb_niceformat("a\x07b"), 1);
11751    }
11752
11753    /// c:4856/4954 — `metafy` + `unmetafy` MUST round-trip for ASCII.
11754    /// A regression in the round-trip corrupts every metafied buffer
11755    /// the lexer/param-subst pipeline produces.
11756    #[test]
11757    fn metafy_unmetafy_round_trips_for_ascii() {
11758        let _g = crate::test_util::global_state_lock();
11759        let s = "hello world";
11760        let m = metafy(s);
11761        let mut buf = m.into_bytes();
11762        unmetafy(&mut buf);
11763        assert_eq!(std::str::from_utf8(&buf).unwrap(), s);
11764    }
11765
11766    /// `ztrlen` counts metafied characters (Meta-pairs as 1).
11767    /// Regression that double-counts Meta-pair bytes would break
11768    /// every fixed-width column path (printf %s).
11769    #[test]
11770    fn ztrlen_counts_ascii_one_per_byte() {
11771        let _g = crate::test_util::global_state_lock();
11772        assert_eq!(ztrlen(""), 0);
11773        assert_eq!(ztrlen("a"), 1);
11774        assert_eq!(ztrlen("hello"), 5);
11775    }
11776
11777    /// `Src/utils.c:5141-5149` — Meta-byte pair counts as 1 char.
11778    /// `*s++ == Meta` advances 2 bytes per iteration but increments
11779    /// `l` only once. Pin: a string with one Meta+X pair counts as 1.
11780    #[test]
11781    fn ztrlen_counts_meta_pair_as_one() {
11782        let _g = crate::test_util::global_state_lock();
11783        // META (0x83) + 0x20 = unmetafies to one '\0' byte (or a NUL).
11784        let meta = char::from_u32(Meta as u32).unwrap();
11785        let s: String = [meta, '\x20'].iter().collect();
11786        assert_eq!(
11787            ztrlen(&s),
11788            1,
11789            "c:5141-5148 — Meta+X pair counts as ONE char, not two"
11790        );
11791        // Mixed: "a" + META + "x" + "b" = 3 unmetafied chars.
11792        let mixed: String = ['a', meta, '\x20', 'b'].iter().collect();
11793        assert_eq!(
11794            ztrlen(&mixed),
11795            3,
11796            "c:5141 — three unmetafied chars from 'a' + Meta+X + 'b'"
11797        );
11798    }
11799
11800    /// `Src/utils.c:2579-2618` — `setblock_fd(turnonblocking, fd, modep)`.
11801    /// Signature pin: previously the Rust port had a 2-arg
11802    /// `(fd, blocking: bool) -> bool` signature, swapping the
11803    /// turnonblocking/fd argument order vs C AND collapsing the
11804    /// 3rd `*modep` out-param entirely. The fix restored canonical
11805    /// C order `(turnonblocking, fd)` with a `(bool, c_long)` tuple
11806    /// return mirroring `int return + long *modep`.
11807    ///
11808    /// Also pin the c:2599 regular-file short-circuit: C only
11809    /// operates on non-regular fds (pipes, sockets, ttys). A regular
11810    /// file returns `(false, -1)` immediately.
11811    #[cfg(unix)]
11812    #[test]
11813    fn setblock_fd_skips_regular_files_per_c_2599() {
11814        let _g = crate::test_util::global_state_lock();
11815        // Open a regular tempfile.
11816        let dir = tempfile::TempDir::new().unwrap();
11817        let f = fs::File::create(dir.path().join("regular")).unwrap();
11818        let fd = f.as_raw_fd();
11819        let (changed, mode) = setblock_fd(true, fd);
11820        assert!(
11821            !changed,
11822            "c:2599 — regular files short-circuit; setblock_fd must NOT report a change"
11823        );
11824        assert_eq!(mode, -1, "c:2614 — `*modep = -1` for regular files");
11825    }
11826
11827    /// `Src/utils.c:2606-2611` — `setblock_fd(turnonblocking=true, fd)`
11828    /// clears O_NONBLOCK on a pipe (non-regular fd). Returns
11829    /// `(true, prior_flags)` if the state was changed.
11830    #[cfg(unix)]
11831    #[test]
11832    fn setblock_fd_clears_o_nonblock_on_pipe() {
11833        let _g = crate::test_util::global_state_lock();
11834        // Create a pipe — non-regular fd.
11835        let mut pipefd: [libc::c_int; 2] = [0; 2];
11836        let r = unsafe { libc::pipe(pipefd.as_mut_ptr()) };
11837        assert_eq!(r, 0, "pipe(2) must succeed");
11838        let read_fd = pipefd[0];
11839        let write_fd = pipefd[1];
11840        // Force NONBLOCK on the read end.
11841        let cur = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11842        unsafe {
11843            libc::fcntl(read_fd, libc::F_SETFL, cur | libc::O_NONBLOCK);
11844        }
11845        // Verify NONBLOCK is set.
11846        let now = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11847        assert_ne!(
11848            now & libc::O_NONBLOCK,
11849            0,
11850            "test setup: NONBLOCK should be set"
11851        );
11852        // Call setblock_fd to ENABLE blocking (clear NONBLOCK).
11853        let (changed, _mode) = setblock_fd(true, read_fd);
11854        assert!(
11855            changed,
11856            "c:2611 — turnonblocking=true on a NONBLOCK pipe must report state change"
11857        );
11858        // Verify NONBLOCK is now cleared.
11859        let after = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11860        assert_eq!(
11861            after & libc::O_NONBLOCK,
11862            0,
11863            "c:2611 — O_NONBLOCK must be cleared after turnonblocking=true"
11864        );
11865        // Cleanup.
11866        unsafe {
11867            libc::close(read_fd);
11868            libc::close(write_fd);
11869        }
11870    }
11871
11872    /// `Src/utils.c:2620-2625` — `setblock_stdin()`. C body
11873    /// `setblock_fd(1, 0, &mode)` enables BLOCKING on fd 0 (stdin).
11874    /// Previously the Rust port called `setblock_fd(0, false)`
11875    /// which DISABLES blocking — exact opposite. Pin via real fd
11876    /// inspection.
11877    #[cfg(unix)]
11878    #[test]
11879    fn setblock_stdin_enables_blocking_on_fd_zero() {
11880        let _g = crate::test_util::global_state_lock();
11881        // Set stdin to NONBLOCKING first to verify the function
11882        // ACTUALLY switches it back to blocking. Skip if stdin is
11883        // not a normal fd (some CI configurations).
11884        let cur = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11885        if cur < 0 {
11886            return;
11887        }
11888        // Force NONBLOCK first.
11889        unsafe {
11890            libc::fcntl(0, libc::F_SETFL, cur | libc::O_NONBLOCK);
11891        }
11892        let after_set_nb = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11893        if after_set_nb & libc::O_NONBLOCK == 0 {
11894            // System rejected the change (regular file? CI tty?) — skip.
11895            unsafe {
11896                libc::fcntl(0, libc::F_SETFL, cur);
11897            }
11898            return;
11899        }
11900        // Call setblock_stdin — should CLEAR O_NONBLOCK.
11901        setblock_stdin();
11902        let after_setblock = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11903        assert_eq!(
11904            after_setblock & libc::O_NONBLOCK,
11905            0,
11906            "c:2624 — setblock_stdin must CLEAR O_NONBLOCK (enable blocking)"
11907        );
11908        // Restore original flags.
11909        unsafe {
11910            libc::fcntl(0, libc::F_SETFL, cur);
11911        }
11912    }
11913
11914    /// `Src/utils.c:2437-2519` — `zstrtol_underscore(s, base, false)`.
11915    /// Base-10 (explicit) parses canonical decimal.
11916    #[test]
11917    fn zstrtol_underscore_base_10_parses_decimal() {
11918        let _g = crate::test_util::global_state_lock();
11919        let (v, rest) = zstrtol_underscore("12345", 10, false);
11920        assert_eq!(v, 12345, "c:2471 — decimal accumulator");
11921        assert_eq!(rest, "", "rest is empty after full consumption");
11922        // With trailing non-digit.
11923        let (v, rest) = zstrtol_underscore("100abc", 10, false);
11924        assert_eq!(v, 100);
11925        assert_eq!(
11926            rest, "abc",
11927            "c:2467 — loop exits at first non-digit; rest carries on"
11928        );
11929    }
11930
11931    /// c:2452-2461 — base==0 autodetect: `0x`→16, `0b`→2, leading
11932    /// `0`→8 (always, unlike `zstrtoul_underscore` which honors
11933    /// OCTALZEROES). `zstrtol_underscore` does NOT consult
11934    /// OCTALZEROES — pure prefix detection.
11935    #[test]
11936    fn zstrtol_underscore_base_zero_autodetects_prefix() {
11937        let _g = crate::test_util::global_state_lock();
11938        // Hex.
11939        assert_eq!(
11940            zstrtol_underscore("0xff", 0, false).0,
11941            255,
11942            "c:2455 — 0x → base 16"
11943        );
11944        assert_eq!(zstrtol_underscore("0XFF", 0, false).0, 255);
11945        // Binary.
11946        assert_eq!(
11947            zstrtol_underscore("0b1010", 0, false).0,
11948            10,
11949            "c:2457 — 0b → base 2"
11950        );
11951        // Leading 0 → octal (always, no OCTALZEROES gate).
11952        assert_eq!(
11953            zstrtol_underscore("0777", 0, false).0,
11954            511,
11955            "c:2460 — leading 0 → octal (no OCTALZEROES gate for zstrtol)"
11956        );
11957        // Decimal default.
11958        assert_eq!(zstrtol_underscore("12345", 0, false).0, 12345);
11959    }
11960
11961    /// c:2447-2450 — leading `-` and `+` consumed; `-` triggers
11962    /// negation at the end (c:2497-2498).
11963    #[test]
11964    fn zstrtol_underscore_handles_sign_chars() {
11965        let _g = crate::test_util::global_state_lock();
11966        assert_eq!(
11967            zstrtol_underscore("-42", 10, false).0,
11968            -42,
11969            "c:2447 — leading `-` → negate"
11970        );
11971        assert_eq!(
11972            zstrtol_underscore("+42", 10, false).0,
11973            42,
11974            "c:2449 — leading `+` consumed, no negation"
11975        );
11976        // Mixed whitespace + sign.
11977        assert_eq!(
11978            zstrtol_underscore("  -100", 10, false).0,
11979            -100,
11980            "c:2444 — leading whitespace skipped, then sign"
11981        );
11982    }
11983
11984    /// c:2466-2492 — base 16 (hex) accumulator: digits 0-9 + letters
11985    /// a-f / A-F via `idigit(*s)` || `'a' ≤ *s < 'a'+base-10`.
11986    /// Pin both upper- and lower-case.
11987    #[test]
11988    fn zstrtol_underscore_base_16_accepts_letters() {
11989        let _g = crate::test_util::global_state_lock();
11990        assert_eq!(
11991            zstrtol_underscore("ff", 16, false).0,
11992            255,
11993            "c:2485 — base-16 'ff' → 255"
11994        );
11995        assert_eq!(
11996            zstrtol_underscore("FF", 16, false).0,
11997            255,
11998            "c:2485 — base-16 'FF' → 255 (upper case via `*s & 0x1f`)"
11999        );
12000        assert_eq!(
12001            zstrtol_underscore("DEADbeef", 16, false).0,
12002            0xDEADBEEF,
12003            "c:2485 — mixed case 32-bit hex"
12004        );
12005    }
12006
12007    /// c:2468/2482 — `underscore` flag enables digit-separator. With
12008    /// underscore=false, `_` is treated as end-of-digits. With
12009    /// underscore=true, `_` is skipped (c:2469-2470).
12010    #[test]
12011    fn zstrtol_underscore_underscore_flag() {
12012        let _g = crate::test_util::global_state_lock();
12013        // underscore=false: `_` terminates parse.
12014        let (v, rest) = zstrtol_underscore("1_000", 10, false);
12015        assert_eq!(v, 1, "c:2467 — underscore=false: `_` terminates");
12016        assert_eq!(rest, "_000");
12017        // underscore=true: `_` accepted and skipped.
12018        let (v, rest) = zstrtol_underscore("1_000_000", 10, true);
12019        assert_eq!(
12020            v, 1_000_000,
12021            "c:2469-2470 — underscore=true: `_` skipped during accumulation"
12022        );
12023        assert_eq!(rest, "", "fully consumed including `_`s");
12024    }
12025
12026    /// `Src/utils.c:2750-2774` — `timespec_diff_us(t1, t2)` returns
12027    /// the signed microsecond delta `t2 - t1`. Pin both sign
12028    /// conventions: t2 after t1 → positive; t2 before t1 → negative.
12029    #[test]
12030    fn timespec_diff_us_sign_matches_c_t2_minus_t1() {
12031        let _g = crate::test_util::global_state_lock();
12032        let t1 = std::time::Instant::now();
12033        std::thread::sleep(std::time::Duration::from_millis(2));
12034        let t2 = std::time::Instant::now();
12035        // c:2759 — `diff_sec = t2 - t1` for the t2 > t1 path. Result
12036        // positive (microseconds elapsed).
12037        let d = timespec_diff_us(&t1, &t2);
12038        assert!(d > 0, "c:2759 — t2 after t1 → positive delta");
12039        assert!(d >= 1000, "expected >= 1ms (1000us); got {}us", d);
12040        // c:2754 — reversed (t1 > t2): result negative.
12041        let r = timespec_diff_us(&t2, &t1);
12042        assert!(r < 0, "c:2770 — t1 after t2 → negative delta");
12043        assert_eq!(d, -r, "swap of args negates the result");
12044    }
12045
12046    /// c:2752 — `timespec_diff_us(t, t)` is 0 (identical Instants).
12047    #[test]
12048    fn timespec_diff_us_same_instant_returns_zero() {
12049        let _g = crate::test_util::global_state_lock();
12050        let t = std::time::Instant::now();
12051        assert_eq!(
12052            timespec_diff_us(&t, &t),
12053            0,
12054            "c:2752 — identical Instants → 0"
12055        );
12056    }
12057
12058    /// `Src/utils.c:6743-6779` — `ucs4toutf8(dest, wval)`. Encodes
12059    /// a Unicode codepoint to UTF-8. Pin canonical 1-, 2-, 3-, and
12060    /// 4-byte encodings.
12061    #[test]
12062    fn ucs4toutf8_encodes_canonical_lengths() {
12063        let _g = crate::test_util::global_state_lock();
12064        // c:6750 — 1 byte: ASCII range [0, 0x80).
12065        assert_eq!(
12066            ucs4toutf8(0x41),
12067            Some("A".to_string()),
12068            "c:6750 — 0x41 → 'A' (1 byte)"
12069        );
12070        // c:6752 — 2 bytes: [0x80, 0x800). 'é' = U+00E9.
12071        assert_eq!(
12072            ucs4toutf8(0xe9),
12073            Some("é".to_string()),
12074            "c:6752 — U+00E9 → 'é' (2 bytes)"
12075        );
12076        // c:6754 — 3 bytes: [0x800, 0x10000). '字' = U+5B57.
12077        assert_eq!(
12078            ucs4toutf8(0x5B57),
12079            Some("字".to_string()),
12080            "c:6754 — U+5B57 → '字' (3 bytes)"
12081        );
12082        // c:6756 — 4 bytes: [0x10000, 0x200000). '𝄞' = U+1D11E.
12083        assert_eq!(
12084            ucs4toutf8(0x1D11E),
12085            Some("𝄞".to_string()),
12086            "c:6756 — U+1D11E → '𝄞' (4 bytes)"
12087        );
12088    }
12089
12090    /// `Src/utils.c:6762-6764` — values `>= 0x80000000` are
12091    /// out-of-range; C emits `zerr("character not in range")` and
12092    /// returns `-1`. Surrogates (U+D800..U+DFFF) and values in
12093    /// 0x110000..0x80000000 are *encoded as raw bytes* by C
12094    /// (matches `wctomb(3)` extended UCS-4 range).
12095    #[test]
12096    fn ucs4toutf8_rejects_invalid_codepoints() {
12097        let _g = crate::test_util::global_state_lock();
12098        // c:6762-6764 — value >= 0x80000000 → None.
12099        assert_eq!(
12100            ucs4toutf8(0xFFFF_FFFE),
12101            None,
12102            "c:6763 — values >= 0x80000000 → None"
12103        );
12104        assert_eq!(
12105            ucs4toutf8(0x8000_0000),
12106            None,
12107            "c:6760-6764 — exactly 0x80000000 is out of range"
12108        );
12109        // Surrogates encode as 3-byte sequences per C bit-pattern
12110        // (c:6754 → len=3). The C source has no Unicode-validity
12111        // gate; that's a wctomb-only concern in callers.
12112        assert!(
12113            ucs4toutf8(0xD800).is_some(),
12114            "c:6754 — surrogates encode as 3-byte UTF-8 in C (no Unicode validity check)"
12115        );
12116    }
12117
12118    /// `Src/utils.c:6648-6723` — `dquotedztrdup(s)`. Default (non-
12119    /// CSHJUNKIEQUOTES) arm: wrap whole string in `"..."`. Backslash
12120    /// doubles, `"`/`$`/`` ` `` get escaped.
12121    #[test]
12122    fn dquotedztrdup_default_path_wraps_whole_string() {
12123        let _g = crate::test_util::global_state_lock();
12124        if isset(CSHJUNKIEQUOTES) {
12125            return; // Default-only test.
12126        }
12127        // c:6690 — `*p++ = '"';` then iterate.
12128        assert_eq!(
12129            dquotedztrdup("hello"),
12130            "\"hello\"",
12131            "c:6690+6711+6718 — wrap plain text in double quotes"
12132        );
12133        // c:6703-6708 — `$` gets `\$`.
12134        assert_eq!(
12135            dquotedztrdup("$var"),
12136            "\"\\$var\"",
12137            "c:6703-6708 — `$` → `\\$`"
12138        );
12139        // c:6703-6708 — `"` gets `\"`.
12140        assert_eq!(
12141            dquotedztrdup("a\"b"),
12142            "\"a\\\"b\"",
12143            "c:6703-6708 — `\"` → `\\\"`"
12144        );
12145    }
12146
12147    /// c:6697-6701 — backslash handling with `pending` state.
12148    /// Single `\` followed by an ordinary char emits just `\` (no
12149    /// doubling). The "pending" extra `\` fires only when the next
12150    /// char is itself a backslash or one of the escaped specials
12151    /// (`"`/`$`/`` ` ``), OR at end-of-string before the closing `"`.
12152    #[test]
12153    fn dquotedztrdup_pending_backslash_only_doubles_when_needed() {
12154        let _g = crate::test_util::global_state_lock();
12155        if isset(CSHJUNKIEQUOTES) {
12156            return;
12157        }
12158        // Mid-string `\` before ordinary char: stays as single `\`.
12159        assert_eq!(
12160            dquotedztrdup("a\\b"),
12161            "\"a\\b\"",
12162            "c:6711+6712 — `\\b` mid-string: no extra (pending=0 after b)"
12163        );
12164        // Trailing `\` triggers pending quirk: c:6716-6717 emits
12165        // an extra `\` before the closing `"`.
12166        let r = dquotedztrdup("a\\");
12167        assert!(
12168            r.ends_with("\\\\\""),
12169            "c:6716-6717 — trailing `\\` → emit extra `\\` before closing `\"`: got {:?}",
12170            r
12171        );
12172    }
12173
12174    /// `Src/utils.c:6464-6549` — `quotedzputs(s)`. Three branches:
12175    ///   - empty → `''`.
12176    ///   - has special chars (via `hasspecial`) → wrap in `'...'`
12177    ///     with `'\''` escape for embedded apostrophes.
12178    ///   - else → return unchanged.
12179    /// Pin all three.
12180    #[test]
12181    fn quotedzputs_empty_string_returns_double_quotes() {
12182        let _g = crate::test_util::global_state_lock();
12183        assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
12184    }
12185
12186    /// c:6514-6543 — needs-quote path: wrap in single quotes,
12187    /// escape embedded `'` as `'\''`.
12188    #[test]
12189    fn quotedzputs_wraps_specials_in_single_quotes() {
12190        let _g = crate::test_util::global_state_lock();
12191        inittyptab();
12192        // Space is special → wrap.
12193        assert_eq!(quotedzputs("hello world"), "'hello world'");
12194        // `=` is special → wrap.
12195        assert_eq!(quotedzputs("foo=bar"), "'foo=bar'");
12196        // Embedded apostrophe — `'\''` escape.
12197        assert_eq!(
12198            quotedzputs("it's"),
12199            "'it'\\''s'",
12200            "c:6517-6519 — apostrophe → '\\''"
12201        );
12202    }
12203
12204    /// c:6512 — no specials → return unchanged.
12205    #[test]
12206    fn quotedzputs_plain_alnum_returns_unchanged() {
12207        let _g = crate::test_util::global_state_lock();
12208        inittyptab();
12209        assert_eq!(
12210            quotedzputs("hello"),
12211            "hello",
12212            "c:6512 — no specials, no wrap"
12213        );
12214        assert_eq!(quotedzputs("abc123"), "abc123");
12215    }
12216
12217    /// c:6578-6637 — Bourne path "avoids empty quoted strings" by
12218    /// tracking `inquote`. Input `'x` should not produce empty `''`
12219    /// at the front; `x'` should not produce trailing `''`.
12220    #[test]
12221    fn quotedzputs_avoids_empty_quoted_runs_on_boundaries() {
12222        let _g = crate::test_util::global_state_lock();
12223        inittyptab();
12224        // Leading `'`: c:6587 with inquote=0 skips the close, emits
12225        // `\'`. Then `x`: c:6604 opens a quote, emits `x`, end emits
12226        // closing `'`. Result: `\''x'` — the visible doubled `''` is
12227        // `\'` (escaped) + `'` (opening single-quote), NOT an empty
12228        // single-quoted string.
12229        assert_eq!(
12230            quotedzputs("'x"),
12231            "\\''x'",
12232            "c:6587-6610 — leading `'` produces `\\'` + opening `'`"
12233        );
12234        // Trailing `'`: open quote at `x`, see `'` with inquote=1 → close
12235        // (`'`), emit `\'`. End-of-string: inquote=0, no trailing `'`.
12236        // Result: `'x'\\'` — NO trailing `''`.
12237        assert_eq!(
12238            quotedzputs("x'"),
12239            "'x'\\'",
12240            "c:6631-6637 — trailing `'` doesn't generate empty `''`"
12241        );
12242    }
12243
12244    /// c:6478-6492 — `is_mb_niceformat` arm of quotedzputs uses
12245    /// mb_niceformat with NICEFLAG_QUOTE so embedded `'` becomes
12246    /// `\'` inside the `$'...'` wrapper (preventing the quote from
12247    /// terminating the wrap). Previously sb_niceformat without
12248    /// NICEFLAG_QUOTE was used, which would have left `'` raw.
12249    #[test]
12250    fn quotedzputs_dollar_quote_escapes_apostrophe_inside_wrap() {
12251        let _g = crate::test_util::global_state_lock();
12252        inittyptab();
12253        // String with embedded `'` AND a control char so the
12254        // is_mb_niceformat arm is taken (controls trigger it).
12255        let r = quotedzputs("a\nb'c");
12256        // Expected: `$'a\nb\'c'` — the embedded `'` is `\'` inside
12257        // dollar-quotes, NOT terminating the wrap.
12258        assert!(
12259            r.starts_with("$'") && r.ends_with("'"),
12260            "c:6488 — must be wrapped in $'...'  got {:?}",
12261            r
12262        );
12263        assert!(
12264            r.contains("\\'"),
12265            "c:5413-5414 — embedded `'` must be `\\'` not raw `'`: got {:?}",
12266            r
12267        );
12268    }
12269
12270    /// c:6533-6576 — RCQUOTES branch: wrap everything in `'…'`,
12271    /// double embedded `'` as `''`.
12272    #[test]
12273    fn quotedzputs_rcquotes_doubles_apostrophe() {
12274        let _g = crate::test_util::global_state_lock();
12275        inittyptab();
12276        let prev = crate::ported::options::opt_state_get("rcquotes").unwrap_or(false);
12277        crate::ported::options::opt_state_set("rcquotes", true);
12278        // RCQUOTES: `it's` → `'it''s'` (doubled `'`, NOT `'\''`).
12279        let got = quotedzputs("it's");
12280        crate::ported::options::opt_state_set("rcquotes", prev);
12281        assert_eq!(
12282            got, "'it''s'",
12283            "c:6548-6553 — RCQUOTES: `'` → `''` (doubled)"
12284        );
12285    }
12286
12287    /// `Src/utils.c:2331-2337` — `strucpy(s, t)`. Appends `t` to
12288    /// `*s` and leaves `*s` pointing at the NUL terminator (Rust
12289    /// equivalent: append-in-place; `.len()` gives the new end).
12290    /// Pin: empty input → no-op; non-empty input → append.
12291    #[test]
12292    fn strucpy_appends_t_to_dest() {
12293        let _g = crate::test_util::global_state_lock();
12294        let mut dest = String::from("prefix-");
12295        strucpy(&mut dest, "suffix");
12296        assert_eq!(
12297            dest, "prefix-suffix",
12298            "c:2335 — strucpy appends t (NOT case-changes — c-name 'u' is pointer-walk, not upper)"
12299        );
12300        // Empty t — no change.
12301        let mut dest = String::from("alone");
12302        strucpy(&mut dest, "");
12303        assert_eq!(dest, "alone");
12304        // Empty dest + non-empty t.
12305        let mut dest = String::new();
12306        strucpy(&mut dest, "hello");
12307        assert_eq!(dest, "hello");
12308    }
12309
12310    /// `Src/utils.c:2341-2350` — `struncpy(s, t, n)`. Appends up to
12311    /// `n` bytes of `t` to `*s`. Pin: n=0 no-op; n < len(t) clips;
12312    /// n >= len(t) appends all.
12313    #[test]
12314    fn struncpy_appends_up_to_n_bytes() {
12315        let _g = crate::test_util::global_state_lock();
12316        // n < len(t).
12317        let mut dest = String::from("X");
12318        struncpy(&mut dest, "abcdef", 3);
12319        assert_eq!(dest, "Xabc", "c:2345 — clipped to n=3 bytes of `abcdef`");
12320        // n >= len(t).
12321        let mut dest = String::from("Y");
12322        struncpy(&mut dest, "abc", 100);
12323        assert_eq!(dest, "Yabc", "c:2345 — full copy when n >= len");
12324        // n=0 → no append.
12325        let mut dest = String::from("Z");
12326        struncpy(&mut dest, "abc", 0);
12327        assert_eq!(dest, "Z", "c:2345 — n=0 stops the copy loop immediately");
12328    }
12329
12330    /// `Src/utils.c:2280-2288` — `has_token(s)`. Returns true iff
12331    /// any byte in `s` triggers `itok()`. Pin: ASCII strings →
12332    /// false; strings containing token bytes (Pound 0x84, Bang
12333    /// 0x9c, Nularg 0xa1) → true.
12334    #[test]
12335    fn has_token_detects_typtab_token_bytes() {
12336        let _g = crate::test_util::global_state_lock();
12337        inittyptab();
12338        // c:2285 — pure ASCII has no token bytes.
12339        assert!(!has_token(""), "empty: no tokens");
12340        assert!(
12341            !has_token("hello world"),
12342            "c:2285 — ASCII text has no token bytes"
12343        );
12344        // Pound (0x84) — first token byte.
12345        let s: String = std::iter::once(0x84u8 as char).collect();
12346        assert!(
12347            has_token(&s),
12348            "c:2285 — Pound (0x84) is itok → has_token=true"
12349        );
12350        // Bang (0x9c) — last_normal_tok.
12351        let s: String = std::iter::once(0x9cu8 as char).collect();
12352        assert!(has_token(&s), "c:2285 — Bang (0x9c) is itok");
12353        // Nularg (0xa1) — upper bound.
12354        let s: String = std::iter::once(0xa1u8 as char).collect();
12355        assert!(has_token(&s), "c:2285 — Nularg (0xa1) is itok");
12356    }
12357
12358    /// `Src/utils.c:2280` — Meta byte (0x83) is NOT a token. Pin
12359    /// the regression: previously the Rust port hardcoded `0x83`
12360    /// as a token byte. Now correctly excludes it.
12361    #[test]
12362    fn has_token_excludes_meta_byte() {
12363        let _g = crate::test_util::global_state_lock();
12364        inittyptab();
12365        // 0x83 is Meta, NOT itok. Should return false.
12366        let s: String = std::iter::once(0x83u8 as char).collect();
12367        assert!(
12368            !has_token(&s),
12369            "c:2285 — Meta (0x83) is NOT itok; previous hardcoded 0x83 list misfired"
12370        );
12371    }
12372
12373    /// `Src/utils.c:6082-6124` — `addunprintable(c)`. Renders bytes
12374    /// using shell-compatible C-string escapes (`\a`/`\b`/`\f`/`\n`/
12375    /// `\r`/`\t`/`\v` for the named controls + `\nnn` octal for
12376    /// others + `\0` for NUL). Previously emitted ZLE caret form —
12377    /// wrong convention.
12378    #[test]
12379    fn addunprintable_named_control_escapes() {
12380        let _g = crate::test_util::global_state_lock();
12381        // c:6106-6112 — named-escape per byte.
12382        assert_eq!(addunprintable('\x07'), "\\a", "c:6106 — BEL → \\a");
12383        assert_eq!(addunprintable('\x08'), "\\b", "c:6107 — BS → \\b");
12384        assert_eq!(addunprintable('\x0c'), "\\f", "c:6108 — FF → \\f");
12385        assert_eq!(addunprintable('\n'), "\\n", "c:6109 — LF → \\n");
12386        assert_eq!(addunprintable('\r'), "\\r", "c:6110 — CR → \\r");
12387        assert_eq!(addunprintable('\t'), "\\t", "c:6111 — TAB → \\t");
12388        assert_eq!(addunprintable('\x0b'), "\\v", "c:6112 — VT → \\v");
12389    }
12390
12391    /// c:6097-6103 — NUL renders as `\0`. (C peeks next byte for
12392    /// `\000` disambiguation; Rust port works one char at a time
12393    /// so emits the short `\0` form.)
12394    #[test]
12395    fn addunprintable_nul_renders_as_backslash_zero() {
12396        let _g = crate::test_util::global_state_lock();
12397        assert_eq!(addunprintable('\0'), "\\0", "c:6097-6098 — NUL → \\0");
12398    }
12399
12400    /// c:6114-6119 — `\nnn` 3-digit octal fallback for un-named
12401    /// control bytes. Pin the format: each digit is one octal digit
12402    /// (0-7), zero-padded to 3 positions.
12403    #[test]
12404    fn addunprintable_octal_fallback_for_unnamed_controls() {
12405        let _g = crate::test_util::global_state_lock();
12406        // 0x01 (SOH) = 001 octal.
12407        assert_eq!(addunprintable('\x01'), "\\001", "c:6116-6118 — SOH → \\001");
12408        // 0x1b (ESC) = 033 octal.
12409        assert_eq!(addunprintable('\x1b'), "\\033", "c:6116-6118 — ESC → \\033");
12410        // 0x7f (DEL) = 177 octal.
12411        assert_eq!(addunprintable('\x7f'), "\\177", "c:6116-6118 — DEL → \\177");
12412        // 0xff (high) = 377 octal.
12413        assert_eq!(
12414            addunprintable(char::from_u32(0xff).unwrap()),
12415            "\\377",
12416            "c:6116-6118 — 0xff → \\377"
12417        );
12418    }
12419
12420    /// `Src/utils.c:6072-6082` — `hasspecial(s)`. Pin canonical
12421    /// special chars from SPECCHARS (`Src/zsh.h:228`): `#$^*()=|{}[]
12422    /// \`<>?~;&\\n\\t \\\\\\'\\"`.
12423    #[test]
12424    fn hasspecial_recognises_canonical_special_chars() {
12425        let _g = crate::test_util::global_state_lock();
12426        // typtab access is read-only here (no flag mutation); concurrent
12427        // inittyptab() rebuilds are idempotent for the default flag set.
12428        inittyptab();
12429        // c:6075 — every char in SPECCHARS triggers true.
12430        for c in "#$^*()=|{}[]<>?~;&".chars() {
12431            let s = c.to_string();
12432            assert!(
12433                hasspecial(&s),
12434                "c:6075 — '{}' is in SPECCHARS, must be special",
12435                c
12436            );
12437        }
12438        // Whitespace specials.
12439        assert!(hasspecial(" "), "space in SPECCHARS");
12440        assert!(hasspecial("\t"), "tab in SPECCHARS");
12441        assert!(hasspecial("\n"), "newline in SPECCHARS");
12442        // Non-special chars → false.
12443        assert!(
12444            !hasspecial("hello"),
12445            "c:6075 — plain alphanumerics are NOT special"
12446        );
12447        assert!(!hasspecial("ABC012"));
12448    }
12449
12450    /// `Src/utils.c:6072-6075` — Meta-byte handling: `*s == Meta ?
12451    /// *++s ^ 32 : *s`. Pin Meta+X pair gets decoded before special-
12452    /// check. A Meta+'A' decodes to 'a' (not special), so a string
12453    /// containing only Meta+'A' returns false.
12454    #[test]
12455    fn hasspecial_decodes_meta_byte_before_check() {
12456        let _g = crate::test_util::global_state_lock();
12457        // Same as above — read-only after inittyptab.
12458        inittyptab();
12459        // Meta + 0x41 ('A') → 'a' (0x61) → not special.
12460        let bytes: Vec<u8> = vec![Meta, 0x41u8];
12461        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12462        assert!(
12463            !hasspecial(s),
12464            "c:6075 — Meta+0x41 decodes to 'a' which is NOT special"
12465        );
12466    }
12467
12468    /// `Src/utils.c:5849-5910` — `sb_niceformat(s)`. Pin: ASCII
12469    /// printable passes through unchanged; controls escape.
12470    /// C-equivalent call shape: `char *out; sb_niceformat(s, NULL, &out, 0);`
12471    #[test]
12472    fn sb_niceformat_passes_printable_ascii() {
12473        let _g = crate::test_util::global_state_lock();
12474        let mut out: Option<String> = None;
12475        let l = sb_niceformat("hello", None, Some(&mut out), 0);
12476        assert_eq!(
12477            out.as_deref(),
12478            Some("hello"),
12479            "c:5886 — nicechar_sel passes printable through"
12480        );
12481        assert_eq!(l, 5, "c:5928 — return length equals output bytes");
12482
12483        let mut out: Option<String> = None;
12484        sb_niceformat("", None, Some(&mut out), 0);
12485        assert_eq!(out.as_deref(), Some(""));
12486
12487        let mut out: Option<String> = None;
12488        sb_niceformat("ABC012!?@", None, Some(&mut out), 0);
12489        assert_eq!(out.as_deref(), Some("ABC012!?@"));
12490    }
12491
12492    /// c:5886 — controls get `\n`/`\t`/`^X`/`^?` escapes via nicechar_sel.
12493    #[test]
12494    fn sb_niceformat_escapes_controls() {
12495        let _g = crate::test_util::global_state_lock();
12496        let mut out: Option<String> = None;
12497        sb_niceformat("a\nb", None, Some(&mut out), 0);
12498        assert_eq!(out.as_deref(), Some("a\\nb"), "c:5886 — newline → \\n");
12499
12500        let mut out: Option<String> = None;
12501        sb_niceformat("a\tb", None, Some(&mut out), 0);
12502        assert_eq!(out.as_deref(), Some("a\\tb"));
12503
12504        let mut out: Option<String> = None;
12505        sb_niceformat("\x01", None, Some(&mut out), 0);
12506        assert_eq!(
12507            out.as_deref(),
12508            Some("^A"),
12509            "c:5886 — control char → ^X form"
12510        );
12511
12512        let mut out: Option<String> = None;
12513        sb_niceformat("\x7f", None, Some(&mut out), 0);
12514        assert_eq!(out.as_deref(), Some("^?"));
12515    }
12516
12517    /// `Src/utils.c:5872` — unmetafy step before formatting. Pin:
12518    /// metafied Meta+X pair gets unescaped first, then run through
12519    /// nicechar_sel.
12520    #[test]
12521    fn sb_niceformat_unmetafies_before_formatting() {
12522        let _g = crate::test_util::global_state_lock();
12523        // META + 0x41 ('A') → decodes to 'a' (0x61). 'a' is printable
12524        // → passes through unchanged.
12525        let bytes: Vec<u8> = vec![Meta, 0x41u8];
12526        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12527        let mut out: Option<String> = None;
12528        sb_niceformat(s, None, Some(&mut out), 0);
12529        assert_eq!(
12530            out.as_deref(),
12531            Some("a"),
12532            "c:5872 — unmetafy first: Meta+0x41 → 'a' → printable passthrough"
12533        );
12534        // META + 0x20 → decodes to NUL → "^@" form.
12535        let bytes: Vec<u8> = vec![Meta, 0x20u8];
12536        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12537        let mut out: Option<String> = None;
12538        sb_niceformat(s, None, Some(&mut out), 0);
12539        let r = out.unwrap_or_default();
12540        assert!(
12541            !r.is_empty(),
12542            "c:5872 — Meta+0x20 → NUL → must emit some escape, not empty"
12543        );
12544    }
12545
12546    /// `Src/utils.c:5937-5959` — `is_sb_niceformat(s)`. Returns 1
12547    /// if sb_niceformat would change the input, else 0.
12548    #[test]
12549    fn is_sb_niceformat_true_for_strings_with_controls() {
12550        let _g = crate::test_util::global_state_lock();
12551        assert_eq!(is_sb_niceformat("\n"), 1, "newline is nice");
12552        assert_eq!(is_sb_niceformat("a\tb"), 1);
12553        assert_eq!(is_sb_niceformat("\x01"), 1);
12554        // Non-control ASCII → 0.
12555        assert_eq!(is_sb_niceformat("hello"), 0);
12556        assert_eq!(is_sb_niceformat(""), 0);
12557    }
12558
12559    /// `Src/utils.c:5810-5826` — `metacharlenconv(x, c)`. Returns
12560    /// `(2, decoded)` for Meta+X pair, `(1, byte)` for plain byte.
12561    /// Pin both branches.
12562    #[test]
12563    fn metacharlenconv_plain_ascii_returns_one_byte() {
12564        let _g = crate::test_util::global_state_lock();
12565        // c:5823-5825 — plain byte.
12566        let (n, c) = metacharlenconv("a");
12567        assert_eq!((n, c), (1, Some('a')), "c:5823 — plain byte → (1, byte)");
12568        let (n, c) = metacharlenconv("X");
12569        assert_eq!((n, c), (1, Some('X')));
12570    }
12571
12572    /// c:5818-5821 — Meta+X pair: `*c = x[1] ^ 32; return 2`.
12573    #[test]
12574    fn metacharlenconv_meta_pair_xor_decodes() {
12575        let _g = crate::test_util::global_state_lock();
12576        // Meta + 0x41 ('A') → 'A' ^ 32 = 'a'.
12577        let bytes: Vec<u8> = vec![Meta, 0x41u8];
12578        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12579        let (n, c) = metacharlenconv(s);
12580        assert_eq!(
12581            (n, c),
12582            (2, Some('a')),
12583            "c:5820 — Meta+0x41 → 'a' (XOR 32), consumed 2 bytes"
12584        );
12585        // Empty input.
12586        let (n, c) = metacharlenconv("");
12587        assert_eq!((n, c), (0, None));
12588    }
12589
12590    /// `Src/utils.c:5832-5843` — `charlenconv(x, len, c)`. Returns
12591    /// `(0, NUL)` when len=0, `(1, byte)` otherwise.
12592    #[test]
12593    fn charlenconv_zero_len_returns_zero() {
12594        let _g = crate::test_util::global_state_lock();
12595        // c:5834 — `if (!len) { *c = '\0'; return 0; }`.
12596        let (n, c) = charlenconv("abc", 0);
12597        assert_eq!((n, c), (0, None), "c:5834-5837 — len=0 returns 0");
12598    }
12599
12600    /// c:5840-5842 — non-zero len: read one byte, return (1, byte).
12601    #[test]
12602    fn charlenconv_returns_first_byte_for_nonzero_len() {
12603        let _g = crate::test_util::global_state_lock();
12604        let (n, c) = charlenconv("abc", 3);
12605        assert_eq!((n, c), (1, Some('a')), "c:5841 — *c = *x; return 1");
12606        let (n, c) = charlenconv("xy", 2);
12607        assert_eq!((n, c), (1, Some('x')));
12608    }
12609
12610    /// `Src/utils.c:5474-5524` — `is_mb_niceformat(s)`. Predicate:
12611    /// would mb_niceformat produce a different output? Pin canonical
12612    /// branches:
12613    ///   - Pure ASCII printable → false (no escape needed).
12614    ///   - Contains control char → true.
12615    ///   - Pure UTF-8 wide chars → false under default PRINTEIGHTBIT-off
12616    ///     since is_wcs_nicechar treats them as printable.
12617    #[test]
12618    fn is_mb_niceformat_false_for_pure_printable_ascii() {
12619        let _g = crate::test_util::global_state_lock();
12620        assert_eq!(
12621            is_mb_niceformat("hello"),
12622            0,
12623            "c:5509 — printable ASCII needs no nice-format"
12624        );
12625        assert_eq!(
12626            is_mb_niceformat(""),
12627            0,
12628            "c:5486 — empty string has no chars to flag"
12629        );
12630        assert_eq!(is_mb_niceformat("ABC012!?@"), 0);
12631    }
12632
12633    /// c:5509 + `is_wcs_nicechar` — newline/tab/control chars trigger
12634    /// the predicate.
12635    #[test]
12636    fn is_mb_niceformat_true_for_strings_with_controls() {
12637        let _g = crate::test_util::global_state_lock();
12638        assert_eq!(is_mb_niceformat("a\nb"), 1, "c:5509 — newline is nice");
12639        assert_eq!(is_mb_niceformat("\t"), 1);
12640        assert_eq!(is_mb_niceformat("\x01"), 1, "c:5509 — control char is nice");
12641        assert_eq!(is_mb_niceformat("\x7f"), 1, "c:5509 — DEL is nice");
12642    }
12643
12644    /// `Src/utils.c:5366-5460` — `mb_niceformat(s)`. Multibyte-aware
12645    /// nice-formatter. Calls `wcs_nicechar` per char. Test uses
12646    /// C-equivalent call shape: `char *out; mb_niceformat(s, NULL, &out, 0);`
12647    #[test]
12648    fn mb_niceformat_preserves_printable_wide_chars() {
12649        let _g = crate::test_util::global_state_lock();
12650        let mut out: Option<String> = None;
12651        mb_niceformat("hello", None, Some(&mut out), 0);
12652        assert_eq!(
12653            out.as_deref(),
12654            Some("hello"),
12655            "c:5407 — printable ASCII passes through"
12656        );
12657
12658        let mut out: Option<String> = None;
12659        mb_niceformat("café", None, Some(&mut out), 0);
12660        assert_eq!(
12661            out.as_deref(),
12662            Some("café"),
12663            "c:5407 — Latin-1 'é' must NOT byte-mask to \\M-X"
12664        );
12665
12666        let mut out: Option<String> = None;
12667        mb_niceformat("字", None, Some(&mut out), 0);
12668        assert_eq!(
12669            out.as_deref(),
12670            Some("字"),
12671            "c:5407 — CJK printable passes through"
12672        );
12673
12674        let mut out: Option<String> = None;
12675        mb_niceformat("abcéxyz", None, Some(&mut out), 0);
12676        assert_eq!(out.as_deref(), Some("abcéxyz"));
12677    }
12678
12679    /// `Src/utils.c:5407` + `wcs_nicechar` — controls still escape.
12680    #[test]
12681    fn mb_niceformat_escapes_controls() {
12682        let _g = crate::test_util::global_state_lock();
12683        let mut out: Option<String> = None;
12684        mb_niceformat("\n", None, Some(&mut out), 0);
12685        assert_eq!(
12686            out.as_deref(),
12687            Some("\\n"),
12688            "c:5407 → wcs_nicechar c:625 — newline escapes"
12689        );
12690
12691        let mut out: Option<String> = None;
12692        mb_niceformat("\t", None, Some(&mut out), 0);
12693        assert_eq!(
12694            out.as_deref(),
12695            Some("\\t"),
12696            "c:5407 → wcs_nicechar c:628 — tab escapes"
12697        );
12698
12699        let mut out: Option<String> = None;
12700        mb_niceformat("a\nb", None, Some(&mut out), 0);
12701        assert_eq!(out.as_deref(), Some("a\\nb"));
12702    }
12703
12704    /// `Src/utils.c:4971-4983` — `metalen(s, len)`. **Input `len` is
12705    /// the UNMETAFIED char count; output is the METAFIED byte count.**
12706    /// Pin a metafied string with one Meta+X pair: 3 unmetafied chars
12707    /// → 4 metafied bytes.
12708    #[test]
12709    fn metalen_returns_metafied_byte_count() {
12710        let _g = crate::test_util::global_state_lock();
12711        // Pure ASCII: 5 chars → 5 bytes (no Meta encountered).
12712        assert_eq!(
12713            metalen("hello", 5),
12714            5,
12715            "c:4972 — ASCII: metafied bytes == unmetafied chars"
12716        );
12717        // [a, Meta, X, b] = 4 metafied bytes representing 3 chars.
12718        let bytes: Vec<u8> = vec![b'a', Meta, 0x41, b'b'];
12719        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12720        assert_eq!(
12721            metalen(s, 3),
12722            4,
12723            "c:4978 — 3 unmetafied chars + 1 Meta = 4 metafied bytes"
12724        );
12725        // Two Meta pairs: [Meta, X, Meta, Y] = 4 bytes for 2 chars.
12726        let bytes: Vec<u8> = vec![Meta, 0x41, Meta, 0x42];
12727        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12728        assert_eq!(
12729            metalen(s, 2),
12730            4,
12731            "c:4978 — 2 unmetafied chars (both Meta+X) = 4 metafied bytes"
12732        );
12733    }
12734
12735    /// c:4974 — `mlen = len;` is the initial value (no Meta found,
12736    /// returns the input length unchanged). Pin len=0 and pure-ASCII.
12737    #[test]
12738    fn metalen_returns_input_for_no_meta_chars() {
12739        let _g = crate::test_util::global_state_lock();
12740        assert_eq!(metalen("", 0), 0, "c:4974 — empty input returns 0");
12741        assert_eq!(
12742            metalen("abc", 3),
12743            3,
12744            "c:4974 — no Meta chars: output == input len"
12745        );
12746    }
12747
12748    /// `Src/utils.c:5070` — `if (!in || !*in) return 0;`.
12749    /// Empty input returns `('\0', 0)`. Pin: empty &str → 0 bytes consumed.
12750    #[test]
12751    fn unmeta_one_empty_input_returns_zero() {
12752        let _g = crate::test_util::global_state_lock();
12753        let (c, n) = unmeta_one("");
12754        assert_eq!(c, '\0', "c:5070 — empty input returns NUL char");
12755        assert_eq!(n, 0, "c:5070 — empty input consumes 0 bytes");
12756    }
12757
12758    /// `Src/utils.c:5081-5082` — Non-Meta byte: `*sz = 1; wc = byte`.
12759    #[test]
12760    fn unmeta_one_plain_ascii_consumes_one_byte() {
12761        let _g = crate::test_util::global_state_lock();
12762        for c in "aA0!~".chars() {
12763            let s = c.to_string();
12764            let (got, n) = unmeta_one(&s);
12765            assert_eq!(got, c, "c:5082 — '{}' decodes to itself", c);
12766            assert_eq!(n, 1, "c:5081 — non-Meta byte consumes 1");
12767        }
12768    }
12769
12770    /// `Src/utils.c:5077-5079` — Meta byte: `*sz = 2; wc = in[1] ^ 32`.
12771    /// Pin via constructed Meta byte sequence.
12772    #[test]
12773    fn unmeta_one_meta_pair_decodes_xor_32() {
12774        let _g = crate::test_util::global_state_lock();
12775        // META + 0x41 ('A') → 'A' ^ 32 = 0x61 ('a'). 2 bytes consumed.
12776        let bytes: Vec<u8> = vec![Meta, 0x41u8];
12777        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12778        let (got, n) = unmeta_one(s);
12779        assert_eq!(got, 'a', "c:5079 — Meta+0x41 decodes to 0x41^32 = 'a'");
12780        assert_eq!(n, 2, "c:5078 — Meta pair consumes 2 bytes");
12781        // META + 0x20 = 0x20^32 = 0x00 (NUL).
12782        let bytes: Vec<u8> = vec![Meta, 0x20u8];
12783        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12784        let (got, n) = unmeta_one(s);
12785        assert_eq!(
12786            got, '\0',
12787            "c:5079 — Meta+0x20 decodes to NUL (the canonical metafy-NUL pattern)"
12788        );
12789        assert_eq!(n, 2);
12790    }
12791
12792    /// Edge case: bare Meta byte at end with no follower. C source
12793    /// would read past the buffer (UB); Rust port's `bytes.len() > 1`
12794    /// guard handles this by falling through to the non-Meta arm.
12795    #[test]
12796    fn unmeta_one_trailing_meta_byte_falls_through() {
12797        let _g = crate::test_util::global_state_lock();
12798        let bytes: Vec<u8> = vec![Meta];
12799        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12800        let (_, n) = unmeta_one(s);
12801        // Defensive: Rust returns the Meta byte itself as char + 1.
12802        assert_eq!(n, 1, "trailing Meta — no panic, no read past buffer");
12803    }
12804
12805    /// `Src/utils.c:5185-5203` — `ztrsub(t, s)`. Count of unmetafied
12806    /// chars between two pointers in the same metafied string.
12807    /// Rust port takes (buf, start, end) byte-offsets since raw
12808    /// pointer subtraction across distinct &str isn't safe.
12809    /// Pin pure-ASCII subrange (no Meta) → returns end-start.
12810    #[test]
12811    fn ztrsub_ascii_no_meta_returns_byte_distance() {
12812        let _g = crate::test_util::global_state_lock();
12813        // "hello world" — substring [0..5) → "hello" → 5 chars.
12814        assert_eq!(
12815            ztrsub("hello world", 0, 5),
12816            5,
12817            "c:5189 — no Meta: returns t-s byte distance"
12818        );
12819        // [6..11) → "world" → 5 chars.
12820        assert_eq!(ztrsub("hello world", 6, 11), 5);
12821        // Empty range.
12822        assert_eq!(ztrsub("hello", 2, 2), 0);
12823    }
12824
12825    /// c:5191-5199 — each Meta+X pair decrements `l` by 1 (since the
12826    /// initial l = byte-distance counts both bytes but the pair is
12827    /// one logical char). Pin via constructed Meta string.
12828    #[test]
12829    fn ztrsub_subtracts_one_per_meta_pair() {
12830        let _g = crate::test_util::global_state_lock();
12831        // Build "a" + Meta + 0x20 + "b" = 4 bytes total.
12832        // Unmetafied: "a" + 1-char-from-meta + "b" = 3 chars.
12833        let meta_byte = Meta;
12834        let buf_bytes: Vec<u8> = vec![b'a', meta_byte, 0x20, b'b'];
12835        let buf = unsafe { std::str::from_utf8_unchecked(&buf_bytes) };
12836        // [0..4) byte distance = 4; Meta pair found → l-- → 3.
12837        assert_eq!(
12838            ztrsub(buf, 0, 4),
12839            3,
12840            "c:5192-5199 — Meta pair decrements distance by 1"
12841        );
12842    }
12843
12844    /// c:5189 — `int l = t - s;` start of count. Pin edge case
12845    /// where start > end (caller bug) doesn't underflow or panic;
12846    /// Rust port clamps via `start.min(end)`.
12847    #[test]
12848    fn ztrsub_clamps_inverted_range_to_zero() {
12849        let _g = crate::test_util::global_state_lock();
12850        assert_eq!(
12851            ztrsub("hello", 4, 2),
12852            0,
12853            "inverted range start>end → 0 (defensive; not in C contract)"
12854        );
12855        // Beyond-buffer end gets clamped.
12856        assert_eq!(
12857            ztrsub("hi", 0, 100),
12858            2,
12859            "end > buf.len() clamps to buffer length"
12860        );
12861    }
12862
12863    /// `Src/utils.c:5141` — trailing Meta byte with no following char
12864    /// is a malformed-string edge case. C has a DPUTS debug-only
12865    /// fprintf for "unexpected end" but otherwise the loop terminates
12866    /// because `*s` is now NUL. Rust port must not panic on this
12867    /// truncated input.
12868    #[test]
12869    fn ztrlen_handles_trailing_meta_byte_without_panic() {
12870        let _g = crate::test_util::global_state_lock();
12871        let meta = char::from_u32(Meta as u32).unwrap();
12872        let trailing: String = ['a', meta].iter().collect();
12873        // c:5141-5149 — increment l, then encounter Meta, then loop
12874        // condition `*s` is empty → terminate. Count: 'a' + Meta = 2
12875        // (Rust port counts Meta as 1 char when no follower).
12876        let r = ztrlen(&trailing);
12877        assert!(r >= 1, "trailing Meta must not panic; got {}", r);
12878    }
12879
12880    /// c:params.c:1288 — `isident` rejects digit-leading names.
12881    /// A regression accepting them lets `typeset 1foo=bar` install
12882    /// a poisoned param that no later expansion can address.
12883    #[test]
12884    fn isident_rejects_digit_leading_names() {
12885        let _g = crate::test_util::global_state_lock(); // c:1288
12886                                                        // c:1315-1319 — C `isident`: if first char is digit, ALL
12887                                                        // chars must be digit (all-digit names are valid positional
12888                                                        // params like $99). So `1foo` is rejected (digit-then-alpha)
12889                                                        // but `99` is accepted as a valid positional-param name.
12890                                                        // The previous utils.rs fake rejected all-digit names too,
12891                                                        // which was non-faithful — the canonical `params::isident`
12892                                                        // at `params.rs:2056` matches the C semantics.
12893        assert!(!isident("1foo")); // c:1318
12894        assert!(
12895            isident("99"), // c:1318
12896            "c:1315-1319 — all-digit names are valid positional params"
12897        );
12898        assert!(!isident("")); // c:1290
12899    }
12900
12901    /// `isident` MUST accept underscore-leading + alpha-leading names —
12902    /// `_foo`, `Foo_BAR_42` are valid POSIX shell idents.
12903    #[test]
12904    fn isident_accepts_underscore_and_alpha_leading() {
12905        let _g = crate::test_util::global_state_lock(); // c:1288
12906        assert!(isident("foo")); // c:1288
12907        assert!(isident("_foo")); // c:1288
12908        assert!(isident("Foo_BAR_42")); // c:1288
12909        assert!(isident("a")); // c:1288
12910    }
12911
12912    /// `isident` rejects whitespace/punctuation/$. A regression
12913    /// accepting them would let assignments install names no
12914    /// subsequent lookup can address (`typeset 'foo bar'=baz`).
12915    #[test]
12916    fn isident_rejects_special_chars() {
12917        let _g = crate::test_util::global_state_lock(); // c:1288
12918                                                        // c:1320-1322 — non-digit-leading names use itype_end with
12919                                                        // INAMESPC bits (alnum + `_` + `.`); other chars terminate.
12920                                                        // The previous utils.rs fake rejected `foo.` outright, which
12921                                                        // is incorrect — C accepts trailing `.` (it's an INAMESPC
12922                                                        // member). Whitespace, `-`, and `$` are NOT INAMESPC chars
12923                                                        // and remain rejected.
12924        assert!(!isident("foo bar")); // c:1322
12925        assert!(!isident("foo-bar")); // c:1322
12926        assert!(!isident("a$b")); // c:1322
12927    }
12928
12929    /// `convbase(0, 10)` MUST produce `"0"`, not `""`. The literal-zero
12930    /// edge case is a real C divergence point — regression here would
12931    /// make `$(( 0 ))` print nothing.
12932    #[test]
12933    fn convbase_zero_renders_as_zero_literal() {
12934        let _g = crate::test_util::global_state_lock();
12935        assert_eq!(convbase(0, 10), "0");
12936    }
12937
12938    /// `convbase` uses zsh's canonical `BASE#NUMBER` syntax for
12939    /// non-decimal output (matches `$(( [#16] 255 ))` → `16#FF`).
12940    /// This is the format `printf "%X"` and `$(( ... ))` both produce.
12941    #[test]
12942    fn convbase_uses_base_prefix_syntax_for_non_decimal() {
12943        let _g = crate::test_util::global_state_lock();
12944        assert_eq!(convbase(255, 16), "16#FF");
12945        assert_eq!(convbase(8, 8), "8#10");
12946        assert_eq!(convbase(5, 2), "2#101");
12947        // Base 10 has no prefix.
12948        assert_eq!(convbase(42, 10), "42");
12949    }
12950
12951    /// Negative number renders with leading `-`. Regression that drops
12952    /// the sign would silently flip arithmetic semantics in `$((...))`.
12953    #[test]
12954    fn convbase_preserves_negative_sign() {
12955        let _g = crate::test_util::global_state_lock();
12956        assert_eq!(convbase(-42, 10), "-42");
12957    }
12958
12959    /// `Src/utils.c:837` — `slashsplit` keeps an EMPTY leading segment
12960    /// when the input has a leading `/`. The caller `xsymlinks`
12961    /// (c:879) reads this empty as the signal "absolute path, start
12962    /// xbuf from /". Previous Rust test asserted filtering of all
12963    /// empties (matching the previous buggy port); that contract was
12964    /// wrong relative to C. Updated to assert the C contract.
12965    #[test]
12966    fn slashsplit_keeps_leading_empty_segment() {
12967        let _g = crate::test_util::global_state_lock();
12968        // c:851 with t==s on first iter → empty prefix segment.
12969        assert_eq!(
12970            slashsplit("/usr/local/bin"),
12971            vec![
12972                "".to_string(),
12973                "usr".to_string(),
12974                "local".to_string(),
12975                "bin".to_string()
12976            ],
12977            "c:851 — leading `/` produces empty first segment"
12978        );
12979        // Single `/`: one empty segment only.
12980        assert_eq!(
12981            slashsplit("/"),
12982            vec!["".to_string()],
12983            "c:854-857 — trailing `/` after first iter returns ['']"
12984        );
12985        // Consecutive slashes collapse (c:852-853 inner while-loop).
12986        assert_eq!(
12987            slashsplit("//foo"),
12988            vec!["".to_string(), "foo".to_string()],
12989            "c:852-853 — consecutive `/` collapse, leading empty kept"
12990        );
12991        // Empty input still empty result.
12992        assert_eq!(
12993            slashsplit(""),
12994            Vec::<String>::new(),
12995            "c:842 — empty input → empty array"
12996        );
12997    }
12998
12999    /// `Src/utils.c:837` — relative paths (no leading `/`) start
13000    /// at the first non-slash; trailing `/` doesn't add a segment
13001    /// (c:854-857 returns before tail emit).
13002    #[test]
13003    fn slashsplit_relative_path_no_trailing_empty() {
13004        let _g = crate::test_util::global_state_lock();
13005        assert_eq!(
13006            slashsplit("a/b/"),
13007            vec!["a".to_string(), "b".to_string()],
13008            "c:854-857 — trailing `/` doesn't add segment"
13009        );
13010        // Mid-slash collapse in relative path.
13011        assert_eq!(
13012            slashsplit("a//b"),
13013            vec!["a".to_string(), "b".to_string()],
13014            "c:852-853 — mid `//` collapses"
13015        );
13016    }
13017
13018    /// `equalsplit("foo=bar")` returns Some(("foo","bar")). Used by
13019    /// `typeset NAME=val` parsing. Regression returning None on a
13020    /// well-formed assignment would silently break every export/typeset.
13021    #[test]
13022    fn equalsplit_returns_first_equals_split() {
13023        let _g = crate::test_util::global_state_lock();
13024        assert_eq!(
13025            equalsplit("foo=bar"),
13026            Some(("foo".to_string(), "bar".to_string()))
13027        );
13028        assert_eq!(
13029            equalsplit("a=b=c"),
13030            Some(("a".to_string(), "b=c".to_string())),
13031            "splits on FIRST `=` only"
13032        );
13033    }
13034
13035    /// `equalsplit("foo")` returns None — no `=` to split on. Catches
13036    /// a regression returning Some(("foo","")) which would let bare
13037    /// names install with empty values silently.
13038    #[test]
13039    fn equalsplit_no_equals_returns_none() {
13040        let _g = crate::test_util::global_state_lock();
13041        assert_eq!(equalsplit("foo"), None);
13042        assert_eq!(equalsplit(""), None);
13043    }
13044
13045    /// c:5106 — `ztrcmp` is the canonical zsh string compare. Same
13046    /// inputs MUST produce same Ordering (deterministic). Regression
13047    /// to a randomised hash-order would mis-sort `${(o)array}` output.
13048    #[test]
13049    fn ztrcmp_deterministic_and_lexicographic() {
13050        let _g = crate::test_util::global_state_lock();
13051        assert_eq!(ztrcmp("abc", "abc"), std::cmp::Ordering::Equal);
13052        assert_eq!(ztrcmp("abc", "abd"), std::cmp::Ordering::Less);
13053        assert_eq!(ztrcmp("abd", "abc"), std::cmp::Ordering::Greater);
13054        // Empty strings.
13055        assert_eq!(ztrcmp("", ""), std::cmp::Ordering::Equal);
13056        assert_eq!(ztrcmp("", "a"), std::cmp::Ordering::Less);
13057    }
13058
13059    /// c:5106 — shorter-as-prefix sorts before longer (like strcmp).
13060    /// Catches a regression where length-then-content would mis-sort.
13061    #[test]
13062    fn ztrcmp_shorter_prefix_is_less() {
13063        let _g = crate::test_util::global_state_lock();
13064        assert_eq!(ztrcmp("a", "ab"), std::cmp::Ordering::Less);
13065        assert_eq!(ztrcmp("foo", "foob"), std::cmp::Ordering::Less);
13066    }
13067
13068    /// `Src/utils.c:5117-5122` — Meta+X pair decodes to `X ^ 32`
13069    /// for comparison purposes. Pin: two strings differing only in
13070    /// the metafied byte's decoded value compare correctly.
13071    #[test]
13072    fn ztrcmp_decodes_meta_byte_for_comparison() {
13073        let _g = crate::test_util::global_state_lock();
13074        // META+0x21 ('!') = NUL? Let me use safer chars.
13075        // META+'A' (0x41) decodes to 'A' ^ 32 = 'a' (0x61).
13076        // So a "META a" pair represents the byte 'a' (0x61).
13077        // Compare "Ma" (M=Meta+a-byte-pair) vs "b" (>a).
13078        let meta_byte = Meta;
13079        let s1_bytes: Vec<u8> = vec![meta_byte, 0x41u8]; // decodes to 0x61 = 'a'
13080        let s2_bytes: Vec<u8> = vec![b'b'];
13081        let s1 = unsafe { std::str::from_utf8_unchecked(&s1_bytes) };
13082        let s2 = unsafe { std::str::from_utf8_unchecked(&s2_bytes) };
13083        // 'a' (0x61) < 'b' (0x62) → Less.
13084        assert_eq!(
13085            ztrcmp(s1, s2),
13086            std::cmp::Ordering::Less,
13087            "c:5117-5118 — Meta+0x41 decodes to 'a' which < 'b'"
13088        );
13089    }
13090
13091    /// `Src/utils.c:531-539` — `is_nicechar`. Returns true for chars
13092    /// needing escape-formatting:
13093    ///   - c:534 — printable ASCII (0x20-0x7e) → false.
13094    ///   - c:536 — high-bit byte (>= 0x80) → !PRINTEIGHTBIT.
13095    ///   - c:538 — DEL/\n/\t/<0x20 → true.
13096    /// Pin the three branches.
13097    #[test]
13098    fn is_nicechar_printable_ascii_is_not_nice() {
13099        let _g = crate::test_util::global_state_lock();
13100        // c:534 — letters, digits, punctuation in 0x20-0x7e all NOT nice.
13101        for c in "abcXYZ012!?@~".chars() {
13102            assert!(
13103                !is_nicechar(c),
13104                "c:534 — '{}' (ASCII printable) must NOT be nice",
13105                c
13106            );
13107        }
13108        // c:534 — space (0x20) is printable.
13109        assert!(!is_nicechar(' '));
13110    }
13111
13112    /// c:538 — control chars (DEL, newline, tab, <0x20) ARE nice.
13113    #[test]
13114    fn is_nicechar_control_chars_are_nice() {
13115        let _g = crate::test_util::global_state_lock();
13116        assert!(is_nicechar('\n'), "c:538 — newline is nice");
13117        assert!(is_nicechar('\t'), "c:538 — tab is nice");
13118        assert!(is_nicechar('\x7f'), "c:538 — DEL is nice");
13119        assert!(is_nicechar('\x00'), "c:538 — NUL is nice (<0x20)");
13120        assert!(is_nicechar('\x07'), "c:538 — BEL is nice (<0x20)");
13121        assert!(is_nicechar('\x1b'), "c:538 — ESC is nice (<0x20)");
13122        assert!(is_nicechar('\x1f'), "c:538 — boundary 0x1f is nice");
13123    }
13124
13125    /// `Src/utils.c:2528-2570` — `zstrtoul_underscore(s)`. Pins
13126    /// the base-prefix dispatch: `0x` → 16, `0b` → 2, leading `0`
13127    /// only when OCTALZEROES is set. Tests the default state.
13128    #[test]
13129    fn zstrtoul_underscore_recognises_hex_binary_decimal() {
13130        let _g = crate::test_util::global_state_lock();
13131        // c:2538 — hex.
13132        assert_eq!(zstrtoul_underscore("0xff"), Some(255));
13133        assert_eq!(zstrtoul_underscore("0XFF"), Some(255));
13134        // c:2540 — binary.
13135        assert_eq!(zstrtoul_underscore("0b1010"), Some(10));
13136        assert_eq!(zstrtoul_underscore("0B11"), Some(3));
13137        // c:2537 — pure decimal (no leading zero).
13138        assert_eq!(zstrtoul_underscore("12345"), Some(12345));
13139        // c:2543 — leading-zero with OCTALZEROES off (default) → decimal.
13140        if !isset(OCTALZEROES) {
13141            assert_eq!(
13142                zstrtoul_underscore("0777"),
13143                Some(777),
13144                "c:2543 — OCTALZEROES off: leading-0 parses as decimal"
13145            );
13146            assert_eq!(zstrtoul_underscore("010"), Some(10));
13147        }
13148    }
13149
13150    /// `Src/utils.c:2547-2548` — underscores are stripped before
13151    /// parsing. C: `if (*s == '_') continue;`. Used to support
13152    /// human-readable big numbers like `1_000_000`.
13153    #[test]
13154    fn zstrtoul_underscore_strips_underscores() {
13155        let _g = crate::test_util::global_state_lock();
13156        assert_eq!(
13157            zstrtoul_underscore("1_000_000"),
13158            Some(1_000_000),
13159            "c:2547-2548 — `_` stripped from numeric input"
13160        );
13161        assert_eq!(zstrtoul_underscore("0xff_ff"), Some(0xffff));
13162        assert_eq!(zstrtoul_underscore("0b1010_1010"), Some(0xaa));
13163    }
13164
13165    /// `Src/utils.c:2533-2534` — leading `+` sign is consumed.
13166    /// `zulong` is unsigned so `-` is not handled here (caller
13167    /// handles negation at `zstrtol_underscore`).
13168    #[test]
13169    fn zstrtoul_underscore_consumes_leading_plus() {
13170        let _g = crate::test_util::global_state_lock();
13171        assert_eq!(zstrtoul_underscore("+42"), Some(42));
13172        assert_eq!(zstrtoul_underscore("+0xff"), Some(255));
13173    }
13174
13175    /// `Src/utils.c:467` — `if (ZISPRINT(c)) goto done;`. Printable
13176    /// ASCII passes through nicechar_sel unchanged. Pin a single-char
13177    /// printable input → single-char output.
13178    #[test]
13179    fn nicechar_sel_passes_printable_ascii_unchanged() {
13180        let _g = crate::test_util::global_state_lock();
13181        for c in "aA0!~".chars() {
13182            assert_eq!(
13183                nicechar_sel(c, false),
13184                c.to_string(),
13185                "c:467 — printable ASCII '{}' passes through",
13186                c
13187            );
13188        }
13189    }
13190
13191    /// `Src/utils.c:487-492` — `\n` becomes `\n` (backslash-n) and
13192    /// `\t` becomes `\t`. Both two-char outputs. Pin the canonical
13193    /// escapes.
13194    #[test]
13195    fn nicechar_sel_escapes_newline_and_tab() {
13196        let _g = crate::test_util::global_state_lock();
13197        assert_eq!(
13198            nicechar_sel('\n', false),
13199            "\\n",
13200            "c:487-489 — newline escape"
13201        );
13202        assert_eq!(nicechar_sel('\t', false), "\\t", "c:490-492 — tab escape");
13203    }
13204
13205    /// `Src/utils.c:493-501` — control chars (<0x20 except \n/\t)
13206    /// get `^X` prefix in non-quotable mode and `\C-X` in quotable.
13207    /// c:500 — `c += 0x40` adds 0x40 to map 0x01 → 0x41 ('A').
13208    #[test]
13209    fn nicechar_sel_control_chars_use_caret_or_c_prefix() {
13210        let _g = crate::test_util::global_state_lock();
13211        // Non-quotable: ^A
13212        assert_eq!(
13213            nicechar_sel('\x01', false),
13214            "^A",
13215            "c:499-500 — \\x01 → ^A non-quotable"
13216        );
13217        // Quotable: \C-A
13218        assert_eq!(
13219            nicechar_sel('\x01', true),
13220            "\\C-A",
13221            "c:495-500 — \\x01 → \\C-A quotable"
13222        );
13223        // ^G (BEL = 0x07 → 0x47 = 'G')
13224        assert_eq!(nicechar_sel('\x07', false), "^G");
13225        // ^[ (ESC = 0x1b → 0x5b = '[')
13226        assert_eq!(nicechar_sel('\x1b', false), "^[");
13227    }
13228
13229    /// `Src/utils.c:479-486` — DEL (0x7f) renders as `^?` or `\C-?`.
13230    #[test]
13231    fn nicechar_sel_del_renders_as_caret_question() {
13232        let _g = crate::test_util::global_state_lock();
13233        assert_eq!(nicechar_sel('\x7f', false), "^?", "c:485-486 — DEL is `^?`");
13234        assert_eq!(
13235            nicechar_sel('\x7f', true),
13236            "\\C-?",
13237            "c:481-486 — DEL is `\\C-?` quotable"
13238        );
13239    }
13240
13241    /// `Src/utils.c:469-478` — high-bit bytes (>= 0x80) under default
13242    /// PRINTEIGHTBIT-off path: write `\M-` then mask to low ASCII.
13243    /// If the masked char is printable, output is `\M-<char>`. Pin
13244    /// the canonical 0xc1 → \M-A and 0xe1 → \M-a cases.
13245    #[test]
13246    fn nicechar_sel_highbit_uses_meta_prefix_when_printeightbit_off() {
13247        let _g = crate::test_util::global_state_lock();
13248        if isset(PRINTEIGHTBIT) {
13249            return; // Test only valid when PRINTEIGHTBIT off (default).
13250        }
13251        // 0xc1 = 'A' + 0x80 → \M-A
13252        let c = char::from_u32(0xc1).unwrap();
13253        assert_eq!(
13254            nicechar_sel(c, false),
13255            "\\M-A",
13256            "c:472-477 — high-bit 0xc1 → \\M-A under PRINTEIGHTBIT-off"
13257        );
13258        // 0xe1 = 'a' + 0x80 → \M-a
13259        let c = char::from_u32(0xe1).unwrap();
13260        assert_eq!(nicechar_sel(c, false), "\\M-a");
13261    }
13262
13263    /// `Src/utils.c:593-705` — `wcs_nicechar_sel`. Wide-char variant
13264    /// of `nicechar_sel`: control chars escape, printable wides
13265    /// emit raw UTF-8, large codepoints get `\u`/`\U` hex escape.
13266    /// Pin every branch. Previously delegated to `nicechar_sel`
13267    /// which byte-masked via `c & 0xff` — every UTF-8 codepoint
13268    /// emerged mangled.
13269    #[test]
13270    fn wcs_nicechar_sel_printable_wide_emits_utf8() {
13271        let _g = crate::test_util::global_state_lock();
13272        // c:644-678 — printable wide char emits raw UTF-8.
13273        assert_eq!(
13274            wcs_nicechar_sel('a', None, None, false),
13275            "a",
13276            "ASCII printable"
13277        );
13278        assert_eq!(
13279            wcs_nicechar_sel('é', None, None, false),
13280            "é",
13281            "Latin-1 printable"
13282        );
13283        assert_eq!(
13284            wcs_nicechar_sel('字', None, None, false),
13285            "字",
13286            "CJK printable"
13287        );
13288    }
13289
13290    /// c:625-630 — `\n` and `\t` escape (same as nicechar_sel for ASCII).
13291    #[test]
13292    fn wcs_nicechar_sel_escapes_newline_and_tab() {
13293        let _g = crate::test_util::global_state_lock();
13294        assert_eq!(wcs_nicechar_sel('\n', None, None, false), "\\n");
13295        assert_eq!(wcs_nicechar_sel('\t', None, None, false), "\\t");
13296    }
13297
13298    /// c:617-623 — DEL (0x7f) renders as `^?` (non-quotable) or
13299    /// `\C-?` (quotable). Same as the byte version.
13300    #[test]
13301    fn wcs_nicechar_sel_del_uses_caret_question() {
13302        let _g = crate::test_util::global_state_lock();
13303        assert_eq!(wcs_nicechar_sel('\x7f', None, None, false), "^?");
13304        assert_eq!(wcs_nicechar_sel('\x7f', None, None, true), "\\C-?");
13305    }
13306
13307    /// c:631-638 — control chars (0x01-0x1f, except \n/\t) emit
13308    /// `^X` or `\C-X` with the +0x40 offset.
13309    #[test]
13310    fn wcs_nicechar_sel_control_chars_use_caret_prefix() {
13311        let _g = crate::test_util::global_state_lock();
13312        assert_eq!(wcs_nicechar_sel('\x01', None, None, false), "^A");
13313        assert_eq!(wcs_nicechar_sel('\x07', None, None, false), "^G");
13314        assert_eq!(wcs_nicechar_sel('\x1b', None, None, false), "^[");
13315        assert_eq!(wcs_nicechar_sel('\x01', None, None, true), "\\C-A");
13316    }
13317
13318    /// `Src/utils.c:656-663` — large non-printable codepoints get
13319    /// hex escape: `\U%.8x` (>= 0x10000), `\u%.4x` (>= 0x100).
13320    /// Both lowercase per C's `%x`. Use a non-printable wide char
13321    /// to test (most BMP chars are printable; pick a control area
13322    /// like U+0085 NEXT LINE).
13323    #[test]
13324    fn wcs_nicechar_sel_large_nonprintable_uses_hex_escape() {
13325        let _g = crate::test_util::global_state_lock();
13326        // U+0085 NEL — control in C1 range, not iswprint.
13327        let nel = char::from_u32(0x85).unwrap();
13328        // Since 0x85 < 0x100, falls into nicechar_sel fallback.
13329        // After u9_iswprint says false, cv < 0x80 is false (cv=0x85),
13330        // PRINTEIGHTBIT off → enters !is_printable && cv < 0x80 branch?
13331        // Actually 0x85 >= 0x80 → !print_eightbit true → enters non-print branch.
13332        // 0x85 not 0x7f, not \n/\t, not <0x20. Falls past all if-elses to
13333        // post-branch logic → !is_printable, cv >= 0x100 false, cv >= 0x100 false
13334        // → falls to nicechar_sel fallback.
13335        let r = wcs_nicechar_sel(nel, None, None, false);
13336        assert!(!r.is_empty(), "must emit something for U+0085");
13337        // U+200B ZERO WIDTH SPACE — non-printable, 0x100-0xffff range.
13338        let zwsp = char::from_u32(0x200B).unwrap();
13339        let r = wcs_nicechar_sel(zwsp, None, None, false);
13340        // u9_iswprint(0x200B) returns true via unicode-width 0-width.
13341        // Per the impl: u9_iswprint true → emit raw. So r should be
13342        // the raw zwsp char.
13343        assert!(!r.is_empty());
13344    }
13345
13346    /// `Src/utils.c:709-714` — `wcs_nicechar(c)` delegates to
13347    /// `wcs_nicechar_sel(c, widthp, swidep, 0)`. Pin the parity.
13348    #[test]
13349    fn wcs_nicechar_matches_wcs_nicechar_sel_with_zero() {
13350        let _g = crate::test_util::global_state_lock();
13351        for c in ['a', '\n', '\t', '\x7f', '\x01', 'é', '字'] {
13352            assert_eq!(
13353                wcs_nicechar(c, None, None),
13354                wcs_nicechar_sel(c, None, None, false),
13355                "c:711 — `wcs_nicechar(c, NULL, NULL)` must equal `wcs_nicechar_sel(c, NULL, NULL, 0)` for {:?}",
13356                c
13357            );
13358        }
13359    }
13360
13361    /// `Src/utils.c:1989-2012` — `movefd(fd)`. Three contracts:
13362    /// 1. fd == -1 → returned as-is (no syscalls).
13363    /// 2. fd >= 10 → returned unchanged (already high).
13364    /// 3. fd < 10 → dupped with F_DUPFD >= 10, original closed
13365    ///    unconditionally (even on dup failure).
13366    #[test]
13367    #[cfg(unix)]
13368    fn movefd_returns_minus_one_unchanged() {
13369        let _g = crate::test_util::global_state_lock();
13370        // c:1992 — `if (fd != -1 && fd < 10)` skipped for fd=-1.
13371        assert_eq!(movefd(-1), -1, "fd=-1 must be returned unchanged");
13372    }
13373
13374    /// c:1992 — `fd >= 10` skips the dup-and-close path.
13375    #[test]
13376    #[cfg(unix)]
13377    fn movefd_returns_high_fd_unchanged() {
13378        let _g = crate::test_util::global_state_lock();
13379        // Use a known-open fd. /dev/null open with fd guaranteed to be
13380        // <10 because new fds get the lowest free slot — but we can
13381        // dup it to 10 directly to test the early-return path.
13382        let f = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13383        if f < 0 {
13384            return;
13385        } // Skip on unusual systems.
13386        let high = unsafe { libc::fcntl(f, libc::F_DUPFD, 20) };
13387        unsafe {
13388            libc::close(f);
13389        }
13390        if high < 0 {
13391            return;
13392        }
13393        // movefd(20) must return 20 unchanged (no dup, no close).
13394        let r = movefd(high);
13395        assert_eq!(r, high, "c:1992 — fd >= 10 returned unchanged");
13396        unsafe {
13397            libc::close(high);
13398        }
13399    }
13400
13401    /// `Src/utils.c:1968-1983` — `check_fd_table(fd)`. Grows the
13402    /// fdtable Vec to `fd+1` slots (filling new entries with
13403    /// FDT_UNUSED) and bumps `MAX_ZSH_FD` to `fd`. Pin: a small
13404    /// fd grows the table to `fd+1` length; MAX_ZSH_FD reflects it.
13405    /// fd ≤ cur_max early-returns without growing (c:1971-1972).
13406    #[test]
13407    fn check_fd_table_grows_to_fd_plus_one() {
13408        let _g = crate::test_util::global_state_lock();
13409        // Reset globals so the test is deterministic regardless
13410        // of prior ordering — other tests may have grown the
13411        // table or bumped MAX_ZSH_FD.
13412        MAX_ZSH_FD.store(-1, Ordering::Relaxed);
13413        {
13414            let mut g = fdtable_lock().lock().unwrap();
13415            g.clear();
13416        }
13417
13418        // c:1971-1972 — fd ≤ cur_max is the early-return path.
13419        // With cur_max=-1, fd=-1 hits `fd <= cur_max` → true.
13420        assert!(check_fd_table(-1));
13421        assert_eq!(
13422            MAX_ZSH_FD.load(Ordering::Relaxed),
13423            -1,
13424            "fd ≤ cur_max early-returns; MAX_ZSH_FD untouched"
13425        );
13426
13427        // c:1974-1981 — fd > cur_max grows fdtable to fd+1 slots.
13428        assert!(check_fd_table(7));
13429        assert_eq!(
13430            MAX_ZSH_FD.load(Ordering::Relaxed),
13431            7,
13432            "c:1982 — MAX_ZSH_FD := fd"
13433        );
13434        let len = {
13435            let g = fdtable_lock().lock().unwrap();
13436            g.len()
13437        };
13438        assert!(
13439            len >= 8,
13440            "c:1975-1979 — fdtable grew to ≥ fd+1 = 8 slots, got {}",
13441            len
13442        );
13443
13444        // Idempotence under fd ≤ cur_max.
13445        assert!(check_fd_table(3));
13446        assert_eq!(
13447            MAX_ZSH_FD.load(Ordering::Relaxed),
13448            7,
13449            "fd ≤ cur_max keeps MAX_ZSH_FD at 7"
13450        );
13451    }
13452
13453    /// `Src/utils.c:4364,4367` — `wcsitype(c, IWORD/ISEP)` reads
13454    /// from the `wordchars`/`ifs` GLOBALS (writable by
13455    /// `wordcharssetfn`/`ifssetfn`). Previously read from
13456    /// `std::env::var` — the libc process env — which never
13457    /// reflects runtime `WORDCHARS=…` shell-side assignments.
13458    /// Pin: set wordchars via `wordcharssetfn`, verify `wcsitype`
13459    /// returns true for chars in the new set.
13460    #[test]
13461    fn wcsitype_iword_reads_from_canonical_wordchars_global() {
13462        let _g = crate::test_util::global_state_lock();
13463        // Test only meaningful in MULTIBYTE mode for non-ASCII chars;
13464        // ASCII chars (< 0x80) route through TYPTAB directly.
13465        // Skip if MULTIBYTE off.
13466        if !isset(MULTIBYTE) {
13467            return;
13468        }
13469        // Save and set WORDCHARS to a single non-ASCII char.
13470        // C dispatches `pm->gsu.s->{get,set}fn(pm, val)`; mirror via
13471        // paramtab lookup.
13472        let saved = crate::ported::params::paramtab()
13473            .read()
13474            .ok()
13475            .and_then(|t| {
13476                t.get("WORDCHARS")
13477                    .map(|pm| crate::ported::params::wordcharsgetfn(pm))
13478            })
13479            .unwrap_or_default();
13480        // wordcharssetfn ignores its `_pm` arg and re-enters paramtab's
13481        // read lock via inittyptab (sibling of the IFS deadlock above).
13482        // Use a stack-local dummy pm instead of going through paramtab's
13483        // write lock so the test never holds two paramtab locks at once.
13484        let mut dummy = crate::ported::zsh_h::param::default();
13485        let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
13486            wordcharssetfn(dummy, val);
13487        };
13488        do_set(&mut dummy, "é".to_string());
13489        // 'é' is alphanumeric per Unicode → IWORD returns true via
13490        // is_alphanumeric short-circuit at c:4353. So we can't pin
13491        // the WORDCHARS-specific path with 'é'. Use a non-alnum char.
13492        do_set(&mut dummy, ":".to_string());
13493        // ':' is ASCII so wcsitype routes through TYPTAB (which now
13494        // has IWORD on ':' because wordcharssetfn called inittyptab).
13495        assert!(
13496            wcsitype(':', IWORD as u32),
13497            "c:4364 — wordchars membership through canonical global"
13498        );
13499        // Restore.
13500        do_set(&mut dummy, saved);
13501    }
13502
13503    /// `Src/utils.c:2090-2097` — `addmodulefd(fd, fdt)`. Stores the
13504    /// provided fdt in the fdtable slot for the given fd. Previously
13505    /// Rust port hardcoded FDT_MODULE and added CLOEXEC unconditionally.
13506    /// Pin: pass FDT_EXTERNAL, verify slot stores FDT_EXTERNAL (not
13507    /// FDT_MODULE).
13508    #[cfg(unix)]
13509    #[test]
13510    fn addmodulefd_respects_fdt_parameter() {
13511        let _g = crate::test_util::global_state_lock();
13512        // Open a real fd to use.
13513        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13514        if fd < 0 {
13515            return;
13516        }
13517        addmodulefd(fd, FDT_EXTERNAL);
13518        assert_eq!(
13519            fdtable_get(fd),
13520            FDT_EXTERNAL,
13521            "c:2095 — fdt parameter must be stored verbatim"
13522        );
13523        // Switch to FDT_MODULE.
13524        addmodulefd(fd, FDT_MODULE);
13525        assert_eq!(
13526            fdtable_get(fd),
13527            FDT_MODULE,
13528            "c:2095 — second call overwrites with new fdt"
13529        );
13530        // Cleanup.
13531        unsafe {
13532            libc::close(fd);
13533        }
13534    }
13535
13536    /// `Src/utils.c:2093` — `if (fd >= 0)`. Negative fd is silently
13537    /// ignored (no fdtable update). Pin the guard.
13538    #[test]
13539    fn addmodulefd_ignores_negative_fd() {
13540        let _g = crate::test_util::global_state_lock();
13541        // Should not panic, no side effect.
13542        addmodulefd(-1, FDT_MODULE);
13543        // Nothing to assert on side-effect-free path; just verify no panic.
13544    }
13545
13546    /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns -1 for
13547    /// non-lock fds, 0 for FDT_FLOCK/FDT_FLOCK_EXEC. Previously the
13548    /// Rust port always returned 0 — broke `zsystem flock -u <fd>`
13549    /// distinguishing "fd never flocked" from "successfully released".
13550    #[cfg(unix)]
13551    #[test]
13552    fn zcloselockfd_returns_minus_one_for_non_lock_fd() {
13553        let _g = crate::test_util::global_state_lock();
13554        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13555        if fd < 0 {
13556            return;
13557        }
13558        // No addlockfd call → fd is NOT in the flock table.
13559        let r = zcloselockfd(fd);
13560        assert_eq!(r, -1, "c:2160-2161 — non-lock fd must return -1, NOT 0");
13561        unsafe {
13562            libc::close(fd);
13563        }
13564    }
13565
13566    /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns 0 for an
13567    /// fd that was registered via `addlockfd`. Pin the end-to-end
13568    /// round-trip: addlockfd → zcloselockfd returns 0.
13569    #[cfg(unix)]
13570    #[test]
13571    fn zcloselockfd_returns_zero_for_flock_fd_then_closes() {
13572        let _g = crate::test_util::global_state_lock();
13573        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13574        if fd < 0 {
13575            return;
13576        }
13577        addlockfd(fd, true);
13578        let r = zcloselockfd(fd);
13579        assert_eq!(r, 0, "c:2162-2163 — FDT_FLOCK fd → zclose + return 0");
13580        // After zcloselockfd, the underlying close() has fired.
13581        // close() returning -1 with EBADF would confirm that, but
13582        // that's a libc side-effect; just verify the return value.
13583    }
13584
13585    /// `Src/utils.c:1982` — `check_fd_table` sets `max_zsh_fd = fd`
13586    /// when fd is new. `fdtable_set` inlines this to keep the guard
13587    /// in `zcloselockfd` (`if (fd > max_zsh_fd) return -1`) working
13588    /// against fds populated by `addmodulefd`/`addlockfd`. Pin: after
13589    /// `fdtable_set(fd, FDT_FLOCK)`, MAX_ZSH_FD >= fd.
13590    #[cfg(unix)]
13591    #[test]
13592    fn fdtable_set_bumps_max_zsh_fd() {
13593        let _g = crate::test_util::global_state_lock();
13594        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13595        if fd < 0 {
13596            return;
13597        }
13598        fdtable_set(fd, FDT_FLOCK);
13599        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
13600        assert!(
13601            max_fd >= fd,
13602            "c:1982 — max_zsh_fd ({}) must be >= newly-set fd ({})",
13603            max_fd,
13604            fd
13605        );
13606        // Cleanup.
13607        fdtable_set(fd, FDT_UNUSED);
13608        unsafe {
13609            libc::close(fd);
13610        }
13611    }
13612
13613    /// `Src/utils.c:2111-2121` — `addlockfd(fd, cloexec)`. Selects
13614    /// between FDT_FLOCK (when cloexec=true, lock dies on exec) and
13615    /// FDT_FLOCK_EXEC (when cloexec=false, lock survives exec).
13616    /// Previously the Rust port did `fcntl(F_SETFD, CLOEXEC)` —
13617    /// totally wrong semantics. Pin the FDT slot.
13618    #[cfg(unix)]
13619    #[test]
13620    fn addlockfd_selects_flock_category_per_cloexec_flag() {
13621        let _g = crate::test_util::global_state_lock();
13622        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13623        if fd < 0 {
13624            return;
13625        }
13626        // cloexec=true → FDT_FLOCK (lock dies on exec).
13627        addlockfd(fd, true);
13628        assert_eq!(
13629            fdtable_get(fd),
13630            FDT_FLOCK,
13631            "c:2117 — cloexec=true → FDT_FLOCK"
13632        );
13633        // cloexec=false → FDT_FLOCK_EXEC (lock survives exec).
13634        addlockfd(fd, false);
13635        assert_eq!(
13636            fdtable_get(fd),
13637            FDT_FLOCK_EXEC,
13638            "c:2119 — cloexec=false → FDT_FLOCK_EXEC"
13639        );
13640        // Cleanup.
13641        unsafe {
13642            libc::close(fd);
13643        }
13644    }
13645
13646    /// `Src/utils.c:1211` — `adduserdir` rejects paths `>= PATH_MAX`
13647    /// by removing the existing entry (treating as "can't use this
13648    /// value as a directory"). Pin with a path constructed to exceed
13649    /// PATH_MAX, verify no insertion happens.
13650    #[cfg(unix)]
13651    #[test]
13652    fn adduserdir_rejects_paths_at_or_above_path_max() {
13653        let _g = crate::test_util::global_state_lock();
13654        if !interact() {
13655            // c:1193 — non-interactive shells skip the table entirely.
13656            // Test inactive in non-interactive contexts.
13657            return;
13658        }
13659        // Ensure the entry doesn't exist beforehand.
13660        let name = "ZSHRS_TEST_PATHMAX_DIR";
13661        let _ = removenameddirnode(name);
13662        // Construct an oversized path.
13663        let mut over: String = "/".to_string();
13664        over.push_str(&"a".repeat(libc::PATH_MAX as usize));
13665        // adduserdir with the oversized path + AUTONAMEDIRS-off path
13666        // via always=true (so the AUTONAMEDIRS guard doesn't reject).
13667        adduserdir(name, &over, 0, true);
13668        // c:1211 — too-long → remove path triggered, no entry added.
13669        let tab = nameddirtab().lock().unwrap();
13670        assert!(
13671            !tab.contains_key(name),
13672            "c:1211 — strlen(t) >= PATH_MAX must NOT insert entry"
13673        );
13674    }
13675
13676    /// `Src/utils.c:760-786` — `pathprog`. Finds any regular file
13677    /// (not directory, no executable-bit check) in `$PATH`.
13678    /// Previously required `mode & 0o111 != 0` — silently missed
13679    /// non-executable autoload-function plaintext scripts. Pin
13680    /// using a known-existing file in /tmp.
13681    #[test]
13682    #[cfg(unix)]
13683    fn pathprog_finds_non_executable_files() {
13684        let _g = crate::test_util::global_state_lock();
13685        // Create a non-executable file in /tmp and add /tmp to PATH.
13686        let test_name = format!("zshrs_test_pathprog_{}", unsafe { libc::getpid() });
13687        let path = PathBuf::from("/tmp").join(&test_name);
13688        // Write content, NO executable bit.
13689        if fs::write(&path, b"plain content").is_err() {
13690            return; // /tmp may be unwritable on some systems.
13691        }
13692        // Verify it's not executable.
13693        let meta = fs::metadata(&path).unwrap();
13694        assert_eq!(
13695            meta.permissions().mode() & 0o111,
13696            0,
13697            "test setup: must be non-executable"
13698        );
13699        // Set PATH to /tmp.
13700        let saved_path = getsparam("PATH");
13701        assignsparam("PATH", "/tmp", 0);
13702        // pathprog should find the file.
13703        let r = pathprog(&test_name);
13704        // Cleanup.
13705        let _ = fs::remove_file(&path);
13706        if let Some(prev) = saved_path {
13707            assignsparam("PATH", &prev, 0);
13708        }
13709        assert_eq!(
13710            r,
13711            Some(path),
13712            "c:776 — pathprog finds non-executable files (only F_OK + !S_ISDIR)"
13713        );
13714    }
13715
13716    /// `Src/utils.c:776-778` — pathprog skips directories. Pin: a
13717    /// directory in $PATH with the queried name must NOT be returned.
13718    #[test]
13719    #[cfg(unix)]
13720    fn pathprog_skips_directories() {
13721        let _g = crate::test_util::global_state_lock();
13722        let test_name = format!("zshrs_test_pathprog_dir_{}", unsafe { libc::getpid() });
13723        let path = PathBuf::from("/tmp").join(&test_name);
13724        if fs::create_dir(&path).is_err() {
13725            return;
13726        }
13727        let saved_path = getsparam("PATH");
13728        assignsparam("PATH", "/tmp", 0);
13729        let r = pathprog(&test_name);
13730        // Cleanup.
13731        let _ = fs::remove_dir(&path);
13732        if let Some(prev) = saved_path {
13733            assignsparam("PATH", &prev, 0);
13734        }
13735        assert!(
13736            r.is_none(),
13737            "c:778 — pathprog must skip directories (!S_ISDIR)"
13738        );
13739    }
13740
13741    /// `Src/utils.c:4367` — `wcsitype(c, ISEP)` reads from canonical
13742    /// `ifs` global. Pin via `ifssetfn`. ASCII path routes through
13743    /// TYPTAB which gets ISEP bit from inittyptab's IFS walk
13744    /// (added earlier this session). End-to-end pin.
13745    #[test]
13746    fn wcsitype_isep_reads_from_canonical_ifs_global() {
13747        let _g = crate::test_util::global_state_lock();
13748        if !isset(MULTIBYTE) {
13749            return;
13750        }
13751        // C: `pm->gsu.s->{get,set}fn(pm, val)`. Mirror via paramtab.
13752        let saved = crate::ported::params::paramtab()
13753            .read()
13754            .ok()
13755            .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
13756            .unwrap_or_default();
13757        // ifssetfn ignores its `_pm` arg (the C `gsu.s->setfn` receives
13758        // the param but the IFS callback only touches the global `ifs`
13759        // store + calls inittyptab). Calling through paramtab's write
13760        // lock would deadlock because inittyptab() re-takes paramtab's
13761        // read lock for the IFS walk (utils.rs:5028) — pthread rwlock
13762        // forbids same-thread write→read upgrade. Pass a stack-local
13763        // dummy pm instead so we never hold the paramtab lock during
13764        // ifssetfn.
13765        let mut dummy = crate::ported::zsh_h::param::default();
13766        let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
13767            ifssetfn(dummy, val);
13768        };
13769        do_set(&mut dummy, ":".to_string());
13770        assert!(
13771            wcsitype(':', ISEP as u32),
13772            "c:4367 — IFS membership through canonical global"
13773        );
13774        // Restore.
13775        do_set(&mut dummy, saved);
13776    }
13777
13778    /// c:536 — high-bit byte (>= 0x80) is nice when PRINTEIGHTBIT is
13779    /// OFF (the default). Pin the default case; the alternate
13780    /// behavior under PRINTEIGHTBIT-on is harder to exercise from
13781    /// a unit test due to opt-state global mutation.
13782    #[test]
13783    fn is_nicechar_highbit_is_nice_when_printeightbit_off() {
13784        let _g = crate::test_util::global_state_lock();
13785        // Default state: PRINTEIGHTBIT is off → high-bit bytes are nice.
13786        // Use char with high-bit equivalent low byte.
13787        // 0xb5 (Meta+5 territory) — masked to 0xff → still 0xb5.
13788        let c = char::from_u32(0xb5).unwrap();
13789        // ZISPRINT(0xb5) is false (>0x7e). 0xb5 & 0x80 set → check PRINTEIGHTBIT.
13790        // Default PRINTEIGHTBIT off → returns true (nice).
13791        if !isset(PRINTEIGHTBIT) {
13792            assert!(
13793                is_nicechar(c),
13794                "c:536 — high-bit byte 0x{:x} must be nice when PRINTEIGHTBIT off",
13795                0xb5_u32
13796            );
13797        }
13798    }
13799
13800    /// `Src/utils.c:1969-1983` — `check_fd_table(fd)` grows the
13801    /// fdtable so it can index `fd` and bumps `max_zsh_fd`. The
13802    /// previous Rust port was a no-op shim that always returned true;
13803    /// real behavior is "fdtable.len() must be > fd" and
13804    /// "MAX_ZSH_FD >= fd" after the call.
13805    #[test]
13806    fn check_fd_table_grows_fdtable_and_bumps_max_zsh_fd() {
13807        let _g = crate::test_util::global_state_lock();
13808        // Snapshot prior state so we don't poison other tests.
13809        let saved_max = MAX_ZSH_FD.load(Ordering::Relaxed);
13810        // Pick a target fd well above the typical 0/1/2.
13811        let target = saved_max.max(50) + 7;
13812        check_fd_table(target);
13813        let new_max = MAX_ZSH_FD.load(Ordering::Relaxed);
13814        assert!(
13815            new_max >= target,
13816            "c:1982 — max_zsh_fd must be >= target after grow (got {})",
13817            new_max
13818        );
13819        // Calling again with a lower fd MUST NOT shrink max_zsh_fd
13820        // (c:1971-1972 early return).
13821        check_fd_table(target - 3);
13822        assert_eq!(
13823            MAX_ZSH_FD.load(Ordering::Relaxed),
13824            new_max,
13825            "c:1971 — fd <= max_zsh_fd path must not change max"
13826        );
13827        // Restore approximately — best we can do without exposing
13828        // fdtable internals; subsequent fdtable_set callers cope.
13829        MAX_ZSH_FD.store(saved_max, Ordering::Relaxed);
13830    }
13831
13832    /// `Src/utils.c:1971` — `if (fd <= max_zsh_fd) return;` early
13833    /// exit. Calling with a small fd (<= current max) must be a
13834    /// no-op.
13835    #[test]
13836    fn check_fd_table_small_fd_is_noop() {
13837        let _g = crate::test_util::global_state_lock();
13838        // Ensure max_zsh_fd is non-trivial.
13839        let _ = check_fd_table(100);
13840        let max_before = MAX_ZSH_FD.load(Ordering::Relaxed);
13841        check_fd_table(5);
13842        assert_eq!(
13843            MAX_ZSH_FD.load(Ordering::Relaxed),
13844            max_before,
13845            "c:1971 — small fd path must not touch max_zsh_fd"
13846        );
13847    }
13848
13849    /// `Src/utils.c:1969-1983` — defensive: negative fd shouldn't
13850    /// panic. C-side would have indexed past the start of `fdtable`
13851    /// (UB). Rust port should fail-soft.
13852    #[test]
13853    fn check_fd_table_negative_fd_does_not_panic() {
13854        let _g = crate::test_util::global_state_lock();
13855        // Should not panic; behavior beyond "no panic" is unspecified.
13856        let _ = check_fd_table(-1);
13857        let _ = check_fd_table(-100);
13858    }
13859
13860    /// `Src/zsh.h:3274` — under MULTIBYTE_SUPPORT, the C source
13861    /// defines `nicezputs(str, outs)` as a macro:
13862    ///   `(void)mb_niceformat((str), (outs), NULL, 0)`.
13863    /// So Rust `nicezputs(s)` must equal `mb_niceformat(s)` for
13864    /// every input. Pins the macro-equivalence after fixing the
13865    /// previous `chars().map(nicechar)` impl (which corrupted
13866    /// non-ASCII multibyte codepoints into `\M-X` mangle).
13867    #[test]
13868    fn nicezputs_matches_mb_niceformat_under_multibyte() {
13869        let _g = crate::test_util::global_state_lock();
13870        // C-equivalent pattern: write to a stream, compare to
13871        // mb_niceformat outstrp-form output. Inline rather than via a
13872        // Rust-only helper.
13873        for input in ["hello", "a\nb", "é", ""] {
13874            let mut nz_buf: Vec<u8> = Vec::new();
13875            let _ = nicezputs(input, &mut nz_buf);
13876            let nz = String::from_utf8(nz_buf).expect("utf8");
13877
13878            let mut mb_out: Option<String> = None;
13879            let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
13880            assert_eq!(
13881                nz,
13882                mb_out.unwrap_or_default(),
13883                "nicezputs and mb_niceformat must agree for {:?}",
13884                input
13885            );
13886        }
13887        // \n must produce literal `\n` (backslash + n).
13888        let mut nz_buf: Vec<u8> = Vec::new();
13889        let _ = nicezputs("a\nb", &mut nz_buf);
13890        let nz = String::from_utf8(nz_buf).expect("utf8");
13891        assert!(nz.contains("\\n"), "nicechar emits `\\n`, not raw 0x0a");
13892    }
13893
13894    /// `Src/utils.c:5530` — under MULTIBYTE_SUPPORT, `nicedup(s, heap)`
13895    /// body is `(void)mb_niceformat(s, NULL, &retstr, …); return retstr;`
13896    /// so the returned string MUST equal mb_niceformat output.
13897    #[test]
13898    fn nicedup_matches_mb_niceformat_under_multibyte() {
13899        let _g = crate::test_util::global_state_lock();
13900        for input in ["hello", "a\nb", "é"] {
13901            let nd = nicedup(input, 0);
13902            let mut mb_out: Option<String> = None;
13903            let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
13904            assert_eq!(
13905                nd,
13906                mb_out.unwrap_or_default(),
13907                "nicedup must equal mb_niceformat outstrp form for {:?}",
13908                input
13909            );
13910        }
13911        // nicedupstring delegates to nicedup(s, 1).
13912        assert_eq!(nicedupstring("hé\nllo"), nicedup("hé\nllo", 1));
13913    }
13914
13915    /// `Src/utils.c:734-744` — `zwcwidth(wc)` returns 1 when MULTIBYTE
13916    /// option is unset (c:738), regardless of the codepoint's actual
13917    /// display width. The previous Rust port skipped the option gate
13918    /// and always used the Unicode width table. Pin the option-gated
13919    /// behavior end-to-end: unset MULTIBYTE → width 1 for CJK; set →
13920    /// width 2 for CJK.
13921    #[test]
13922    fn zwcwidth_returns_1_when_multibyte_unset() {
13923        let _g = crate::test_util::global_state_lock();
13924        let saved = isset(MULTIBYTE);
13925        // c:738 — unset(MULTIBYTE) path returns 1 for everything.
13926        dosetopt(MULTIBYTE, 0, 1);
13927        assert_eq!(zwcwidth('a'), 1, "c:738 — ASCII width 1 under nomultibyte");
13928        assert_eq!(
13929            zwcwidth('字'),
13930            1,
13931            "c:738 — CJK collapses to 1 under nomultibyte (was 2 via Unicode-width)"
13932        );
13933        assert_eq!(
13934            zwcwidth('\u{200B}'),
13935            1,
13936            "c:738 — zero-width space → 1 under nomultibyte (was 0 via Unicode-width)"
13937        );
13938        // c:740-744 — MULTIBYTE on: Unicode-width table applies.
13939        dosetopt(MULTIBYTE, 1, 1);
13940        assert_eq!(zwcwidth('a'), 1, "ASCII width is 1");
13941        assert_eq!(zwcwidth('字'), 2, "c:740 — CJK width 2 under multibyte");
13942        // Restore prior state.
13943        dosetopt(MULTIBYTE, if saved { 1 } else { 0 }, 1);
13944    }
13945
13946    /// `Src/utils.c:6478-6492` — `quotedzputs(s, NULL)` under
13947    /// MULTIBYTE_SUPPORT detects "needs nice-format" inputs via
13948    /// `is_mb_niceformat(s)` and emits `$'<nice-formatted body>'`
13949    /// (the dollar-quoted form so embedded `\n`/`\t`/escape sequences
13950    /// round-trip through `typeset -p`/`set`/`set -x`). The previous
13951    /// Rust port skipped this branch entirely and single-quoted such
13952    /// strings — POSIX `'…'` is *strong* (no escapes interpreted),
13953    /// breaking round-trip for control bytes.
13954    #[test]
13955    fn quotedzputs_uses_dollar_quotes_for_control_chars() {
13956        let _g = crate::test_util::global_state_lock();
13957        // Ensure typtab is initialised — hasspecial depends on ISPECIAL
13958        // bits which are populated by inittyptab. Without this the
13959        // SPECCHARS arm short-circuits to "no specials".
13960        inittyptab();
13961        // Empty stays `''`.
13962        assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
13963        // Plain alphanumeric: no specials, no controls → bare.
13964        assert_eq!(
13965            quotedzputs("hello"),
13966            "hello",
13967            "c:6511-6517 — no SPECCHARS member → return unchanged"
13968        );
13969        // Control char `\n` needs `$'…'` to round-trip.
13970        let r = quotedzputs("a\nb");
13971        assert!(
13972            r.starts_with("$'") && r.ends_with('\''),
13973            "c:6488 — control char must use $'…' form (got {:?})",
13974            r
13975        );
13976        // Tab and ESC also force the niceformat arm.
13977        let r = quotedzputs("\t");
13978        assert!(
13979            r.starts_with("$'"),
13980            "c:6488 — TAB forces $'…' arm (got {:?})",
13981            r
13982        );
13983        let r = quotedzputs("\u{1b}[31m");
13984        assert!(
13985            r.starts_with("$'"),
13986            "c:6488 — ESC sequence forces $'…' arm (got {:?})",
13987            r
13988        );
13989        // Single quote forces single-quote arm via SPECCHARS membership
13990        // (Src/zsh.h:228 — SPECCHARS includes `'`). Embedded `'`
13991        // rewrites to `'\''`.
13992        let r = quotedzputs("a'b");
13993        assert!(
13994            r.contains("'\\''"),
13995            "c:6573-6587 — embedded ' → '\\''  (got {:?})",
13996            r
13997        );
13998    }
13999
14000    /// `Src/utils.c:2923-2945` — `read_loop` returns the requested
14001    /// length on full read, or the partial count on EOF. Pin: writing
14002    /// a known buffer to a pipe and reading it back returns the same
14003    /// content + correct length. Drives the no-side-effect path that
14004    /// the diagnostic-message fix doesn't touch.
14005    #[test]
14006    #[cfg(unix)]
14007    fn read_loop_round_trips_pipe_bytes() {
14008        let _g = crate::test_util::global_state_lock();
14009        // Create a pipe; write 16 bytes; read them back.
14010        let mut fds: [libc::c_int; 2] = [0; 2];
14011        unsafe {
14012            assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
14013        }
14014        let payload = b"hello-world-1234";
14015        let written = unsafe {
14016            libc::write(
14017                fds[1],
14018                payload.as_ptr() as *const libc::c_void,
14019                payload.len(),
14020            )
14021        };
14022        assert_eq!(written, payload.len() as isize, "write all 16 bytes");
14023        unsafe {
14024            libc::close(fds[1]);
14025        }
14026        let mut buf = [0u8; 16];
14027        let got = read_loop(fds[0], &mut buf).expect("read_loop ok");
14028        assert_eq!(got, payload.len(), "c:2929 — read_loop returns full length");
14029        assert_eq!(
14030            &buf[..],
14031            &payload[..],
14032            "c:2940-2941 — buffer copied verbatim"
14033        );
14034        unsafe {
14035            libc::close(fds[0]);
14036        }
14037    }
14038
14039    /// `Src/utils.c:2949-2970` — `write_loop` returns the requested
14040    /// length when the kernel accepts all bytes (the common case for
14041    /// a pipe with room). Pin the no-side-effect happy path.
14042    #[test]
14043    #[cfg(unix)]
14044    fn write_loop_writes_all_bytes_to_pipe() {
14045        let _g = crate::test_util::global_state_lock();
14046        let mut fds: [libc::c_int; 2] = [0; 2];
14047        unsafe {
14048            assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
14049        }
14050        let payload = b"abcdef";
14051        let got = write_loop(fds[1], payload).expect("write_loop ok");
14052        assert_eq!(
14053            got,
14054            payload.len(),
14055            "c:2955-2956 — write_loop returns full length on accept"
14056        );
14057        // Read back to verify.
14058        unsafe {
14059            libc::close(fds[1]);
14060        }
14061        let mut buf = [0u8; 6];
14062        let _ = unsafe { libc::read(fds[0], buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
14063        assert_eq!(&buf[..], &payload[..]);
14064        unsafe {
14065            libc::close(fds[0]);
14066        }
14067    }
14068
14069    /// `Src/utils.c:2935-2936` — `read_loop` on a closed/invalid fd
14070    /// returns an io::Error (the C path returns `ret` and emits the
14071    /// `zwarn` to stderr). Pin the error propagation; the zwarn
14072    /// emission is a stderr side-effect tested only by inspecting
14073    /// log output (out of scope here).
14074    #[test]
14075    #[cfg(unix)]
14076    fn read_loop_returns_error_on_invalid_fd() {
14077        let _g = crate::test_util::global_state_lock();
14078        let mut buf = [0u8; 4];
14079        // fd 9999 is essentially guaranteed-not-open in a test
14080        // process; the canonical "bad fd" error path.
14081        let r = read_loop(9999, &mut buf);
14082        assert!(
14083            r.is_err(),
14084            "c:2935 — invalid fd → io::Error (and zwarn to stderr)"
14085        );
14086    }
14087
14088    /// `Src/utils.c:2972-2988` — `read1char(echo)` reads from SHTTY,
14089    /// not stdin. With SHTTY uninitialised (default -1 in test
14090    /// environment), the function MUST return -1 immediately rather
14091    /// than blocking on a stdin read. The previous Rust port read
14092    /// from stdin and returned None — which in some test runners
14093    /// would block waiting for input.
14094    #[test]
14095    #[cfg(unix)]
14096    fn read1char_returns_minus_one_when_shtty_unset() {
14097        let _g = crate::test_util::global_state_lock();
14098        // Test environment: SHTTY is -1 (no controlling tty bound
14099        // by the port). C-side would `read(-1, ...)` which fails;
14100        // Rust port should fail-fast with -1.
14101        let saved = SHTTY.load(Ordering::Relaxed);
14102        SHTTY.store(-1, Ordering::Relaxed);
14103        let got = read1char(0); // echo=0
14104        assert_eq!(got, -1, "c:2978 — SHTTY=-1 → read fails → return -1");
14105        // Restore.
14106        SHTTY.store(saved, Ordering::Relaxed);
14107    }
14108
14109    /// `Src/utils.c:1989-2012` — `movefd(fd)` dups fd to >= 10 (so it
14110    /// stays out of the user fd range 0..=9), closes the original,
14111    /// AND marks the new fd as `FDT_INTERNAL` in fdtable. Previous
14112    /// Rust port omitted the fdtable_set call — internal-fd tracking
14113    /// (`closeallelse`, forkexec) silently never saw zshrs-internal
14114    /// fds. Pin: after movefd, fdtable[new_fd] == FDT_INTERNAL.
14115    #[test]
14116    #[cfg(unix)]
14117    fn movefd_marks_fdtable_internal() {
14118        let _g = crate::test_util::global_state_lock();
14119        // Open /dev/null to get a small (< 10) fd...
14120        let dev_null = CString::new("/dev/null").unwrap();
14121        let fd = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14122        assert!(fd >= 0, "open /dev/null returned -1");
14123        let new_fd = movefd(fd);
14124        assert!(
14125            new_fd >= 10,
14126            "c:1992-1994 — movefd dups to fd >= 10 (got {})",
14127            new_fd
14128        );
14129        // c:2009 — fdtable[new_fd] := FDT_INTERNAL.
14130        let entry = fdtable_get(new_fd);
14131        assert_eq!(
14132            entry, FDT_INTERNAL,
14133            "c:2009 — movefd must mark new_fd as FDT_INTERNAL (got {})",
14134            entry
14135        );
14136        unsafe {
14137            libc::close(new_fd);
14138        }
14139    }
14140
14141    /// `Src/utils.c:1989` — `movefd(-1)` is a defensive no-op: the
14142    /// `fd != -1 && fd < 10` gate at c:1992 fails, the c:2007
14143    /// `if (fd != -1)` post-check skips, and -1 is returned
14144    /// unchanged. Pin so a refactor of the early-out doesn't
14145    /// accidentally call fcntl(-1, ...).
14146    #[test]
14147    #[cfg(unix)]
14148    fn movefd_minus_one_returns_minus_one() {
14149        let _g = crate::test_util::global_state_lock();
14150        assert_eq!(
14151            movefd(-1),
14152            -1,
14153            "c:1992 — movefd(-1) bypasses both gates and returns -1"
14154        );
14155    }
14156
14157    /// `Src/utils.c:2021-2068` — `redup(x, y)` after a successful
14158    /// `dup2(x, y)` must:
14159    ///   * Copy fdtable[x] to fdtable[y] (c:2054).
14160    ///   * Promote FDT_FLOCK/FDT_FLOCK_EXEC to FDT_INTERNAL on the
14161    ///     dup target (c:2055-2056) — the lock doesn't transfer.
14162    ///   * Then close x (c:2064).
14163    ///
14164    /// Previous Rust port skipped the fdtable updates entirely.
14165    /// Pin the ownership-transfer with two fds the test allocates.
14166    #[test]
14167    #[cfg(unix)]
14168    fn redup_copies_fdtable_ownership_to_target() {
14169        let _g = crate::test_util::global_state_lock();
14170        // Open two distinct fds — both will land in the fdtable.
14171        let dev_null = CString::new("/dev/null").unwrap();
14172        let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14173        let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14174        assert!(x >= 0 && y >= 0, "open /dev/null returned -1");
14175        assert_ne!(x, y);
14176
14177        // Mark x as FDT_INTERNAL so we can observe the copy to y.
14178        check_fd_table(x);
14179        check_fd_table(y);
14180        fdtable_set(x, FDT_INTERNAL);
14181        fdtable_set(y, FDT_UNUSED);
14182
14183        let ret = redup(x, y);
14184        assert_eq!(ret, y, "c:2067 — successful redup returns y");
14185        // c:2054 — fdtable[y] inherited from fdtable[x].
14186        assert_eq!(
14187            fdtable_get(y),
14188            FDT_INTERNAL,
14189            "c:2054 — fdtable[y] = fdtable[x] (FDT_INTERNAL)"
14190        );
14191        // x is closed by c:2064.
14192        unsafe {
14193            libc::close(y);
14194        }
14195    }
14196
14197    /// `Src/utils.c:872-908` — `xsymlinks(path)` resolves `.`/`..`
14198    /// AND follows ONE LEVEL of symlinks via `readlink(2)`. Pin the
14199    /// symlink-following behavior with a temp-dir-managed symlink
14200    /// (the actual bug-fix pin — previous Rust port just normalised
14201    /// `.`/`..` without ever calling readlink).
14202    #[test]
14203    #[cfg(unix)]
14204    fn xsymlinks_follows_one_level_of_symlinks() {
14205        let _g = crate::test_util::global_state_lock();
14206        let tmp = std::env::temp_dir();
14207        let target = tmp.join(format!("zshrs_xsymlinks_target_{}", std::process::id()));
14208        let link = tmp.join(format!("zshrs_xsymlinks_link_{}", std::process::id()));
14209        // Create target as a regular dir to symlink to.
14210        let _ = fs::create_dir(&target);
14211        let _ = fs::remove_file(&link);
14212        std::os::unix::fs::symlink(&target, &link).unwrap();
14213
14214        let got = xsymlinks(link.to_str().unwrap()).unwrap();
14215        assert_eq!(
14216            got,
14217            target.to_string_lossy(),
14218            "c:908 — xsymlinks must follow the symlink to its target"
14219        );
14220
14221        // Cleanup.
14222        let _ = fs::remove_file(&link);
14223        let _ = fs::remove_dir(&target);
14224    }
14225
14226    /// `Src/utils.c:881-882` — `.` components are skipped.
14227    /// `Src/utils.c:883-896` — `..` components walk back one
14228    /// `/`-segment of xbuf (unless xbuf is empty or `/`).
14229    ///
14230    /// Use a temp directory we create ourselves — we can't pin
14231    /// `/tmp/...` literally because the macOS sandbox symlinks
14232    /// `/tmp -> /private/tmp`, which `xsymlinks` correctly follows
14233    /// (proving the bug-fix works). Test with a directory under
14234    /// the env tempdir + a non-existent sub so readlink fails on
14235    /// every component and the test exercises ONLY the c:881-896
14236    /// `.` / `..` paths.
14237    #[test]
14238    #[cfg(unix)]
14239    fn xsymlinks_normalises_dot_and_dotdot() {
14240        let _g = crate::test_util::global_state_lock();
14241        let tmp = fs::canonicalize(std::env::temp_dir()).unwrap();
14242        let base_dir = tmp.join(format!("zshrs_xs_norm_{}", std::process::id()));
14243        let _ = fs::create_dir(&base_dir);
14244        // `<base>/.` should be `<base>` (c:881 — `.` skipped).
14245        let arg = format!("{}/./.", base_dir.display());
14246        let got = xsymlinks(&arg).unwrap();
14247        assert_eq!(
14248            got,
14249            base_dir.to_string_lossy(),
14250            "c:881 — `.` segments collapse"
14251        );
14252        // `<base>/foo/..` should be `<base>` (c:891-895 — `..` walks back).
14253        let arg = format!("{}/foo/..", base_dir.display());
14254        let got = xsymlinks(&arg).unwrap();
14255        assert_eq!(
14256            got,
14257            base_dir.to_string_lossy(),
14258            "c:891-895 — `..` walks back one segment"
14259        );
14260        let _ = fs::remove_dir(&base_dir);
14261    }
14262
14263    /// `Src/utils.c:2055-2056` — when fdtable[x] is FDT_FLOCK or
14264    /// FDT_FLOCK_EXEC, the dup'd fd y gets promoted to FDT_INTERNAL
14265    /// (the dup doesn't carry the flock). Pin the promotion.
14266    ///
14267    /// Note on fdtable_flocks: C's c:2062-2063 decrements the
14268    /// flock count in `redup` BEFORE `zclose(x)`, and zclose ALSO
14269    /// decrements when it sees fdtable[fd] == FDT_FLOCK (c:2135).
14270    /// So C double-decrements for an FDT_FLOCK source fd in redup —
14271    /// the C comment at c:2058-2061 calls this "isn't expected to
14272    /// happen" (FDT_FLOCK fds aren't normally redup'd). We mirror
14273    /// C exactly: test asserts the count drops by 2 (1 → -1) to
14274    /// pin the faithful-to-C behavior. Reporting this as a C bug
14275    /// upstream is the right path; the port preserves it verbatim.
14276    #[test]
14277    #[cfg(unix)]
14278    fn redup_promotes_flock_to_internal_on_target() {
14279        let _g = crate::test_util::global_state_lock();
14280        let dev_null = CString::new("/dev/null").unwrap();
14281        let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14282        let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14283        assert!(x >= 0 && y >= 0);
14284        assert_ne!(x, y);
14285
14286        check_fd_table(x);
14287        check_fd_table(y);
14288        // Mark x as FDT_FLOCK.
14289        fdtable_set(x, FDT_FLOCK);
14290        FDTABLE_FLOCKS.store(2, Ordering::SeqCst); // start at 2 so double-decrement lands at 0
14291
14292        let _ = redup(x, y);
14293
14294        // c:2055-2056 — promoted to FDT_INTERNAL on y, NOT carried.
14295        assert_eq!(
14296            fdtable_get(y),
14297            FDT_INTERNAL,
14298            "c:2055-2056 — FDT_FLOCK on x promotes to FDT_INTERNAL on y"
14299        );
14300        // c:2062-2063 + zclose c:2135 — double decrement.
14301        assert_eq!(
14302            FDTABLE_FLOCKS.load(Ordering::SeqCst),
14303            0,
14304            "c:2062-2063 + c:2135 — flock count double-decremented (faithful to C)"
14305        );
14306        unsafe {
14307            libc::close(y);
14308        }
14309    }
14310
14311    /// `Src/utils.c:5217-5240` — `zreaddir(dir, ignoredots)` exposes
14312    /// the dot-filter as a parameter and returns one entry per call.
14313    /// Two paths:
14314    /// * `ignoredots=1` (the common case at c:590/655/1653/2884) —
14315    ///   `.` and `..` are filtered out.
14316    /// * `ignoredots=0` (used at c:4648 for spelling correction) —
14317    ///   `.` and `..` are RETAINED as valid candidates.
14318    #[test]
14319    #[cfg(unix)]
14320    fn zreaddir_honors_ignoredots_flag() {
14321        let _g = crate::test_util::global_state_lock();
14322        let tmp = std::env::temp_dir();
14323        let base = tmp.join(format!("zshrs_zreaddir_{}", std::process::id()));
14324        let _ = fs::create_dir(&base);
14325        // Create one real entry to make the test non-trivial.
14326        fs::write(base.join("file"), "x").unwrap();
14327
14328        // c:5232 — ignoredots=1: skip `.` and `..`, keep `file`.
14329        let mut dir = fs::read_dir(&base).unwrap();
14330        let mut with_skip: Vec<String> = Vec::new();
14331        while let Some(n) = zreaddir(&mut dir, 1) {
14332            with_skip.push(n);
14333        }
14334        assert!(
14335            with_skip.contains(&"file".to_string()),
14336            "c:5232 — real entry survives ignoredots=1"
14337        );
14338        assert!(
14339            !with_skip.contains(&".".to_string()),
14340            "c:5232 — `.` filtered with ignoredots=1"
14341        );
14342        assert!(
14343            !with_skip.contains(&"..".to_string()),
14344            "c:5232 — `..` filtered with ignoredots=1"
14345        );
14346
14347        // c:4648-equivalent — ignoredots=0: KEEP `.` and `..`.
14348        // (libstd's fs::read_dir filters them before exposing on
14349        // macOS/Linux, so the without_skip set is functionally
14350        // identical here — pin only that the API path doesn't error.)
14351        let mut dir2 = fs::read_dir(&base).unwrap();
14352        let mut without_skip: Vec<String> = Vec::new();
14353        while let Some(n) = zreaddir(&mut dir2, 0) {
14354            without_skip.push(n);
14355        }
14356        assert!(
14357            without_skip.contains(&"file".to_string()),
14358            "ignoredots=0 still yields real entries"
14359        );
14360
14361        let _ = fs::remove_file(base.join("file"));
14362        let _ = fs::remove_dir(&base);
14363    }
14364
14365    /// `Src/utils.c:1075` — `get_username()` uses `ztrdup_metafy` on
14366    /// `pswd->pw_name`. Previous Rust port returned the raw pw_name
14367    /// verbatim — fine for ASCII usernames, broken for high-bit
14368    /// bytes (downstream paramtab consumers assume metafied entries).
14369    /// Pin: ASCII usernames round-trip identically through metafy,
14370    /// AND the result is a non-empty string for the current uid.
14371    /// `Src/utils.c:3445-3460` — ztrftime zsh-specific %K/%L/%f
14372    /// extensions return values WITHOUT leading zeros (vs the
14373    /// strftime %H/%I/%d which pad).
14374    #[test]
14375    #[cfg(unix)]
14376    fn ztrftime_zsh_extensions_no_leading_zero() {
14377        let _g = crate::test_util::global_state_lock();
14378        // Pick a known time: Jan 5 2024 09:07:42.123456789 UTC.
14379        // Use SystemTime + an offset since we want deterministic values
14380        // independent of TZ; we just verify that the format substitutes
14381        // the right NUMBER OF DIGITS for the leading-zero case.
14382        use std::time::Duration;
14383        // 2024-01-05 09:07:42 UTC = 1704445662
14384        let t = UNIX_EPOCH + Duration::new(1704445662, 123_456_789);
14385        // %H gives leading zero "09"; %K should give "9" (in some TZ
14386        // hour will be different but the digit-count rule still holds).
14387        let h_padded = ztrftime("%H", t, false);
14388        let k_unpadded = ztrftime("%K", t, false);
14389        // Both represent the same hour. If h_padded starts with `0`,
14390        // k_unpadded must be 1-char and not start with `0`.
14391        if h_padded.starts_with('0') && h_padded.len() == 2 {
14392            assert!(
14393                !k_unpadded.starts_with('0'),
14394                "c:3445 — %K must strip the leading 0 from %H={}, got %K={}",
14395                h_padded,
14396                k_unpadded
14397            );
14398            assert_eq!(
14399                k_unpadded.len(),
14400                1,
14401                "c:3445 — %K should be 1 digit when hour < 10"
14402            );
14403        }
14404        // %f for day-of-month: t is Jan 5 → in any reasonable TZ
14405        // day is between 4 and 6; format should be 1 digit when day < 10.
14406        let d_padded = ztrftime("%d", t, false);
14407        let f_unpadded = ztrftime("%f", t, false);
14408        if d_padded.starts_with('0') && d_padded.len() == 2 {
14409            assert!(!f_unpadded.starts_with('0'));
14410            assert_eq!(
14411                f_unpadded.len(),
14412                1,
14413                "c:3457 — %f should be 1 digit when day < 10"
14414            );
14415        }
14416        // %3. fractional seconds: input nsec is 123456789 → first 3
14417        // digits should be 123. Note zsh's syntax is `%N.` where N is
14418        // the digit count (c:3374-3384 + c:3409).
14419        let frac = ztrftime("%3.", t, false);
14420        assert_eq!(
14421            frac, "123",
14422            "c:3409-3438 — %3. must emit first 3 digits of nsec"
14423        );
14424        // %. with no digit prefix defaults to 3 digits per c:3409.
14425        let frac_default = ztrftime("%.", t, false);
14426        assert_eq!(frac_default, "123", "c:3409 — %. defaults to 3 digits");
14427    }
14428
14429    #[test]
14430    #[cfg(unix)]
14431    fn get_username_returns_metafied_non_empty_string() {
14432        let _g = crate::test_util::global_state_lock();
14433        let name = get_username();
14434        // CI environments may have a username from `whoami(1)`. In
14435        // weird sandbox-only setups it might be empty, but on every
14436        // standard build it should be set.
14437        if name.is_empty() {
14438            return;
14439        }
14440        // ASCII round-trip via metafy is identity. If the username
14441        // had high-bit bytes (the bug case), the output would be
14442        // Meta-escaped — both forms are non-empty so this pin
14443        // doesn't reject either, just ensures the fn returns
14444        // SOMETHING usable (not an empty string).
14445        assert!(
14446            !name.is_empty(),
14447            "c:1086 — getpwuid result must yield a non-empty username"
14448        );
14449        // Sanity: metafy(name) == name for ASCII input (every byte
14450        // is below Meta range). Confirms the c:1086 step preserves
14451        // ASCII paths byte-for-byte.
14452        if name.bytes().all(|b| b < 0x80) {
14453            assert_eq!(
14454                metafy(&name),
14455                name,
14456                "c:1086 — ASCII metafy is identity (so pin holds for ASCII users)"
14457            );
14458        }
14459    }
14460
14461    // ═══════════════════════════════════════════════════════════════════
14462    // Pure-utility tests — zstrtol/zstrtoul (numeric parse) and
14463    // getkeystring (key-escape decode). Each test pinned against either
14464    // C zsh semantics or a direct zsh shell invocation where applicable.
14465    // ═══════════════════════════════════════════════════════════════════
14466
14467    // ── zstrtol: numeric string → (value, unconsumed_tail) ──────────
14468    /// `zstrtol("42", 10)` → (42, "") — full consumption.
14469    #[test]
14470    fn zstrtol_decimal_full_consumption() {
14471        let (v, t) = zstrtol("42", 10);
14472        assert_eq!(v, 42);
14473        assert_eq!(t, "");
14474    }
14475
14476    /// `zstrtol("42abc", 10)` → (42, "abc") — stops at first non-digit.
14477    #[test]
14478    fn zstrtol_decimal_stops_at_non_digit() {
14479        let (v, t) = zstrtol("42abc", 10);
14480        assert_eq!(v, 42);
14481        assert_eq!(t, "abc");
14482    }
14483
14484    /// `zstrtol("-7", 10)` → (-7, "") — sign handling.
14485    #[test]
14486    fn zstrtol_negative_decimal() {
14487        let (v, t) = zstrtol("-7", 10);
14488        assert_eq!(v, -7);
14489        assert_eq!(t, "");
14490    }
14491
14492    /// `zstrtol("+12", 10)` → (12, "") — explicit `+` sign.
14493    #[test]
14494    fn zstrtol_explicit_plus_sign_consumed() {
14495        let (v, t) = zstrtol("+12", 10);
14496        assert_eq!(v, 12);
14497        assert_eq!(t, "");
14498    }
14499
14500    /// `zstrtol("ff", 16)` → (255, "") — hex base.
14501    #[test]
14502    fn zstrtol_hex_base_16() {
14503        let (v, t) = zstrtol("ff", 16);
14504        assert_eq!(v, 255);
14505        assert_eq!(t, "");
14506    }
14507
14508    /// `zstrtol("FF", 16)` → (255, "") — case-insensitive hex.
14509    #[test]
14510    fn zstrtol_hex_uppercase() {
14511        let (v, t) = zstrtol("FF", 16);
14512        assert_eq!(v, 255);
14513    }
14514
14515    /// `zstrtol("1010", 2)` → (10, "") — binary base.
14516    #[test]
14517    fn zstrtol_binary_base_2() {
14518        let (v, t) = zstrtol("1010", 2);
14519        assert_eq!(v, 10);
14520        assert_eq!(t, "");
14521    }
14522
14523    /// `zstrtol("17", 8)` → (15, "") — octal base.
14524    #[test]
14525    fn zstrtol_octal_base_8() {
14526        let (v, t) = zstrtol("17", 8);
14527        assert_eq!(v, 15);
14528    }
14529
14530    /// `zstrtol("0x1A", 0)` → base-detect picks hex from `0x` prefix → 26.
14531    #[test]
14532    fn zstrtol_base_zero_detects_hex_prefix() {
14533        let (v, _) = zstrtol("0x1A", 0);
14534        assert_eq!(v, 26, "0x1A with base=0 → hex 26");
14535    }
14536
14537    /// `zstrtol("0b101", 0)` → base-detect picks binary → 5.
14538    #[test]
14539    fn zstrtol_base_zero_detects_binary_prefix() {
14540        let (v, _) = zstrtol("0b101", 0);
14541        assert_eq!(v, 5, "0b101 with base=0 → binary 5");
14542    }
14543
14544    /// `zstrtol("017", 0)` → base-detect: leading `0` → octal → 15.
14545    #[test]
14546    fn zstrtol_base_zero_leading_zero_means_octal() {
14547        let (v, _) = zstrtol("017", 0);
14548        assert_eq!(v, 15, "017 with base=0 → octal 15");
14549    }
14550
14551    /// `zstrtol("  42", 10)` → (42, "") — leading whitespace skipped.
14552    #[test]
14553    fn zstrtol_leading_whitespace_skipped() {
14554        let (v, _) = zstrtol("  42", 10);
14555        assert_eq!(v, 42);
14556    }
14557
14558    /// `zstrtol("0", 10)` → (0, "") — zero is valid input.
14559    #[test]
14560    fn zstrtol_zero_input() {
14561        let (v, t) = zstrtol("0", 10);
14562        assert_eq!(v, 0);
14563        assert_eq!(t, "");
14564    }
14565
14566    // ── zstrtol_underscore: digit-separator support ─────────────────
14567    /// `zstrtol_underscore("1_000_000", 10, true)` → (1_000_000, "")
14568    /// — underscores skipped during digit accumulation.
14569    #[test]
14570    fn zstrtol_underscore_separator_in_decimal() {
14571        let (v, _) = zstrtol_underscore("1_000_000", 10, true);
14572        assert_eq!(v, 1_000_000);
14573    }
14574
14575    /// Without underscore flag, `_` stops parsing.
14576    #[test]
14577    fn zstrtol_underscore_disabled_stops_at_underscore() {
14578        let (v, t) = zstrtol_underscore("123_456", 10, false);
14579        assert_eq!(v, 123);
14580        assert_eq!(t, "_456");
14581    }
14582
14583    // ── zstrtoul_underscore: unsigned variant ───────────────────────
14584    /// Parses a plain decimal unsigned.
14585    #[test]
14586    fn zstrtoul_underscore_basic_decimal() {
14587        let v = zstrtoul_underscore("12345");
14588        assert_eq!(v, Some(12345));
14589    }
14590
14591    /// Empty string → None (no number to parse).
14592    #[test]
14593    fn zstrtoul_underscore_empty_returns_none() {
14594        let v = zstrtoul_underscore("");
14595        assert_eq!(v, None);
14596    }
14597
14598    // ── getkeystring: shell-escape decode ───────────────────────────
14599    /// `\n` → newline, `\t` → tab, `\r` → CR.
14600    /// Anchor: `print -r -- $'\n\t\r'` produces the three bytes.
14601    #[test]
14602    fn getkeystring_decodes_common_escapes() {
14603        assert_eq!(getkeystring("\\n").0, "\n");
14604        assert_eq!(getkeystring("\\t").0, "\t");
14605        assert_eq!(getkeystring("\\r").0, "\r");
14606    }
14607
14608    /// `\\` → single backslash.
14609    #[test]
14610    fn getkeystring_double_backslash_yields_single() {
14611        assert_eq!(getkeystring("\\\\").0, "\\");
14612    }
14613
14614    /// `\xNN` hex escape → byte value.
14615    /// Anchor: `print -r -- $'\x41'` → "A" (0x41 = 65 = 'A').
14616    #[test]
14617    fn getkeystring_hex_escape_lowercase_x() {
14618        assert_eq!(getkeystring("\\x41").0, "A");
14619        assert_eq!(getkeystring("\\x7E").0, "~");
14620    }
14621
14622    /// `\u{NNNN}` Unicode escape → corresponding char.
14623    /// Anchor: `print -r -- $'é'` → "é".
14624    #[test]
14625    fn getkeystring_unicode_escape_u_four_digits() {
14626        assert_eq!(getkeystring("\\u00E9").0, "é");
14627    }
14628
14629    /// Plain text passes through unchanged.
14630    #[test]
14631    fn getkeystring_plain_text_passes_through() {
14632        assert_eq!(getkeystring("hello").0, "hello");
14633        assert_eq!(getkeystring("").0, "");
14634    }
14635
14636    /// Mixed plain + escapes → both handled.
14637    #[test]
14638    fn getkeystring_mixed_text_and_escapes() {
14639        assert_eq!(getkeystring("a\\nb").0, "a\nb");
14640        assert_eq!(
14641            getkeystring("line1\\nline2\\tindented").0,
14642            "line1\nline2\tindented"
14643        );
14644    }
14645
14646    // ── metafy / unmetafy round-trip on ASCII (identity) ────────────
14647    /// ASCII strings: metafy is identity (no meta-bytes to escape).
14648    #[test]
14649    fn metafy_then_unmetafy_ascii_roundtrips_to_input() {
14650        let s = "hello world";
14651        let m = metafy(s);
14652        assert_eq!(m, s);
14653        let mut bytes = m.into_bytes();
14654        let _len = unmetafy(&mut bytes);
14655        assert_eq!(String::from_utf8(bytes).unwrap(), "hello world");
14656    }
14657
14658    // ═══════════════════════════════════════════════════════════════════
14659    // quotestring — emit a quoted shell-safe form of the input string.
14660    // Each QT_* mode produces a different quoting style. Tests pin
14661    // each mode for both empty and non-empty input.
14662    // ═══════════════════════════════════════════════════════════════════
14663
14664    use crate::zsh_h::{
14665        QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_DOLLARS, QT_DOUBLE, QT_NONE,
14666        QT_SINGLE, QT_SINGLE_OPTIONAL,
14667    };
14668
14669    /// QT_NONE: no quoting; passes input through unchanged.
14670    #[test]
14671    fn quotestring_qt_none_passes_through_unchanged() {
14672        let _g = crate::test_util::global_state_lock();
14673        assert_eq!(quotestring("hello world", QT_NONE), "hello world");
14674        assert_eq!(quotestring("", QT_NONE), "");
14675        assert_eq!(quotestring("a*b?c", QT_NONE), "a*b?c");
14676    }
14677
14678    /// QT_NONE empty → empty.
14679    #[test]
14680    fn quotestring_qt_none_empty_is_empty() {
14681        let _g = crate::test_util::global_state_lock();
14682        assert_eq!(quotestring("", QT_NONE), "");
14683    }
14684
14685    /// QT_BACKSLASH on empty → "''" (single-quote pair).
14686    #[test]
14687    fn quotestring_qt_backslash_empty_yields_empty() {
14688        let _g = crate::test_util::global_state_lock();
14689        // c:6194 — shownull is 0 for plain QT_BACKSLASH, so empty → "".
14690        assert_eq!(quotestring("", QT_BACKSLASH), "");
14691    }
14692
14693    /// QT_BACKSLASH_SHOWNULL on empty → "''" too.
14694    #[test]
14695    fn quotestring_qt_backslash_shownull_empty_yields_empty_single_quotes() {
14696        let _g = crate::test_util::global_state_lock();
14697        assert_eq!(quotestring("", QT_BACKSLASH_SHOWNULL), "''");
14698    }
14699
14700    /// QT_SINGLE on empty → "''" (single-quote pair).
14701    #[test]
14702    fn quotestring_qt_single_empty_yields_empty_single_quotes() {
14703        let _g = crate::test_util::global_state_lock();
14704        assert_eq!(quotestring("", QT_SINGLE), "''");
14705    }
14706
14707    /// QT_SINGLE_OPTIONAL on empty → "''" too.
14708    #[test]
14709    fn quotestring_qt_single_optional_empty_yields_empty_single_quotes() {
14710        let _g = crate::test_util::global_state_lock();
14711        assert_eq!(quotestring("", QT_SINGLE_OPTIONAL), "''");
14712    }
14713
14714    /// QT_DOUBLE on empty → "" (empty double-quote pair).
14715    #[test]
14716    fn quotestring_qt_double_empty_yields_empty_double_quotes() {
14717        let _g = crate::test_util::global_state_lock();
14718        assert_eq!(quotestring("", QT_DOUBLE), "\"\"");
14719    }
14720
14721    /// QT_DOLLARS on empty → "$''".
14722    #[test]
14723    fn quotestring_qt_dollars_empty_yields_dollar_quote_pair() {
14724        let _g = crate::test_util::global_state_lock();
14725        assert_eq!(quotestring("", QT_DOLLARS), "$''");
14726    }
14727
14728    /// QT_BACKSLASH_PATTERN escapes only pattern meta-chars.
14729    /// Input "a*b?c[d]" → "a\\*b\\?c\\[d\\]".
14730    #[test]
14731    fn quotestring_qt_backslash_pattern_escapes_glob_metas() {
14732        let _g = crate::test_util::global_state_lock();
14733        let r = quotestring("a*b?c[d]", QT_BACKSLASH_PATTERN);
14734        assert!(
14735            r.contains("\\*") && r.contains("\\?") && r.contains("\\[") && r.contains("\\]"),
14736            "all globs must be escaped; got {r:?}"
14737        );
14738    }
14739
14740    /// QT_BACKSLASH_PATTERN does NOT escape plain ASCII letters/digits.
14741    #[test]
14742    fn quotestring_qt_backslash_pattern_doesnt_escape_plain_chars() {
14743        let _g = crate::test_util::global_state_lock();
14744        let r = quotestring("plain", QT_BACKSLASH_PATTERN);
14745        assert_eq!(r, "plain", "plain text passes through");
14746    }
14747
14748    /// QT_BACKSLASH_PATTERN escapes `<`, `>`, `(`, `)`, `|`, `#`, `^`, `~`.
14749    #[test]
14750    fn quotestring_qt_backslash_pattern_escapes_all_meta_set() {
14751        let _g = crate::test_util::global_state_lock();
14752        for ch in ['<', '>', '(', ')', '|', '#', '^', '~'] {
14753            let s = format!("x{ch}y");
14754            let r = quotestring(&s, QT_BACKSLASH_PATTERN);
14755            assert!(
14756                r.contains(&format!("\\{ch}")),
14757                "{ch:?} must be escaped, got {r:?}"
14758            );
14759        }
14760    }
14761
14762    /// QT_SINGLE on simple input → "'simple'" (wrapped in single quotes).
14763    #[test]
14764    fn quotestring_qt_single_wraps_simple_input() {
14765        let _g = crate::test_util::global_state_lock();
14766        assert_eq!(quotestring("simple", QT_SINGLE), "'simple'");
14767    }
14768
14769    // ═══════════════════════════════════════════════════════════════════
14770    // metafy / unmetafy edge cases — bytes that NEED meta-encoding
14771    // (NUL, Meta byte 0x83, Nularg 0xa1, etc.). Pin that metafy escapes
14772    // them via the Meta + (byte ^ 32) scheme and unmetafy reverses it.
14773    // ═══════════════════════════════════════════════════════════════════
14774
14775    /// `metafy` is identity for plain ASCII (no meta bytes).
14776    #[test]
14777    fn metafy_ascii_is_identity_byte_for_byte() {
14778        let s = "abc 123 XYZ!@#";
14779        assert_eq!(metafy(s), s);
14780    }
14781
14782    /// Empty string round-trips.
14783    #[test]
14784    fn metafy_empty_string_returns_empty() {
14785        assert_eq!(metafy(""), "");
14786    }
14787
14788    /// metafy → unmetafy is round-trip identity for ASCII input.
14789    #[test]
14790    fn metafy_unmetafy_roundtrip_with_punctuation() {
14791        let s = "Hello, World! 123 +-=";
14792        let m = metafy(s);
14793        let mut bytes = m.into_bytes();
14794        let _ = unmetafy(&mut bytes);
14795        let result = String::from_utf8(bytes).expect("valid utf-8");
14796        assert_eq!(result, s);
14797    }
14798
14799    /// Multi-byte UTF-8 chars get meta-encoded then unmetafied. The
14800    /// round-trip MUST preserve the original bytes. Inline the byte-
14801    /// level metafy here — `metafy()`'s String return goes through
14802    /// `from_utf8_lossy` for non-UTF-8 outputs (correct for String
14803    /// consumers like the lexer or pattern compile, but breaks byte-
14804    /// round-trip because U+FFFD inserts EF BF BD where the
14805    /// original Meta-escaped bytes lived). The unmetafy step
14806    /// reverses the Meta-escape to recover the source UTF-8 verbatim.
14807    #[test]
14808    fn metafy_unmetafy_roundtrip_with_utf8_multibyte_anchored() {
14809        let s = "日本語";
14810        // Inline of metafy() byte path (no String materialisation).
14811        let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
14812        for &b in s.as_bytes() {
14813            if imeta_byte(b) {
14814                bytes.push(Meta);
14815                bytes.push(b ^ 32);
14816            } else {
14817                bytes.push(b);
14818            }
14819        }
14820        let _ = unmetafy(&mut bytes);
14821        let result = String::from_utf8(bytes).expect("valid utf-8");
14822        assert_eq!(result, s, "UTF-8 round-trip must preserve content");
14823    }
14824
14825    // ─── zsh-corpus pins for quotestring per quote_type ─────────────
14826
14827    /// `quotestring(QT_NONE)` is identity on any input.
14828    #[test]
14829    fn quotestring_corpus_qt_none_is_identity() {
14830        let s = "anything `goes` $here";
14831        assert_eq!(quotestring(s, QT_NONE), s);
14832    }
14833
14834    /// `quotestring(QT_SINGLE)` on plain word wraps in single quotes.
14835    #[test]
14836    fn quotestring_corpus_qt_single_wraps_plain_word() {
14837        let out = quotestring("hello", QT_SINGLE);
14838        assert!(
14839            out.starts_with('\'') && out.ends_with('\''),
14840            "single-quoted = wraps with ', got {out:?}"
14841        );
14842        assert!(out.contains("hello"), "content preserved");
14843    }
14844
14845    /// `quotestring(QT_DOUBLE)` on plain word wraps in double quotes.
14846    #[test]
14847    fn quotestring_corpus_qt_double_wraps_plain_word() {
14848        let out = quotestring("hello", QT_DOUBLE);
14849        assert!(
14850            out.starts_with('"') && out.ends_with('"'),
14851            "double-quoted = wraps with \", got {out:?}"
14852        );
14853    }
14854
14855    /// `quotestring(QT_SINGLE)` on string with apostrophe escapes the
14856    /// apostrophe by closing+escape+reopen: `it's` → `'it'\''s'`.
14857    #[test]
14858    fn quotestring_corpus_qt_single_escapes_apostrophe() {
14859        let out = quotestring("it's", QT_SINGLE);
14860        assert!(
14861            out.contains("\\'") || out.contains("'\\''"),
14862            "apostrophe gets escaped in single-quote form, got {out:?}",
14863        );
14864    }
14865
14866    /// `quotestring(QT_BACKSLASH)` escapes shell special chars.
14867    /// Pattern chars like `*`, `?`, `$`, `(`, `)` should be backslashed.
14868    #[test]
14869    fn quotestring_corpus_qt_backslash_escapes_glob_chars() {
14870        let out = quotestring("a*b?c", QT_BACKSLASH);
14871        assert!(out.contains("\\*"), "* gets backslashed, got {out:?}");
14872        assert!(out.contains("\\?"), "? gets backslashed, got {out:?}");
14873    }
14874
14875    /// `quotestring("", QT_DOUBLE)` returns `""` literal.
14876    #[test]
14877    fn quotestring_corpus_qt_double_empty_yields_double_quotes() {
14878        let out = quotestring("", QT_DOUBLE);
14879        assert_eq!(out, "\"\"", "empty double-quoted = \"\"");
14880    }
14881
14882    /// `quotestring("", QT_SINGLE)` returns `''` literal.
14883    #[test]
14884    fn quotestring_corpus_qt_single_empty_yields_single_quotes() {
14885        let out = quotestring("", QT_SINGLE);
14886        assert_eq!(out, "''", "empty single-quoted = ''");
14887    }
14888
14889    /// `quotestring(QT_BACKSLASH)` on plain alphanumeric is identity.
14890    /// "abc123" should round-trip with no backslashes added.
14891    #[test]
14892    fn quotestring_corpus_qt_backslash_plain_word_unchanged() {
14893        let out = quotestring("abc123", QT_BACKSLASH);
14894        assert_eq!(
14895            out, "abc123",
14896            "plain alphanumeric needs no escape, got {out:?}"
14897        );
14898    }
14899
14900    /// `ztrlen("hello")` = 5 (no meta chars).
14901    #[test]
14902    fn ztrlen_corpus_plain_ascii_byte_count() {
14903        assert_eq!(ztrlen("hello"), 5);
14904    }
14905
14906    /// `ztrlen("")` = 0.
14907    #[test]
14908    fn ztrlen_corpus_empty_is_zero() {
14909        assert_eq!(ztrlen(""), 0);
14910    }
14911
14912    // ═══════════════════════════════════════════════════════════════════
14913    // itype_end C-parity tests — pin `utils.c:4395-4495` per-itype-flag
14914    // behavior. One assertion per flag/path so a regression in the
14915    // char-class walk points at the exact branch that drifted.
14916    //
14917    // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"]
14918    // so the bug stays surfaced but CI doesn't fail until the fix.
14919    // ═══════════════════════════════════════════════════════════════════
14920
14921    /// `itype_end("abc123", IIDENT, false)` walks the full identifier.
14922    /// C utils.c:4395 — IIDENT matches letters/digits/`_` via TYPTAB.
14923    /// Requires `inittyptab()` for TYPTAB population — call at entry.
14924    #[test]
14925    fn itype_end_ident_walks_full_identifier() {
14926        let _g = crate::test_util::global_state_lock();
14927        inittyptab();
14928        use crate::ported::ztype_h::IIDENT;
14929        assert_eq!(itype_end("abc123", IIDENT as u32, false), 6);
14930    }
14931
14932    /// `itype_end("abc-def", IIDENT, false)` stops at `-` (non-IIDENT).
14933    #[test]
14934    fn itype_end_ident_stops_at_dash() {
14935        let _g = crate::test_util::global_state_lock();
14936        inittyptab();
14937        use crate::ported::ztype_h::IIDENT;
14938        assert_eq!(itype_end("abc-def", IIDENT as u32, false), 3);
14939    }
14940
14941    /// `itype_end("a", IIDENT, true)` with `once=true` stops after 1.
14942    #[test]
14943    fn itype_end_once_stops_after_first_match() {
14944        let _g = crate::test_util::global_state_lock();
14945        inittyptab();
14946        use crate::ported::ztype_h::IIDENT;
14947        assert_eq!(itype_end("abc", IIDENT as u32, true), 1);
14948    }
14949
14950    /// `itype_end("", IIDENT, false)` on empty string returns 0.
14951    #[test]
14952    fn itype_end_empty_string_returns_zero() {
14953        let _g = crate::test_util::global_state_lock();
14954        inittyptab();
14955        use crate::ported::ztype_h::IIDENT;
14956        assert_eq!(itype_end("", IIDENT as u32, false), 0);
14957    }
14958
14959    /// `itype_end(":", IIDENT, false)` on non-IIDENT first char = 0.
14960    #[test]
14961    fn itype_end_non_ident_first_char_returns_zero() {
14962        let _g = crate::test_util::global_state_lock();
14963        inittyptab();
14964        use crate::ported::ztype_h::IIDENT;
14965        assert_eq!(itype_end(":foo", IIDENT as u32, false), 0);
14966    }
14967
14968    /// `itype_end("ns.foo", INAMESPC, false)` — C utils.c:4399-4413
14969    /// INAMESPC special-cases ksh93 namespace `.` separators. With
14970    /// non-POSIXIDENTIFIERS / non-KSH emulation, dotted names should
14971    /// walk through the `.` to consume `ns.foo` as a single name.
14972    /// The Rust port may stop at `.` since dotted-namespace support
14973    /// requires POSIXIDENTIFIERS check + ksh emulation gating that
14974    /// isn't fully wired.
14975    #[test]
14976    fn itype_end_inamespc_walks_through_ksh93_dot() {
14977        let _g = crate::test_util::global_state_lock();
14978        inittyptab();
14979        use crate::ported::ztype_h::INAMESPC;
14980        // Expected: full "ns.foo" = 6 bytes walked.
14981        assert_eq!(itype_end("ns.foo", INAMESPC, false), 6);
14982    }
14983
14984    /// `itype_end("a b c", ISEP, false)` from a space char.
14985    /// ISEP matches IFS chars. Default IFS includes ` \t\n`.
14986    /// Walk should consume only the leading separator(s).
14987    #[test]
14988    fn itype_end_isep_walks_separator_run() {
14989        let _g = crate::test_util::global_state_lock();
14990        inittyptab();
14991        use crate::ported::ztype_h::ISEP;
14992        // " \t " then 'a' — 3 sep bytes.
14993        assert_eq!(itype_end(" \t a", ISEP as u32, false), 3);
14994    }
14995
14996    // ═══════════════════════════════════════════════════════════════════
14997    // unmetafy C-parity tests — pin `utils.c:4954-4983`. Meta-byte
14998    // collapse (0x83 + (b^0x20) → b) must be in-place + return new len.
14999    // ═══════════════════════════════════════════════════════════════════
15000
15001    /// `unmetafy("abc")` is a no-op for non-metafied input.
15002    #[test]
15003    fn unmetafy_pure_ascii_unchanged() {
15004        let _g = crate::test_util::global_state_lock();
15005        let mut v = b"abc".to_vec();
15006        let n = unmetafy(&mut v);
15007        assert_eq!(n, 3);
15008        assert_eq!(v, b"abc");
15009    }
15010
15011    /// `unmetafy("a\x83a")` → "a\x01" because Meta(0x83) + next ^ 0x20
15012    /// = 'a' ^ 0x20 = 0x41 ^ 0x20 = 0x61... wait, 'a'=0x61, 0x61 ^ 0x20
15013    /// = 0x41 = 'A'. So unmetafy("a\x83a") → "aA" (2 bytes).
15014    #[test]
15015    fn unmetafy_meta_pair_collapses_to_xor_byte() {
15016        let _g = crate::test_util::global_state_lock();
15017        // 'a' + Meta(0x83) + 'a' → 'a' + ('a' ^ 0x20) = 'a' + 'A' = "aA"
15018        let mut v: Vec<u8> = vec![b'a', 0x83, b'a'];
15019        let n = unmetafy(&mut v);
15020        assert_eq!(n, 2);
15021        assert_eq!(v, vec![b'a', b'A']);
15022    }
15023
15024    /// `unmetafy("")` returns 0 with empty buffer.
15025    #[test]
15026    fn unmetafy_empty_returns_zero() {
15027        let _g = crate::test_util::global_state_lock();
15028        let mut v: Vec<u8> = Vec::new();
15029        let n = unmetafy(&mut v);
15030        assert_eq!(n, 0);
15031    }
15032
15033    /// `unmetafy` with trailing lone Meta byte. C `unmetafy` checks
15034    /// `p < s.len()` before reading the byte after Meta — trailing
15035    /// Meta at EOF is preserved as a single literal Meta byte.
15036    #[test]
15037    fn unmetafy_trailing_lone_meta_preserved() {
15038        let _g = crate::test_util::global_state_lock();
15039        let mut v: Vec<u8> = vec![b'a', 0x83];
15040        let n = unmetafy(&mut v);
15041        // Rust port copies the Meta byte to t, then loop exits with
15042        // p == s.len() → no second-byte read. Final = "a\x83" (2).
15043        assert_eq!(n, 2);
15044        assert_eq!(v, vec![b'a', 0x83]);
15045    }
15046
15047    // ═══════════════════════════════════════════════════════════════════
15048    // nicechar / wcs_nicechar C-parity tests — pin display-form
15049    // conversion. Required by stradd, prompt expansion, completion etc.
15050    // ═══════════════════════════════════════════════════════════════════
15051
15052    /// `nicechar('a')` returns the printable char unchanged.
15053    #[test]
15054    fn nicechar_printable_ascii_passes_through() {
15055        let _g = crate::test_util::global_state_lock();
15056        assert_eq!(nicechar('a'), "a");
15057    }
15058
15059    /// `nicechar('\n')` returns `\n` escape sequence per C `nicechar`
15060    /// (utils.c:464+ — control chars get `^M` style or `\n` escape).
15061    /// zsh emits `\n` as literal `\n` (two chars) for non-quotable
15062    /// nicechar; the `nicechar_sel(c, false)` form is non-quotable.
15063    #[test]
15064    fn nicechar_newline_emits_escape() {
15065        let _g = crate::test_util::global_state_lock();
15066        let out = nicechar('\n');
15067        assert!(
15068            out == "\\n" || out == "\n",
15069            "newline should be escaped or literal; got {out:?}"
15070        );
15071    }
15072
15073    /// `wcs_nicechar('A', None, None)` returns "A" for printable wide
15074    /// chars per utils.c:689.
15075    #[test]
15076    fn wcs_nicechar_printable_ascii_passes_through() {
15077        let _g = crate::test_util::global_state_lock();
15078        assert_eq!(wcs_nicechar('A', None, None), "A");
15079    }
15080
15081    // ═══════════════════════════════════════════════════════════════════
15082    // Additional C-parity tests for Src/utils.c nicechar dispatch.
15083    // ═══════════════════════════════════════════════════════════════════
15084
15085    /// c:462 — nicechar for printable ASCII passes through.
15086    #[test]
15087    fn nicechar_printable_ascii_passes_through_pin() {
15088        let _g = crate::test_util::global_state_lock();
15089        assert_eq!(nicechar('a'), "a");
15090        assert_eq!(nicechar('Z'), "Z");
15091        assert_eq!(nicechar('5'), "5");
15092        assert_eq!(nicechar(' '), " ");
15093    }
15094
15095    /// c:487 — nicechar('\\n') returns '\\n' (backslash + n).
15096    #[test]
15097    fn nicechar_newline_returns_backslash_n() {
15098        let _g = crate::test_util::global_state_lock();
15099        assert_eq!(nicechar('\n'), "\\n");
15100    }
15101
15102    /// c:490 — nicechar('\\t') returns '\\t' (backslash + t).
15103    #[test]
15104    fn nicechar_tab_returns_backslash_t() {
15105        let _g = crate::test_util::global_state_lock();
15106        assert_eq!(nicechar('\t'), "\\t");
15107    }
15108
15109    /// c:479 — nicechar(0x7f) returns '^?' (DEL → ^?).
15110    #[test]
15111    fn nicechar_del_returns_caret_question() {
15112        let _g = crate::test_util::global_state_lock();
15113        assert_eq!(nicechar('\x7f'), "^?");
15114    }
15115
15116    /// c:493 — nicechar(0x01) returns '^A' (Ctrl-A).
15117    #[test]
15118    fn nicechar_ctrl_a_returns_caret_A() {
15119        let _g = crate::test_util::global_state_lock();
15120        assert_eq!(nicechar('\x01'), "^A");
15121    }
15122
15123    /// c:493 — nicechar(0x1b) returns '^[' (ESC).
15124    #[test]
15125    fn nicechar_esc_returns_caret_bracket() {
15126        let _g = crate::test_util::global_state_lock();
15127        assert_eq!(nicechar('\x1b'), "^[");
15128    }
15129
15130    /// c:462 — nicechar_sel quotable=true uses '\\C-' instead of '^'
15131    /// for control chars.
15132    #[test]
15133    fn nicechar_sel_quotable_uses_backslash_C() {
15134        let _g = crate::test_util::global_state_lock();
15135        let r = nicechar_sel('\x01', true);
15136        assert!(
15137            r.contains("\\C-") || r.contains("^"),
15138            "quotable should emit \\C- form, got {:?}",
15139            r
15140        );
15141    }
15142
15143    /// c:531 — is_nicechar('a') returns FALSE: printable chars don't
15144    /// NEED nice (escape) representation, they pass through verbatim.
15145    /// Per the C comment in utils.c:531: "Return whether the char needs
15146    /// nice representation".
15147    #[test]
15148    fn is_nicechar_printable_returns_false() {
15149        let _g = crate::test_util::global_state_lock();
15150        assert!(!is_nicechar('a'), "printable doesn't NEED nice repr");
15151        assert!(!is_nicechar('Z'));
15152        assert!(!is_nicechar('5'));
15153    }
15154
15155    /// c:531 — is_nicechar for any byte 0..127 is safe (no panic).
15156    #[test]
15157    fn is_nicechar_all_ascii_safe() {
15158        let _g = crate::test_util::global_state_lock();
15159        for c in 0u8..=127 {
15160            let _ = is_nicechar(c as char); // no panic
15161        }
15162    }
15163
15164    /// c:462 — nicechar deterministic.
15165    #[test]
15166    fn nicechar_is_deterministic() {
15167        let _g = crate::test_util::global_state_lock();
15168        for c in ['a', '\n', '\t', '\x7f', '\x01', '\x1b'] {
15169            let first = nicechar(c);
15170            for _ in 0..5 {
15171                assert_eq!(nicechar(c), first, "{:?} must be pure", c);
15172            }
15173        }
15174    }
15175
15176    /// c:706 — `pathprog("")` returns None (empty path invalid).
15177    #[test]
15178    fn pathprog_empty_returns_none() {
15179        let _g = crate::test_util::global_state_lock();
15180        assert!(pathprog("").is_none());
15181    }
15182
15183    /// c:776 — `pathprog("/nonexistent/zshrs_xyz")` returns None.
15184    #[test]
15185    fn pathprog_nonexistent_returns_none() {
15186        let _g = crate::test_util::global_state_lock();
15187        let r = pathprog("/__never_exists_zshrs_pathprog_xyz");
15188        assert!(r.is_none());
15189    }
15190
15191    // ═══════════════════════════════════════════════════════════════════
15192    // Additional C-parity tests for Src/utils.c
15193    // c:99 set_widearray / c:447 nicechar_sel / c:521 is_nicechar /
15194    // c:742 zwcwidth / c:836 findpwd / c:928 slashsplit /
15195    // c:1258 get_username / c:1426 getnameddir / c:1475 dircmp /
15196    // c:1576 callhookfunc
15197    // ═══════════════════════════════════════════════════════════════════
15198
15199    /// c:99 — `set_widearray("")` empty returns empty Vec.
15200    #[test]
15201    fn set_widearray_empty_returns_empty() {
15202        let _g = crate::test_util::global_state_lock();
15203        assert!(set_widearray("").is_empty());
15204    }
15205
15206    /// c:99 — `set_widearray` returns Vec<char>.
15207    #[test]
15208    fn set_widearray_returns_vec_char_type() {
15209        let _g = crate::test_util::global_state_lock();
15210        let _: Vec<char> = set_widearray("a");
15211    }
15212
15213    /// c:447 — `nicechar_sel('a', false)` returns "a" (printable pass-through).
15214    #[test]
15215    fn nicechar_sel_ascii_letter_passthrough() {
15216        assert_eq!(nicechar_sel('a', false), "a", "printable letter as-is");
15217    }
15218
15219    /// c:447 — `nicechar_sel` is pure.
15220    #[test]
15221    fn nicechar_sel_is_pure() {
15222        for c in ['a', '\t', '\n', '\x1b', '\x7f', '日'] {
15223            let first = nicechar_sel(c, false);
15224            for _ in 0..3 {
15225                assert_eq!(
15226                    nicechar_sel(c, false),
15227                    first,
15228                    "nicechar_sel({:?}, false) must be pure",
15229                    c
15230                );
15231            }
15232        }
15233    }
15234
15235    /// c:742 — `zwcwidth` returns usize (compile-time type pin).
15236    #[test]
15237    fn zwcwidth_returns_usize_type() {
15238        let _: usize = zwcwidth('a');
15239    }
15240
15241    /// c:742 — `zwcwidth('a')` ASCII letter returns 1.
15242    #[test]
15243    fn zwcwidth_ascii_returns_one() {
15244        assert_eq!(zwcwidth('a'), 1, "ASCII letter width = 1");
15245    }
15246
15247    /// c:836 — `findpwd("")` empty returns Option<String>.
15248    #[test]
15249    fn findpwd_empty_returns_option_string_type() {
15250        let _g = crate::test_util::global_state_lock();
15251        let _: Option<String> = findpwd("");
15252    }
15253
15254    /// c:928 — `slashsplit("")` empty returns empty Vec.
15255    #[test]
15256    fn slashsplit_empty_returns_empty() {
15257        assert!(slashsplit("").is_empty());
15258    }
15259
15260    /// c:928 — `slashsplit("a/b/c")` splits to 3 elements.
15261    #[test]
15262    fn slashsplit_simple_three_components() {
15263        let r = slashsplit("a/b/c");
15264        assert_eq!(r, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
15265    }
15266
15267    /// c:1475 — `dircmp("", "")` both empty returns bool (type pin + safe).
15268    #[test]
15269    fn dircmp_both_empty_returns_bool_type() {
15270        let _g = crate::test_util::global_state_lock();
15271        let _: bool = dircmp("", "");
15272    }
15273
15274    /// c:1426 — `getnameddir("")` empty returns None.
15275    #[test]
15276    fn getnameddir_empty_returns_none() {
15277        let _g = crate::test_util::global_state_lock();
15278        assert!(getnameddir("").is_none(), "empty → None");
15279    }
15280
15281    /// c:1258 — `get_username` returns String (compile-time type pin).
15282    #[test]
15283    fn get_username_returns_string_type() {
15284        let _g = crate::test_util::global_state_lock();
15285        let _: String = get_username();
15286    }
15287
15288    /// c:1921-1935 — adjustwinsize(1) seeds `$LINES`/`$COLUMNS` from the
15289    /// TIOCGWINSZ probe of SHTTY. The port gated the setiparam calls on
15290    /// LINES/COLUMNS being in the OS environment (a misread of C's
15291    /// zgetenv guard, which only covers env re-publication — the C param
15292    /// is always live via its zterm_lines valptr alias), so every
15293    /// interactive shell ran with LINES=0 → `$(( LINES / 3 ))` division
15294    /// by zero in history-search-multi-word pagination (_hsmw_main:81).
15295    #[test]
15296    fn adjustwinsize_seeds_lines_columns_from_shtty() {
15297        let _g = crate::test_util::global_state_lock();
15298        unsafe {
15299            let mut master: libc::c_int = -1;
15300            let mut slave: libc::c_int = -1;
15301            let mut ws: libc::winsize = std::mem::zeroed();
15302            ws.ws_row = 33;
15303            ws.ws_col = 77;
15304            if libc::openpty(
15305                &mut master,
15306                &mut slave,
15307                std::ptr::null_mut(),
15308                std::ptr::null_mut(),
15309                &mut ws,
15310            ) != 0
15311            {
15312                return; // no pty available (sandboxed CI) — skip
15313            }
15314            let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
15315            crate::ported::init::SHTTY.store(slave, Ordering::Relaxed);
15316            let _ = adjustwinsize(1);
15317            crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
15318            libc::close(slave);
15319            libc::close(master);
15320        }
15321        assert_eq!(
15322            crate::ported::params::getiparam("LINES"),
15323            33,
15324            "LINES must reflect the SHTTY winsize probe"
15325        );
15326        assert_eq!(
15327            crate::ported::params::getiparam("COLUMNS"),
15328            77,
15329            "COLUMNS must reflect the SHTTY winsize probe"
15330        );
15331    }
15332}
15333
15334// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
15335// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
15336// pipeline. Extracted body of zerrmsg's `%e` arm (Src/utils.c:355-366):
15337// EINTR → "interrupt", EIO verbatim, else lowercased strerror.
15338// zerrmsg's errno branch routes through this so the prefix
15339// formatting matches C; module callers (zsh/system) embed the
15340// string in their own zwarnnam messages where C uses `%e`.
15341/// Render an errno the way zsh's `%e` warning format does —
15342/// `zerrmsg` case `'e'` at `Src/utils.c:352-368`:
15343///
15344/// - `EINTR` → the literal string `interrupt` (C also sets
15345///   `errflag |= ERRFLAG_ERROR` and truncates the rest of the
15346///   format; every zshrs call site uses `%e` as the final
15347///   conversion so the truncation is a no-op here).
15348/// - `EIO` → `strerror(EIO)` verbatim ("I/O" reads wrong
15349///   lowercased, per the c:361-364 comment).
15350/// - everything else → `strerror(num)` with the first letter
15351///   lowercased (`tulower(errmsg[0])`, c:366).
15352///
15353/// `std::io::Error::from_raw_os_error(n)` Display is NOT this
15354/// format — it appends " (os error N)" and keeps the capital.
15355/// That mismatch was the residual byte-diff in zsh/system's
15356/// `sysopen`/`zsystem flock` warnings (docs/BUGS.md #316).
15357pub fn zsh_errno_msg(num: i32) -> String {
15358    if num == libc::EINTR {
15359        return "interrupt".to_string(); // c:355-358
15360    }
15361    let msg = unsafe {
15362        let p = libc::strerror(num); // c:360
15363        if p.is_null() {
15364            return String::new();
15365        }
15366        std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
15367    };
15368    if num == libc::EIO {
15369        return msg; // c:363-364
15370    }
15371    // c:366 — `fputc(tulower(errmsg[0]), file);`
15372    let mut chars = msg.chars();
15373    match chars.next() {
15374        Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
15375        None => msg,
15376    }
15377}
15378
15379// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
15380// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
15381// pipeline. Byte-exact inverse of metafy (Src/utils.c:5022 unmetafy):
15382// decodes Meta (\u{83}) + c^32 pairs back to raw bytes. C
15383// unmetafy works in place on char*; Rust strings are UTF-8 so
15384// the decode returns Vec<u8> for byte-exact writes.
15385/// Decode a char-level metafied String back to RAW bytes for a write
15386/// or exec boundary. c:Src/utils.c:4954 unmetafy — `if (*t++ == Meta
15387/// && *p) t[-1] = *p++ ^ 32;` — transposed onto the char-level
15388/// encoding `meta_encode_byte` produces: U+0083 followed by a scalar
15389/// in U+0080..=U+00FF yields the single raw byte `payload ^ 32`;
15390/// everything else emits its UTF-8 bytes verbatim. A U+0083 followed
15391/// by anything outside that range is not our encoding and passes
15392/// through untouched. Bug #127.
15393pub fn unmetafy_str(s: &str) -> Vec<u8> {
15394    let mut out = Vec::with_capacity(s.len());
15395    let mut it = s.chars().peekable();
15396    let mut buf = [0u8; 4];
15397    while let Some(c) = it.next() {
15398        if c == '\u{83}' {
15399            if let Some(&n) = it.peek() {
15400                let nu = n as u32;
15401                if (0x80..=0xff).contains(&nu) {
15402                    out.push((nu as u8) ^ 32);
15403                    it.next();
15404                    continue;
15405                }
15406            }
15407        }
15408        out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
15409    }
15410    out
15411}