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    // c:1582-1589 — periodic-hook dispatch on PERIOD cadence.
1809    if period > 0 {
1810        let now = std::time::SystemTime::now()
1811            .duration_since(UNIX_EPOCH)
1812            .map(|d| d.as_secs() as i64)
1813            .unwrap_or(0);
1814        if now > LAST_PERIODIC.load(Ordering::Relaxed) + period
1815            && callhookfunc("periodic", None, 1, std::ptr::null_mut()) == 0
1816        {
1817            LAST_PERIODIC.store(now, Ordering::Relaxed);
1818        }
1819    }
1820    // c:1588-1589 — `if (errflag) return;` post-periodic bail.
1821    if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
1822        return;
1823    }
1824
1825    // c:1591-1611 — mail check: `if (mailcheck && difftime(now,
1826    // lastmailcheck) > mailcheck)` walk MAILPATH (or scalar MAIL) via
1827    // checkmailpath. Faithful port — checkmailpath already exists at
1828    // utils.rs:1623, getaparam exists for the MAILPATH array.
1829    let currentmailcheck = std::time::SystemTime::now()
1830        .duration_since(UNIX_EPOCH)
1831        .map(|d| d.as_secs() as i64)
1832        .unwrap_or(0);
1833    if mailcheck > 0 && (currentmailcheck - LAST_MAILCHECK.load(Ordering::Relaxed)) > mailcheck {
1834        // c:1597 — `if (mailpath && *mailpath && **mailpath)`
1835        let mailpath = crate::ported::params::getaparam("MAILPATH");
1836        let has_mailpath = mailpath
1837            .as_ref()
1838            .map(|p| !p.is_empty() && p.first().map(|s| !s.is_empty()).unwrap_or(false))
1839            .unwrap_or(false);
1840        if has_mailpath {
1841            // c:1598 `checkmailpath(mailpath);` — emit each message to the
1842            // terminal (C writes directly to `shout`).
1843            for m in checkmailpath(mailpath.as_deref().unwrap()) {
1844                println!("{}", m);
1845            }
1846        } else {
1847            // c:1600-1608 — `if ((mailfile = getsparam("MAIL")) && *mailfile)
1848            //                  { x[0]=mailfile; x[1]=NULL; checkmailpath(x); }`
1849            crate::ported::signals::queue_signals(); // c:1600
1850            if let Some(mailfile) = crate::ported::params::getsparam("MAIL") {
1851                if !mailfile.is_empty() {
1852                    let x = vec![mailfile]; // c:1604-1605
1853                    for m in checkmailpath(&x) {
1854                        println!("{}", m); // c:1606
1855                    }
1856                }
1857            }
1858            crate::ported::signals::unqueue_signals(); // c:1608
1859        }
1860        LAST_MAILCHECK.store(currentmailcheck, Ordering::Relaxed); // c:1610
1861    }
1862
1863    // c:1613-1618 — `if (prepromptfns) for (...) ppnode->func();`
1864    let snapshot: Vec<fn()> = PREPROMPT_FNS.lock().unwrap().clone();
1865    for f in snapshot {
1866        f();
1867    }
1868}
1869
1870/// `static time_t lastmailcheck;` (Src/utils.c:1447) — the timestamp of the
1871/// previous mail check. `checkmail()` reads it (mtime/atime comparisons happen
1872/// against the PREVIOUS check) and writes it after walking the paths.
1873pub static LAST_MAILCHECK: AtomicI64 = AtomicI64::new(0);
1874
1875/// Direct port of `void checkmailpath(char **s)` from `Src/utils.c:1620`.
1876/// Walks each `PATH` / `PATH?message` MAILPATH component:
1877///   - empty component → `zerr` (c:1634-1636)
1878///   - `mailstat` failure → `zerr` unless ENOENT (c:1637-1639)
1879///   - directory → list entries (`PATH/entry[?message]`) and recurse
1880///     (c:1640-1669)
1881///   - file, interactive (`shout`) only: "You have new mail." or the
1882///     `?`-suffixed message expanded with `$_` bound to the file
1883///     (`parsestr`+`singsub`, c:1670-1699); and, under `MAILWARNING`,
1884///     "The mail in PATH has been read." (c:1700-1704)
1885///
1886/// **Signature note:** C is `void` and writes to `shout`; the zshrs caller
1887/// drives output, so this returns the messages (in C emission order) instead.
1888/// The new-mail / read-warning conditions are now ported faithfully — the
1889/// previous adhoc port used an unfaithful "mtime within 60s" heuristic and its
1890/// result was discarded by the caller, so the feature did nothing.
1891pub fn checkmailpath(s: &[String]) -> Vec<String> {
1892    // c:1620
1893    let mut messages = Vec::new();
1894    let interactive = *crate::ported::init::shout.lock().unwrap() != 0; // c:1670 `else if (shout)`
1895    let lastmailcheck = LAST_MAILCHECK.load(Ordering::Relaxed);
1896
1897    for path in s {
1898        // c:1627-1633 — split on the first '?': `file` before, `u` after (or None).
1899        let (file, u) = match path.find('?') {
1900            Some(p) => (&path[..p], Some(&path[p + 1..])),
1901            None => (path.as_str(), None),
1902        };
1903
1904        if file.is_empty() {
1905            // c:1634-1636 — `if (**s == 0) zerr("empty MAILPATH component: %s", *s);`
1906            zerr(&format!("empty MAILPATH component: {}", path));
1907            continue;
1908        }
1909
1910        let mut st: libc::stat = unsafe { std::mem::zeroed() };
1911        let real = unmeta(file); // c:1637 — mailstat(unmeta(*s), &st)
1912        if mailstat(&real, &mut st) == -1 {
1913            // c:1638-1639 — report anything other than "no such file".
1914            let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1915            if e != libc::ENOENT {
1916                zerr(&format!("{}: {}", zsh_errno_msg(e), path));
1917            }
1918        } else if (st.st_mode as libc::mode_t & libc::S_IFMT) == libc::S_IFDIR {
1919            // c:1640-1669 — a directory: enumerate entries and recurse.
1920            match fs::read_dir(&real) {
1921                Err(_) => {
1922                    let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1923                    zerr(&format!("{}: {}", zsh_errno_msg(e), path)); // c:1647
1924                }
1925                Ok(mut rd) => {
1926                    let mut arr = Vec::new();
1927                    // c:1653-1662 — `while ((fn = zreaddir(lock, 1)) && !errflag)`.
1928                    while let Some(fname) = zreaddir(&mut rd, 1) {
1929                        if errflag.load(Ordering::Relaxed) != 0 {
1930                            break;
1931                        }
1932                        // c:1654-1657 — keep the `?message` suffix on each entry.
1933                        arr.push(match u {
1934                            Some(u) => format!("{}/{}?{}", path, fname, u),
1935                            None => format!("{}/{}", path, fname),
1936                        });
1937                    }
1938                    messages.extend(checkmailpath(&arr)); // c:1667 — recurse
1939                }
1940            }
1941        } else if interactive {
1942            // c:1671-1672 — new mail: non-empty, not yet read, newer than the
1943            // last check.
1944            if st.st_size != 0 && st.st_atime <= st.st_mtime && st.st_mtime >= lastmailcheck {
1945                match u {
1946                    // c:1673-1675 — `fprintf(shout, "You have new mail.\n");`
1947                    None => messages.push("You have new mail.".to_string()),
1948                    // c:1676-1698 — custom message: bind `$_` to the file, then
1949                    // parsestr + singsub the message (prompt-style expansion).
1950                    Some(u) => {
1951                        let usav = crate::ported::init::zunderscore.lock().unwrap().clone();
1952                        crate::ported::exec::setunderscore(path); // c:1685 setunderscore(*s)
1953                        if let Ok(parsed) = crate::ported::lex::parsestr(u) {
1954                            // c:1688-1691 — parse ok → singsub then emit.
1955                            messages.push(crate::ported::subst::singsub(&parsed));
1956                        }
1957                        crate::ported::exec::setunderscore(&usav); // c:1694-1697 restore $_
1958                    }
1959                }
1960            }
1961            // c:1700-1704 — MAILWARNING: file read since the last check.
1962            if crate::ported::zsh_h::isset(crate::ported::zsh_h::MAILWARNING)
1963                && st.st_atime > st.st_mtime
1964                && st.st_atime > lastmailcheck
1965                && st.st_size != 0
1966            {
1967                messages.push(format!("The mail in {} has been read.", unmeta(file)));
1968            }
1969        }
1970    }
1971    messages
1972}
1973
1974/// Port of `printprompt4()` from `Src/utils.c:1718`.
1975///
1976/// Render the PS4 / PROMPT4 prefix and write it to stderr. zsh's
1977/// implementation reads `prompt4` global, suppresses XTRACE during
1978/// promptexpand (so subshells inside `%(?…)` don't recursively trace),
1979/// then fprintf's the expanded prefix to xtrerr. zshrs uses the same
1980/// suppress-XTRACE-around-expand pattern; ksh/sh emulation defaults
1981/// to `+ ` per Src/init.c:1192, zsh default to `+%N:%i> `.
1982///
1983/// The C source's caller (Src/exec.c::tracingcond etc.) follows this
1984/// with the per-line/per-arg fprintf — same shape mirrored at the two
1985/// zshrs call sites in fusevm_bridge.rs (BUILTIN_XTRACE_LINE / ARGS).
1986pub(crate) fn printprompt4() {
1987    // c:utils.c:1720 — `if (!isset(XTRACE)) return;`. C tests
1988    // `xtrerr` first then conditionally; the read-the-option early-
1989    // return path is equivalent for our purposes since we don't ship
1990    // the `xtrerr` separate-stream support.
1991    if !isset(XTRACE) {
1992        return;
1993    }
1994    // c:utils.c:1722-1724 — `if (prompt4) { ... s = dupstring(prompt4);`
1995    // C `prompt4` is a global initialized in init.c:1192 from the
1996    // emulation bits; PS4/PROMPT4 paramtab entries alias it via
1997    // IPDEF7R/IPDEF7 (params.c:381, 421). Read from paramtab to
1998    // honour user-set values, fall back to the same emulation
1999    // default C uses at init.c:1192.
2000    let posix = EMULATION(EMULATE_KSH | EMULATE_SH); // c:init.c:1192
2001    let prefix_template = getsparam("PS4")
2002        .or_else(|| getsparam("PROMPT4"))
2003        .unwrap_or_else(|| {
2004            if posix {
2005                "+ ".to_string() // c:init.c:1192
2006            } else {
2007                "+%N:%i> ".to_string() // c:init.c:1193
2008            }
2009        });
2010    // c:utils.c:1723,1726,1730 — `t = opts[XTRACE]; opts[XTRACE] = 0;
2011    //                              promptexpand(...);  opts[XTRACE] = t;`
2012    let saved = isset(XTRACE);
2013    opt_state_set(&opt_name(XTRACE), false);
2014    let prefix = crate::prompt::expand_prompt(&prefix_template);
2015    opt_state_set(&opt_name(XTRACE), saved);
2016    // c:utils.c:1736 — `fprintf(xtrerr, "%s", s)`. Append to the xtrerr
2017    // line buffer rather than writing straight to stderr; the emitter
2018    // appends the command/args and flushes the whole line in one write,
2019    // matching C's single `fflush(xtrerr)` (so concurrent pipeline-stage
2020    // trace lines never interleave).
2021    crate::fusevm_bridge::xtrerr_fputs(&prefix);
2022}
2023
2024/// Port of `freestr(void *a)` from `Src/utils.c:1739`.
2025///
2026/// C body:
2027/// ```c
2028/// void freestr(void *a) { zsfree(a); }
2029/// ```
2030/// The C function is registered as the `freenode` callback for
2031/// hashtables holding plain string values. The Rust port consumes
2032/// `a` by value; Rust's `Drop` runs the equivalent of `zsfree` when
2033/// the `String` is moved into this fn and falls out of scope at the
2034/// closing brace — the no-op body is the correct port. Param name
2035/// `a` matches C exactly per Rule E.
2036pub fn freestr(_a: String) {}
2037
2038/// Port of `int gettempfile(const char *prefix, int use_heap, char **tempname)`
2039/// from Src/utils.c:2231. Creates a fresh tempfile with `O_RDWR|O_CREAT|O_EXCL`
2040/// and mode 0600 under umask 0177; returns `(fd, path)` on success.
2041///
2042/// C signature uses an out-param for the filename and returns the fd; Rust
2043/// returns both as a tuple. Matches the C control-flow: open with O_EXCL,
2044/// retry up to 16 times on EEXIST (the non-mkstemp branch, c:2255-2269).
2045pub fn gettempfile(prefix: Option<&str>) -> Option<(i32, String)> {
2046    // c:2231
2047    #[cfg(unix)]
2048    {
2049        queue_signals(); // c:2239
2050        let old_umask = unsafe { libc::umask(0o177) }; // c:2240
2051        let mut failures = 0; // c:2255
2052        let mut result: Option<(i32, String)> = None;
2053        loop {
2054            let fn_ = match gettempname(prefix, false) {
2055                // c:2260
2056                Some(n) => n,
2057                None => break,
2058            };
2059            let cn = match CString::new(fn_.clone()) {
2060                Ok(c) => c,
2061                Err(_) => break,
2062            };
2063            let fd = unsafe {
2064                libc::open(
2065                    cn.as_ptr(),
2066                    libc::O_RDWR | libc::O_CREAT | libc::O_EXCL, // c:2264
2067                    0o600 as libc::c_int,
2068                )
2069            };
2070            if fd >= 0 {
2071                result = Some((fd, fn_));
2072                break;
2073            }
2074            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2075            if err != libc::EEXIST {
2076                // c:2269
2077                break;
2078            }
2079            failures += 1;
2080            if failures >= 16 {
2081                // c:2269
2082                break;
2083            }
2084        }
2085        unsafe {
2086            libc::umask(old_umask);
2087        } // c:2273
2088        unqueue_signals(); // c:2274
2089        result
2090    }
2091    #[cfg(not(unix))]
2092    {
2093        let _ = prefix;
2094        None
2095    }
2096}
2097
2098/// Port of `void gettyinfo(struct ttyinfo *ti)` from Src/utils.c:1746.
2099///
2100/// The saved baseline terminal mode — C's `struct ttyinfo shttyinfo`
2101/// (Src/init.c). Captured ONCE while the terminal is still in its
2102/// original (cooked / canonical) mode, before ZLE ever puts it into
2103/// raw mode via `zsetterm()`. `trashzle()` restores this baseline via
2104/// `settyinfo(&shttyinfo)` so that external commands the user runs
2105/// (`cat`, `less`, …) execute with a cooked terminal — where `^D` is
2106/// EOF, `^C` is SIGINT, and line editing works. Without it the tty
2107/// stayed in ZLE raw mode across command execution, so `cat` never
2108/// saw EOF on `^D`.
2109#[cfg(unix)]
2110pub static SHTTYINFO: std::sync::Mutex<Option<libc::termios>> = std::sync::Mutex::new(None);
2111
2112/// Reads the current termios from the global `SHTTY`. Returns
2113/// `None` when SHTTY is closed or the call fails (matching C's
2114/// silent return when SHTTY == -1).
2115/// C body (single statement): `fdgettyinfo(SHTTY, ti);`
2116/// Rust returns `Option<termios>` instead of taking an out-ptr;
2117/// the SHTTY=-1 case naturally returns Err from fdgettyinfo → None.
2118#[cfg(unix)]
2119pub fn gettyinfo() -> Option<libc::termios> {
2120    // c:1746
2121    fdgettyinfo(SHTTY.load(Ordering::Relaxed)).ok() // c:1748
2122}
2123
2124/// Emit the `$PS4` xtrace prefix to stderr.
2125/// Read terminal mode from a file descriptor.
2126/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1753. C source uses
2127/// `tcgetattr(SHTTY, &ti->tio)`; we return the populated termios
2128/// or an io::Error on failure (caller equivalent to zsh's `zerr`).
2129#[cfg(unix)]
2130/// WARNING: param names don't match C — Rust=(fd) vs C=(SHTTY, ti)
2131pub fn fdgettyinfo(fd: i32) -> io::Result<libc::termios> {
2132    let mut tio: libc::termios = unsafe { std::mem::zeroed() };
2133    if unsafe { libc::tcgetattr(fd, &mut tio) } == -1 {
2134        Err(io::Error::last_os_error())
2135    } else {
2136        Ok(tio)
2137    }
2138}
2139
2140#[cfg(not(unix))]
2141/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from `Src/utils.c:1753`.
2142/// WARNING: param names don't match C — Rust=(_fd) vs C=(SHTTY, ti)
2143pub fn fdgettyinfo(_fd: i32) -> std::io::Result<()> {
2144    Err(std::io::Error::new(
2145        std::io::ErrorKind::Unsupported,
2146        "no termios",
2147    ))
2148}
2149
2150/// Port of `void settyinfo(struct ttyinfo *ti)` from Src/utils.c:1778.
2151///
2152/// Restores the termios state on the global `SHTTY` with EINTR
2153/// retry; no-op when SHTTY is closed.
2154/// C body (single statement): `fdsettyinfo(SHTTY, ti);`
2155/// Rust returns bool; SHTTY=-1 yields Err → false from fdsettyinfo.
2156#[cfg(unix)]
2157pub fn settyinfo(ti: &libc::termios) -> bool {
2158    // c:1778
2159    fdsettyinfo(SHTTY.load(Ordering::Relaxed), ti).is_ok() // c:1780
2160}
2161
2162/// Apply terminal mode to a file descriptor, with EINTR retry.
2163/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1785. C source loops
2164/// `while (tcsetattr(SHTTY, TCSADRAIN, &ti->tio) == -1 && errno
2165/// == EINTR)` — same retry shape here.
2166#[cfg(unix)]
2167pub fn fdsettyinfo(SHTTY2: i32, ti: &libc::termios) -> io::Result<()> {
2168    loop {
2169        if unsafe { libc::tcsetattr(SHTTY2, libc::TCSADRAIN, ti) } != -1 {
2170            return Ok(());
2171        }
2172        let err = io::Error::last_os_error();
2173        if err.kind() != io::ErrorKind::Interrupted {
2174            return Err(err);
2175        }
2176    }
2177}
2178
2179#[cfg(not(unix))]
2180/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from `Src/utils.c:1785`.
2181pub fn fdsettyinfo(SHTTY: i32, ti: &()) -> std::io::Result<()> {
2182    Err(std::io::Error::new(
2183        std::io::ErrorKind::Unsupported,
2184        "no termios",
2185    ))
2186}
2187
2188// window size changed                                                     // c:1831
2189/// Port of `adjustlines(int signalled)` from Src/utils.c:1831 — TIOCGWINSZ
2190/// lookup that seeds `$LINES`. The C variant updates the global
2191/// `zterm_lines` and returns whether it changed; this Rust port
2192/// returns the row count directly. Falls back to `$LINES` env var,
2193/// then 24, mirroring the C source's `tclines > 0 ? tclines : 24`
2194/// fallback at line 1844.
2195/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2196pub fn adjustlines() -> usize {
2197    // c:1831
2198    #[cfg(unix)]
2199    {
2200        unsafe {
2201            let mut ws: libc::winsize = std::mem::zeroed();
2202            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_row > 0 {
2203                return ws.ws_row as usize;
2204            }
2205        }
2206    }
2207    // c:1844 fallback — paramtab `$LINES`, not OS env.
2208    getsparam("LINES")
2209        .and_then(|s| s.parse().ok())
2210        .unwrap_or(24)
2211}
2212
2213/// Port of `adjustcolumns(int signalled)` from Src/utils.c:1856 — TIOCGWINSZ
2214/// lookup that seeds `$COLUMNS`. The C variant updates the global
2215/// `zterm_columns` and returns whether it changed; this Rust port
2216/// returns the column count directly. Falls back to `$COLUMNS` env
2217/// var, then 80, mirroring the C source's `tccolumns > 0 ? tccolumns : 80`
2218/// fallback at line 1869.
2219/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2220pub fn adjustcolumns() -> usize {
2221    // c:1856
2222    #[cfg(unix)]
2223    {
2224        unsafe {
2225            let mut ws: libc::winsize = std::mem::zeroed();
2226            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
2227                return ws.ws_col as usize;
2228            }
2229        }
2230    }
2231    // c:1820 fallback — `if (zterm_columns <= 0) zterm_columns =
2232    //                    tccolumns > 0 ? tccolumns : 80`. C consults
2233    //                    `getsparam("COLUMNS")` (paramtab), not OS env.
2234    getsparam("COLUMNS")
2235        .and_then(|s| s.parse().ok())
2236        .unwrap_or(80)
2237}
2238
2239// window size changed                                                      // c:1824
2240/// Port of `void adjustwinsize(int from)` from `Src/utils.c:1889`.
2241/// SIGWINCH handler + LINES/COLUMNS-update entry. Reads the tty's
2242/// current geometry via `TIOCGWINSZ` ioctl, updates the cached
2243/// `zterm_lines`/`zterm_columns`, and writes `$LINES` / `$COLUMNS`
2244/// when they're already set in the environment.
2245/// ```c
2246/// void
2247/// adjustwinsize(int from)
2248/// {
2249///     static int getwinsz = 1;
2250///     int ttyrows = shttyinfo.winsize.ws_row;
2251///     int ttycols = shttyinfo.winsize.ws_col;
2252///     int resetzle = 0;
2253///     if (getwinsz || from == 1) {
2254///         if (SHTTY == -1) return;
2255///         if (ioctl(SHTTY, TIOCGWINSZ, &shttyinfo.winsize) == 0) {
2256///             resetzle = (ttyrows != ... || ttycols != ...);
2257///             ...
2258///         } else {
2259///             shttyinfo.winsize.ws_row = zterm_lines;
2260///             shttyinfo.winsize.ws_col = zterm_columns;
2261///         }
2262///     }
2263///     switch (from) {
2264///     case 0: case 1:
2265///         getwinsz = 0;
2266///         if (adjustlines(from) && zgetenv("LINES")) setiparam("LINES", zterm_lines);
2267///         if (adjustcolumns(from) && zgetenv("COLUMNS")) setiparam("COLUMNS", zterm_columns);
2268///         getwinsz = 1;
2269///         break;
2270///     case 2: resetzle = adjustlines(0); break;
2271///     case 3: resetzle = adjustcolumns(0); break;
2272///     }
2273///     if (interact && resetzle) zleentry(ZLE_CMD_REFRESH);
2274/// }
2275/// ```
2276pub fn adjustwinsize(from: i32) -> (usize, usize) {
2277    // c:1889
2278
2279    // c:1891 — `static int getwinsz = 1;`
2280    let getwinsz = ADJUSTWINSIZE_GETWINSZ.load(Ordering::SeqCst);
2281
2282    let mut ttyrows: i32 = 0;
2283    let mut ttycols: i32 = 0;
2284
2285    // c:1898-1917 — TIOCGWINSZ probe.
2286    if getwinsz != 0 || from == 1 {
2287        // c:1898
2288        let shtty = SHTTY.load(Ordering::Relaxed);
2289        if shtty == -1 {
2290            // c:1900
2291            return (adjustcolumns(), adjustlines()); // c:1901
2292        }
2293        #[cfg(unix)]
2294        unsafe {
2295            let mut ws: libc::winsize = std::mem::zeroed();
2296            if libc::ioctl(shtty, libc::TIOCGWINSZ, &mut ws as *mut _) == 0 {
2297                // c:1902
2298                ttyrows = ws.ws_row as i32; // c:1907
2299                ttycols = ws.ws_col as i32; // c:1908
2300            }
2301        }
2302    }
2303
2304    let mut resetzle = 0i32;
2305    match from {
2306        // c:1921
2307        0 | 1 => {
2308            // c:1922-1923
2309            ADJUSTWINSIZE_GETWINSZ.store(0, Ordering::SeqCst); // c:1924
2310            // c:1931-1934 — C: `if (adjustlines(from) && zgetenv("LINES"))
2311            // setiparam("LINES", zterm_lines);` (same for COLUMNS). The
2312            // zgetenv gate only guards re-publishing to an ENV-exported
2313            // LINES — in C the param itself is ALWAYS live because its
2314            // valptr aliases the `zterm_lines` global that adjustlines
2315            // just wrote (createparamtable IPDEF5 → zlevarsetfn/intvargetfn
2316            // on &zterm_lines). zshrs params store their own u_val copy
2317            // (no valptr aliasing), so the probe result must be written
2318            // through setiparam unconditionally — gating on the env left
2319            // `$LINES`/`$COLUMNS` at 0 for every interactive shell
2320            // (LINES is not in the environment; zsh doesn't export it).
2321            // Effect: `$(( LINES / 3 ))` = 0 → division-by-zero in
2322            // history-search-multi-word pagination (_hsmw_main:81), and
2323            // every LINES/COLUMNS-derived layout was wrong. Recursion is
2324            // safe: setiparam → zlevarsetfn → adjustwinsize(2|3) never
2325            // re-enters this arm.
2326            let lines = if ttyrows > 0 {
2327                ttyrows as i64 // c:1837 — zterm_lines = shttyinfo.winsize.ws_row
2328            } else {
2329                adjustlines() as i64 // c:1844 — tclines/24 fallback chain
2330            };
2331            setiparam("LINES", lines); // c:1932
2332            let cols = if ttycols > 0 {
2333                ttycols as i64 // c:1862 — zterm_columns = ws_col
2334            } else {
2335                adjustcolumns() as i64 // c:1869 — tccolumns/80 fallback chain
2336            };
2337            setiparam("COLUMNS", cols); // c:1934
2338            ADJUSTWINSIZE_GETWINSZ.store(1, Ordering::SeqCst); // c:1935
2339        }
2340        2 => {
2341            // c:1937
2342            resetzle = adjustlines() as i32; // c:1938
2343        }
2344        3 => {
2345            // c:1940
2346            resetzle = adjustcolumns() as i32; // c:1941
2347        }
2348        _ => {}
2349    }
2350
2351    // c:1946-1958 — resetzle + zleentry(ZLE_CMD_REFRESH) when interact.
2352    if from >= 2 && resetzle != 0 {
2353        // ZLE refresh dispatch via zleentry(ZLE_CMD_REFRESH) lands here
2354        // once the C signal handler shape ports.
2355        let _ = ttyrows;
2356        let _ = ttycols;
2357    }
2358
2359    (adjustcolumns(), adjustlines())
2360}
2361
2362/// Port of `static int getwinsz` from `Src/utils.c:1891`. Local
2363/// reentry guard inside adjustwinsize — bumped to 0 around the
2364/// setiparam recursion so the recursive call short-circuits.
2365pub static ADJUSTWINSIZE_GETWINSZ: std::sync::atomic::AtomicI32 =
2366    std::sync::atomic::AtomicI32::new(1); // c:1891
2367
2368/// Port of `check_fd_table(int fd)` from `Src/utils.c:1968-1983`.
2369/// ```c
2370/// if (fd <= max_zsh_fd) return;
2371/// if (fd >= fdtable_size) {
2372///     int old_size = fdtable_size;
2373///     while (fd >= fdtable_size)
2374///         fdtable = zrealloc(fdtable, (fdtable_size *= 2) * sizeof(*fdtable));
2375///     memset(fdtable + old_size, 0, (fdtable_size - old_size) * sizeof(*fdtable));
2376/// }
2377/// max_zsh_fd = fd;
2378/// ```
2379/// C semantics: GROW the `fdtable` array so it can index `fd`, then
2380/// update `max_zsh_fd`. Returns void.
2381///
2382/// The Rust port keeps the same signature for name-parity but the
2383/// fdtable global isn't yet modeled — so this is a no-op shim. The
2384/// previous Rust impl was an `fcntl(F_GETFD)` validity check —
2385/// COMPLETELY DIFFERENT SEMANTICS from C (which doesn't validate
2386/// the fd at all, just grows the table). Fixed to no-op + bool
2387/// return for caller compatibility (no live callers).
2388pub fn check_fd_table(fd: i32) -> bool {
2389    // c:1969
2390    // c:1971-1972 — `if (fd <= max_zsh_fd) return;`
2391    let cur_max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:1971
2392    if fd <= cur_max {
2393        // c:1971
2394        return true; // c:1972 (return; in C)
2395    }
2396    if fd < 0 {
2397        // defensive — fdtable index must be ≥0
2398        return false;
2399    }
2400    // c:1974-1981 — `if (fd >= fdtable_size) { while (fd >= fdtable_size)
2401    //                fdtable_size *= 2; zrealloc; memset(new_slots, 0); }`.
2402    // Rust Vec::resize handles the realloc + zero-fill in one call (the
2403    // expansion bumps capacity geometrically too).
2404    {
2405        let mut g = fdtable_lock().lock().unwrap();
2406        if (fd as usize) >= g.len() {
2407            g.resize((fd as usize) + 1, FDT_UNUSED); // c:1975-1979 grow
2408        }
2409    }
2410    // c:1982 — `max_zsh_fd = fd;`.
2411    MAX_ZSH_FD.store(fd, Ordering::Relaxed); // c:1982
2412    true
2413}
2414
2415/// Port of `movefd(int fd)` from `Src/utils.c:1989-2012`.
2416/// ```c
2417/// if (fd != -1 && fd < 10) {
2418///     int fe = fcntl(fd, F_DUPFD, 10);
2419///     zclose(fd);          // unconditional close, even when fe == -1
2420///     fd = fe;
2421/// }
2422/// if (fd != -1) {
2423///     check_fd_table(fd);
2424///     fdtable[fd] = FDT_INTERNAL;
2425/// }
2426/// return fd;
2427/// ```
2428/// Two divergences fixed this iteration:
2429///   1. Old Rust closed the source fd ONLY on success; C closes
2430///      unconditionally per the c:1999-2004 comment "probably better
2431///      to avoid a leak."
2432///   2. Old Rust added `FD_CLOEXEC` after the dup; C does NOT —
2433///      CLOEXEC is added later by `addmodulefd`/`addlockfd` callers
2434///      that need it. movefd itself does not.
2435pub fn movefd(fd: i32) -> i32 {
2436    #[cfg(unix)]
2437    {
2438        let mut fd = fd;
2439        if fd != -1 && fd < 10 {
2440            // c:1992
2441            let fe = unsafe { libc::fcntl(fd, libc::F_DUPFD, 10) }; // c:1994
2442            unsafe { libc::close(fd) }; // c:2004 zclose(fd) — unconditional
2443            fd = fe; // c:2005
2444        }
2445        // c:2007-2010 — `if (fd != -1) { check_fd_table(fd); fdtable[fd]
2446        // = FDT_INTERNAL; }`. The fdtable global IS now modeled (port
2447        // of utils.c:63 — `Vec<u8>` behind `fdtable_lock`). Mark the
2448        // new fd as FDT_INTERNAL so the rest of the shell knows the
2449        // shell is using it (a later `addmodulefd` / `addlockfd`
2450        // upgrade may reclassify; raw external code shouldn't touch
2451        // it). The previous port skipped this so internal-fd tracking
2452        // (which `closeallelse` and forkexec rely on) silently
2453        // never saw zshrs-internal fds.
2454        if fd != -1 {
2455            // c:2007
2456            check_fd_table(fd); // c:2008
2457            fdtable_set(fd, FDT_INTERNAL); // c:2009
2458        }
2459        fd
2460    }
2461    #[cfg(not(unix))]
2462    {
2463        fd
2464    }
2465}
2466
2467/// Port of `redup(int x, int y)` from `Src/utils.c:2021`.
2468///
2469/// C signature: `int redup(int x, int y)`.
2470/// "Move fd x to y. If x == -1, fd y is closed. Returns y for
2471/// success, -1 for failure." (c:2014-2016 docstring.)
2472///
2473/// Body mirrors c:2023-2068: when `x == -1`, close `y` (return `y`);
2474/// when `x == y`, no-op (return `y`); otherwise `dup2(x, y)` +
2475/// `close(x)`.
2476///
2477/// C body fdtable updates (c:2053-2063) that the previous Rust port
2478/// SKIPPED with a stale "fdtable global not yet ported" comment —
2479/// fdtable IS now ported and these updates are load-bearing:
2480///   * `fdtable[y] = fdtable[x]` — the new fd inherits the old fd's
2481///     ownership category (FDT_INTERNAL / FDT_MODULE / etc.).
2482///   * If the inherited type is `FDT_FLOCK` / `FDT_FLOCK_EXEC`, promote
2483///     to `FDT_INTERNAL` (the dup'd fd doesn't carry the flock).
2484///   * If `fdtable[x] == FDT_FLOCK`, decrement `fdtable_flocks` (the
2485///     original lock-holding fd is about to be closed).
2486///
2487/// Without these updates, redup'd fds had stale `FDT_UNUSED` ownership
2488/// and `closeallelse(FDT_EXTERNAL)` etc. couldn't classify them.
2489pub fn redup(x: i32, y: i32) -> i32 {
2490    // c:2021
2491    let mut ret = y; // c:2023
2492    #[cfg(unix)]
2493    {
2494        if x < 0 {
2495            // c:2047
2496            zclose(y); // c:2048
2497        } else if x != y {
2498            // c:2049
2499            // c:2053-2057 — successful dup2: copy fdtable + FLOCK promote.
2500            if unsafe { libc::dup2(x, y) } == -1 {
2501                // c:2050
2502                ret = -1; // c:2051
2503            } else {
2504                check_fd_table(y); // c:2053
2505                let kind_x = fdtable_get(x); // c:2054
2506                let kind_y = if kind_x == FDT_FLOCK || kind_x == FDT_FLOCK_EXEC {
2507                    FDT_INTERNAL // c:2055-2056
2508                } else {
2509                    kind_x // c:2054
2510                };
2511                fdtable_set(y, kind_y);
2512            }
2513            // c:2062-2063 — `if (fdtable[x] == FDT_FLOCK) fdtable_flocks--;`
2514            // The original lock-holding fd is about to be closed, so the
2515            // flock-count tracker must drop. Even on dup2 failure C still
2516            // closes x, so this runs in both arms.
2517            if fdtable_get(x) == FDT_FLOCK {
2518                // c:2062
2519                FDTABLE_FLOCKS.fetch_sub(1, Ordering::SeqCst); // c:2063
2520            }
2521            zclose(x); // c:2064
2522        }
2523    }
2524    #[cfg(not(unix))]
2525    {
2526        let _ = (x, y);
2527    }
2528    ret // c:2067
2529}
2530
2531/// Port of `addmodulefd(int fd, int fdt)` from `Src/utils.c:2090-2097`.
2532/// ```c
2533/// if (fd >= 0) {
2534///     check_fd_table(fd);
2535///     fdtable[fd] = fdt;
2536/// }
2537/// ```
2538/// Two divergences fixed this iteration:
2539///   1. C accepts an `fdt` parameter (FDT_MODULE / FDT_INTERNAL /
2540///      FDT_EXTERNAL — see callers in Src/Modules/{random,socket,tcp,
2541///      db_gdbm}.c). Rust port hardcoded `FDT_MODULE` — silently
2542///      ignored caller intent. Now takes `fdt` per C.
2543///   2. C does NOT set `FD_CLOEXEC`. Rust port was adding it
2544///      unconditionally — divergent. CLOEXEC is the caller's
2545///      responsibility based on the fdt semantics.
2546pub fn addmodulefd(fd: i32, fdt: i32) {
2547    // c:2091
2548    // c:2093 — `if (fd >= 0)`.
2549    if fd >= 0 {
2550        // c:2094 — `check_fd_table(fd)` — grow fdtable to cover fd.
2551        check_fd_table(fd); // c:2094
2552                            // c:2095 — `fdtable[fd] = fdt`. Routes through the canonical
2553                            // helper which mirrors C's direct `fdtable[fd] = fdt` write.
2554        fdtable_set(fd, fdt); // c:2095
2555    }
2556}
2557
2558/// Port of `addlockfd(int fd, int cloexec)` from `Src/utils.c:2111-2121`.
2559/// ```c
2560/// if (cloexec) {
2561///     if (fdtable[fd] != FDT_FLOCK)
2562///         fdtable_flocks++;
2563///     fdtable[fd] = FDT_FLOCK;
2564/// } else {
2565///     fdtable[fd] = FDT_FLOCK_EXEC;
2566/// }
2567/// ```
2568/// Critical divergence fixed: C updates `fdtable[fd]` to FDT_FLOCK
2569/// (cloexec=true) or FDT_FLOCK_EXEC (cloexec=false). Previous Rust
2570/// port did the OPPOSITE — called `fcntl(F_SETFD, FD_CLOEXEC)` when
2571/// `cloexec` was true. That's wrong on two counts:
2572///   1. C's "cloexec" parameter selects the FDT category, not a
2573///      libc fcntl flag.
2574///   2. FDT_FLOCK means "lock survives across exec via fd inheritance"
2575///      — the OPPOSITE of close-on-exec. The Rust port was adding
2576///      CLOEXEC for the very case where the fd should be inheritable.
2577pub fn addlockfd(fd: i32, cloexec: bool) {
2578    // c:2112
2579    if cloexec {
2580        // c:2114
2581        // c:2115-2117 — track flock count, set FDT_FLOCK.
2582        if fdtable_get(fd) != FDT_FLOCK {
2583            FDTABLE_FLOCKS.fetch_add(1, Ordering::SeqCst);
2584        }
2585        fdtable_set(fd, FDT_FLOCK);
2586    } else {
2587        // c:2118
2588        // c:2119 — FDT_FLOCK_EXEC means flock inherited by exec.
2589        fdtable_set(fd, FDT_FLOCK_EXEC);
2590    }
2591}
2592
2593// FDTABLE_FLOCKS canonical declaration lives at utils.rs:5219
2594// (the exec.c global with same name). Reuse, don't redeclare.
2595
2596/// Close the given fd, and clear it from fdtable.                          // c:2123
2597/// Port of `int zclose(int fd)` from `Src/utils.c:2127`.
2598pub fn zclose(fd: i32) -> i32 {
2599    // c:2127
2600    if fd >= 0 {
2601        // c:2129
2602        // c:2130-2133 — comment carry: "Careful: we allow closing of
2603        // arbitrary fd's, beyond max_zsh_fd. In that case we don't
2604        // try anything clever."
2605        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed); // c:2134
2606        if fd <= max_fd {
2607            // c:2134
2608            // !!! ZSHRS THREADING ADAPTATION — deviates from C here !!!
2609            // C closes blindly; it is single-threaded, so a shell-recorded
2610            // fd number can never be concurrently recycled by someone else.
2611            // zshrs runs 18 worker threads whose Rust-owned fds (ReadDir
2612            // dirfds, SQLite, File) recycle freed numbers immediately — a
2613            // stale zclose then closes a FOREIGN fd, and std panics in
2614            // ReadDir::drop ("unexpected error during closedir: EBADF",
2615            // observed from the compinit bg scan). An in-range fd the
2616            // fdtable does NOT own is by definition not ours to close.
2617            if fdtable_get(fd) == FDT_UNUSED {
2618                tracing::debug!(fd, "zclose: skipping unowned in-range fd (thread-safety guard)");
2619                return 0;
2620            }
2621            if fdtable_get(fd) == FDT_FLOCK {
2622                // c:2135
2623                FDTABLE_FLOCKS.fetch_sub(1, Ordering::Relaxed);
2624                // c:2136
2625            }
2626            fdtable_set(fd, FDT_UNUSED); // c:2137
2627                                         // c:2138-2139 — shrink max_zsh_fd past trailing UNUSED slots.
2628            let mut m = MAX_ZSH_FD.load(Ordering::Relaxed);
2629            while m > 0 && fdtable_get(m) == FDT_UNUSED {
2630                m -= 1;
2631            }
2632            MAX_ZSH_FD.store(m, Ordering::Relaxed);
2633            // c:2140-2143 — coproc fd tracking.
2634            if fd == coprocin.load(Ordering::Relaxed) {
2635                coprocin.store(-1, Ordering::Relaxed);
2636            }
2637            if fd == coprocout.load(Ordering::Relaxed) {
2638                coprocout.store(-1, Ordering::Relaxed);
2639            }
2640        }
2641        #[cfg(unix)]
2642        unsafe {
2643            return libc::close(fd);
2644        } // c:2145
2645        #[cfg(not(unix))]
2646        return 0;
2647    }
2648    -1 // c:2147
2649}
2650
2651/// Port of `int zcloselockfd(int fd)` from `Src/utils.c:2155-2164`.
2652/// ```c
2653/// if (fd > max_zsh_fd) return -1;
2654/// if (fdtable[fd] != FDT_FLOCK && fdtable[fd] != FDT_FLOCK_EXEC)
2655///     return -1;
2656/// zclose(fd);
2657/// return 0;
2658/// ```
2659/// "Close an fd returning 0 if used for locking; return -1 if it
2660/// isn't." Caller (`bin_zsystem_flock -u`) uses the -1 return to
2661/// distinguish "fd not in the flock table" from "successfully
2662/// released a lock."
2663///
2664/// Previously the Rust port skipped the FDT_FLOCK / FDT_FLOCK_EXEC
2665/// check entirely and always returned 0 — meaning `zsystem flock -u
2666/// <unlocked-fd>` reported success on any fd. Now that the
2667/// `addlockfd` port (this iteration) populates the fdtable with the
2668/// canonical FDT_FLOCK / FDT_FLOCK_EXEC slots, the check can fire
2669/// faithfully.
2670pub fn zcloselockfd(fd: i32) -> i32 {
2671    // c:2156
2672    let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2673    // c:2158 — `if (fd > max_zsh_fd) return -1;`.
2674    if fd > max_fd {
2675        return -1;
2676    }
2677    // c:2160-2161 — `if (fdtable[fd] != FDT_FLOCK && != FDT_FLOCK_EXEC)
2678    //                   return -1;`.
2679    let slot = fdtable_get(fd);
2680    if slot != FDT_FLOCK && slot != FDT_FLOCK_EXEC {
2681        return -1;
2682    }
2683    zclose(fd); // c:2162
2684    0 // c:2163
2685}
2686
2687/// Port of `char *gettempname(const char *prefix, int use_heap)` from Src/utils.c:2178.
2688///
2689/// Returns a unique tempfile name templated like C `mktemp(3)` —
2690/// `{prefix}.XXXXXX`. Falls back to `getsparam("TMPPREFIX")` and
2691/// then `DEFAULT_TMPPREFIX` when `prefix` is None. Does NOT create
2692/// the file (matches C — only `gettempfile` creates).
2693pub fn gettempname(prefix: Option<&str>, _use_heap: bool) -> Option<String> {
2694    // c:2178
2695    let suffix = if prefix.is_some() {
2696        ".XXXXXX"
2697    } else {
2698        "XXXXXX"
2699    }; // c:2178
2700    queue_signals(); // c:2182
2701    let prefix_owned: String = match prefix {
2702        // c:2183
2703        Some(p) => p.to_string(),
2704        // c:2184 — `getsparam("TMPPREFIX")`. Read from paramtab (not OS
2705        //          env); fall back to compile-time default when unset.
2706        None => getsparam("TMPPREFIX")
2707            .unwrap_or_else(|| crate::ported::config_h::DEFAULT_TMPPREFIX.to_string()),
2708    };
2709    let template = format!("{}{}", prefix_owned, suffix); // c:2186-2188
2710                                                          // C uses mktemp(3) which mutates the X's into a unique name              // c:2192/2219
2711                                                          // without creating the file. Rust has no mktemp; emulate with
2712                                                          // pid+timestamp. Caller is responsible for O_EXCL open.
2713    let pid = std::process::id();
2714    let nanos = std::time::SystemTime::now()
2715        .duration_since(UNIX_EPOCH)
2716        .map(|d| d.as_nanos())
2717        .unwrap_or(0);
2718    let unique = format!("{:x}{:x}", pid, nanos & 0xffffff);
2719    let name = template.replace("XXXXXX", &unique);
2720    unqueue_signals(); // c:2221
2721    Some(name)
2722}
2723
2724/// Check if metafied - port from zsh/Src/utils.c has_token()
2725// Check if a string contains a token                                       // c:2282
2726/// Port of `has_token(const char *s)` from `Src/utils.c:2282` — used by
2727/// `ecstrcode` (parse.rs:989) to flip the token-marker bit on the
2728/// encoded string offset. Token markers live in 0x83..=0x9f (Pound,
2729/// Stringg, Hat, Star, ..., Bnull, Nularg). Earlier impl checked
2730/// only 0x83 (Meta) which missed Dash/Equals/Inbrack/Inbrace/etc.,
2731/// so any string containing those (e.g. `dart-lang/dart` →
2732/// `dart\u{9b}lang/dart`) got encoded with the no-token bit set,
2733/// breaking byte parity with C's wordcode-emitter output.
2734/// Port of `int has_token(const char *s)` from `Src/utils.c:2280-2288`.
2735/// ```c
2736/// while (*s)
2737///     if (itok(*s++)) return 1;
2738/// return 0;
2739/// ```
2740/// Routes through the canonical `itok()` typtab-driven predicate.
2741/// Previous Rust port used hardcoded `0x83..=0x9f` which was:
2742///   - Wrong on the low end: includes `0x83` (Meta, NOT a token).
2743///   - Wrong on the high end: missing `0xa0..=0xa1` (part of the
2744///     Snull..Nularg range that `itok` legitimately covers).
2745/// The full canonical ITOK range per `Src/zsh.h:152-159` is
2746/// `0x84..=0xa1` (Pound..Nularg), with possible gaps in the middle
2747/// (Snull at 0x9d follows Bang at 0x9c).
2748pub fn has_token(s: &str) -> bool {
2749    // c:2282
2750    s.bytes().any(itok) // c:2285
2751}
2752
2753// Delete a character in a string                                           // c:2294
2754/// Remove character from string (from utils.c chuck)
2755pub fn chuck(s: &mut String, pos: usize) {
2756    // c:2294
2757    if pos < s.len() {
2758        s.remove(pos);
2759    }
2760}
2761
2762/// Convert character to lowercase
2763/// To-lowercase that respects locale.
2764/// Port of `tulower(int c)` from Src/utils.c.
2765pub fn tulower(c: char) -> char {
2766    // c:2302
2767    c.to_lowercase().next().unwrap_or(c)
2768}
2769
2770/// Convert character to uppercase
2771/// To-uppercase that respects locale.
2772/// Port of `tuupper(int c)` from Src/utils.c.
2773pub fn tuupper(c: char) -> char {
2774    // c:2310
2775    c.to_uppercase().next().unwrap_or(c)
2776}
2777
2778/// Port of `void ztrncpy(char *s, char *t, int len)` from `Src/utils.c:2320`.
2779///
2780/// C body (c:2320-2326):
2781/// ```c
2782/// while (len--)
2783///     *s++ = *t++;
2784/// *s = '\0';
2785/// ```
2786///
2787/// Copy `len` bytes from `t` into `s`, then NUL-terminate. C does no
2788/// bounds check; the caller must ensure `s` has `len + 1` bytes of
2789/// capacity. Rust port preserves the (s, t, len) parameter shape:
2790/// `s: &mut String` (output), `t: &str` (source), `len: usize` (copy
2791/// count). The C NUL terminator at c:2325 is implicit in Rust's
2792/// length-prefixed String. The Rust port clamps `len` to `t.len()` to
2793/// avoid the out-of-bounds read C's `*s++ = *t++` would perform when
2794/// `len > strlen(t)` (UB in C; explicitly bounded in Rust).
2795pub fn ztrncpy(s: &mut String, t: &str, len: usize) {
2796    // c:2320
2797    s.clear(); // C overwrites from start of *s
2798    let take = len.min(t.len()); // c:2322 while (len--)
2799    s.push_str(&t[..take]); // c:2322 *s++ = *t++
2800                            // c:2325 — `*s = '\0';` (implicit in Rust String length)
2801}
2802
2803/// Port of `void strucpy(char **s, char *t)` from `Src/utils.c:2331-2337`.
2804/// ```c
2805/// char *u = *s;
2806/// while ((*u++ = *t++));
2807/// *s = u - 1;       // leave *s pointing at the NUL terminator
2808/// ```
2809/// Body: `strcpy(*s, t)` + advance `*s` to point at the new
2810/// NUL terminator. The "u" in the name is a C convention for the
2811/// local pointer-walker, NOT "upper-case" — the function does NOT
2812/// change case.
2813///
2814/// Rust API: param name `s` matches C exactly per Rule E (despite Rust
2815/// convention of `dest` for output buffers). The C pointer-advance
2816/// `*s = u - 1` translates to "the new end of s is at s.len()", which
2817/// is implicit in Rust's owning-String model.
2818pub fn strucpy(s: &mut String, t: &str) {
2819    // c:2331
2820    s.push_str(t); // c:2335 `*u++ = *t++` loop
2821                   // c:2336 — `*s = u - 1;` — pointer-advance is implicit in Rust
2822                   // (`s.len()` is the new end-of-string position).
2823}
2824
2825/// Port of `void struncpy(char **s, char *t, int n)` from `Src/utils.c:2341-2350`.
2826/// ```c
2827/// char *u = *s;
2828/// while (n-- && (*u = *t++)) u++;
2829/// *s = u;
2830/// if (n > 0) *u = '\0';
2831/// ```
2832/// Body: copy up to `n` bytes from `t` to `*s`, NUL-terminate.
2833/// Note c:2348 — "just one null-byte will do, unlike strncpy(3)";
2834/// doesn't pad with NULs.
2835///
2836/// Param names match C exactly per Rule E.
2837pub fn struncpy(s: &mut String, t: &str, n: usize) {
2838    // c:2341
2839    // c:2345 — `while (n-- && (*u = *t++)) u++;` — copy up to n
2840    // bytes, stop at NUL (which in Rust &str is the end-of-string).
2841    let take = n.min(t.len());
2842    s.push_str(&t[..take]);
2843}
2844
2845/// Array length - port from arrlen()
2846/// Port of `arrlen(char **s)` from `Src/utils.c:2357`.
2847pub fn arrlen<T>(s: &[T]) -> usize {
2848    // c:2357
2849    s.len()
2850}
2851
2852// Return TRUE iff arrlen(s) >= lower_bound, but more efficiently.          // c:2382
2853/// Check if array length >= n (from utils.c arrlen_ge)
2854pub fn arrlen_ge<T>(arr: &[T], n: usize) -> bool {
2855    // c:2369
2856    arr.len() >= n
2857}
2858
2859// Return TRUE iff arrlen(s) > lower_bound, but more efficiently.           // c:2400
2860/// Check if array length > n (from utils.c arrlen_gt)
2861pub fn arrlen_gt<T>(arr: &[T], n: usize) -> bool {
2862    // c:2382
2863    arr.len() > n
2864}
2865
2866// Return TRUE iff arrlen(s) <= upper_bound, but more efficiently.          // c:2409
2867/// Check if array length <= n (from utils.c arrlen_le)
2868pub fn arrlen_le<T>(arr: &[T], n: usize) -> bool {
2869    // c:2391
2870    arr.len() <= n
2871}
2872
2873// Return TRUE iff arrlen(s) < upper_bound, but more efficiently.           // c:2400
2874/// Check if array length < n (from utils.c arrlen_lt)
2875pub fn arrlen_lt<T>(arr: &[T], n: usize) -> bool {
2876    // c:2400
2877    arr.len() < n
2878}
2879
2880/// Direct port of `int skipparens(char inpar, char outpar, char **s)`
2881/// from `Src/utils.c:2409`. Walks `*s` past a balanced `inpar`/`outpar`
2882/// pair, advancing the input slice in place. Returns 0 on full
2883/// balance, `-1` when `**s != inpar` (no opening paren), or positive
2884/// when the scan ran off the end of `*s` with `level > 0`
2885/// (unbalanced).
2886///
2887/// Rust signature: `s: &mut &str` mirrors C's `char **s` out-arg —
2888/// the caller's slice handle is rewritten to point past the closing
2889/// paren (or to the empty tail on unbalanced input).
2890/// WARNING: param names don't match C — Rust=(open, close, s) vs C=(inpar, outpar, s)
2891pub fn skipparens(open: char, close: char, s: &mut &str) -> i32 {
2892    // c:2409
2893    let mut chars = s.char_indices();
2894    // c:2412 — `if (**s != inpar) return -1;`
2895    match chars.next() {
2896        Some((_, c)) if c == open => {}
2897        _ => return -1,
2898    }
2899    // c:2414 — `for (level = 1; *++*s && level;)`
2900    let mut level: i32 = 1;
2901    let mut consumed = open.len_utf8();
2902    for (i, c) in chars {
2903        consumed = i + c.len_utf8();
2904        if c == open {
2905            level += 1; // c:2416-2417
2906        } else if c == close {
2907            level -= 1; // c:2418-2419
2908        }
2909        if level == 0 {
2910            break;
2911        }
2912    }
2913    *s = &s[consumed..];
2914    // c:2421 — `return level;`. 0 on balanced match, positive on
2915    // ran-off-end (unbalanced input).
2916    level
2917}
2918
2919/// zlong with base autodetection. Returns the parsed value AND a
2920/// `&str` slice pointing to the first unconsumed character —
2921/// matching C's `char **t` out-arg.
2922///
2923/// C signature: `zlong zstrtol(const char *s, char **t, int base)`.
2924/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
2925/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Otherwise `base`
2926/// must be in [2, 36]; values outside that range emit `zerr` and
2927/// return 0.
2928///
2929/// On overflow, mirrors C's "number truncated after N digits"
2930/// behaviour: emits a `zwarn` and returns the truncated value
2931/// (last in-range computation).
2932/// Port of `zstrtol(const char *s, char **t, int base)` from `Src/utils.c:2427`.
2933/// WARNING: param names don't match C — Rust=(s, base) vs C=(s, t, base)
2934pub fn zstrtol(s: &str, base: i32) -> (i64, &str) {
2935    // c:2427
2936    zstrtol_underscore(s, base, false) // c:2427
2937}
2938
2939/// Port of `zstrtol_underscore(const char *s, char **t, int base, int underscore)` from `Src/utils.c:2438`.
2940///
2941/// C signature: `zlong zstrtol_underscore(const char *s, char **t,
2942///                                         int base, int underscore)`.
2943///
2944/// Convert string to zlong with optional `_` digit-separator support.
2945/// `underscore != 0` allows underscores to appear inside the digit
2946/// run (skipped during accumulation). Returns the parsed value AND
2947/// the unconsumed-tail slice (matching C's `*t = (char *)s` writeback
2948/// at line 2516).
2949///
2950/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
2951/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Bases outside [2, 36]
2952/// emit `zerr` and return `(0, original_s)`. Overflow truncates and
2953/// emits a `zwarn` per c:2510-2512.
2954pub fn zstrtol_underscore(s: &str, base: i32, underscore: bool) -> (i64, &str) {
2955    // c:2438
2956    let bytes = s.as_bytes();
2957    let mut i = 0usize;
2958    // c:2444-2445 — skip leading `inblank` whitespace.
2959    while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
2960        i += 1;
2961    }
2962    // c:2447-2450 — handle sign.
2963    let neg = if i < bytes.len() && (bytes[i] == b'-') {
2964        // c:2447 IS_DASH
2965        i += 1;
2966        true
2967    } else if i < bytes.len() && bytes[i] == b'+' {
2968        // c:2449
2969        i += 1;
2970        false
2971    } else {
2972        false
2973    };
2974
2975    // c:2452-2461 — base autodetect when base == 0.
2976    let mut base = base;
2977    if base == 0 {
2978        // c:2452
2979        if i >= bytes.len() || bytes[i] != b'0' {
2980            // c:2453
2981            base = 10; // c:2454
2982        } else {
2983            i += 1; // c:2455 ++s
2984            if i < bytes.len() && (bytes[i] == b'x' || bytes[i] == b'X') {
2985                // c:2455
2986                base = 16;
2987                i += 1;
2988            } else if i < bytes.len() && (bytes[i] == b'b' || bytes[i] == b'B') {
2989                // c:2457
2990                base = 2;
2991                i += 1;
2992            } else {
2993                // c:2459
2994                base = 8; // c:2460
2995            }
2996        }
2997    }
2998    let inp_idx = i; // c:2462 inp = s
2999    if base < 2 || base > 36 {
3000        // c:2463
3001        zerr(&format!(
3002            "invalid base (must be 2 to 36 inclusive): {}",
3003            base
3004        )); // c:2464
3005        return (0, s); // c:2465 return 0
3006    }
3007
3008    let mut calc: u64 = 0; // c:2441
3009    let mut trunc_idx: Option<usize> = None; // c:2440 trunc = NULL
3010    if base <= 10 {
3011        // c:2466
3012        // c:2467-2479 — digit accumulator, low bases.
3013        let max_d = b'0' + base as u8;
3014        while i < bytes.len() {
3015            let c = bytes[i];
3016            if c >= b'0' && c < max_d {
3017                if trunc_idx.is_none() {
3018                    let newcalc = calc
3019                        .wrapping_mul(base as u64)
3020                        .wrapping_add((c - b'0') as u64);
3021                    if newcalc < calc {
3022                        // c:2473
3023                        trunc_idx = Some(i); // c:2474
3024                    } else {
3025                        calc = newcalc;
3026                    }
3027                }
3028                i += 1;
3029            } else if underscore && c == b'_' {
3030                // c:2467 underscore && *s == '_'
3031                i += 1;
3032            } else {
3033                break;
3034            }
3035        }
3036    } else {
3037        // c:2480
3038        // c:2481-2495 — digit accumulator, high bases (>10).
3039        while i < bytes.len() {
3040            let c = bytes[i];
3041            let digit = if c.is_ascii_digit() {
3042                // c:2481 idigit
3043                Some((c - b'0') as u32)
3044            } else if c >= b'a' && c < b'a' + base as u8 - 10 {
3045                // c:2482
3046                Some(((c & 0x1f) + 9) as u32) // c:2486 (*s & 0x1f) + 9
3047            } else if c >= b'A' && c < b'A' + base as u8 - 10 {
3048                // c:2483
3049                Some(((c & 0x1f) + 9) as u32)
3050            } else if underscore && c == b'_' {
3051                // c:2484
3052                i += 1;
3053                continue;
3054            } else {
3055                None
3056            };
3057            match digit {
3058                Some(d) => {
3059                    if trunc_idx.is_none() {
3060                        let newcalc = calc.wrapping_mul(base as u64).wrapping_add(d as u64);
3061                        if newcalc < calc {
3062                            // c:2489
3063                            trunc_idx = Some(i); // c:2490
3064                        } else {
3065                            calc = newcalc;
3066                        }
3067                    }
3068                    i += 1;
3069                }
3070                None => break,
3071            }
3072        }
3073    }
3074
3075    // c:2504-2511 — special-case: signed-overflow into top bit.
3076    // The lowest negative number triggers the first test but is
3077    // representable correctly; check it explicitly.
3078    if trunc_idx.is_none() && (calc as i64) < 0 {
3079        // c:2504
3080        let top_bit_only = calc & !(1u64 << 63); // c:2506
3081        if !neg || top_bit_only != 0 {
3082            // c:2505
3083            trunc_idx = Some(i.saturating_sub(1)); // c:2508
3084            calc /= base as u64; // c:2509
3085        }
3086    }
3087
3088    if let Some(t) = trunc_idx {
3089        // c:2512
3090        let digits = t.saturating_sub(inp_idx);
3091        let inp_str = &s[inp_idx..];
3092        zwarn(&format!(
3093            "number truncated after {} digits: {}",
3094            digits, inp_str
3095        )); // c:2513
3096    }
3097
3098    let result = if neg {
3099        // c:2517
3100        -(calc as i64)
3101    } else {
3102        calc as i64
3103    };
3104    (result, &s[i..])
3105}
3106
3107/// Parse unsigned integer with underscore support
3108/// Port from zsh/Src/utils.c zstrtoul_underscore() lines 2528-2575
3109/// Port of `zstrtoul_underscore(const char *s, zulong *retval)` from
3110/// `Src/utils.c:2527-2590`. C body:
3111/// ```c
3112/// if (*s == '+') s++;
3113/// if      (*s != '0')                    base = 10;
3114/// else if (*++s == 'x' || *s == 'X')     base = 16, s++;
3115/// else if (*s == 'b' || *s == 'B')       base = 2,  s++;
3116/// else                                   base = isset(OCTALZEROES) ? 8 : 10;
3117/// ```
3118/// The leading-zero case is option-gated on OCTALZEROES — without
3119/// the option (default), `0777` parses as decimal 777, NOT octal 511.
3120/// Previously the Rust port always treated leading-zero as octal —
3121/// divergent for the default shell setup.
3122pub fn zstrtoul_underscore(s: &str) -> Option<u64> {
3123    // c:2529
3124    let s = s.trim();
3125    let s = s.strip_prefix('+').unwrap_or(s); // c:2533-2534
3126
3127    let (base, rest) = if s.starts_with("0x") || s.starts_with("0X") {
3128        // c:2538
3129        (16, &s[2..])
3130    } else if s.starts_with("0b") || s.starts_with("0B") {
3131        // c:2540
3132        (2, &s[2..])
3133    } else if s.starts_with('0') && s.len() > 1 && isset(OCTALZEROES)
3134    // c:2543
3135    {
3136        (8, &s[1..])
3137    } else {
3138        (10, s)
3139    };
3140
3141    let rest = rest.replace('_', "");
3142    u64::from_str_radix(&rest, base).ok()
3143}
3144
3145/// Port of `int setblock_fd(int turnonblocking, int fd, long *modep)`
3146/// from `Src/utils.c:2579`. Toggles O_NONBLOCK on `fd`.
3147///
3148/// **Signature matches C exactly** (param order `turnonblocking, fd`):
3149/// - `turnonblocking=1` → clear O_NONBLOCK (enable blocking).
3150/// - `turnonblocking=0` → set O_NONBLOCK (disable blocking).
3151///
3152/// Returns:
3153/// - `(true, mode)` if the fd's state was changed.
3154/// - `(false, -1)` if the fd is a regular file (C skips regular
3155///   files entirely — only operates on pipes/sockets/ttys per
3156///   `c:2599 if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`).
3157/// - `(false, mode)` if state was already correct.
3158///
3159/// Rust returns `(state_changed, modep_value)` as a tuple to mirror
3160/// C's `int` return + `long *modep` out-param.
3161pub fn setblock_fd(turnonblocking: bool, fd: i32) -> (bool, libc::c_long) {
3162    // c:2579
3163    #[cfg(unix)]
3164    unsafe {
3165        // c:2598-2600 — `if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`.
3166        let mut st: libc::stat = std::mem::zeroed();
3167        if libc::fstat(fd, &mut st) != 0 {
3168            return (false, -1);
3169        }
3170        // c:2599 — skip regular files (no nonblock concept).
3171        let mode_bits = st.st_mode as u32;
3172        if (mode_bits & libc::S_IFMT as u32) == libc::S_IFREG as u32 {
3173            return (false, -1); // c:2614 *modep = -1; return 0
3174        }
3175        // c:2601 — `*modep = fcntl(fd, F_GETFL, 0);`
3176        let modep = libc::fcntl(fd, libc::F_GETFL, 0) as libc::c_long;
3177        if modep < 0 {
3178            return (false, -1); // c:2602 if (*modep != -1)
3179        }
3180        const NONBLOCK: libc::c_long = libc::O_NONBLOCK as libc::c_long;
3181        if !turnonblocking {
3182            // c:2603-2606 — want to KNOW if blocking was off; set it on.
3183            if (modep & NONBLOCK) != 0 {
3184                return (true, modep); // already nonblock — no-op, but "off"
3185            }
3186            if libc::fcntl(fd, libc::F_SETFL, modep | NONBLOCK) == 0 {
3187                return (true, modep); // c:2606
3188            }
3189        } else {
3190            // c:2607-2611 — want to clear NONBLOCK if currently set.
3191            if (modep & NONBLOCK) != 0 && libc::fcntl(fd, libc::F_SETFL, modep & !NONBLOCK) == 0 {
3192                return (true, modep); // c:2611 state changed
3193            }
3194        }
3195        (false, modep)
3196    }
3197    #[cfg(not(unix))]
3198    {
3199        let _ = (turnonblocking, fd);
3200        (false, -1)
3201    }
3202}
3203
3204/// Port of `int setblock_stdin(void)` from `Src/utils.c:2620-2625`.
3205/// ```c
3206/// long mode;
3207/// return setblock_fd(1, 0, &mode);
3208/// ```
3209/// `turnonblocking=1, fd=0` — turn ON blocking on stdin (clear
3210/// O_NONBLOCK).
3211pub fn setblock_stdin() -> i32 {
3212    // c:2624 — `return setblock_fd(1, 0, &mode);`.
3213    let (changed, _mode) = setblock_fd(true, 0); // c:2624
3214    changed as i32
3215}
3216
3217/// Port of `int read_poll(int fd, int *readchar, int polltty, zlong microseconds)`
3218/// from `Src/utils.c:2645-2738`.
3219///
3220/// Check whether input is pending on `fd` within `timeout_us`
3221/// microseconds. Faithful port of all three C paths:
3222/// 1. The `polltty` non-canonical-tty path (VMIN/VTIME termios
3223///    setup so a raw tty can be polled — c:2665-2696).
3224/// 2. The `HAVE_SELECT` wait (c:2697-2705). Rust uses `poll(2)`
3225///    as the idiom substitution for `select(2)`; `poll` return
3226///    semantics are mapped onto C's `ret` so the `ret < 0`
3227///    error-fallback still triggers.
3228/// 3. The final `setblock_fd` + non-blocking `read` fallback that
3229///    captures a byte when `select`/`poll` errored (c:2718-2729).
3230///
3231/// `readchar` is C's `int *readchar` out-param: when a byte is
3232/// actually consumed during the poll it is written through
3233/// `readchar` (so `read -t -k` on a raw tty gets the byte). `polltty`
3234/// is C's `int polltty` truthiness. Returns C's `(ret > 0)`.
3235///
3236/// WARNING: param order differs from C — Rust=(fd, readchar,
3237/// polltty, timeout_us) vs C=(fd, readchar, polltty, microseconds);
3238/// C's `microseconds` is `timeout_us` here.
3239pub fn read_poll(fd: i32, readchar: &mut i32, polltty: bool, timeout_us: i64) -> bool {
3240    // c:2645
3241    #[cfg(unix)]
3242    {
3243        let mut ret: i32 = -1; // c:2647
3244        let mut mode: libc::c_long = -1; // c:2648
3245        // C reassigns the `polltty` param; hold it as the VMIN-derived int.
3246        let mut polltty: i32 = polltty as i32; // c:2645
3247        let mut ti: Option<libc::termios> = None; // c:2659 `struct ttyinfo ti;`
3248
3249        // c:2662-2663 — `if (fd < 0 || (polltty && !isatty(fd))) polltty = 0;`
3250        if fd < 0 || (polltty != 0 && unsafe { libc::isatty(fd) } == 0) {
3251            polltty = 0; // no tty to poll
3252        }
3253
3254        // c:2665-2696 — HAS_TIO && !__CYGWIN__: non-canonical VMIN poll setup.
3255        if polltty != 0 && fd >= 0 {
3256            // c:2686 — `gettyinfo(&ti);` (operates on global SHTTY, as in C).
3257            if let Some(mut t) = gettyinfo() {
3258                // c:2687 — `if ((polltty = ti.tio.c_cc[VMIN]))`
3259                polltty = t.c_cc[libc::VMIN] as i32;
3260                if polltty != 0 {
3261                    t.c_cc[libc::VMIN] = 0; // c:2688
3262                    // c:2690 — termios timeout is 10ths of a second.
3263                    t.c_cc[libc::VTIME] = (timeout_us / 100_000) as libc::cc_t; // c:2690
3264                    settyinfo(&t); // c:2691
3265                }
3266                ti = Some(t);
3267            } else {
3268                // gettyinfo failed (SHTTY closed) — nothing to poll via VMIN.
3269                polltty = 0;
3270            }
3271        }
3272
3273        // c:2697-2705 — HAVE_SELECT wait, using poll(2) as the select(2) idiom.
3274        let timeout_ms = (timeout_us / 1000) as i32; // c:2698-2699 (expire_tv)
3275        if fd > -1 {
3276            // c:2701-2703
3277            let mut fds = [libc::pollfd {
3278                fd: fd as RawFd,
3279                events: libc::POLLIN,
3280                revents: 0,
3281            }];
3282            let raw = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
3283            ret = if raw > 0 {
3284                // Map poll revents onto select's "fd is readable" ret>0.
3285                if (fds[0].revents & libc::POLLIN) != 0 {
3286                    1
3287                } else {
3288                    0
3289                }
3290            } else {
3291                raw // 0 = timeout, -1 = error (triggers fallback below)
3292            };
3293        } else {
3294            // c:2705 — `select(0, NULL, NULL, NULL, &expire_tv)`: pure sleep.
3295            ret = unsafe { libc::poll(std::ptr::null_mut(), 0, timeout_ms) };
3296        }
3297
3298        // c:2718 — `if (fd >= 0 && ret < 0 && !errflag)`
3299        if fd >= 0 && ret < 0 && errflag.load(Ordering::Relaxed) == 0 {
3300            // c:2723 — final attempt: non-blocking read of a single char.
3301            // `(polltty || setblock_fd(0, fd, &mode))`
3302            let can_read = if polltty != 0 {
3303                true // polltty short-circuits setblock_fd; mode stays -1
3304            } else {
3305                let (changed, m) = setblock_fd(false, fd); // c:2723
3306                mode = m; // C writes *modep unconditionally
3307                changed
3308            };
3309            if can_read {
3310                let mut c: u8 = 0;
3311                let n = unsafe {
3312                    libc::read(fd, &mut c as *mut u8 as *mut libc::c_void, 1)
3313                };
3314                if n > 0 {
3315                    *readchar = c as i32; // c:2724
3316                    ret = 1; // c:2725
3317                }
3318            }
3319            // c:2727-2728 — `if (mode != -1) fcntl(fd, F_SETFL, mode);`
3320            if mode != -1 {
3321                unsafe {
3322                    libc::fcntl(fd, libc::F_SETFL, mode);
3323                }
3324            }
3325        }
3326
3327        // c:2730-2736 — HAS_TIO: restore canonical VMIN=1 / VTIME=0.
3328        if polltty != 0 {
3329            if let Some(mut t) = ti {
3330                t.c_cc[libc::VMIN] = 1; // c:2732
3331                t.c_cc[libc::VTIME] = 0; // c:2733
3332                settyinfo(&t); // c:2734
3333            }
3334        }
3335
3336        ret > 0 // c:2737
3337    }
3338    #[cfg(not(unix))]
3339    {
3340        let _ = (fd, readchar, polltty, timeout_us);
3341        false
3342    }
3343}
3344
3345/// Compute time difference in microseconds (from utils.c timespec_diff_us)
3346/// Port of `timespec_diff_us(const struct timespec *t1, const struct timespec *t2)` from `Src/utils.c:2752`.
3347/// Rust idiom replacement: `Instant::duration_since().as_micros()`
3348/// replaces the C tv_sec/tv_nsec arithmetic + sign-flip dance.
3349pub fn timespec_diff_us(t1: &std::time::Instant, t2: &std::time::Instant) -> i64 {
3350    if *t2 > *t1 {
3351        t2.duration_since(*t1).as_micros() as i64
3352    } else {
3353        -(t1.duration_since(*t2).as_micros() as i64)
3354    }
3355}
3356
3357/// Port of `int zmonotime(time_t *tloc)` from Src/utils.c:2780.
3358///
3359/// "Like time(), but uses the monotonic clock." Reads CLOCK_MONOTONIC
3360/// and returns `tv_sec`; writes the same value through `tloc` if
3361/// non-NULL (Rust: `Some(&mut t)`).
3362pub fn zmonotime(tloc: Option<&mut i64>) -> i64 {
3363    // c:2780
3364    #[cfg(unix)]
3365    {
3366        // c:2782 — `struct timespec ts;`
3367        let mut ts = libc::timespec {
3368            // c:2782
3369            tv_sec: 0,
3370            tv_nsec: 0,
3371        };
3372        // c:2783 — `zgettime_monotonic_if_available(&ts);`
3373        unsafe {
3374            libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); // c:2783
3375        }
3376        // c:2784-2785 — `if (tloc) *tloc = ts.tv_sec;`
3377        if let Some(t) = tloc {
3378            // c:2784
3379            *t = ts.tv_sec as i64; // c:2785
3380        }
3381        ts.tv_sec as i64 // c:2786 return ts.tv_sec
3382    }
3383    #[cfg(not(unix))]
3384    {
3385        let _ = tloc;
3386        0
3387    }
3388}
3389
3390/// Check if string looks like a number
3391/// Check whether a string parses as a decimal integer.
3392/// Sleep for a given number of seconds (fractional)
3393/// Sleep for a fractional number of seconds.
3394/// Port of `int zsleep(long us)` from Src/utils.c:2797.
3395///
3396/// "Sleep for the given number of microseconds." Wraps
3397/// `nanosleep(2)` with EINTR retry, returning 1 on completion, 0
3398/// on permanent error.
3399pub fn zsleep(us: i64) -> i32 {
3400    // c:2797
3401    let mut sleeptime = libc::timespec {
3402        // c:2797-2803
3403        tv_sec: (us / 1_000_000) as libc::time_t,
3404        tv_nsec: ((us % 1_000_000) * 1000) as libc::c_long,
3405    };
3406    #[cfg(unix)]
3407    {
3408        loop {
3409            // c:2804
3410            let mut rem = libc::timespec {
3411                tv_sec: 0,
3412                tv_nsec: 0,
3413            };
3414            let ret = unsafe { libc::nanosleep(&sleeptime, &mut rem) }; // c:2806
3415            if ret == 0 {
3416                // c:2808
3417                return 1;
3418            }
3419            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
3420            if err != libc::EINTR {
3421                // c:2810
3422                return 0; // c:2811
3423            }
3424            sleeptime = rem; // c:2812
3425        }
3426    }
3427    #[cfg(not(unix))]
3428    {
3429        let _ = sleeptime;
3430        std::thread::sleep(std::time::Duration::from_micros(us.max(0) as u64));
3431        1
3432    }
3433}
3434
3435/// Sleep random amount up to max microseconds (from utils.c zsleep_random)
3436/// Port of `int zsleep_random(long max_us, time_t end_time)`
3437/// from Src/utils.c:2833.
3438///
3439/// "Sleep for time (fairly) randomly up to max_us microseconds.
3440/// Don't let the time extend beyond end_time. end_time is compared
3441/// to the current *monotonic* clock time. Return 1 if that seemed
3442/// to work, else 0."
3443pub fn zsleep_random(max_us: i64, end_time: i64) -> i32 {
3444    // c:2833
3445    let now = zmonotime(None); // c:2833
3446    let r16 = unsafe { libc::rand() } & 0xFFFF; // c:2845
3447    let mut r: i64 = (max_us >> 16) * (r16 as i64); // c:2852
3448    while r != 0 && now + (r / 1_000_000) > end_time {
3449        // c:2858
3450        r >>= 1; // c:2859
3451    }
3452    if r != 0 {
3453        // c:2860
3454        zsleep(r) // c:2861
3455    } else {
3456        0 // c:2862
3457    }
3458}
3459
3460/// Port of `int checkrmall(char *s)` from `Src/utils.c:2867`.
3461///
3462/// C body (c:2867-2919): count files in directory `s` (capped at 100,
3463/// honoring GLOBDOTS), emit one of 4 prompts based on count, optionally
3464/// sleep 10s under RMSTARWAIT, then `getquery("ny", 1)` for confirm.
3465///
3466/// Previous Rust port was a FAKE — single generic prompt, no file
3467/// count, no shout/errflag checks, no RMSTARWAIT, wrong valid-chars
3468/// order ("yn" instead of "ny" so 'n' isn't the default-first-arm).
3469/// Lost 9 of the 10 C semantic branches.
3470pub fn checkrmall(s: &str) -> bool {
3471    // c:2867
3472    // c:2871-2872 — `if (!shout) return 1;` — no controlling tty for
3473    // prompt output, default to yes. Rust analogue: if stderr isn't
3474    // a tty, skip the prompt and approve.
3475    let shout_is_tty = unsafe { libc::isatty(2) != 0 }; // c:2871
3476    if !shout_is_tty {
3477        // c:2871
3478        return true; // c:2872
3479    }
3480    // c:2873-2878 — `if (*s != '/')` build absolute via pwd prefix.
3481    let s_owned: String; // c:2873
3482    let s_abs: &str = if !s.starts_with('/') {
3483        // c:2873
3484        let pwd = getsparam("PWD").unwrap_or_default(); // c:2874 pwd[1]
3485        s_owned = if pwd.len() > 1 {
3486            // c:2874 if (pwd[1])
3487            crate::ported::string::tricat(&pwd, "/", s) // c:2875 zhtricat
3488        } else {
3489            // c:2876
3490            crate::ported::string::dyncat("/", s) // c:2877
3491        };
3492        s_owned.as_str()
3493    } else {
3494        s
3495    };
3496    let max_count: i32 = 100; // c:2879
3497    let mut count: i32 = 0; // c:2870 count = 0
3498                            // c:2880-2892 — opendir + readdir loop, count entries (skip
3499                            // dotfiles when !GLOBDOTS), cap at max_count.
3500    let ignoredots = !isset(GLOBDOTS); // c:2881
3501                                       // c:2880 — `if (!(dir = opendir(unmeta(s)))) return;` then
3502                                       //          `while ((fn = zreaddir(dir, 1))) { ... }`.
3503    if let Ok(mut dir) = fs::read_dir(s_abs) {
3504        while let Some(fname) = zreaddir(&mut dir, 1) {
3505            if ignoredots && fname.starts_with('.') {
3506                // c:2885
3507                continue; // c:2886
3508            }
3509            count += 1; // c:2887
3510            if count > max_count {
3511                // c:2888
3512                break; // c:2889
3513            }
3514        }
3515        // c:closedir(dir) auto on drop
3516    }
3517    // c:2893-2904 — four-arm prompt based on file count.
3518    let stderr_h = io::stderr();
3519    let mut stderr_w = stderr_h.lock();
3520    if count > max_count {
3521        // c:2893
3522        let _ = write!(
3523            stderr_w,
3524            "zsh: sure you want to delete more than {} files in ",
3525            max_count
3526        ); // c:2894
3527    } else if count == 1 {
3528        // c:2896
3529        let _ = write!(stderr_w, "zsh: sure you want to delete the only file in ");
3530    // c:2897
3531    } else if count > 0 {
3532        // c:2898
3533        let _ = write!(
3534            stderr_w,
3535            "zsh: sure you want to delete all {} files in ",
3536            count
3537        ); // c:2899
3538    } else {
3539        // c:2901
3540        // c:2902 — `We don't know how many files the glob will expand to`
3541        let _ = write!(stderr_w, "zsh: sure you want to delete all the files in ");
3542        // c:2903
3543    }
3544    // c:2905 — `nicezputs(s, shout)` — escape non-printables in path.
3545    let _ = nicezputs(s_abs, &mut stderr_w); // c:2905
3546                                             // c:2906-2912 — RMSTARWAIT: print "? (waiting ten seconds)",
3547                                             // flush, beep, sleep 10, then newline.
3548    if isset(RMSTARWAIT) {
3549        // c:2906
3550        let _ = write!(stderr_w, "? (waiting ten seconds)"); // c:2907
3551        let _ = stderr_w.flush(); // c:2908
3552        drop(stderr_w); // release lock around zbeep + sleep
3553        zbeep(); // c:2909
3554        std::thread::sleep(std::time::Duration::from_secs(10)); // c:2910
3555        let _ = writeln!(io::stderr()); // c:2911 fputc('\n', shout)
3556    } else {
3557        drop(stderr_w);
3558    }
3559    // c:2913-2914 — `if (errflag) return 0;`
3560    if errflag.load(Ordering::Relaxed) != 0 {
3561        // c:2913
3562        return false; // c:2914
3563    }
3564    // c:2915-2917 — emit "[yn]? ", flush, beep.
3565    let _ = io::stderr().write_all(b" [yn]? "); // c:2915 nicezputs of " [yn]? "
3566    let _ = io::stderr().flush(); // c:2916
3567    zbeep(); // c:2917
3568             // c:2918 — `return (getquery("ny", 1) == 'y');`. Default-first-
3569             // char is 'n' (no) — getquery's first valid char is the default
3570             // returned when the user just hits Enter.
3571    getquery(Some("ny"), 1) == b'y' as i32 // c:2918
3572}
3573
3574/// Port of `ssize_t read_loop(int fd, char *buf, size_t len)` from
3575/// `Src/utils.c:2923`.
3576///
3577/// C body (c:2923-2945):
3578/// ```c
3579/// while (1) {
3580///     ssize_t ret = read(fd, buf, len);
3581///     if (ret == len) break;
3582///     if (ret <= 0) {
3583///         if (ret < 0) {
3584///             if (errno == EINTR) continue;          // c:2933
3585///             if (fd != SHTTY)                       // c:2935
3586///                 zwarn("read failed: %e", errno);   // c:2936
3587///         }
3588///         return ret;
3589///     }
3590///     buf += ret; len -= ret;
3591/// }
3592/// ```
3593///
3594/// The previous Rust port omitted the `zwarn("read failed: %e", errno)`
3595/// emission at c:2935-2936. That's a real diagnostic divergence:
3596/// any non-SHTTY read failure in zsh emits a stderr message; the
3597/// Rust port silently propagated the io::Error to the caller, which
3598/// in most call sites was discarded (`let _ = read_loop(...)`).
3599/// Users debugging an interrupted file read (e.g. `< /broken/fd`)
3600/// would see no error message at all.
3601/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
3602pub fn read_loop(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
3603    // c:2923
3604    #[cfg(unix)]
3605    {
3606        let mut total = 0;
3607        while total < buf.len() {
3608            let n = unsafe {
3609                libc::read(
3610                    // c:2928
3611                    fd,
3612                    buf[total..].as_mut_ptr() as *mut libc::c_void,
3613                    buf.len() - total,
3614                )
3615            };
3616            if n <= 0 {
3617                // c:2931
3618                if n < 0 {
3619                    let e = io::Error::last_os_error();
3620                    if e.kind() == io::ErrorKind::Interrupted {
3621                        // c:2933
3622                        continue; // c:2934
3623                    }
3624                    // c:2935-2936 — `if (fd != SHTTY) zwarn("read failed: %e", errno);`
3625                    let shtty = SHTTY.load(Ordering::Relaxed);
3626                    if fd != shtty {
3627                        // c:2935
3628                        zwarn(
3629                            // c:2936
3630                            &format!("read failed: {}", e),
3631                        );
3632                    }
3633                    return Err(e);
3634                }
3635                break;
3636            }
3637            total += n as usize;
3638        }
3639        Ok(total)
3640    }
3641    #[cfg(not(unix))]
3642    {
3643        let _ = (fd, buf);
3644        Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
3645    }
3646}
3647
3648/// Port of `ssize_t write_loop(int fd, const char *buf, size_t len)` from
3649/// `Src/utils.c:2949`.
3650///
3651/// C body (c:2949-2970):
3652/// ```c
3653/// while (1) {
3654///     ssize_t ret = write(fd, buf, len);
3655///     if (ret == len) break;
3656///     if (ret < 0) {
3657///         if (errno == EINTR) continue;
3658///         if (fd != SHTTY)                            // c:2960
3659///             zwarn("write failed: %e", errno);       // c:2961
3660///         return -1;
3661///     }
3662///     buf += ret; len -= ret;
3663/// }
3664/// ```
3665///
3666/// Same divergence as `read_loop`: previous Rust port omitted the
3667/// `zwarn("write failed: %e", errno)` emission for non-SHTTY fds.
3668/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
3669pub fn write_loop(fd: i32, buf: &[u8]) -> io::Result<usize> {
3670    // c:2949
3671    #[cfg(unix)]
3672    {
3673        let mut total = 0;
3674        while total < buf.len() {
3675            let n = unsafe {
3676                libc::write(
3677                    // c:2954
3678                    fd,
3679                    buf[total..].as_ptr() as *const libc::c_void,
3680                    buf.len() - total,
3681                )
3682            };
3683            if n <= 0 {
3684                if n < 0 {
3685                    let e = io::Error::last_os_error();
3686                    if e.kind() == io::ErrorKind::Interrupted {
3687                        // c:2958
3688                        continue; // c:2959
3689                    }
3690                    // c:2960-2961 — `if (fd != SHTTY) zwarn("write failed: %e", errno);`
3691                    let shtty = SHTTY.load(Ordering::Relaxed);
3692                    if fd != shtty {
3693                        // c:2960
3694                        zwarn(
3695                            // c:2961
3696                            &format!("write failed: {}", e),
3697                        );
3698                    }
3699                    return Err(e);
3700                }
3701                break;
3702            }
3703            total += n as usize;
3704        }
3705        Ok(total)
3706    }
3707    #[cfg(not(unix))]
3708    {
3709        let _ = (fd, buf);
3710        Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
3711    }
3712}
3713
3714/// Read a single character (from utils.c read1char).
3715///
3716/// Port of `static int read1char(int echo)` from `Src/utils.c:2972`.
3717///
3718/// C body (c:2972-2988):
3719/// ```c
3720/// char c;
3721/// int q = queue_signal_level();
3722/// dont_queue_signals();
3723/// while (read(SHTTY, &c, 1) != 1) {
3724///     if (errno != EINTR || errflag || retflag || breaks || contflag) {
3725///         restore_queue_signals(q);
3726///         return -1;
3727///     }
3728/// }
3729/// restore_queue_signals(q);
3730/// if (echo) write_loop(SHTTY, &c, 1);
3731/// return (unsigned char) c;
3732/// ```
3733///
3734/// Returns the byte read (0..=255) on success, `-1` on error. Three
3735/// divergences in the previous Rust port:
3736///   1. **Read from stdin** instead of SHTTY (the tty fd). zsh reads
3737///      single chars during `read -k`, `bindkey`, query prompts —
3738///      always against the controlling terminal, not the standard
3739///      input which may be a pipe.
3740///   2. **No `echo` parameter.** When `echo=1` C writes the byte
3741///      back to SHTTY so the user sees what they typed.
3742///   3. **No `errflag` / `retflag` / `breaks` / `contflag` early-exit.**
3743///      A SIGINT-driven errflag should abort the read; previous Rust
3744///      port had no break path.
3745/// WARNING: returns i32 not `Option<char>` to match C's signature
3746/// (`(unsigned char) c` for success, `-1` for error).
3747pub fn read1char(echo: i32) -> i32 {
3748    // c:2972
3749    #[cfg(unix)]
3750    {
3751        let shtty = SHTTY.load(Ordering::Relaxed);
3752        if shtty < 0 {
3753            return -1;
3754        }
3755        let mut c: u8 = 0;
3756        loop {
3757            let rc = unsafe { libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1) };
3758            if rc == 1 {
3759                // c:2978
3760                break;
3761            }
3762            // c:2979 — `if (errno != EINTR || errflag || retflag || breaks
3763            //              || contflag) return -1;`
3764            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
3765            if err != libc::EINTR {
3766                return -1;
3767            }
3768            // Check errflag interrupt flag (the most observable of the
3769            // four guards; retflag/breaks/contflag are control-flow
3770            // states that don't apply outside the exec engine).
3771            if errflag.load(Ordering::Relaxed) != 0 {
3772                return -1;
3773            }
3774        }
3775        // c:2985-2986 — `if (echo) write_loop(SHTTY, &c, 1);`
3776        if echo != 0 {
3777            // c:2985
3778            let _ = write_loop(shtty, &[c]); // c:2986
3779        }
3780        // c:2987 — `return (unsigned char) c;`
3781        c as i32
3782    }
3783    #[cfg(not(unix))]
3784    {
3785        let _ = echo;
3786        -1
3787    }
3788}
3789
3790/// Port of `int noquery(int purge)` from Src/utils.c:2992.
3791///
3792/// "If anything has been typed before the query, return without
3793/// asking. Optionally also purge the input queue." Returns the
3794/// number of bytes pending on `SHTTY` (via FIONREAD ioctl); when
3795/// `purge` is set, drains them before returning.
3796pub fn noquery(purge: bool) -> i32 {
3797    // c:2992
3798    let mut val: libc::c_int = 0; // c:2992
3799    #[cfg(unix)]
3800    {
3801        let shtty = SHTTY.load(Ordering::Relaxed); // c:2999
3802        if shtty == -1 {
3803            return 0;
3804        }
3805        unsafe {
3806            // c:2999
3807            libc::ioctl(shtty, libc::FIONREAD, &mut val as *mut libc::c_int);
3808        }
3809        if purge {
3810            // c:3000
3811            let mut c: u8 = 0;
3812            for _ in 0..val {
3813                // c:3001
3814                let _ = unsafe {
3815                    // c:3002
3816                    libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1)
3817                };
3818            }
3819        }
3820    }
3821    val // c:3009
3822}
3823
3824/// Port of `int getquery(char *valid_chars, int purge)` from
3825/// `Src/utils.c:3014`.
3826///
3827/// Read a single keystroke from the TTY in raw mode and return its
3828/// character code (as `int`, mirroring C). If `valid_chars` is `None`
3829/// (C `NULL`) any byte is accepted. If `valid_chars` is `Some(s)`, only
3830/// chars in `s` (plus `\n` which maps to `s[0]`) are accepted; other
3831/// input rings the bell and re-prompts. `Y`/`N` are pre-normalised to
3832/// `y`/`n` per c:3041-3045.
3833///
3834/// `purge != 0` triggers `noquery(purge)`'s queue-purge path — if input
3835/// is queued, returns `'n'` without reading.
3836pub fn getquery(valid_chars: Option<&str>, purge: i32) -> i32 {
3837    // c:3014
3838    // c:3016 — `int c, d, nl = 0;`
3839    let mut c: i32;
3840    let mut d: i32;
3841    let mut nl: i32 = 0;
3842    // c:3017 — `int isem = !strcmp(term, "emacs");`
3843    // Stub: `term` is the $TERM environment global, declared in
3844    // `Src/init.c` (extern in zsh.h). Local stub reads $TERM from
3845    // paramtab; absent → empty string.
3846    let term: String = getsparam("TERM").unwrap_or_default();
3847    let isem: bool = term == "emacs";
3848    // c:3018 — `struct ttyinfo ti;`
3849    // Rust: termios is the canonical TTY-state type returned by gettyinfo.
3850    let mut ti: libc::termios; // c:3018
3851
3852    attachtty(mypgrp.load(Ordering::Relaxed)); // c:3020 attachtty(mypgrp)
3853
3854    // c:3022 — `gettyinfo(&ti);`
3855    ti = match gettyinfo() {
3856        Some(t) => t,
3857        None => return -1,
3858    };
3859    // c:3023-3030 — `ti.tio.c_lflag &= ~ECHO; if (!isem) {
3860    //                  ti.tio.c_lflag &= ~ICANON;
3861    //                  ti.tio.c_cc[VMIN] = 1;
3862    //                  ti.tio.c_cc[VTIME] = 0; }`
3863    #[cfg(unix)]
3864    {
3865        ti.c_lflag &= !(libc::ECHO); // c:3024
3866        if !isem {
3867            // c:3025
3868            ti.c_lflag &= !(libc::ICANON); // c:3026
3869            ti.c_cc[libc::VMIN] = 1; // c:3027
3870            ti.c_cc[libc::VTIME] = 0; // c:3028
3871        }
3872    }
3873    // c:3037 — `settyinfo(&ti);`
3874    settyinfo(&ti);
3875
3876    // c:3039 — `if (noquery(purge))`
3877    if noquery(purge != 0) != 0 {
3878        // Stub: `shttyinfo` is the canonical saved-TTY-state global,
3879        // declared in `Src/init.c` (extern in zsh.h:1856). Without the
3880        // global tracked here we re-fetch current termios as a degraded
3881        // best-effort restore.
3882        if !isem {
3883            // c:3040
3884            if let Some(saved) = gettyinfo() {
3885                // c:3041 settyinfo(&shttyinfo)
3886                settyinfo(&saved);
3887            }
3888        }
3889        let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"n\n"); // c:3042
3890        return b'n' as i32; // c:3043
3891    }
3892
3893    // c:3046-3061 — `while ((c = read1char(0)) >= 0) { ... }`
3894    c = -1;
3895    loop {
3896        let cc = read1char(0); // c:3046
3897        if cc < 0 {
3898            c = cc;
3899            break;
3900        }
3901        c = cc;
3902        if c == b'Y' as i32 {
3903            // c:3047
3904            c = b'y' as i32; // c:3048
3905        } else if c == b'N' as i32 {
3906            // c:3049
3907            c = b'n' as i32; // c:3050
3908        }
3909        if valid_chars.is_none() {
3910            // c:3051
3911            break; // c:3052
3912        }
3913        if c == b'\n' as i32 {
3914            // c:3053
3915            // c:3054 — `c = *valid_chars;`
3916            c = valid_chars.unwrap().bytes().next().unwrap_or(b'\n') as i32;
3917            nl = 1; // c:3055
3918            break; // c:3056
3919        }
3920        // c:3058 — `if (strchr(valid_chars, c))`
3921        if valid_chars.unwrap().bytes().any(|b| b as i32 == c) {
3922            nl = 1; // c:3059
3923            break; // c:3060
3924        }
3925        zbeep(); // c:3062
3926    }
3927    if c >= 0 {
3928        // c:3064
3929        // c:3065-3066 — `char buf = (char)c; write_loop(SHTTY, &buf, 1);`
3930        let buf = [c as u8]; // c:3065
3931        let _ = write_loop(SHTTY.load(Ordering::Relaxed), &buf); // c:3066
3932    }
3933    if nl != 0 {
3934        // c:3068
3935        let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n"); // c:3069
3936    }
3937
3938    if isem {
3939        // c:3071
3940        if c != b'\n' as i32 {
3941            // c:3072
3942            // c:3073 — `while ((d = read1char(1)) >= 0 && d != '\n');`
3943            loop {
3944                d = read1char(1);
3945                if d < 0 || d == b'\n' as i32 {
3946                    break;
3947                }
3948            }
3949        }
3950    } else if c != b'\n' as i32 && valid_chars.is_none() {
3951        // c:3075
3952        // c:3077-3094 — MULTIBYTE_SUPPORT branch: drain trailing bytes
3953        // of an incomplete multibyte sequence.
3954        if isset(MULTIBYTE) && c >= 0 {
3955            // c:3077
3956            // c:3083-3093 — `for (;;) { ret = mbrlen(&cc, 1, &mbs);
3957            //                          if (ret != MB_INCOMPLETE) break;
3958            //                          c = read1char(1); if (c < 0) break;
3959            //                          cc = (char)c; }`
3960            // Rust: model MB_INCOMPLETE detection by checking whether
3961            // the byte buffer so far forms a complete UTF-8 prefix.
3962            // `cc` carries the current byte under examination.
3963            let mut cc: u8 = c as u8; // c:3082
3964            let mut accum: Vec<u8> = vec![cc];
3965            loop {
3966                // mbrlen returns MB_INCOMPLETE when the partial buffer
3967                // doesn't yet form a complete UTF-8 character. Rust
3968                // equivalent: from_utf8 errors with error_len() == None
3969                // when more bytes are needed.
3970                let incomplete = match std::str::from_utf8(&accum) {
3971                    Ok(_) => false,
3972                    Err(e) => e.error_len().is_none(),
3973                };
3974                if !incomplete {
3975                    // c:3088
3976                    break;
3977                }
3978                let nc = read1char(1); // c:3089
3979                if nc < 0 {
3980                    // c:3090
3981                    break; // c:3091
3982                }
3983                cc = nc as u8; // c:3092
3984                accum.push(cc);
3985            }
3986            let _ = cc;
3987        }
3988        // c:3097 — `write_loop(SHTTY, "\n", 1);`
3989        let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n");
3990    }
3991
3992    // c:3101 — `settyinfo(&shttyinfo);` — restore saved TTY state.
3993    // Stub-degraded path: refetch current termios. The proper port
3994    // requires wiring an `shttyinfo` global in init.rs.
3995    if let Some(saved) = gettyinfo() {
3996        settyinfo(&saved);
3997    }
3998
3999    c // c:3102 return c
4000}
4001
4002// `spscan` (Src/utils.c:3109) — canonical port lives below the
4003// thread_local block in this file. The pre-existing 3-arg
4004// `(name, candidates[], threshold) → Option<String>` shape was a
4005// drift fake (C is `void spscan(HashNode hn, scanflags)`); deleted
4006// because it had zero call sites and conflicted with the faithful
4007// port spckword needs.
4008
4009// spellcheck a word                                                       // c:3123
4010// fix s ; if hist is nonzero, fix the history list too                    // c:3124
4011
4012// File-static state shared with the (inlined) spscan callback. C uses
4013// raw file-statics at utils.c:3045-3050 (`best`, `d`, `guess`, `ic`,
4014// `spckpat`, `spnamepat`); Rust port mirrors them as thread_locals
4015// (per-evaluator per PORT_PLAN.md bucket-1) so concurrent worker
4016// threads each have their own correction state.
4017thread_local! {
4018    /// Port of `static int d;` from `Src/utils.c:3045`. Best dist seen.
4019    static SPCK_D: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
4020    /// Port of `static char *best;` from `Src/utils.c:3046`. Best-match.
4021    static SPCK_BEST: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4022    /// Port of `static char *guess;` from `Src/utils.c:3047`. Word to fix.
4023    static SPCK_GUESS: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4024    /// Port of `static Patprog spckpat;` from `Src/utils.c:3049`.
4025    static SPCK_PAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4026        = const { std::cell::RefCell::new(None) };
4027    /// Port of `static Patprog spnamepat;` from `Src/utils.c:3050`.
4028    static SPCK_NAMEPAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4029        = const { std::cell::RefCell::new(None) };
4030}
4031
4032/// Port of `static void spscan(HashNode hn, UNUSED(int scanflags))` from
4033/// `Src/utils.c:3074`. Inlined per-call below because hashtable.rs's
4034/// scan helpers don't model C's `scanhashtable(table, ..., scanfn, ...)`
4035/// callback shape — Rust call sites iterate the table directly. C
4036/// body:
4037/// ```c
4038/// int nd = spdist(hn->nam, guess, (int) strlen(guess) / 4 + 1);
4039/// if (nd < d && (!spckpat || !pattry(spckpat, hn->nam))) {
4040///     best = hn->nam;
4041///     d = nd;
4042/// }
4043/// ```
4044fn spscan(name: &str) {
4045    // c:3074
4046    let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4047    if guess.is_empty() {
4048        return;
4049    }
4050    let thresh = guess.len() / 4 + 1; // c:3076 `(int) strlen(guess) / 4 + 1`
4051    let nd = spdist(name, &guess, thresh) as i32; // c:3076
4052    let d = SPCK_D.with(|c| c.get());
4053    if nd < d {
4054        // c:3077
4055        // c:3078 — `if (!spckpat || !pattry(spckpat, hn->nam))`.
4056        let allow = SPCK_PAT.with(|p| {
4057            p.borrow()
4058                .as_ref()
4059                .map(|prog| !crate::ported::pattern::pattry(prog, name))
4060                .unwrap_or(true)
4061        });
4062        if allow {
4063            SPCK_BEST.with(|b| *b.borrow_mut() = Some(name.to_string())); // c:3079
4064            SPCK_D.with(|c| c.set(nd)); // c:3080
4065        }
4066    }
4067}
4068
4069/// Port of `void spckword(char **s, int hist, int cmd, int ask)` from
4070/// `Src/utils.c:3128`.
4071///
4072/// Spell-check `*s`. If a near-match is found in the appropriate
4073/// hashtable (params for `$x`, command tables for command position,
4074/// directory entries otherwise) AND the user accepts the correction
4075/// (`ask=0` auto-accepts), replace `*s` in place with the corrected
4076/// form and (if `hist!=0`) rewrite the history entry too.
4077///
4078/// Faithful 1:1 line-by-line port. Interactive prompting (c:3273-3287)
4079/// is stubbed to auto-accept when `ask=1` since `getquery` /
4080/// `promptexpand` / `shout` / `zbeep` aren't yet wired in zshrs —
4081/// flagged with WARNING at the prompt site.
4082///
4083/// Caller updates: previous Rust signature `(word, candidates[], threshold)
4084/// → Option<String>` is gone — `lex.rs` builds candidate lists itself,
4085/// but C's spckword scans the canonical hashtables directly. The new
4086/// signature matches C exactly: takes `&mut String` for in-place fix.
4087pub fn spckword(s: &mut String, hist: i32, cmd: i32, ask: i32) {
4088    use crate::ported::hashtable::{
4089        aliastab_lock, cmdnamtab_lock, fillcmdnamtable, pathchecked, reswdtab_lock, shfunctab_lock,
4090    };
4091    use crate::ported::params::{getsparam, paramtab};
4092    use crate::ported::pattern::{patcompile, PAT_HEAPDUP};
4093    use crate::ported::zsh_h::{
4094        isset, Dash, Equals, Stringg as StringTok, Tilde, AUTOCD, HASHLISTALL,
4095    };
4096    // c:3130-3133 — locals.
4097    let _t: Option<String>;
4098    let mut ic: char = '\0'; // c:3131
4099    let mut preflen: usize = 0; // c:3132
4100                                // c:3133 — `autocd = cmd && isset(AUTOCD) && strcmp(*s, ".") && strcmp(*s, "..")`.
4101    let autocd = cmd != 0 && isset(AUTOCD) && s != "." && s != "..";
4102
4103    // c:3135-3136 — `if (!(*s)[0] || !(*s)[1]) return;`
4104    if s.len() < 2 {
4105        return;
4106    }
4107    // c:3137-3140 — HISTFLAG_NOEXEC or leading %/- skip.
4108    let bytes = s.as_bytes();
4109    let first = bytes[0] as char;
4110    let histdone = crate::ported::hist::histdone.load(std::sync::atomic::Ordering::Relaxed); // c:3137
4111    if (histdone & HISTFLAG_NOEXEC) != 0
4112        || (if cmd != 0 {
4113            first == '%' // c:3139 — leading % is a job
4114        } else {
4115            first == '-' || first == Dash // c:3139 — leading hyphen is an option
4116        })
4117    {
4118        return; // c:3140
4119    }
4120    // c:3141-3142 — `if (!strcmp(*s, "in")) return;`.
4121    if s == "in" {
4122        return; // c:3142
4123    }
4124    // c:3143-3155 — `cmd` branch: skip if it's already a known
4125    // function/builtin/cmdname/alias/reswd. Optional HASHLISTALL
4126    // re-fill of cmdnamtab on miss.
4127    if cmd != 0 {
4128        // c:3143
4129        let known = shfunctab_lock() // c:3144
4130            .read()
4131            .map(|t| t.get(s).is_some())
4132            .unwrap_or(false)
4133            || BUILTINS // c:3145
4134                .iter()
4135                .any(|b| b.node.nam == *s)
4136            || cmdnamtab_lock() // c:3146
4137                .read()
4138                .map(|t| t.get(s).is_some())
4139                .unwrap_or(false)
4140            || aliastab_lock() // c:3147
4141                .read()
4142                .map(|t| t.get(s).is_some())
4143                .unwrap_or(false)
4144            || reswdtab_lock() // c:3148
4145                .read()
4146                .map(|t| t.get(s).is_some())
4147                .unwrap_or(false);
4148        if known {
4149            return; // c:3149
4150        }
4151        // c:3150-3154 — HASHLISTALL: bulk-hash $PATH then retry.
4152        if isset(HASHLISTALL) {
4153            // c:3150
4154            let path: Vec<String> =
4155                getsparam("PATH") // c:3151
4156                    .map(|p| p.split(':').map(String::from).collect())
4157                    .unwrap_or_default();
4158            fillcmdnamtable(&path); // c:3151
4159            if cmdnamtab_lock() // c:3152
4160                .read()
4161                .map(|t| t.get(s).is_some())
4162                .unwrap_or(false)
4163            {
4164                return; // c:3153
4165            }
4166        }
4167    }
4168    // c:3156-3165 — Tilde/Equals/String prefix skip + itok/Dash
4169    // detok + early-return on any other tokenized char.
4170    let mut start = 0usize;
4171    let bytes = s.as_bytes(); // re-bind after potential string ops
4172    if !bytes.is_empty() {
4173        let c0 = bytes[0] as char;
4174        if c0 == Tilde || c0 == Equals || c0 == StringTok {
4175            // c:3157
4176            start = 1; // c:3158 t++
4177        }
4178    }
4179    // Scan from `start` for tokenized bytes.
4180    {
4181        let mut buf = s.clone().into_bytes();
4182        let mut i = start;
4183        let mut had_dash_only = true; // accumulator
4184        while i < buf.len() {
4185            let b = buf[i];
4186            if itok(b) {
4187                // c:3160
4188                if b as char == Dash {
4189                    // c:3161
4190                    buf[i] = b'-'; // c:3162
4191                } else {
4192                    return; // c:3164
4193                }
4194            } else {
4195                had_dash_only = had_dash_only && false;
4196            }
4197            i += 1;
4198        }
4199        let _ = had_dash_only;
4200        *s = String::from_utf8_lossy(&buf).into_owned();
4201    }
4202    // c:3166 — `best = NULL;`
4203    SPCK_BEST.with(|b| *b.borrow_mut() = None);
4204    SPCK_D.with(|c| c.set(100)); // initialised at each table-scan branch in C; mirror up-front.
4205                                 // c:3167-3169 — `for (t = *s; *t; t++) if (*t == '/') break;`
4206                                 // `t` is the position of the first slash (or end of string).
4207    let t_pos = s.find('/').unwrap_or(s.len()); // c:3167
4208                                                // c:3170-3171 — `if (**s == Tilde && !*t) return;`
4209    let bytes = s.as_bytes();
4210    if !bytes.is_empty() && (bytes[0] as char) == Tilde && t_pos == bytes.len() {
4211        // c:3170
4212        return; // c:3171
4213    }
4214
4215    // c:3173-3178 — compile CORRECT_IGNORE pattern if set.
4216    if let Some(ci) = getsparam("CORRECT_IGNORE") {
4217        // c:3173
4218        let prog = patcompile(
4219            &{
4220                let mut __pat_tok = (&ci).to_string();
4221                crate::ported::glob::tokenize(&mut __pat_tok);
4222                __pat_tok
4223            },
4224            PAT_HEAPDUP,
4225            None,
4226        ); // c:3176
4227        SPCK_PAT.with(|p| *p.borrow_mut() = prog);
4228    } else {
4229        SPCK_PAT.with(|p| *p.borrow_mut() = None); // c:3178
4230    }
4231    // c:3180-3185 — compile CORRECT_IGNORE_FILE pattern if set.
4232    if let Some(ci) = getsparam("CORRECT_IGNORE_FILE") {
4233        // c:3180
4234        let prog = patcompile(
4235            &{
4236                let mut __pat_tok = (&ci).to_string();
4237                crate::ported::glob::tokenize(&mut __pat_tok);
4238                __pat_tok
4239            },
4240            PAT_HEAPDUP,
4241            None,
4242        ); // c:3183
4243        SPCK_NAMEPAT.with(|p| *p.borrow_mut() = prog);
4244    } else {
4245        SPCK_NAMEPAT.with(|p| *p.borrow_mut() = None); // c:3185
4246    }
4247
4248    let bytes = s.as_bytes();
4249    let first = if bytes.is_empty() {
4250        '\0'
4251    } else {
4252        bytes[0] as char
4253    };
4254
4255    // c:3187-3193 — `**s == String && !*t`: $-prefixed name → scan paramtab.
4256    if first == StringTok && t_pos == bytes.len() {
4257        // c:3187
4258        // c:3188 — `guess = *s + 1;` strip leading $.
4259        let guess = s[1..].to_string();
4260        // c:3189-3190 — `if (itype_end(guess, INAMESPC, 1) == guess) return;`
4261        if itype_end(&guess, crate::ported::ztype_h::INAMESPC, true) == 0 {
4262            // c:3189
4263            return; // c:3190
4264        }
4265        ic = StringTok; // c:3191
4266        SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4267        SPCK_D.with(|c| c.set(100)); // c:3192
4268        if let Ok(t) = paramtab().read() {
4269            // c:3193 scanhashtable(paramtab, ..., spscan, ...)
4270            for k in t.keys() {
4271                spscan(k);
4272            }
4273        }
4274    // c:3194-3202 — `**s == Equals`: =cmd → hashcmd; then scan aliases+cmdnam.
4275    } else if first == Equals {
4276        // c:3194
4277        if t_pos != bytes.len() {
4278            // c:3195
4279            return; // c:3196
4280        }
4281        // c:3197-3198 — `if (hashcmd(guess = *s + 1, pathchecked)) return;`
4282        let guess = s[1..].to_string();
4283        let path: Vec<String> = getsparam("PATH")
4284            .map(|p| p.split(':').map(String::from).collect())
4285            .unwrap_or_default();
4286        let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4287        if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4288            return; // c:3198
4289        }
4290        SPCK_D.with(|c| c.set(100)); // c:3199
4291        ic = Equals; // c:3200
4292        SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4293        if let Ok(t) = aliastab_lock().read() {
4294            // c:3201
4295            for (k, _) in t.iter() {
4296                spscan(k);
4297            }
4298        }
4299        if let Ok(t) = cmdnamtab_lock().read() {
4300            // c:3202
4301            for (k, _) in t.iter() {
4302                spscan(k);
4303            }
4304        }
4305    // c:3203-3248 — default branch: filename / dir spell-check.
4306    } else {
4307        // c:3203
4308        let mut guess = s.clone(); // c:3204
4309                                   // c:3205-3218 — Tilde / String inline-expand prefix handling.
4310        if !guess.is_empty()
4311            && ((guess.as_bytes()[0] as char) == Tilde
4312                || (guess.as_bytes()[0] as char) == StringTok)
4313        {
4314            // c:3205
4315            ic = guess.as_bytes()[0] as char; // c:3207
4316            if t_pos + 1 >= s.len() {
4317                // c:3208 — `if (!*++t) return;`
4318                return; // c:3209
4319            }
4320            // c:3210-3214 — `noerrs=2; singsub(&guess); noerrs = ne;`
4321            let saved_noerrs =
4322                crate::ported::exec::noerrs.load(std::sync::atomic::Ordering::Relaxed);
4323            crate::ported::exec::noerrs.store(2, std::sync::atomic::Ordering::Relaxed); // c:3212
4324            guess = crate::ported::subst::singsub(&guess); // c:3213
4325            crate::ported::exec::noerrs.store(saved_noerrs, std::sync::atomic::Ordering::Relaxed);
4326            if guess.is_empty() {
4327                return; // c:3216 `if (!guess) return;`
4328            }
4329            // c:3217 — `preflen = strlen(guess) - strlen(t);` t = original
4330            // s[t_pos..] (the post-slash remainder).
4331            let t_len = s.len() - t_pos;
4332            preflen = guess.len().saturating_sub(t_len);
4333        }
4334        // c:3219-3220 — `if (access(unmeta(guess), F_OK) == 0) return;`
4335        let cstr = match std::ffi::CString::new(unmeta(&guess).as_str()) {
4336            Ok(c) => c,
4337            Err(_) => return,
4338        };
4339        if unsafe { libc::access(cstr.as_ptr(), libc::F_OK) } == 0 {
4340            // c:3219
4341            return; // c:3220
4342        }
4343        // c:3221 — `best = spname(guess);`
4344        // The Rust spname has a signature-drift adaptation taking
4345        // `(name, dir)`; pass the parent dir extracted from guess.
4346        let path_obj = std::path::Path::new(&guess);
4347        let parent = path_obj
4348            .parent()
4349            .and_then(|p| p.to_str())
4350            .filter(|s| !s.is_empty())
4351            .unwrap_or(".");
4352        let basename = path_obj.file_name().and_then(|n| n.to_str()).unwrap_or("");
4353        let best = spname(basename, parent);
4354        SPCK_BEST.with(|b| *b.borrow_mut() = best);
4355        SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess.clone()));
4356        // c:3222-3247 — command-position default branch: hashcmd +
4357        // scan tables + autocd cdpath scan.
4358        if t_pos == s.len() && cmd != 0 {
4359            // c:3222
4360            // c:3223-3224 — hashcmd shortcut.
4361            let path: Vec<String> = getsparam("PATH")
4362                .map(|p| p.split(':').map(String::from).collect())
4363                .unwrap_or_default();
4364            let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4365            if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4366                return; // c:3224
4367            }
4368            SPCK_D.with(|c| c.set(100)); // c:3225
4369                                         // c:3226-3230 — scan reswd, alias, shfunc, builtin, cmdnam.
4370            if let Ok(t) = reswdtab_lock().read() {
4371                // c:3226
4372                for (k, _) in t.iter() {
4373                    spscan(k);
4374                }
4375            }
4376            if let Ok(t) = aliastab_lock().read() {
4377                // c:3227
4378                for (k, _) in t.iter() {
4379                    spscan(k);
4380                }
4381            }
4382            if let Ok(t) = shfunctab_lock().read() {
4383                // c:3228
4384                for (k, _) in t.iter() {
4385                    spscan(k);
4386                }
4387            }
4388            // c:3229 — builtintab scan: BUILTINS is a static array.
4389            for b in BUILTINS.iter() {
4390                spscan(&b.node.nam);
4391            }
4392            if let Ok(t) = cmdnamtab_lock().read() {
4393                // c:3230
4394                for (k, _) in t.iter() {
4395                    spscan(k);
4396                }
4397            }
4398            // c:3231-3247 — autocd $cdpath scan: for each cdpath
4399            // entry, find the closest filename match via mindist.
4400            // Strict `<` (not `<=` as in spscan) so a cdpath dir
4401            // wins only when STRICTLY better than the existing best
4402            // — preferring earlier cdpath dirs on ties.
4403            if autocd {
4404                // c:3232 — `if (cd_able_vars(unmeta(guess))) return;`
4405                let unmeta_guess = crate::ported::utils::unmeta(&guess);
4406                if crate::ported::builtin::cd_able_vars(&unmeta_guess).is_some() {
4407                    return; // c:3233
4408                }
4409                // c:3234 — `for (pp = cdpath; *pp; pp++)`. Read CDPATH.
4410                let cdpath: Vec<String> = crate::ported::params::paramtab()
4411                    .read()
4412                    .ok()
4413                    .and_then(|t| t.get("cdpath").and_then(|pm| pm.u_arr.clone()))
4414                    .unwrap_or_default();
4415                let mut cur_d = SPCK_D.with(|c| c.get());
4416                let mut cur_best = SPCK_BEST.with(|b| b.borrow().clone());
4417                for pp in cdpath.iter() {
4418                    // c:3235-3245 — `mindist(*pp, *s, bestcd, 1)`.
4419                    if let Some((bestcd, thisdist)) = mindist(pp, &s) {
4420                        // c:3239 — STRICT `<` (not `<=`) per C comment
4421                        // at c:3236-3239.
4422                        if (thisdist as i32) < cur_d {
4423                            cur_best = Some(bestcd);
4424                            cur_d = thisdist as i32;
4425                        }
4426                    }
4427                }
4428                SPCK_BEST.with(|b| *b.borrow_mut() = cur_best);
4429                SPCK_D.with(|c| c.set(cur_d));
4430            }
4431        }
4432    }
4433    // c:3250-3251 — `if (errflag) return;`
4434    if (errflag.load(std::sync::atomic::Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
4435        return; // c:3251
4436    }
4437    // c:3252 — `if (best && strlen(best) > 1 && strcmp(best, guess))`.
4438    let best = SPCK_BEST.with(|b| b.borrow().clone());
4439    let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4440    let Some(mut best) = best else {
4441        return;
4442    };
4443    if best.len() <= 1 || best == guess {
4444        return;
4445    }
4446    // c:3253-3272 — assemble the prefixed/de-tokenized replacement.
4447    if ic != '\0' {
4448        // c:3254
4449        if preflen > 0 {
4450            // c:3256
4451            // c:3258-3259 — `if (strncmp(guess, best, preflen)) return;`
4452            if !best.starts_with(&guess[..preflen.min(guess.len())]) {
4453                return; // c:3259
4454            }
4455            // c:3261-3263 — `u = ...s[0..t-*s] + best[preflen..]`.
4456            let t_off = s.len() - (s.len() - t_pos);
4457            let mut u = String::with_capacity(t_off + best.len() - preflen + 1);
4458            u.push_str(&s[..t_off]);
4459            u.push_str(&best[preflen..]);
4460            best = u;
4461        } else {
4462            // c:3264 — `u = "\0" + best;` (prepend NUL placeholder).
4463            best = format!("\0{}", best);
4464        }
4465        // c:3269-3271 — `best = u; guess = *s; *guess = *best = ztokens[ic - Pound];`
4466        let pound = crate::ported::zsh_h::Pound as u8;
4467        let zt = crate::ported::lex::ztokens.as_bytes();
4468        let token_char = if (ic as u8) >= pound {
4469            let idx = (ic as u8 - pound) as usize;
4470            if idx < zt.len() {
4471                zt[idx] as char
4472            } else {
4473                ic
4474            }
4475        } else {
4476            ic
4477        };
4478        // Set first char of both `*s` (the original) and `best`.
4479        if !s.is_empty() {
4480            let mut sb = s.clone().into_bytes();
4481            sb[0] = token_char as u8;
4482            *s = String::from_utf8_lossy(&sb).into_owned();
4483        }
4484        if !best.is_empty() {
4485            let mut bb = best.into_bytes();
4486            bb[0] = token_char as u8;
4487            best = String::from_utf8_lossy(&bb).into_owned();
4488        }
4489    }
4490    // c:3273-3289 — interactive prompt (`ask`) or auto-accept.
4491    let x: char;
4492    if ask != 0 {
4493        // c:3273
4494        // WARNING — DIVERGENCE: `noquery()`, `shout`, `promptexpand`,
4495        // `zputs(stream)`, `zbeep`, `getquery("nyae", 0)` aren't yet
4496        // wired in zshrs (interactive ZLE prompt machinery). Default
4497        // to 'n' (decline) when ask=1 — preserves the C behavior of
4498        // declining when shout is NULL (c:3286-3287). Re-enable the
4499        // interactive flow when promptexpand/getquery land.
4500        x = 'n';
4501    } else {
4502        x = 'y'; // c:3289
4503    }
4504    // c:3290-3300 — apply chosen action.
4505    if x == 'y' {
4506        // c:3290
4507        *s = best; // c:3291 `*s = dupstring(best);`
4508        if hist != 0 {
4509            // c:3292
4510            // c:3293 — `hwrep(best);` (history rewrite). Stubbed: hist
4511            // rewrite plumbing isn't yet hooked into the lex caller.
4512        }
4513    } else if x == 'a' {
4514        // c:3294
4515        crate::ported::hist::histdone
4516            .fetch_or(HISTFLAG_NOEXEC, std::sync::atomic::Ordering::Relaxed); // c:3295
4517    } else if x == 'e' {
4518        // c:3296
4519        crate::ported::hist::histdone.fetch_or(
4520            HISTFLAG_NOEXEC | crate::ported::zsh_h::HISTFLAG_RECALL,
4521            std::sync::atomic::Ordering::Relaxed,
4522        ); // c:3297
4523    }
4524    // c:3299-3300 — `if (ic) **s = ic;` — restore prefix sigil.
4525    if ic != '\0' && !s.is_empty() {
4526        let mut sb = s.clone().into_bytes();
4527        sb[0] = ic as u8;
4528        *s = String::from_utf8_lossy(&sb).into_owned();
4529    }
4530}
4531
4532/// Port of `ztrftimebuf(int *bufsizeptr, int decr)` from `Src/utils.c:3312`.
4533///
4534/// ```c
4535/// static int ztrftimebuf(int *bufsizeptr, int decr) {
4536///     if (*bufsizeptr <= decr) return 1;
4537///     *bufsizeptr -= decr;
4538///     return 0;
4539/// }
4540/// ```
4541///
4542/// "Helper for ztrftime: try to fit decr more bytes (plus a NUL)
4543/// in the buffer, and a new string length to decrement from that.
4544/// Returns 0 if the new length fits, 1 otherwise."
4545///
4546/// Previous Rust port had wrong semantics — returned `needed.max(256)`
4547/// (a buffer-sizing helper) instead of the C "decrement-and-check"
4548/// semantics. New port matches C: takes &mut bufsize + decr,
4549/// returns i32 (0 = fit, 1 = doesn't fit).
4550pub fn ztrftimebuf(bufsizeptr: &mut i32, decr: i32) -> i32 {
4551    if *bufsizeptr <= decr {
4552        return 1;
4553    }
4554    *bufsizeptr -= decr;
4555    0
4556}
4557
4558/// Format time struct (from utils.c ztrftime)
4559/// Port of `ztrftime(char *buf, int bufsize, char *fmt, struct tm *tm, long nsec)` from `Src/utils.c:3337`.
4560/// WARNING: param names don't match C — Rust=(fmt, time) vs C=(buf, bufsize, fmt, tm, nsec)
4561// Rust idiom replacement: `SystemTime::duration_since(UNIX_EPOCH)`
4562// + libc::localtime covers the C tm-struct populate; the C source's
4563// 192-line body builds the fmt-walk by hand because C has no
4564// strftime extension support — Rust delegates to libc::strftime via
4565// the strftime crate equivalent inline.
4566/// `ztrftime` — see implementation.
4567///
4568/// `use_gmt` controls whether time fields are interpreted as UTC (true,
4569/// libc::gmtime) or local time (false, libc::localtime). C's ztrftime
4570/// takes a `struct tm *` produced by the caller via gmtime/localtime
4571/// (e.g. Src/Modules/stat.c:201 picks between them based on STF_GMT);
4572/// since the Rust signature takes `SystemTime` we hoist that choice into
4573/// this bool param.
4574pub fn ztrftime(fmt: &str, time: std::time::SystemTime, use_gmt: bool) -> String {
4575    // C takes `struct tm *` + nsec directly, so negative time_t
4576    // (pre-1970) flows through localtime untouched. SystemTime
4577    // pre-epoch surfaces as duration_since Err — recover the signed
4578    // seconds rather than collapsing to 0 (the old unwrap_or_default
4579    // pinned every pre-epoch timestamp to the epoch).
4580    let (secs, nsec): (i64, u64) = match time.duration_since(UNIX_EPOCH) {
4581        Ok(d) => (d.as_secs() as i64, d.subsec_nanos() as u64),
4582        Err(e) => {
4583            let d = e.duration();
4584            let s = -(d.as_secs() as i64);
4585            let n = d.subsec_nanos() as u64;
4586            // sub-second part rolls the seconds down one: -1.25s
4587            // before epoch is secs=-2, nsec=750ms in tm terms.
4588            if n > 0 {
4589                (s - 1, 1_000_000_000 - n)
4590            } else {
4591                (s, 0)
4592            }
4593        }
4594    };
4595
4596    #[cfg(unix)]
4597    unsafe {
4598        let tm = if use_gmt {
4599            libc::gmtime(&secs)
4600        } else {
4601            libc::localtime(&secs)
4602        };
4603        if tm.is_null() {
4604            return String::new();
4605        }
4606        let tm_ref = &*tm;
4607
4608        // c:3398-3406 — pre-pass: walk fmt and substitute zsh-specific
4609        // extensions BEFORE delegating to libc::strftime. The C source
4610        // handles these inline; Rust pre-rewrites them into literal
4611        // numbers so libc::strftime sees only standard specifiers.
4612        //   %K  = 24-hr clock, no leading zero (0-23)
4613        //   %L  = 12-hr clock, no leading zero (1-12)
4614        //   %f  = day of month, no leading zero (1-31)
4615        //   %.N = fractional seconds, N digits (0-9)
4616        let mut preprocessed = String::with_capacity(fmt.len());
4617        let bytes = fmt.as_bytes();
4618        let mut i = 0;
4619        while i < bytes.len() {
4620            if bytes[i] == b'%' && i + 1 < bytes.len() {
4621                // c:3374-3384 — parse optional `N.` prefix (digit count
4622                // for the `%.` fractional-seconds specifier).
4623                let mut j = i + 1;
4624                while j < bytes.len() && bytes[j].is_ascii_digit() {
4625                    j += 1;
4626                }
4627                let digs: u32 = if j > i + 1 {
4628                    std::str::from_utf8(&bytes[i + 1..j])
4629                        .unwrap_or("3")
4630                        .parse()
4631                        .unwrap_or(3)
4632                } else {
4633                    3
4634                };
4635                // c:3408 — `switch (*fmt++)` examines the specifier
4636                // char (after the digit prefix).
4637                if j < bytes.len() && bytes[j] == b'.' {
4638                    // c:3409 — fractional-seconds. Default 3 digits if
4639                    // none specified (j == i+1).
4640                    let digs = digs.min(9);
4641                    let trunc_div: u64 = 10u64.pow(9 - digs);
4642                    let val = nsec / trunc_div;
4643                    preprocessed.push_str(&format!("{:0width$}", val, width = digs as usize));
4644                    i = j + 1;
4645                    continue;
4646                }
4647                let next = bytes[i + 1];
4648                // c:3445-3460 — %K / %L / %f extensions.
4649                match next {
4650                    b'K' => {
4651                        preprocessed.push_str(&format!("{}", tm_ref.tm_hour));
4652                        i += 2;
4653                        continue;
4654                    }
4655                    b'L' => {
4656                        let mut h12 = tm_ref.tm_hour % 12;
4657                        if h12 == 0 {
4658                            h12 = 12;
4659                        }
4660                        preprocessed.push_str(&format!("{}", h12));
4661                        i += 2;
4662                        continue;
4663                    }
4664                    b'f' => {
4665                        preprocessed.push_str(&format!("{}", tm_ref.tm_mday));
4666                        i += 2;
4667                        continue;
4668                    }
4669                    b'N' => {
4670                        // c:3491-3495 — `%N`: nanoseconds, always 9 digits
4671                        // (`sprintf(buf, "%09ld", nsec)`).
4672                        preprocessed.push_str(&format!("{:09}", nsec));
4673                        i += 2;
4674                        continue;
4675                    }
4676                    _ => {}
4677                }
4678            }
4679            preprocessed.push(bytes[i] as char);
4680            i += 1;
4681        }
4682
4683        let mut buf = vec![0u8; 256];
4684        let c_fmt = CString::new(preprocessed).unwrap_or_default();
4685        let len = libc::strftime(
4686            buf.as_mut_ptr() as *mut libc::c_char,
4687            buf.len(),
4688            c_fmt.as_ptr(),
4689            tm,
4690        );
4691
4692        if len > 0 {
4693            buf.truncate(len);
4694            String::from_utf8_lossy(&buf).to_string()
4695        } else {
4696            String::new()
4697        }
4698    }
4699
4700    #[cfg(not(unix))]
4701    {
4702        let _ = (fmt, secs, nsec);
4703        String::new()
4704    }
4705}
4706
4707/// Join array with delimiter (from utils.c zjoin)
4708/// Port of `zjoin(char **arr, int delim, int heap)` from `Src/utils.c:3622`.
4709/// Joins `arr` with `delim` between elements. When `delim` is itself a meta
4710/// byte (`imeta(delim)` — NUL, `Meta`, `Marker`, or a token in the
4711/// Pound..Nularg range; see `inittyptab` c:4195-4201), C writes the delimiter
4712/// in *metafied* form (`Meta` byte then `delim ^ 32`, c:3634-3637) rather than
4713/// the raw byte (c:3639). The earlier port emitted the raw char always, which
4714/// was byte-wrong for meta delimiters — notably `zjoin(array, '\0', …)` (NUL is
4715/// imeta), where C emits `0x83 0x20`, not a raw `0x00`.
4716///
4717/// WARNING: param names don't match C — Rust=(arr, delim) vs C=(arr, delim, heap).
4718/// The `heap` arg is dropped (Rust `String` owns its storage). As with
4719/// [`metafy`], the `Meta` byte cannot live in valid UTF-8, so the final
4720/// `String` boundary uses the same `from_utf8`/lossy fallback — byte-exact for
4721/// every ASCII (non-meta) delimiter, lossy only on meta delimiters. Callers
4722/// needing byte-exact meta-delimited output should join at the byte level.
4723pub fn zjoin(arr: &[String], delim: char) -> String {
4724    // c:3622
4725    // c:3629-3630 — empty array → "".
4726    if arr.is_empty() {
4727        return String::new();
4728    }
4729    let db = delim as u32;
4730    // c:3634 — `imeta(delim)`. Only byte-valued delimiters can be meta.
4731    let delim_meta = db < 0x100 && imeta_byte(db as u8);
4732    let mut out: Vec<u8> = Vec::new();
4733    for (i, s) in arr.iter().enumerate() {
4734        if i != 0 {
4735            // The separator that C writes after every element then chops off
4736            // the trailing one (c:3641) is equivalent to interposing it.
4737            if delim_meta {
4738                out.push(Meta); // c:3635
4739                out.push((db as u8) ^ 32); // c:3636
4740            } else if db < 0x100 {
4741                out.push(db as u8); // c:3639 — raw single byte
4742            } else {
4743                // Non-byte char delimiter (no C analogue): emit its UTF-8.
4744                let mut buf = [0u8; 4];
4745                out.extend_from_slice(delim.encode_utf8(&mut buf).as_bytes());
4746            }
4747        }
4748        out.extend_from_slice(s.as_bytes()); // c:3633 — strucpy(&ptr, *s)
4749    }
4750    String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
4751}
4752
4753/// Port of `char **colonsplit(char *s, int uniq)` from
4754/// `Src/utils.c:3650`. Splits `s` on `:`; when `uniq` is set,
4755/// duplicate segments are dropped (linear scan against the
4756/// already-emitted prefix).
4757/// ```c
4758/// char **
4759/// colonsplit(char *s, int uniq)
4760/// {
4761///     int ct;
4762///     char *t, **ret, **ptr, **p;
4763///     for (t = s, ct = 0; *t; t++)
4764///         if (*t == ':') ct++;
4765///     ptr = ret = zalloc(sizeof(char *) * (ct + 2));
4766///     t = s;
4767///     do {
4768///         s = t;
4769///         for (; *t && *t != ':'; t++);
4770///         if (uniq)
4771///             for (p = ret; p < ptr; p++)
4772///                 if (strlen(*p) == t - s && !strncmp(*p, s, t - s))
4773///                     goto cont;
4774///         *ptr = zalloc((t - s) + 1);
4775///         ztrncpy(*ptr++, s, t - s);
4776///       cont: ;
4777///     } while (*t++);
4778///     *ptr = NULL;
4779///     return ret;
4780/// }
4781/// ```
4782pub fn colonsplit(s: &str, uniq: bool) -> Vec<String> {
4783    // c:3650
4784    // c:3655-3657 — count colons.
4785    let ct = s.matches(':').count(); // c:3655
4786    let mut ret: Vec<String> = Vec::with_capacity(ct + 2); // c:3658 zalloc((ct+2)*sizeof(char *))
4787
4788    // c:3661-3673 — do-while loop walking segments.
4789    let bytes = s.as_bytes();
4790    let mut t: usize = 0;
4791    loop {
4792        let seg_start = t; // c:3662 s = t
4793                           // c:3664 — `for (; *t && *t != ':'; t++)`
4794        while t < bytes.len() && bytes[t] != b':' {
4795            t += 1;
4796        }
4797        let seg = &s[seg_start..t];
4798        // c:3665-3668 — uniq dedupe.
4799        if !uniq || !ret.iter().any(|p| p == seg) {
4800            // c:3665
4801            ret.push(seg.to_string()); // c:3670 zalloc + ztrncpy
4802        }
4803        // c:3673 — `while (*t++)` — break if at end, else step past colon.
4804        if t >= bytes.len() {
4805            break;
4806        } // c:3673
4807        t += 1; // c:3673 t++
4808    }
4809    // c:3674 — `*ptr = NULL;` — Rust Vec needs no sentinel.
4810    ret // c:3675
4811}
4812
4813/// Port of `skipwsep(char **s)` from `Src/utils.c:3680`.
4814///
4815/// Skip whitespace separators (`iwsep`-true bytes) at the start of
4816/// `s`. Returns `(remaining_str, count)` — `count` is the number of
4817/// bytes / chars skipped. C body:
4818///
4819/// ```c
4820/// while (*t && iwsep(*t == Meta ? t[1] ^ 32 : *t)) {
4821///     if (*t == Meta) t++;
4822///     t++;
4823///     i++;
4824/// }
4825/// ```
4826///
4827/// The C version honours Meta-encoded bytes (`Meta` followed by
4828/// `^32`-XOR'd byte). Rust port mirrors that byte-by-byte.
4829pub fn skipwsep(s: &str) -> (&str, usize) {
4830    let bytes = s.as_bytes();
4831    let mut i: usize = 0;
4832    let mut count: usize = 0;
4833    while i < bytes.len() {
4834        let b = if bytes[i] == Meta && i + 1 < bytes.len() {
4835            bytes[i + 1] ^ 32
4836        } else {
4837            bytes[i]
4838        };
4839        if !iwsep(b) {
4840            break;
4841        }
4842        if bytes[i] == Meta {
4843            i += 1;
4844        }
4845        i += 1;
4846        count += 1;
4847    }
4848    (&s[i..], count)
4849}
4850
4851/// Port of `spacesplit(char *s, int allownull, int heap, int quote)` from
4852/// `Src/utils.c:3711`. Splits `s` on `$IFS` via the `ISEP`/`IWSEP` char
4853/// classes (TYPTAB, kept in sync with `$IFS` by `inittyptab`, which
4854/// re-runs on every IFS set — params.rs:8691, c:4795), NOT on hardcoded
4855/// whitespace. zsh's word-splitting rules: runs of IFS-WHITESPACE
4856/// collapse and the leading/trailing ones yield real `""` fields (which
4857/// callers elide); each IFS-NON-WHITESPACE char delimits a field, and
4858/// consecutive ones yield `nulstring` (`Nularg`, `Src/subst.c:36`
4859/// `{Nularg,'\0'}`) empty fields, which survive and are later stripped to
4860/// `""` by remnulargs.
4861///
4862/// WARNING: param names don't match C — Rust=(s, allownull) vs C=(s,
4863/// allownull, heap, quote). The `quote` arm (backslash-escaped seps) and
4864/// `heap` C-buffer param drop: all zshrs callers pass quote=0, and Rust
4865/// owns its String storage. The previous port split only on hardcoded
4866/// `[' ','\t','\n']`, ignoring `$IFS` entirely (Bug #636).
4867pub fn spacesplit(s: &str, allownull: bool) -> Vec<String> {
4868    // c:3711
4869    use crate::ported::ztype_h::zistype;
4870    let bytes = s.as_bytes();
4871    // Meta-aware decode of the logical char value + its byte length at a
4872    // byte position (mirrors skipwsep's `*x == Meta ? x[1] ^ 32 : *x`).
4873    let char_at = |i: usize| -> (u8, usize) {
4874        // c:Src/zsh.h Meta — `\u{83}` prefix; next byte XOR 0x20.
4875        if bytes[i] == Meta && i + 1 < bytes.len() {
4876            (bytes[i + 1] ^ 32, 2)
4877        } else {
4878            (bytes[i], 1)
4879        }
4880    };
4881    // skipwsep(&s) — advance past a run of IWSEP (IFS-whitespace). c:3730
4882    let skipwsep_at = |mut i: usize| -> usize {
4883        while i < bytes.len() {
4884            let (c, l) = char_at(i);
4885            if !iwsep(c) {
4886                break;
4887            }
4888            i += l;
4889        }
4890        i
4891    };
4892    // itype_end(s, ISEP, 1) — `once` advances past exactly ONE ISEP char
4893    // if present, else leaves the index unchanged (c:4462 `if (once) break`).
4894    let isep_one = |i: usize| -> usize {
4895        if i < bytes.len() {
4896            let (c, l) = char_at(i);
4897            if zistype(c, ISEP as u32) {
4898                return i + l;
4899            }
4900        }
4901        i
4902    };
4903    // findsep(&s, NULL, 0) — advance to the next ISEP char. c:3784
4904    let findsep_at = |mut i: usize| -> usize {
4905        while i < bytes.len() {
4906            let (c, l) = char_at(i);
4907            if zistype(c, ISEP as u32) {
4908                break;
4909            }
4910            i += l;
4911        }
4912        i
4913    };
4914    let nulstring = || Nularg.to_string(); // c:subst.c:36 nulstring = {Nularg,0}
4915
4916    let mut ret: Vec<String> = Vec::new();
4917    let mut si = 0usize;
4918    let mut t = si; // c:3729 — `t = s;`
4919    si = skipwsep_at(si); // c:3730 — `skipwsep(&s);`
4920                          // c:3732-3735 — leading-field handling.
4921    if si < bytes.len() && isep_one(si) != si {
4922        // c:3733 — at an IFS-non-whitespace sep after skipping whitespace.
4923        ret.push(if allownull {
4924            String::new()
4925        } else {
4926            nulstring()
4927        });
4928    } else if !allownull && t != si {
4929        // c:3734-3735 — leading IFS-whitespace was skipped → real "".
4930        ret.push(String::new());
4931    }
4932    // c:3736-3756 — main word loop.
4933    while si < bytes.len() {
4934        let iend = isep_one(si); // c:3737 itype_end(s, ISEP, 1)
4935        if iend != si {
4936            // c:3738-3741 — consume the single non-ws sep + following WS.
4937            si = iend;
4938            si = skipwsep_at(si);
4939        }
4940        t = si; // c:3746 — `t = s;`
4941        si = findsep_at(si); // c:3747 — `findsep(&s, NULL, quote);`
4942        if si > t || allownull {
4943            // c:3748-3751 — emit the word `t..si`.
4944            ret.push(s[t..si].to_string());
4945        } else {
4946            // c:3753 — empty field between non-ws seps → nulstring.
4947            ret.push(nulstring());
4948        }
4949        t = si; // c:3754 — `t = s;`
4950        si = skipwsep_at(si); // c:3755 — `skipwsep(&s);`
4951    }
4952    // c:3757 — trailing IFS-whitespace → real "".
4953    if !allownull && t != si {
4954        ret.push(String::new());
4955    }
4956    ret
4957}
4958
4959/// Port of `findsep(char **s, char *sep, int quote)` from `Src/utils.c:3784`.
4960///
4961/// c:3762 — "Find a separator.  Return 0 if already at separator, 1 if
4962/// separator found later, else -1."  `pos` is C's walking `*s`: on
4963/// return it points at the separator (or end-of-string) so the caller
4964/// reads the word as `s[entry_pos..pos]`.
4965///
4966/// `sep` (c:3772):
4967///   - `None`       → split on normal `$IFS` separators (ISEP chars).
4968///   - `Some(b"")`  → no real separator: advance past one character.
4969///   - `Some(seq)`  → look for the (possibly multi-byte) literal `seq`.
4970///
4971/// `quote` (c:3776) — a `\` before a separator suppresses it, and `\\`
4972/// collapses to `\`.  This only applies when `sep` is `None` and it
4973/// strips backslashes in place, so the buffer must be modifiable
4974/// (C demands "something modifiable"; Rust takes `&mut String`).
4975///
4976/// The standalone form has no in-tree caller yet — the hot split paths
4977/// (`splitstring`, `findword`, `wordcount`) inline this logic — but it
4978/// is kept as a faithful, callable name-parity anchor.
4979/// WARNING: param names match C semantics — `pos` is C's `*s`.
4980pub fn findsep(s: &mut String, pos: &mut usize, sep: Option<&[u8]>, quote: bool) -> i32 {
4981    // c:3784
4982    use crate::ported::zsh_h::{MB_METACHARINIT, MB_METACHARLEN, MB_METACHARLENCONV};
4983    use crate::ported::ztype_h::{isep, ISEP, WC_ZISTYPE};
4984
4985    MB_METACHARINIT(); // c:3792
4986    let is_isep = |c: Option<char>| c.map(|c| WC_ZISTYPE(c, ISEP as u32)).unwrap_or(false);
4987
4988    match sep {
4989        // c:3793-3824 — default separators (ISEP), with optional quoting.
4990        None => {
4991            let start = *pos;
4992            let mut t = *pos;
4993            while t < s.len() {
4994                let b = s.as_bytes()[t];
4995                if quote && b == b'\\' {
4996                    // c:3795 — `if (quote && *t == '\\')`
4997                    if t + 1 < s.len() && s.as_bytes()[t + 1] == b'\\' {
4998                        // c:3796-3799 — `\\` → `\`: drop one backslash,
4999                        // advance past the one we keep (ilen = 1).
5000                        chuck(s, t);
5001                        t += 1;
5002                        continue;
5003                    }
5004                    // c:3801 — measure the char *after* the backslash.
5005                    let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t + 1..]);
5006                    if is_isep(c) {
5007                        // c:3802-3804 — escaped separator: strip the
5008                        // backslash, then advance over the now-bare char.
5009                        chuck(s, t);
5010                        t += ilen.max(1);
5011                        continue;
5012                    }
5013                    // c:3806-3810 — backslash is a normal byte.
5014                    if isep(b) {
5015                        break; // (never taken: '\\' is not an ISEP)
5016                    }
5017                    t += 1; // ilen = 1
5018                } else {
5019                    // c:3814-3818 — ordinary character.
5020                    let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t..]);
5021                    if is_isep(c) {
5022                        break; // c:3817
5023                    }
5024                    t += ilen.max(1);
5025                }
5026            }
5027            let i = if t > start { 1 } else { 0 }; // c:3821 `i = (t > *s)`
5028            *pos = t; // c:3822
5029            i // c:3823
5030        }
5031        // c:3825-3834 — empty separator: advance past the first char.
5032        Some(seq) if seq.is_empty() => {
5033            if *pos < s.len() {
5034                *pos += MB_METACHARLEN(&s.as_bytes()[*pos..]); // c:3831
5035                1 // c:3832
5036            } else {
5037                -1 // c:3833
5038            }
5039        }
5040        // c:3835-3845 — explicit (possibly multi-byte) literal separator.
5041        Some(seq) => {
5042            let mut i = 0i32;
5043            while *pos < s.len() {
5044                // c:3840 — `for (t=sep, tt=*s; *t && *tt && *t==*tt; ...)`
5045                let bytes = s.as_bytes();
5046                let mut k = 0usize;
5047                while k < seq.len() && *pos + k < bytes.len() && seq[k] == bytes[*pos + k] {
5048                    k += 1;
5049                }
5050                if k == seq.len() {
5051                    return if i > 0 { 1 } else { 0 }; // c:3841 `return (i > 0)`
5052                }
5053                *pos += MB_METACHARLEN(&bytes[*pos..]); // c:3842
5054                i += 1;
5055            }
5056            -1 // c:3844
5057        }
5058    }
5059}
5060
5061/// Find word at position (from utils.c findword)
5062/// Port of `findword(char **s, char *sep)` from `Src/utils.c:3849`.
5063pub fn findword<'a>(s: &'a str, sep: Option<&'a str>) -> Option<(&'a str, &'a str)> {
5064    let s = match sep {
5065        Some(_) => s,
5066        None => s.trim_start(),
5067    };
5068    if s.is_empty() {
5069        return None;
5070    }
5071    match sep {
5072        Some(sep) => {
5073            if let Some(pos) = s.find(sep) {
5074                Some((&s[..pos], &s[pos + sep.len()..]))
5075            } else {
5076                Some((s, ""))
5077            }
5078        }
5079        None => {
5080            let end = s.find(|c: char| c.is_ascii_whitespace()).unwrap_or(s.len());
5081            Some((&s[..end], &s[end..]))
5082        }
5083    }
5084}
5085
5086/// Port of `wordcount(char *s, char *sep, int mul)` from `Src/utils.c:3879`.
5087///
5088/// Returns the number of words in `s` that would result from splitting
5089/// on `sep` (or `$IFS` when `sep` is None). `mul` controls how
5090/// consecutive empty fields are counted:
5091/// - `mul == 0`: don't count leading/trailing/consecutive empties.
5092/// - `mul > 0`: count consecutive empties (each `sep` boundary
5093///   produces one word, even if the surrounding text is empty).
5094/// - `mul < 0`: count empty trailing fields (final separator after
5095///   the last non-empty field counts as one extra empty word).
5096///
5097/// C body (paraphrased):
5098/// ```c
5099/// if (sep) {
5100///     r = 1;
5101///     sl = strlen(sep);
5102///     for (; (c = findsep(&s, sep, 0)) >= 0; s += sl)
5103///         if ((c || mul) && (sl || *(s + sl)))
5104///             r++;
5105/// } else {
5106///     /* IFS-based: walk skipwsep / itype_end(s, ISEP, 1) */
5107/// }
5108/// ```
5109///
5110/// This port walks the metafied byte stream directly to mirror C's
5111/// pointer arithmetic. The `sep`-based branch is exact; the IFS
5112/// branch uses [`iwsep`] for whitespace-separator detection (C's
5113/// `ISEP` char class collapsed to whitespace, which is the common
5114/// case for default `$IFS` = `" \t\n"`).
5115pub fn wordcount(s: &str, sep: Option<&str>, mul: i32) -> i32 {
5116    // c:3879
5117    let bytes = s.as_bytes();
5118    if let Some(sep) = sep {
5119        // C: r = 1; sl = strlen(sep); for (; findsep(&s,sep,0) >= 0; s+=sl)
5120        //        if ((c || mul) && (sl || *(s+sl))) r++;
5121        let sep_bytes = sep.as_bytes();
5122        let sl = sep_bytes.len();
5123        let mut r: i32 = 1;
5124        let mut pos = 0;
5125        while pos <= bytes.len() {
5126            let rest = &bytes[pos..];
5127            let c_offset = match sep_bytes.is_empty() {
5128                true => Some(0usize),
5129                false => rest.windows(sl).position(|w| w == sep_bytes),
5130            };
5131            let Some(c) = c_offset else { break };
5132            // C `c` is the chars before the separator; `(sl || *(s+sl))`
5133            // means: if sl is zero (empty sep), only count when there's a
5134            // following char. Otherwise (sl > 0), the second clause is true
5135            // when sep is non-empty AND there are bytes after sep.
5136            let after_off = pos + c + sl;
5137            let following_nonempty = after_off < bytes.len();
5138            let cond_b = sl != 0 || following_nonempty;
5139            if (c != 0 || mul != 0) && cond_b {
5140                r += 1;
5141            }
5142            if sl == 0 {
5143                // Avoid infinite loop on empty sep — mirrors C's findsep
5144                // which advances by 1 byte when sep is empty.
5145                pos += 1;
5146            } else {
5147                pos += c + sl;
5148            }
5149        }
5150        r
5151    } else {
5152        // IFS branch (sep == NULL). C source uses itype_end(s, ISEP, 1)
5153        // to skip ISEP chars (default $IFS = " \t\n"). We use iwsep.
5154        let mut s_pos = 0usize;
5155        let t_orig = s_pos;
5156        let mut r: i32 = 0;
5157        // C: if (mul <= 0) skipwsep(&s);
5158        if mul <= 0 {
5159            while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5160                s_pos += 1;
5161            }
5162        }
5163        // C: if ((*s && itype_end(s,ISEP,1)!=s) || (mul<0 && t!=s)) r++;
5164        let has_word_now = s_pos < bytes.len() && !iwsep(bytes[s_pos]);
5165        if has_word_now || (mul < 0 && t_orig != s_pos) {
5166            r += 1;
5167        }
5168        // C: for (; *s; r++) { advance over word + maybe-skipwsep + findsep + maybe-skipwsep }
5169        while s_pos < bytes.len() {
5170            // Advance past the current word (non-ISEP chars).
5171            let word_start = s_pos;
5172            while s_pos < bytes.len() && !iwsep(bytes[s_pos]) {
5173                s_pos += 1;
5174            }
5175            if s_pos > word_start && mul <= 0 {
5176                while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5177                    s_pos += 1;
5178                }
5179            }
5180            // C: (void)findsep(&s, NULL, 0) — advance past one sep run.
5181            // Already handled above when mul<=0; for mul>0 we still need
5182            // to consume one separator byte to make progress.
5183            if s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5184                s_pos += 1;
5185            }
5186            let t_after = s_pos;
5187            if mul <= 0 {
5188                while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5189                    s_pos += 1;
5190                }
5191            }
5192            if s_pos < bytes.len() {
5193                r += 1;
5194            } else {
5195                // C: if (mul < 0 && t != s) r++;
5196                if mul < 0 && t_after != s_pos {
5197                    r += 1;
5198                }
5199                break;
5200            }
5201        }
5202        r
5203    }
5204}
5205
5206/// Join array with separator - port from zsh/Src/utils.c sepjoin() lines 3926-3958
5207///
5208/// If sep is None, uses first char of IFS (defaults to space).
5209/// Join an array with separator.
5210/// Port of `sepjoin(char **s, char *sep, int heap)` from Src/utils.c:3928.
5211/// WARNING: param names don't match C — Rust=(arr, sep) vs C=(s, sep, heap)
5212// Rust idiom replacement: `slice::join` covers the C `zalloc`+`strcpy`
5213// loop with running length pre-compute; the `heap` arg drops since
5214// String owns its own allocation.
5215/// `sepjoin` — see implementation.
5216pub fn sepjoin(arr: &[String], sep: Option<&str>) -> String {
5217    // c:3928
5218    // c:3934-3935 — if (!*s) return heap ? dupstring("") : ztrdup("");
5219    if arr.is_empty() {
5220        return String::new();
5221    }
5222    // c:3936-3945 — `if (!sep)` default-sep arm:
5223    //   if (ifs && *ifs != ' ') sep = dupstrpfx(ifs, MB_METACHARLEN(ifs));
5224    //   else sep = " ";
5225    // IFS SET-but-EMPTY takes the first branch (`*ifs` is '\0' != ' ')
5226    // and dupstrpfx("", …) yields an EMPTY separator — `"$*"` with
5227    // IFS="" concatenates. Only IFS UNSET (ifs == NULL → getsparam
5228    // None) or starting with a space falls back to " ".
5229    let ifs_storage: String;
5230    let sep_str: &str = match sep {
5231        Some(s) => s, // c:3936
5232        None => {
5233            ifs_storage = match getsparam("IFS") {
5234                // c:3938-3940 — set, first char not space (empty → "").
5235                Some(ifs) if !ifs.starts_with(' ') => ifs
5236                    .chars()
5237                    .next()
5238                    .map(|c| c.to_string())
5239                    .unwrap_or_default(),
5240                // c:3941-3944 — unset (NULL) or leading space → " ".
5241                _ => " ".to_string(),
5242            };
5243            &ifs_storage
5244        }
5245    };
5246    // c:3947-3956 — pre-compute total length, alloc, copy elements
5247    //               interleaving sep. Rust slice::join collapses all that
5248    //               into the one canonical call.
5249    arr.join(sep_str)
5250}
5251
5252/// Split string by separator - port from zsh/Src/utils.c sepsplit() lines 3961-3992
5253///
5254/// If sep is None, performs IFS-style word splitting (spacesplit).
5255/// Otherwise splits on the given separator string.
5256/// allownull: if true, allows empty strings in result
5257/// Split a string on `IFS` separators.
5258/// Port of `sepsplit(char *s, char *sep, int allownull, int heap)` from Src/utils.c:3962.
5259/// WARNING: param names don't match C — Rust=(s, sep, allownull) vs C=(s, sep, allownull, heap)
5260pub fn sepsplit(s: &str, sep: Option<&str>, allownull: bool) -> Vec<String> {
5261    // c:3962
5262    // Handle Nularg at start (zsh internal marker) - line 3968
5263    let s = if s.starts_with('\x00') && s.len() > 1 {
5264        &s[1..]
5265    } else {
5266        s
5267    };
5268
5269    match sep {
5270        None => spacesplit(s, allownull),
5271        Some("") => {
5272            // Empty separator: split into characters
5273            if allownull {
5274                s.chars().map(|c| c.to_string()).collect()
5275            } else {
5276                s.chars()
5277                    .map(|c| c.to_string())
5278                    .filter(|c| !c.is_empty())
5279                    .collect()
5280            }
5281        }
5282        Some(sep) => {
5283            let parts: Vec<String> = s.split(sep).map(|p| p.to_string()).collect();
5284            if allownull {
5285                parts
5286            } else {
5287                parts.into_iter().filter(|p| !p.is_empty()).collect()
5288            }
5289        }
5290    }
5291}
5292
5293/// Port of `getshfunc(char *nam)` from `Src/utils.c:3998`.
5294///
5295/// C body:
5296/// ```c
5297/// Shfunc getshfunc(char *nam) {
5298///     return (Shfunc) shfunctab->getnode(shfunctab, nam);
5299/// }
5300/// ```
5301///
5302/// Routes through the global `shfunctab` singleton in
5303/// hashtable.rs (hashtable::shfunctab_lock). Returns an owned
5304/// `shfunc` clone so callers can read `flags` (to detect
5305/// `PM_UNDEFINED` autoload stubs), `funcdef`, `filename`, and
5306/// `body` without holding the table lock. Owned clone vs C's
5307/// `*Shfunc` raw pointer trades one allocation for Rust
5308/// lifetime safety — `getshfunc` is a function-lookup site,
5309/// not per-statement, so the cost is irrelevant.
5310///
5311/// Returning `Option<String>` of just `body` (the old contract)
5312/// made every PM_UNDEFINED autoload stub invisible because
5313/// `body=None` collapsed via `and_then` to `None`, which callers
5314/// then read as "function doesn't exist."
5315pub fn getshfunc(nam: &str) -> Option<shfunc> {
5316    let tab = shfunctab_lock().read().expect("shfunctab poisoned");
5317    tab.get(nam).cloned()
5318}
5319
5320/// Port of `char **subst_string_by_func(Shfunc func, char *arg1, char *orig)`
5321/// from Src/utils.c:4017.
5322///
5323/// Calls the named shell function with `[func, arg1?, orig]` as
5324/// positional args under `sfcontext = SFC_SUBST` and returns the
5325/// `$reply` array on success. Routes through `callhookfunc` (the
5326/// static-linked equivalent of `doshfunc`), then reads `$reply`
5327/// from the env-var fallback because the global `paramtab` is not
5328/// yet a singleton in the Rust port (params::getaparam takes a
5329/// `&ParamTable` arg).
5330pub fn subst_string_by_func(
5331    func_name: &str,
5332    arg1: Option<&str>,
5333    orig: &str,
5334) -> Option<Vec<String>> // c:4017
5335{
5336    let osc = SFCONTEXT.load(Ordering::Relaxed); // c:4019
5337    let osm = STOPMSG.load(Ordering::Relaxed);
5338    let old_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
5339    let mut args: Vec<String> = Vec::with_capacity(3); // c:4020-4026
5340    args.push(func_name.to_string()); // c:4023
5341    if let Some(a) = arg1 {
5342        // c:4024
5343        args.push(a.to_string()); // c:4025
5344    }
5345    args.push(orig.to_string()); // c:4026
5346    SFCONTEXT // c:4027
5347        .store(SFC_SUBST, Ordering::Relaxed);
5348    INCOMPFUNC.store(0, Ordering::Relaxed); // c:4028
5349
5350    // c:4030 — `if (doshfunc(func, l, 1))`. Direct doshfunc call
5351    // mirrors C. body_runner routes through the host body-only entry
5352    // (matching the same shfunc that `callhookfunc` would resolve).
5353    let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
5354        .read()
5355        .ok()
5356        .and_then(|t| t.get(func_name).cloned());
5357    let rc = if let Some(mut shf) = shf_clone {
5358        let name_for_body = func_name.to_string();
5359        let body_args = args.clone();
5360        let body_runner = move || -> i32 {
5361            crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
5362        };
5363        crate::ported::exec::doshfunc(&mut shf, args.clone(), true, body_runner)
5364    } else {
5365        callhookfunc(func_name, Some(&args), 0, std::ptr::null_mut())
5366    };
5367    // c:4033 — `ret = getaparam("reply")` against paramtab. `reply`
5368    // is a shell-local PM_ARRAY entry, never exported to env.
5369    let ret: Option<Vec<String>> = if rc != 0 {
5370        None // c:4031
5371    } else {
5372        getaparam("reply") // c:4033
5373    };
5374
5375    SFCONTEXT.store(osc, Ordering::Relaxed); // c:4035
5376    STOPMSG.store(osm, Ordering::Relaxed); // c:4036
5377    INCOMPFUNC.store(old_incompfunc, Ordering::Relaxed); // c:4037
5378    ret // c:4038
5379}
5380
5381/// Port of `char **subst_string_by_hook(char *name, char *arg1, char *orig)`
5382/// from Src/utils.c:4049.
5383///
5384/// Looks up `name` as a shell function and calls
5385/// `subst_string_by_func` on it. If that returns no result, walks
5386/// `${name}_hook` as an array of function names, trying each in
5387/// order until one yields a `$reply`.
5388pub fn subst_string_by_hook(name: &str, arg1: Option<&str>, orig: &str) -> Option<Vec<String>> // c:4049
5389{
5390    let mut ret: Option<Vec<String>> = None;
5391    if getshfunc(name).is_some() {
5392        // c:4054
5393        ret = subst_string_by_func(name, arg1, orig); // c:4055
5394    }
5395    if ret.is_none() {
5396        // c:4058
5397        let arrnam = format!("{}_hook", name); // c:4061-4063
5398                                               // c:4065 — `arr = getaparam(arrnam)`. The previous Rust port
5399                                               // read `env::var(arrnam)` and NUL-split — wrong: the hook
5400                                               // array is a shell-local PM_ARRAY in paramtab, not env.
5401        if let Some(arr) = getaparam(&arrnam) {
5402            // c:4065
5403            for f in arr.iter() {
5404                // c:4068
5405                if f.is_empty() {
5406                    continue;
5407                }
5408                if getshfunc(f).is_some() {
5409                    // c:4069
5410                    ret = subst_string_by_func(f, arg1, orig); // c:4070
5411                    if ret.is_some() {
5412                        // c:4071
5413                        break; // c:4072
5414                    }
5415                }
5416            }
5417        }
5418    }
5419    ret // c:4094
5420}
5421
5422/// Make single-element array (from utils.c mkarray)
5423/// Port of `mkarray(char *s)` from `Src/utils.c:4083`.
5424pub fn mkarray(s: Option<&str>) -> Vec<String> {
5425    match s {
5426        Some(val) => vec![val.to_string()],
5427        None => Vec::new(),
5428    }
5429}
5430
5431/// Make single-element array on heap (from utils.c hmkarray)
5432/// Port of `hmkarray(char *s)` from `Src/utils.c:4094`.
5433pub fn hmkarray(s: &str) -> Vec<String> {
5434    if s.is_empty() {
5435        Vec::new()
5436    } else {
5437        vec![s.to_string()]
5438    }
5439}
5440
5441/// Port of `void zbeep(void)` from Src/utils.c:4105.
5442///
5443/// Honours `$ZBEEP` (a key-string sequence) when set and the BEEP
5444/// option when unset; emits the BEL char (\007) to SHTTY by
5445/// default. The Rust port writes via `write_loop` to mirror C's
5446/// raw-write semantics.
5447pub fn zbeep() {
5448    // c:4105
5449    queue_signals(); // c:4105
5450    if let Ok(zbeep) = std::env::var("ZBEEP") {
5451        // c:4109
5452        let (decoded, _) = getkeystring(&zbeep); // c:4111
5453        #[cfg(unix)]
5454        {
5455            let shtty = SHTTY.load(Ordering::Relaxed);
5456            if shtty != -1 {
5457                let _ = write_loop(shtty, decoded.as_bytes()); // c:4112
5458            } else {
5459                eprint!("{}", decoded);
5460            }
5461        }
5462        #[cfg(not(unix))]
5463        eprint!("{}", decoded);
5464    } else if isset(BEEP) {
5465        // c:4113
5466        #[cfg(unix)]
5467        {
5468            let shtty = SHTTY.load(Ordering::Relaxed);
5469            if shtty != -1 {
5470                let _ = write_loop(shtty, b"\x07"); // c:4114
5471            } else {
5472                eprint!("\x07");
5473            }
5474        }
5475        #[cfg(not(unix))]
5476        eprint!("\x07");
5477    }
5478    unqueue_signals(); // c:4115
5479}
5480
5481/// Free array (no-op in Rust, provided for API compat)
5482/// Port of `freearray(char **s)` from `Src/utils.c:4120`.
5483pub fn freearray(s: Vec<String>) {
5484    // c:4124 — DPUTS(!s, "freearray() with zero argument")
5485    // Rust takes Vec<String> by value (never null), so the C !s
5486    // check maps to no condition that can fire in Rust. Document
5487    // the gap.
5488    let _ = &s; // c:4124 (no-op; Vec is never NULL in Rust)
5489                // Rust Drop handles this
5490}
5491
5492/// Split on '=' returning (name, value) (from utils.c equalsplit)
5493/// Port of `equalsplit(char *s, char **t)` from `Src/utils.c:4133`.
5494/// WARNING: param names don't match C — Rust=(s) vs C=(s, t)
5495pub fn equalsplit(s: &str) -> Option<(String, String)> {
5496    let eq = s.find('=')?;
5497    Some((s[..eq].to_string(), s[eq + 1..].to_string()))
5498}
5499
5500// initialize the ztypes table                                              // c:4151
5501/// Port of `inittyptab()` from `Src/utils.c:4155`. Initialise the
5502/// `typtab[256]` lookup table that backs the `idigit`/`ialnum`/etc.
5503/// predicates in `ztype_h`.
5504///
5505/// C body (c:4155-4250) does:
5506///   1. Zero the table.
5507///   2. Mark 0..=31 and 128..=159 + 127 as ICNTRL.
5508///   3. Mark '0'..='9' as IDIGIT|IALNUM|IWORD|IIDENT|IUSER.
5509///   4. Mark 'a'..='z' and 'A'..='Z' as IALPHA|IALNUM|IIDENT|IUSER|IWORD.
5510///   5. Mark '_' as IIDENT|IUSER.
5511///   6. Mark '-', '.', Dash as IUSER.
5512///   7. Mark ' '/'\t' as IBLANK|INBLANK; '\n' as INBLANK.
5513///   8. Mark '\0', Meta, Marker as IMETA.
5514///   9. Mark Pound..LAST_NORMAL_TOK as ITOK|IMETA.
5515///   10. Mark Snull..Nularg as ITOK|IMETA|INULL.
5516///   11. Walk $IFS adding ISEP and IWSEP for blanks.
5517///
5518/// This first-pass port covers steps 1-7. Steps 8-11 require Meta /
5519/// Marker / Pound / Snull / Nularg constants from zsh_h/zsh.h and
5520/// the `ifs` global; the remaining Meta/IFS marks are skipped until
5521/// those land. Idempotent — safe to call multiple times.
5522pub fn inittyptab() {
5523    // utils.c:4155
5524
5525    // c:4160 — `if (!(typtab_flags & ZTF_INIT))` one-off init.
5526    {
5527        let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5528        if (flags & ZTF_INIT) == 0 {
5529            TYPTAB_FLAGS.store(ZTF_INIT, Ordering::Relaxed);
5530        }
5531    }
5532
5533    // Local scratch, flushed to the atomic table at fn end — C mutates
5534    // typtab[] in place unlocked; the atomic store-per-slot flush keeps
5535    // readers lock-free (see TYPTAB doc in ztype_h.rs).
5536    let mut t = [0u32; 256]; // c:4168 memset(typtab, 0, sizeof(typtab))
5537    // c:4169-4170 — control chars 0..32 and 128..160.
5538    for c in 0..32u32 {
5539        t[c as usize] = ICNTRL as u32;
5540        t[(c + 128) as usize] = ICNTRL as u32;
5541    }
5542    // c:4171 — `typtab[127] = ICNTRL;`
5543    t[127] = ICNTRL as u32;
5544    // c:4172-4173 — '0'..='9'.
5545    for c in (b'0' as usize)..=(b'9' as usize) {
5546        t[c] = (IDIGIT | IALNUM | IWORD | IIDENT | IUSER) as u32;
5547    }
5548    // c:4174-4175 — 'a'..='z' and matching 'A'..='Z'.
5549    for c in (b'a' as usize)..=(b'z' as usize) {
5550        let upper = c - (b'a' as usize) + (b'A' as usize);
5551        let bits = (IALPHA | IALNUM | IIDENT | IUSER | IWORD) as u32;
5552        t[c] = bits;
5553        t[upper] = bits;
5554    }
5555    // c:4190 — `typtab['_'] = IIDENT | IUSER;`
5556    t[b'_' as usize] = (IIDENT | IUSER) as u32;
5557    // c:4191 — `typtab['-'] = typtab['.'] = typtab[(unsigned char) Dash] = IUSER;`.
5558    t[b'-' as usize] = IUSER as u32;
5559    t[b'.' as usize] = IUSER as u32;
5560    // c:4191 — `Dash` token marker (0x9b per zsh.h:182, "Only in patterns").
5561    // Marking it IUSER lets pattern-side $-named-character paths
5562    // accept it as a user-name byte. Previously omitted in the port.
5563    t[crate::ported::zsh_h::Dash as usize] = IUSER as u32;
5564    // c:4192-4194 — blanks.
5565    t[b' ' as usize] |= (IBLANK | INBLANK) as u32;
5566    t[b'\t' as usize] |= (IBLANK | INBLANK) as u32;
5567    t[b'\n' as usize] |= INBLANK as u32;
5568
5569    // c:4195 — `typtab['\0'] |= IMETA;`. Previously omitted in the
5570    // Rust port — '\0' had only ICNTRL (set by the 0..32 loop at
5571    // c:4169) and was missing IMETA. C `imeta('\0')` is true (the
5572    // NUL byte must be Meta-encoded as `Meta + ('\0' ^ 32)`); the
5573    // Rust `imeta()` typtab predicate returned false, so any code
5574    // routing through `imeta(c)` (e.g. `input::shingetline`)
5575    // failed to Meta-encode NUL bytes from stdin, corrupting the
5576    // SHIN buffer when piped binary data hit a `\0`.
5577    t[0] |= IMETA as u32; // c:4195
5578                          // c:4196-4197 — Meta + Marker marked IMETA.
5579    {
5580        t[Meta as usize] |= IMETA as u32;
5581        t[Marker as usize] |= IMETA as u32;
5582    }
5583
5584    // c:4133-4134 — `for (t0 = Pound; t0 <= LAST_NORMAL_TOK; t0++)
5585    //                    typtab[t0] |= ITOK | IMETA;`
5586    // Marks all char-rewrite token markers (Pound, Stringg, Hat,
5587    // Star, ...). Without this, `itok(Stringg)` returns false and
5588    // `is_valid_assignment_target("$NAME")` wrongly accepts the
5589    // leading `$` as part of an identifier prefix → `$=cmd` lexes
5590    // as ENVSTRING instead of STRING.
5591    {
5592        let lo = Pound as usize;
5593        let hi = LAST_NORMAL_TOK as usize;
5594        for t0 in lo..=hi {
5595            t[t0] |= (ITOK | IMETA) as u32;
5596        }
5597    }
5598
5599    // c:4135-4136 — `for (t0 = Snull; t0 <= Nularg; t0++)
5600    //                    typtab[t0] |= ITOK | IMETA | INULL;`
5601    {
5602        let lo = Snull as usize;
5603        let hi = Nularg as usize;
5604        for t0 in lo..=hi {
5605            t[t0] |= (ITOK | IMETA | INULL) as u32;
5606        }
5607    }
5608
5609    // c:4202-4231 — IFS walk. Sets ISEP on every IFS char and IWSEP
5610    // on the blank (inblank) subset. Reads the current `ifs` global
5611    // (defaulting to DEFAULT_IFS), demetafies `Meta+X` pairs, and
5612    // skips a doubled blank (`s[1]==c`) so the IWSEP bit doesn't
5613    // mark "blank repeated → no-skip" IFS chars. Mirrors C exactly.
5614    {
5615        // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; ...)`.
5616        // C reads the global `ifs` variable (the same one `ifssetfn`
5617        // writes). The Rust port walks paramtab first (so the GSU
5618        // dispatch path matches C); on miss, fall through to ifs_lock
5619        // directly so a fresh `ifssetfn` update before any paramtab
5620        // entry exists is still visible to inittyptab.
5621        // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; *s; s++)`.
5622        // The default-IFS fallback is keyed on the `ifs` POINTER being
5623        // NULL (i.e. IFS unset), NOT on it pointing at an empty string.
5624        // `IFS=''` leaves a non-NULL empty `ifs`, the loop body never
5625        // runs, and typtab ends up with ZERO ISEP/IWSEP bits — which is
5626        // exactly how an empty IFS disables all word splitting
5627        // (`IFS=''; v='a b  c'; print -rl -- ${=v}` is one word). The
5628        // port used to coerce "" → DEFAULT_IFS at both the paramtab read
5629        // and the fallback, so an empty IFS still split on whitespace.
5630        let pt_ifs: Option<String> = crate::ported::params::paramtab()
5631            .read()
5632            .ok()
5633            .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)));
5634        let src: String = match pt_ifs {
5635            // IFS is SET (possibly to ""): walk its chars verbatim.
5636            Some(v) => v,
5637            // No paramtab entry yet. Fall through to the ifs_lock global so
5638            // a fresh ifssetfn update landing before the paramtab entry
5639            // exists is still visible; an empty global there means "not
5640            // initialised", which is C's NULL → CURRENT_DEFAULT_IFS.
5641            None => {
5642                let g = crate::ported::params::ifs_lock()
5643                    .lock()
5644                    .map(|g| g.clone())
5645                    .unwrap_or_default();
5646                if g.is_empty() {
5647                    DEFAULT_IFS.to_string() // c:4216 (ifs == NULL)
5648                } else {
5649                    g
5650                }
5651            }
5652        };
5653        let bytes = src.as_bytes();
5654        let mut i = 0;
5655        while i < bytes.len() {
5656            // c:4217 — `int c = (unsigned char) (*s == Meta ? *++s ^ 32 : *s)`.
5657            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
5658                i += 1;
5659                bytes[i] ^ 32
5660            } else {
5661                bytes[i]
5662            };
5663            // c:4218-4223 — MULTIBYTE non-ASCII skip. Bytes >= 0x80
5664            // (after demetafy) are not classified by typtab — they
5665            // reach wcsitype via WC_ZISTYPE instead.
5666            if c >= 0x80 {
5667                i += 1;
5668                continue;
5669            }
5670            let cu = c as usize;
5671            // c:4224-4229 — `if (inblank(c))` — for default-IFS
5672            // chars space/tab/newline, mark IWSEP unless the next
5673            // byte repeats the same char.
5674            let is_inblank = (t[cu] & (INBLANK as u32)) != 0;
5675            if is_inblank {
5676                if i + 1 < bytes.len() && bytes[i + 1] == c {
5677                    i += 1; // c:4226 — skip the dup
5678                } else {
5679                    t[cu] |= IWSEP as u32; // c:4228
5680                }
5681            }
5682            // c:4230 — `typtab[c] |= ISEP;`
5683            t[cu] |= ISEP as u32;
5684            i += 1;
5685        }
5686    }
5687
5688    // c:4232-4252 — wordchars walk. ORs IWORD onto every byte in
5689    // `$WORDCHARS` (or DEFAULT_WORDCHARS when unset). Used by every
5690    // word-class lookup in pattern matching, `${var:#word}`, etc.
5691    // Drops to ASCII-only under MULTIBYTE_SUPPORT (the non-ASCII path
5692    // routes through wordchars_wide).
5693    {
5694        // Same fallback as IFS above: paramtab first, then wordchars_lock
5695        // so wordcharssetfn updates that bypass paramtab still reach
5696        // the typtab rebuild.
5697        let wc = crate::ported::params::paramtab()
5698            .read()
5699            .ok()
5700            .and_then(|t| {
5701                t.get("WORDCHARS")
5702                    .map(|pm| crate::ported::params::wordcharsgetfn(pm))
5703            })
5704            .filter(|s| !s.is_empty())
5705            .unwrap_or_else(|| {
5706                crate::ported::params::wordchars_lock()
5707                    .lock()
5708                    .map(|g| g.clone())
5709                    .unwrap_or_default()
5710            });
5711        let src: String = if wc.is_empty() {
5712            DEFAULT_WORDCHARS.to_string()
5713        } else {
5714            wc
5715        };
5716        let bytes = src.as_bytes();
5717        let mut i = 0;
5718        while i < bytes.len() {
5719            // c:4238 — Meta+X demetafy.
5720            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
5721                i += 1;
5722                bytes[i] ^ 32
5723            } else {
5724                bytes[i]
5725            };
5726            // c:4239-4249 — MULTIBYTE non-ASCII skip.
5727            if c < 0x80 {
5728                t[c as usize] |= IWORD as u32; // c:4251
5729            }
5730            i += 1;
5731        }
5732    }
5733
5734    // c:4253-4254 — SPECCHARS walk. ORs ISPECIAL onto every member
5735    // of the hardcoded SPECCHARS string. Drives glob-special and
5736    // quote-special detection.
5737    {
5738        for &b in SPECCHARS.as_bytes() {
5739            t[b as usize] |= ISPECIAL as u32; // c:4254
5740        }
5741    }
5742
5743    // c:4255-4256 — comma special only when ZTF_SP_COMMA was set
5744    // via `makecommaspecial(1)`. KSH_GLOB / extended-glob path.
5745    {
5746        let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5747        if (flags & ZTF_SP_COMMA) != 0 {
5748            // c:4255
5749            t[b',' as usize] |= ISPECIAL as u32; // c:4256
5750        }
5751    }
5752
5753    // c:4257-4261 — bangchar special when BANGHIST + interact +
5754    // bangchar != 0. Sets ZTF_BANGCHAR flag bit then marks the
5755    // bangchar byte ISPECIAL.
5756    {
5757        let bangchar2 = bangchar.load(Ordering::SeqCst) as usize;
5758        let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5759        let interact_flag = (flags & ZTF_INTERACT) != 0;
5760        let banghist = isset(BANGHIST);
5761        if banghist && bangchar2 != 0 && bangchar2 < 256 && interact_flag {
5762            // c:4257
5763            TYPTAB_FLAGS.fetch_or(ZTF_BANGCHAR, Ordering::Relaxed); // c:4258
5764            t[bangchar2] |= ISPECIAL as u32; // c:4259
5765        } else {
5766            TYPTAB_FLAGS.fetch_and(!ZTF_BANGCHAR, Ordering::Relaxed); // c:4261
5767        }
5768    }
5769
5770    // c:4262-4263 — PATCHARS walk. ORs IPATTERN onto every member.
5771    // Used by pattern compilation to detect glob metachars.
5772    {
5773        for &b in PATCHARS.as_bytes() {
5774            t[b as usize] |= IPATTERN as u32; // c:4263
5775        }
5776    }
5777
5778    // Flush the scratch to the lock-free table (see header note).
5779    for (i, v) in t.iter().enumerate() {
5780        TYPTAB[i].store(*v, Ordering::Relaxed);
5781    }
5782}
5783
5784/// Port of `void makecommaspecial(int yesno)` from Src/utils.c:4270.
5785///
5786/// Toggles `ZTF_SP_COMMA` and the `ISPECIAL` bit on `,` in the
5787/// global typtab — used by glob/extended-glob to flag `,`
5788/// (KSH_GLOB) as a metacharacter.
5789pub fn makecommaspecial(yesno: bool) {
5790    // c:4270
5791    if yesno {
5792        // c:4272
5793        TYPTAB_FLAGS.fetch_or(ZTF_SP_COMMA, Ordering::Relaxed); // c:4273
5794        TYPTAB[b',' as usize].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4274
5795    } else {
5796        TYPTAB_FLAGS.fetch_and(!ZTF_SP_COMMA, Ordering::Relaxed); // c:4276
5797        TYPTAB[b',' as usize].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4277
5798    }
5799}
5800
5801/// Port of `void makebangspecial(int yesno)` from Src/utils.c:4283.
5802///
5803/// Toggles `ISPECIAL` on the current `bangchar`. When `yesno==0`
5804/// always clears; when nonzero, sets only if `ZTF_BANGCHAR` was
5805/// stored by `inittyptab` (i.e. BANGHIST is on).
5806pub fn makebangspecial(yesno: bool) {
5807    // c:4283
5808    let bc = bangchar.load(Ordering::SeqCst) as usize;
5809    if bc == 0 || bc >= 256 {
5810        return;
5811    }
5812    let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5813    if !yesno {
5814        // c:4289
5815        TYPTAB[bc].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4290
5816    } else if (flags & ZTF_BANGCHAR) != 0 {
5817        // c:4291
5818        TYPTAB[bc].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4292
5819    }
5820}
5821
5822/// Port of `wcsiblank(wint_t wc)` from `Src/utils.c:4302`.
5823///
5824/// ```c
5825/// mod_export int wcsiblank(wint_t wc) {
5826///     if (iswspace(wc) && wc != L'\n')
5827///         return 1;
5828///     return 0;
5829/// }
5830/// ```
5831///
5832/// "wide-character version of the iblank() macro" — true for any
5833/// whitespace EXCEPT newline. The previous Rust port included
5834/// newline (since `c.is_whitespace()` returns true for '\n') —
5835/// wrong for callers that use this to find token boundaries.
5836pub fn wcsiblank(wc: char) -> bool {
5837    wc.is_whitespace() && wc != '\n'
5838}
5839
5840/// Port of `int wcsitype(wchar_t c, int itype)` from Src/utils.c:4321.
5841///
5842/// "zistype macro extended to support wide characters. Works for
5843/// IIDENT, IWORD, IALNUM, ISEP."
5844///
5845/// The Rust port checks whether `c` falls in the typtab class
5846/// represented by `itype`. ASCII chars consult the global TYPTAB
5847/// (the same one C's `zistype()` macro indexes); non-ASCII chars
5848/// route through Unicode predicates that mirror the C `iswalnum`
5849/// fallback at line 4346.
5850pub fn wcsitype(c: char, itype: u32) -> bool {
5851    // c:4321
5852    if !isset(MULTIBYTE) {
5853        // c:4327
5854        if (c as u32) < 256 {
5855            return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
5856        }
5857        return false;
5858    }
5859    if (c as u32) < 128 {
5860        // c:4343
5861        return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
5862    }
5863    let cls = itype as u16;
5864    if cls == IIDENT {
5865        // c:4347
5866        if isset(POSIXIDENTIFIERS) {
5867            return false; // c:4348
5868        }
5869        return c.is_alphanumeric(); // c:4350
5870    }
5871    if cls == IWORD {
5872        // c:4352
5873        if c.is_alphanumeric() {
5874            return true;
5875        } // c:4353
5876          // C: IS_COMBINING(c) — no Rust crate-free combining-mark
5877          // predicate. zero-width chars (combining, zero-width-joiner,
5878          // etc.) are treated as word per c:4362.
5879        if unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0 {
5880            // c:4362
5881            return true;
5882        }
5883        // c:4364 — `wmemchr(wordchars_wide.chars, c, …)`. Reads from
5884        // the canonical `wordchars` global (writable by
5885        // `wordcharssetfn` at `Src/params.c:5143`). Previously routed
5886        // through `std::env::var("WORDCHARS")` which is the libc
5887        // process environment — never reflects runtime `WORDCHARS=:`
5888        // assignments inside the shell.
5889        let w = crate::ported::params::paramtab()
5890            .read()
5891            .ok()
5892            .and_then(|t| {
5893                t.get("WORDCHARS")
5894                    .map(|pm| crate::ported::params::wordcharsgetfn(pm))
5895            })
5896            .unwrap_or_default();
5897        return w.chars().any(|x| x == c);
5898    }
5899    if cls == ISEP {
5900        // c:4366
5901        // c:4367 — same canonical-global pattern for IFS.
5902        let ifs = crate::ported::params::paramtab()
5903            .read()
5904            .ok()
5905            .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
5906            .unwrap_or_default();
5907        return ifs.chars().any(|x| x == c);
5908    }
5909    let _ = IALNUM;
5910    c.is_alphanumeric() // c:4370
5911}
5912
5913/// Check if a character type at end of string (from utils.c itype_end)
5914/// Port of `char *itype_end(const char *ptr, int itype, int once)` from `Src/utils.c:4395`.
5915///
5916/// Walks `ptr` byte-by-byte advancing past every char whose
5917/// type-bits include `itype`. Returns the byte offset where the
5918/// walk stops (C returns the advanced pointer; the byte offset is
5919/// the Rust equivalent — `ptr + return_value` reproduces the C
5920/// pointer).
5921///
5922/// C body branches (ported line-for-line):
5923///
5924/// 1. **INAMESPC special-case** (c:4399-4413). If itype == INAMESPC
5925///    and we're not POSIXIDENTIFIERS-strict outside ksh emulation,
5926///    recurse on `ptr+(*ptr=='.')` with IIDENT to allow dotted
5927///    ksh93 namespace names. The recursion result determines
5928///    whether to advance past the dot or fall through.
5929///
5930/// 2. **MULTIBYTE branch** (c:4416-4470, gated `isset(MULTIBYTE)
5931///    && (itype != IIDENT || !isset(POSIXIDENTIFIERS))`). Walks
5932///    multibyte chars via mb_metacharlenconv (multibyte len +
5933///    wchar conversion). Per-codepoint test:
5934///      - itok byte (raw command line): test the byte via zistype
5935///      - Meta-prefixed pair: extract real byte via XOR, test via
5936///        zistype
5937///      - ASCII (< 0x80): test via zistype
5938///      - Non-ASCII codepoint: route through wcsitype (which
5939///        already handles IWORD/ISEP via WORDCHARS/IFS paramtab
5940///        lookup matching c:4364/c:4367)
5941///
5942/// 3. **Non-multibyte branch** (c:4474-4480). Simple Meta-byte +
5943///    zistype byte loop.
5944///
5945/// `once = true` stops after the first matching char (used for
5946/// "is this a valid first char" checks per c:1576 etc).
5947pub fn itype_end(s: &str, mut itype: u32, once: bool) -> usize {
5948    use crate::ported::zsh_h::{isset, Meta, EMULATE_KSH, EMULATION, MULTIBYTE, POSIXIDENTIFIERS};
5949    use crate::ported::ztype_h::{itok, zistype, IIDENT, INAMESPC};
5950
5951    // c:4399-4413 — INAMESPC handling with ksh93 namespace dot.
5952    let mut start = 0usize;
5953    if itype == INAMESPC {
5954        itype = IIDENT as u32;
5955        if !isset(POSIXIDENTIFIERS) || EMULATION(EMULATE_KSH) {
5956            // c:4403 — `t = itype_end(ptr + (*ptr == '.'), itype, 0);`
5957            let first_is_dot = s.as_bytes().first().copied() == Some(b'.');
5958            let recurse_start = if first_is_dot { 1 } else { 0 };
5959            let t = recurse_start + itype_end(&s[recurse_start..], itype, false);
5960            // c:4404-4410 — `if (t > ptr + (*ptr == '.')) {
5961            //                  if (*t == '.') ptr = t + 1;  /* Fall through */
5962            //                  else if (!once) return t;
5963            //              }`
5964            if t > recurse_start {
5965                let t_byte = s.as_bytes().get(t).copied();
5966                if t_byte == Some(b'.') {
5967                    start = t + 1; // c:4407
5968                } else if !once {
5969                    return t; // c:4409
5970                }
5971            }
5972        }
5973    }
5974
5975    let bytes = s.as_bytes();
5976    let mb = isset(MULTIBYTE) && (itype != IIDENT as u32 || !isset(POSIXIDENTIFIERS));
5977
5978    if mb {
5979        // c:4416-4470 — multibyte walk. mb_charinit() in C is a
5980        // no-op stateless reset; not needed in Rust since str::chars
5981        // is already stateless UTF-8.
5982        let mut i = start;
5983        while i < bytes.len() {
5984            let b = bytes[i];
5985            // c:4422-4425 — itok: raw token byte, test via zistype,
5986            // advance 1.
5987            if itok(b) {
5988                if !zistype(b, itype) {
5989                    break;
5990                }
5991                i += 1;
5992            } else if b == Meta {
5993                // c:4438-4440 (WEOF arm) + non-MB Meta fallback:
5994                // Meta-prefixed pair = single unescaped char via XOR.
5995                let nxt = bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20;
5996                if (nxt as u32) > 127 || !zistype(nxt, itype) {
5997                    break;
5998                }
5999                i += 2;
6000            } else if b < 0x80 {
6001                // c:4441-4443 (len == 1 && isascii) — ASCII: direct
6002                // zistype on the byte.
6003                if !zistype(b, itype) {
6004                    break;
6005                }
6006                i += 1;
6007            } else {
6008                // c:4446-4467 — non-ASCII codepoint. Decode the UTF-8
6009                // sequence starting at `i` and route through wcsitype
6010                // (which already handles IWORD via WORDCHARS paramtab
6011                // c:4364, ISEP via IFS paramtab c:4367, default via
6012                // iswalnum/is_alphanumeric c:4370).
6013                let tail = match std::str::from_utf8(&bytes[i..]) {
6014                    Ok(t) => t,
6015                    Err(_) => break, // invalid UTF-8 — stop walk
6016                };
6017                let ch = match tail.chars().next() {
6018                    Some(c) => c,
6019                    None => break,
6020                };
6021                if !wcsitype(ch, itype) {
6022                    break;
6023                }
6024                i += ch.len_utf8();
6025            }
6026            if once {
6027                break;
6028            }
6029        }
6030        i
6031    } else {
6032        // c:4474-4480 — non-multibyte arm. Simple byte loop with
6033        // Meta-prefix collapse.
6034        let mut i = start;
6035        while i < bytes.len() {
6036            let b = bytes[i];
6037            let chr = if b == Meta {
6038                bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20
6039            } else {
6040                b
6041            };
6042            if !zistype(chr, itype) {
6043                break;
6044            }
6045            i += if b == Meta { 2 } else { 1 };
6046            if once {
6047                break;
6048            }
6049        }
6050        i
6051    }
6052}
6053
6054/// Duplicate array (from utils.c arrdup)
6055/// Port of `arrdup(char **s)` from `Src/utils.c:4493`.
6056pub fn arrdup(s: &[String]) -> Vec<String> {
6057    s.to_vec()
6058}
6059
6060/// Duplicate array with max elements (from utils.c arrdup_max)
6061/// Port of `arrdup_max(char **s, unsigned max)` from `Src/utils.c:4508`.
6062pub fn arrdup_max(s: &[String], max: usize) -> Vec<String> {
6063    s.iter().take(max).cloned().collect()
6064}
6065
6066/// Duplicate array with zsh allocation (from utils.c zarrdup)
6067/// Port of `zarrdup(char **s)` from `Src/utils.c:4532`.
6068pub fn zarrdup(s: &[String]) -> Vec<String> {
6069    s.to_vec()
6070}
6071
6072/// Duplicate array of wide strings (from utils.c wcs_zarrdup) - same as zarrdup in Rust
6073/// Port of `wcs_zarrdup(wchar_t **s)` from `Src/utils.c:4547`.
6074pub fn wcs_zarrdup(s: &[String]) -> Vec<String> {
6075    s.to_vec()
6076}
6077
6078/// Spelling correction: find closest match (from utils.c spname)
6079/// Port of `spname(char *oldname)` from `Src/utils.c:4562`.
6080/// WARNING: param names don't match C — Rust=(name, dir) vs C=(oldname)
6081pub fn spname(name: &str, dir: &str) -> Option<String> {
6082    let entries = match fs::read_dir(dir) {
6083        Ok(e) => e,
6084        Err(_) => return None,
6085    };
6086
6087    let mut best = None;
6088    let mut best_dist = 4; // threshold
6089
6090    for entry in entries.flatten() {
6091        if let Some(entry_name) = entry.file_name().to_str() {
6092            let dist = spdist(name, entry_name, best_dist);
6093            if dist < best_dist {
6094                best_dist = dist;
6095                best = Some(entry_name.to_string());
6096            }
6097        }
6098    }
6099    best
6100}
6101
6102/// Spelling correction with full path (from utils.c mindist)
6103/// Port of `mindist(char *dir, char *mindistguess, char *mindistbest, int wantdir)` from `Src/utils.c:4624`.
6104/// WARNING: param names don't match C — Rust=(dir, name) vs C=(dir, mindistguess, mindistbest, wantdir)
6105pub fn mindist(dir: &str, name: &str) -> Option<(String, usize)> {
6106    let entries = match fs::read_dir(dir) {
6107        Ok(e) => e,
6108        Err(_) => return None,
6109    };
6110
6111    let mut best = None;
6112    let mut best_dist = 4;
6113
6114    for entry in entries.flatten() {
6115        if let Some(entry_name) = entry.file_name().to_str() {
6116            let dist = spdist(name, entry_name, best_dist);
6117            if dist < best_dist {
6118                best_dist = dist;
6119                best = Some(entry_name.to_string());
6120            }
6121        }
6122    }
6123    best.map(|name| (name, best_dist))
6124}
6125
6126// spellcheck a word                                                        // c:3123
6127// fix s ; if hist is nonzero, fix the history list too                     // c:3124
6128/// Compute edit distance between two strings (for spelling correction)
6129/// Direct port of `int spdist(char *s, char *t, int thresh)` from
6130/// `Src/utils.c:4675-4750`. Drives the CORRECT-option typo prompt:
6131/// returns 0 for identical, 1 for case-only mistakes, 2 for one
6132/// transposition / missing letter / QWERTY-adjacent mistype, 200 for
6133/// anything farther.
6134///
6135/// **This is NOT a Levenshtein DP — that was the previous Rust impl
6136/// dressed as a port.** The C source uses a keyboard-adjacency model
6137/// (qwertykeymap / dvorakkeymap arrays + tulower equality) which is
6138/// the actual behaviour `setopt CORRECT` depends on. Restored
6139/// faithfully.
6140pub fn spdist(s: &str, t: &str, thresh: usize) -> usize {
6141    // c:4679-4690 — qwerty keymap (rows of 14 chars each: numeric row,
6142    // QWERTY top row, home row, bottom row, then shift versions).
6143    // Embedded `\n` / `\t` mark off-grid cells.
6144    const QWERTYKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6145\t1234567890-=\t\
6146\tqwertyuiop[]\t\
6147\tasdfghjkl;'\n\t\
6148\tzxcvbnm,./\t\t\t\
6149\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6150\t!@#$%^&*()_+\t\
6151\tQWERTYUIOP{}\t\
6152\tASDFGHJKL:\"\n\t\
6153\tZXCVBNM<>?\n\n\t\
6154\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
6155    // c:4691-4702 — dvorak keymap, same shape.
6156    const DVORAKKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6157\t1234567890[]\t\
6158\t',.pyfgcrl/=\t\
6159\taoeuidhtns-\n\t\
6160\t;qjkxbmwvz\t\t\t\
6161\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6162\t!@#$%^&*(){}\t\
6163\t\"<>PYFGCRL?+\t\
6164\tAOEUIDHTNS_\n\t\
6165\t:QJKXBMWVZ\n\n\t\
6166\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
6167
6168    // c:4703-4707 — `keymap = isset(DVORAK) ? dvorakkeymap : qwertykeymap;`
6169    let keymap = if isset(DVORAK) {
6170        DVORAKKEYMAP.as_bytes()
6171    } else {
6172        QWERTYKEYMAP.as_bytes()
6173    };
6174
6175    let s_b = s.as_bytes();
6176    let t_b = t.as_bytes();
6177
6178    // c:4709-4710 — `if (!strcmp(s, t)) return 0;`
6179    if s == t {
6180        return 0;
6181    }
6182
6183    // c:4712-4714 — `for (p, q; *p && tulower(*p) == tulower(*q); p++, q++);
6184    //                  if (!*p && !*q) return 1;` — case-only mismatch.
6185    let mut p = 0usize;
6186    let mut q = 0usize;
6187    while p < s_b.len() && q < t_b.len() && tulower(s_b[p] as char) == tulower(t_b[q] as char) {
6188        p += 1;
6189        q += 1;
6190    }
6191    if p == s_b.len() && q == t_b.len() {
6192        return 1;
6193    }
6194
6195    // c:4715-4716 — `if (!thresh) return 200;`
6196    if thresh == 0 {
6197        return 200;
6198    }
6199
6200    // c:4717-4727 — first walk: detect transposition / missing letter at
6201    // first divergence.
6202    p = 0;
6203    q = 0;
6204    while p < s_b.len() && q < t_b.len() {
6205        if s_b[p] == t_b[q] {
6206            // c:4718-4719 — match: skip (don't count `aa` as transposed).
6207            p += 1;
6208            q += 1;
6209            continue;
6210        }
6211        // c:4720-4721 — `if (p[1] == q[0] && q[1] == p[0]) return
6212        //                  spdist(p+2, q+2, thresh-1) + 1;` transposition.
6213        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] {
6214            // SAFETY: bytes are ASCII for the keymap test below; from_utf8
6215            // is fine on a non-ASCII tail here because spdist takes &str.
6216            let s_tail = std::str::from_utf8(&s_b[p + 2..]).unwrap_or("");
6217            let t_tail = std::str::from_utf8(&t_b[q + 2..]).unwrap_or("");
6218            return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
6219        }
6220        // c:4722-4723 — `if (p[1] == q[0]) return spdist(p+1, q, thresh-1) + 2;`
6221        if p + 1 < s_b.len() && s_b[p + 1] == t_b[q] {
6222            let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6223            let t_tail = std::str::from_utf8(&t_b[q..]).unwrap_or("");
6224            return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6225        }
6226        // c:4724-4725 — `if (p[0] == q[1]) return spdist(p, q+1, thresh-1) + 2;`
6227        if q + 1 < t_b.len() && s_b[p] == t_b[q + 1] {
6228            let s_tail = std::str::from_utf8(&s_b[p..]).unwrap_or("");
6229            let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6230            return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6231        }
6232        // c:4726-4727 — `if (*p != *q) break;`
6233        break;
6234    }
6235    // c:4728-4729 — `if ((!*p && strlen(q) == 1) || (!*q && strlen(p) == 1))
6236    //                  return 2;` — single trailing-char insertion.
6237    if (p == s_b.len() && (t_b.len() - q) == 1) || (q == t_b.len() && (s_b.len() - p) == 1) {
6238        return 2;
6239    }
6240    // c:4730-4748 — second walk: keyboard-adjacency mistype detection.
6241    p = 0;
6242    q = 0;
6243    while p < s_b.len() && q < t_b.len() {
6244        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] {
6245            // c:4737-4738 — `if (!(z = strchr(keymap, p[0])) || *z == '\n' ||
6246            //                       *z == '\t') return spdist(p+1, q+1,
6247            //                       thresh-1) + 1;`
6248            let pos = keymap.iter().position(|&b| b == s_b[p]);
6249            let z_ok = match pos {
6250                Some(i) => keymap[i] != b'\n' && keymap[i] != b'\t',
6251                None => false,
6252            };
6253            if !z_ok {
6254                let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6255                let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6256                return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
6257            }
6258            // c:4739 — `t0 = z - keymap;`
6259            let t0 = pos.unwrap() as isize;
6260            // c:4740-4744 — eight adjacency offsets (-15,-14,-13,-1,+1,
6261            //                +13,+14,+15) → keyboard neighbours.
6262            let offsets: [isize; 8] = [-15, -14, -13, -1, 1, 13, 14, 15];
6263            let adjacent = offsets.iter().any(|&off| {
6264                let idx = t0 + off;
6265                if idx >= 0 && (idx as usize) < keymap.len() {
6266                    keymap[idx as usize] == t_b[q]
6267                } else {
6268                    false
6269                }
6270            });
6271            if adjacent {
6272                // c:4745 — `return spdist(p+1, q+1, thresh-1) + 2;`
6273                let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6274                let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6275                return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6276            }
6277            // c:4746 — `return 200;`
6278            return 200;
6279        } else if p < s_b.len() && q < t_b.len() && s_b[p] != t_b[q] {
6280            // c:4747-4748 — `else if (*p != *q) break;`
6281            break;
6282        }
6283        p += 1;
6284        q += 1;
6285    }
6286    // c:4749 — `return 200;`
6287    200
6288}
6289
6290/// Set terminal to cbreak mode (from utils.c setcbreak)
6291#[cfg(unix)]
6292/// Port of `setcbreak` from `Src/utils.c:4756`.
6293pub fn setcbreak() -> bool {
6294    if let Some(mut ti) = gettyinfo() {
6295        ti.c_lflag &= !(libc::ICANON | libc::ECHO);
6296        ti.c_cc[libc::VMIN] = 1;
6297        ti.c_cc[libc::VTIME] = 0;
6298        settyinfo(&ti)
6299    } else {
6300        false
6301    }
6302}
6303
6304#[cfg(not(unix))]
6305/// Port of `setcbreak` from `Src/utils.c:4756`.
6306pub fn setcbreak() -> bool {
6307    false
6308}
6309
6310/// Port of `void attachtty(pid_t pgrp)` from Src/utils.c:4775.
6311/// Hands the controlling terminal to `pgrp`. Gated by `jobbing &&
6312/// interact`; falls back to `mypgrp` and disables MONITOR on permanent
6313/// failure (matching the C source's recursion + `opts[MONITOR]=0` path).
6314#[cfg(unix)]
6315pub fn attachtty(pgrp: i32) {
6316    // c:4775
6317
6318    if !(jobbing() && interact()) {
6319        return; // c:4779
6320    }
6321    let shtty = SHTTY.load(Ordering::Relaxed); // c:4781
6322    if shtty == -1 {
6323        return;
6324    }
6325    let ep = ATTACHTTY_EP.load(Ordering::Relaxed);
6326    let rc = unsafe { libc::tcsetpgrp(shtty, pgrp) }; // c:4781
6327    if rc == -1 && ep == 0 {
6328        // c:4781
6329        let mypgrp_val = *crate::ported::jobs::MYPGRP // c:4792
6330            .get_or_init(|| Mutex::new(0))
6331            .lock()
6332            .unwrap();
6333        if pgrp != mypgrp_val && unsafe { libc::kill(-pgrp, 0) } == -1 {
6334            attachtty(mypgrp_val); // c:4793
6335        } else {
6336            let errno_val = io::Error::last_os_error().raw_os_error().unwrap_or(0);
6337            if errno_val != libc::ENOTTY {
6338                // c:4795
6339                zwarn(&format!(
6340                    "can't set tty pgrp: {}", // c:4797
6341                    io::Error::from_raw_os_error(errno_val)
6342                ));
6343                let _ = io::stderr().flush(); // c:4798
6344            }
6345            opt_state_set("monitor", false); // c:4815 opts[MONITOR]=0
6346            ATTACHTTY_EP.store(1, Ordering::Relaxed); // c:4815
6347        }
6348    } else if rc != -1 {
6349        // c:4815
6350        *crate::ported::jobs::LAST_ATTACHED_PGRP // c:4815
6351            .get_or_init(|| Mutex::new(0))
6352            .lock()
6353            .unwrap() = pgrp;
6354    }
6355}
6356
6357/// Port of `pid_t gettygrp(void)` from Src/utils.c:4815.
6358#[cfg(unix)]
6359pub fn gettygrp() -> i32 {
6360    // c:4815
6361    let shtty = SHTTY.load(Ordering::Relaxed);
6362    if shtty == -1 {
6363        // c:4819
6364        return -1; // c:4820
6365    }
6366    unsafe { libc::tcgetpgrp(shtty) } // c:4823
6367}
6368
6369/// Convert raw bytes (possibly containing NUL / 0x83-0x9b) to
6370/// zsh's metafied form: each `imeta(b)` byte becomes `Meta` (0x83)
6371/// followed by `b ^ 32`.
6372///
6373/// Port of `metafy(char *buf, int len, int heap)` from Src/utils.c:4856. The C source takes a
6374/// `heap` mode controlling whether the result is `zalloc`'d /
6375/// `zhalloc`'d / written into a static buffer / appended to the
6376/// existing buffer; in Rust we always return an owned `String`
6377/// since allocation strategy is uniform. The byte-level transform
6378/// is identical: walk the input, count metafy hits, allocate
6379/// `len + meta` bytes, expand each `Meta+X` pair in reverse.
6380/// Rust idiom replacement: forward byte-walk + Vec::push covers the
6381/// C two-pass (count + alloc + reverse-expand) approach; Vec grows
6382/// on demand so the pre-count is unnecessary in Rust.
6383/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len, heap)
6384pub fn metafy(buf: &str) -> String {
6385    // c:4856
6386    let bytes = buf.as_bytes();
6387    let mut out = Vec::with_capacity(bytes.len());
6388    for &b in bytes {
6389        // C: `#define imeta(c) ((c) >= Meta)` from Src/zsh.h —
6390        // every byte >= 0x83 needs escaping. The previous
6391        // narrow-range check `(0x83..=0x9b)` was a bug: bytes
6392        // 0x9c..=0xff (e.g. UTF-8 continuation bytes, high-Latin
6393        // characters) escaped C's imeta() but not the Rust
6394        // version, which then fed un-escaped bytes downstream
6395        // and corrupted Meta-aware loops.
6396        if imeta_byte(b) {
6397            out.push(Meta);
6398            out.push(b ^ 32);
6399        } else {
6400            out.push(b);
6401        }
6402    }
6403    // metafied bytes are in [0..=0x7f]∪{0x83}∪[expanded ^ 32 range];
6404    // String::from_utf8 may fail on the high bytes — fall back to
6405    // lossy. The lossy fallback inserts U+FFFD for invalid
6406    // sequences, which is fine for String-level callers (lexer /
6407    // pattern compile / display) but breaks byte-round-trip with
6408    // `unmetafy`. Byte-round-trip callers should use `metafy_bytes`
6409    // (which preserves the metafied byte sequence verbatim).
6410    String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
6411}
6412
6413/// Port of `ztrdup_metafy(const char *s)` from `Src/utils.c:4929`.
6414///
6415/// ```c
6416/// mod_export char *
6417/// ztrdup_metafy(const char *s)
6418/// {
6419///     if (!s) return NULL;
6420///     return metafy((char *)s, -1, META_DUP);
6421/// }
6422/// ```
6423pub fn ztrdup_metafy(s: &str) -> String {
6424    metafy(s)
6425}
6426
6427/// Port of `unmetafy(char *s, int *len)` from `Src/utils.c:4954`.
6428///
6429/// Take a metafied byte buffer in `s` and convert it in place to
6430/// its literal representation. C signature:
6431///
6432/// ```c
6433/// char *unmetafy(char *s, int *len);
6434/// ```
6435///
6436/// The Rust port mutates `s` in place and returns the resulting
6437/// length (mirroring C's `*len` out-parameter). C control flow:
6438///
6439/// ```c
6440/// for (p = s; *p && *p != Meta; p++);     // skip prefix with no Meta
6441/// for (t = p; (*t = *p++);)                 // walk the rest
6442///     if (*t++ == Meta && *p)
6443///         t[-1] = *p++ ^ 32;                // un-escape: XOR with 32
6444/// ```
6445///
6446/// Same algorithm here, byte-indexed against the Vec rather than
6447/// pointer-walked.
6448/// WARNING: param names don't match C — Rust=(s) vs C=(s, len)
6449pub fn unmetafy(s: &mut Vec<u8>) -> usize {
6450    // c:4954
6451    // First loop: find the first `Meta` byte. Everything before it
6452    // stays as-is, so we don't need to copy.
6453    let mut p: usize = 0;
6454    while p < s.len() && s[p] != Meta {
6455        p += 1;
6456    }
6457    // Second loop: walk from `p` onward, copying each byte into the
6458    // `t` slot (which trails `p` by one position per Meta-escape
6459    // we collapse).
6460    let mut t: usize = p;
6461    while p < s.len() {
6462        let b = s[p];
6463        s[t] = b;
6464        p += 1;
6465        if b == Meta && p < s.len() {
6466            // C: t[-1] = *p++ ^ 32; — overwrite the just-written
6467            // Meta with the un-escaped byte.
6468            s[t] = s[p] ^ 32;
6469            p += 1;
6470        }
6471        t += 1;
6472    }
6473    s.truncate(t);
6474    t
6475}
6476
6477/// Port of `int metalen(const char *s, int len)` from `Src/utils.c:4971-4983`.
6478/// ```c
6479/// int mlen = len;
6480/// while (len--) {
6481///     if (*s++ == Meta) { mlen++; s++; }
6482/// }
6483/// return mlen;
6484/// ```
6485/// Doc: "Return the character length of a metafied substring, given
6486/// the unmetafied substring length." So **input `len` is the
6487/// UNMETAFIED char count, output is the METAFIED byte count.**
6488///
6489/// Previously the Rust port had INVERTED semantics: looped while
6490/// `i < len` byte-walk, returned char count. That's the reverse
6491/// operation. Pin the correct C contract.
6492pub fn metalen(s: &str, len: usize) -> usize {
6493    // c:4972
6494    let bytes = s.as_bytes();
6495    let mut mlen = len; // c:4974
6496    let mut remaining = len;
6497    let mut i = 0;
6498    while remaining > 0 && i < bytes.len() {
6499        // c:4976 `while (len--)`
6500        if bytes[i] == Meta {
6501            // c:4977
6502            mlen += 1; // c:4978
6503            i += 2; // c:4979 s++ (already advanced past Meta)
6504        } else {
6505            i += 1;
6506        }
6507        remaining -= 1;
6508    }
6509    mlen
6510}
6511
6512/// Port of `unmeta(const char *file_name)` from `Src/utils.c:4994`.
6513///
6514/// Convert a zsh internal (metafied) string to a system-call-safe
6515/// form (e.g. for passing to `open(2)`).
6516///
6517/// C body shape (c:4994-5010):
6518/// ```c
6519/// meta = 0;
6520/// for (t = file_name; *t; t++)
6521///     if (*t == Meta) { meta = 1; break; }
6522/// if (!meta) return (char *) file_name;        // no-copy fast path
6523/// for (t = file_name, p = fn; *t; p++)
6524///     if ((*p = *t++) == Meta && *t)
6525///         *p = *t++ ^ 32;
6526/// ```
6527// Rust idiom replacement: byte-scan fast path + `unmetafy` covers
6528// the C in-place decode + alloc-on-copy dance; String owns its own
6529// allocation so no `heap` flag needed.
6530/// `unmeta` — see implementation.
6531pub fn unmeta(s: &str) -> String {
6532    // c:4994
6533    let bytes = s.as_bytes();
6534    // c:4995-4996 — Meta-byte scan; no-copy fast path.
6535    if !bytes.iter().any(|&b| b == Meta) {
6536        return s.to_string();
6537    }
6538    let mut buf = bytes.to_vec();
6539    let len = unmetafy(&mut buf); // c:4999-5001
6540    buf.truncate(len);
6541    String::from_utf8_lossy(&buf).into_owned()
6542}
6543
6544/// Port of `convchar_t unmeta_one(const char *in, int *sz)` from `Src/utils.c:5056-5086`.
6545/// Non-MULTIBYTE branch (c:5077-5083):
6546/// ```c
6547/// if (in[0] == Meta) { *sz = 2; wc = (unsigned char)(in[1] ^ 32); }
6548/// else               { *sz = 1; wc = (unsigned char) in[0]; }
6549/// ```
6550/// Returns `(decoded_char, bytes_consumed)`. The c:5070 NULL/empty
6551/// guard returns `(0, 0)`. Previously used hardcoded `0x83` for the
6552/// Meta byte — now routes through the canonical `Meta` constant
6553/// (defined as `'\u{83}'` at zsh.h:144).
6554/// WARNING: param names don't match C — Rust=(s) vs C=(in, sz)
6555pub fn unmeta_one(s: &str) -> (char, usize) {
6556    // c:5058
6557    let bytes = s.as_bytes();
6558    // c:5070 — `if (!in || !*in) return 0;`
6559    if bytes.is_empty() {
6560        return ('\0', 0);
6561    }
6562    // c:5077 — `if (in[0] == Meta)`.
6563    if bytes[0] == Meta && bytes.len() > 1 {
6564        // c:5078-5079 — `*sz = 2; wc = (unsigned char)(in[1] ^ 32);`.
6565        ((bytes[1] ^ 32) as char, 2)
6566    } else {
6567        // c:5081-5082 — `*sz = 1; wc = (unsigned char) in[0];`.
6568        (bytes[0] as char, 1)
6569    }
6570}
6571
6572/// Port of `ztrcmp(char const *s1, char const *s2)` from `Src/utils.c:5106`.
6573///
6574/// Byte-walking compare with lazy Meta resolution. C body skips
6575/// matching bytes wholesale, then on the first differing byte
6576/// un-meta-fies just that byte and compares. Faster than
6577/// `unmeta(s1).cmp(unmeta(s2))` because:
6578///   1. No allocation up front,
6579///   2. Only the first differing position pays the un-meta cost.
6580///
6581/// ```c
6582/// while(*s1 && *s1 == *s2) { s1++; s2++; }
6583/// if (!(c1 = *s1)) c1 = -1;
6584/// else if (c1 == Meta) c1 = *++s1 ^ 32;
6585/// if (!(c2 = *s2)) c2 = -1;
6586/// else if (c2 == Meta) c2 = *++s2 ^ 32;
6587/// return c1 - c2;
6588/// ```
6589pub fn ztrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
6590    // c:5106
6591    let b1 = s1.as_bytes();
6592    let b2 = s2.as_bytes();
6593    let mut i1 = 0;
6594    let mut i2 = 0;
6595    // Skip the matching prefix.
6596    while i1 < b1.len() && i2 < b2.len() && b1[i1] == b2[i2] {
6597        i1 += 1;
6598        i2 += 1;
6599    }
6600    // Resolve c1: -1 for end-of-string, else the next byte
6601    // (un-meta-fied if it's a Meta marker).
6602    let c1: i32 = if i1 >= b1.len() {
6603        -1
6604    } else if b1[i1] == Meta && i1 + 1 < b1.len() {
6605        (b1[i1 + 1] ^ 32) as i32
6606    } else {
6607        b1[i1] as i32
6608    };
6609    let c2: i32 = if i2 >= b2.len() {
6610        -1
6611    } else if b2[i2] == Meta && i2 + 1 < b2.len() {
6612        (b2[i2 + 1] ^ 32) as i32
6613    } else {
6614        b2[i2] as i32
6615    };
6616    c1.cmp(&c2)
6617}
6618
6619// pastebuf() DELETED — was a misplaced fn. The real `pastebuf()`
6620// lives in `Src/Zle/zle_misc.c:558` (a ZLE clipboard helper that
6621// pastes a Cutbuffer into the line-edit buffer). It has nothing to
6622// do with metafication, despite this file's prior body which
6623// reimplemented half of `metafy`. The real metafy lives below at
6624// `pub fn metafy()` (port of utils.c:4856).
6625
6626/// Unmetafied string length (from utils.c ztrlen lines 5135-5152)
6627pub fn ztrlen(s: &str) -> usize {
6628    // c:5136
6629    let mut len = 0;
6630    let chars: Vec<char> = s.chars().collect();
6631    let mut i = 0;
6632    while i < chars.len() {
6633        len += 1;
6634        if chars[i] as u32 == Meta as u32 && i + 1 < chars.len() {
6635            i += 2;
6636        } else {
6637            i += 1;
6638        }
6639    }
6640    len
6641}
6642
6643/// Port of `ztrlenend(char const *s, char const *eptr)` from `Src/utils.c:5162`.
6644///
6645/// ```c
6646/// for (l = 0; s < eptr; l++) {
6647///     if (*s++ == Meta) s++;  // skip past Meta-escaped pair
6648/// }
6649/// return l;
6650/// ```
6651///
6652/// Count the unmetafied character length from `s` up to `end`
6653/// bytes. Each Meta-escaped pair counts as 1 character.
6654/// Previous Rust port called `chars().count()` which counts UTF-8
6655/// codepoints, not byte-walked Meta-pairs — wrong semantics.
6656pub fn ztrlenend(s: &str, eptr: usize) -> usize {
6657    let bytes = s.as_bytes();
6658    let cap = eptr.min(bytes.len());
6659    let mut l = 0;
6660    let mut i = 0;
6661    while i < cap {
6662        if bytes[i] == Meta {
6663            // Meta sentinel + escaped byte = 1 visible char.
6664            i += 2;
6665        } else {
6666            i += 1;
6667        }
6668        l += 1;
6669    }
6670    l
6671}
6672
6673/// Port of `int ztrsub(char const *t, char const *s)` from `Src/utils.c:5185-5203`.
6674/// ```c
6675/// int l = t - s;
6676/// while (s != t) {
6677///     if (*s++ == Meta) { s++; l--; }
6678/// }
6679/// return l;
6680/// ```
6681/// "Subtract two pointers in a metafied string." `s` is the start
6682/// pointer, `t` is the end pointer; both point into the SAME buffer.
6683/// Returns the count of unmetafied chars in `[s, t)` — same as
6684/// `ztrlen` of the substring but expressed via pointer arithmetic.
6685///
6686/// Rust API: takes the full buffer and a byte-offset pair
6687/// `[start..end)` since Rust can't replicate raw-pointer subtraction
6688/// across distinct `&str` arguments without UB risk. The semantics
6689/// stay faithful: count of unmetafied chars between two offsets.
6690/// Previous Rust port took two unrelated `&str` and computed
6691/// `ztrlen(&t[..t.len()-s.len()])` — a fundamentally different
6692/// operation that worked only when `s` was the suffix of `t`.
6693/// WARNING: param names don't match C — Rust=(buf, start, end) vs C=(t, s)
6694pub fn ztrsub(buf: &str, start: usize, end: usize) -> usize {
6695    // c:5187
6696    let bytes = buf.as_bytes();
6697    let end = end.min(bytes.len());
6698    let start = start.min(end);
6699    let mut l = (end - start) as isize; // c:5189
6700    let mut i = start;
6701    while i < end {
6702        if bytes[i] == Meta {
6703            // c:5192
6704            i += 2; // c:5198
6705            l -= 1; // c:5199
6706        } else {
6707            i += 1;
6708        }
6709    }
6710    l.max(0) as usize
6711}
6712
6713/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
6714/// `Src/utils.c:5217`.
6715///
6716/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
6717/// `Src/utils.c:5217-5240`. Pulls the next entry from a DIR* the
6718/// caller owns; returns the entry's bare name (metafied String) or
6719/// `None` at EOF. The C source's `ignoredots` flag (c:5232) skips
6720/// `.` and `..` when set; callers (`spnamepat` at c:4648) that
6721/// want dot entries pass 0.
6722///
6723/// Caller pattern matches C exactly:
6724///   let mut dir = fs::read_dir(path)?;
6725///   while let Some(name) = zreaddir(&mut dir, 1) { ... }
6726///   // dir auto-closed on drop (= closedir)
6727pub fn zreaddir(dir: &mut fs::ReadDir, ignoredots: i32) -> Option<String> {
6728    // c:5217
6729    for entry in dir.by_ref() {
6730        // c:5221 readdir loop
6731        let Ok(e) = entry else { continue };
6732        let Ok(name) = e.file_name().into_string() else {
6733            continue;
6734        };
6735        // c:5232 — `if (ignoredots && de->d_name[0] == '.' &&
6736        //              (!de->d_name[1] || (de->d_name[1] == '.' && !de->d_name[2])))`.
6737        if ignoredots != 0 && (name == "." || name == "..") {
6738            continue; // c:5234
6739        }
6740        return Some(name); // c:5238 return de->d_name
6741    }
6742    None // c:5240 — fell off the end
6743}
6744
6745/// Port of `int zputs(char const *s, FILE *stream)` from `Src/utils.c:5263-5282`.
6746///
6747/// ```c
6748/// while (*s) {
6749///     if (*s == Meta)     c = *++s ^ 32;
6750///     else if (itok(*s)) { s++; continue; }
6751///     else                c = *s;
6752///     s++;
6753///     if (fputc(c, stream) < 0) return EOF;
6754/// }
6755/// return 0;
6756/// ```
6757///
6758/// Writes `s` to `stream` with Meta+X pair decoding and ITOK-byte
6759/// skipping. Returns `0` on success, `-1` on write error (mirroring
6760/// C's `EOF` sentinel for the int return).
6761pub fn zputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
6762    // c:5265
6763    let bytes = s.as_bytes(); // c:5265 *s walk
6764    let mut i = 0;
6765    while i < bytes.len() {
6766        // c:5267 while (*s)
6767        let c: u8; // c:5268 char c
6768        if bytes[i] == Meta {
6769            // c:5269 if (*s == Meta)
6770            // c:5270 — `c = *++s ^ 32;`
6771            if i + 1 < bytes.len() {
6772                c = bytes[i + 1] ^ 32;
6773                i += 1; // c:5270 ++s
6774            } else {
6775                i += 1;
6776                continue;
6777            }
6778        } else if itok(bytes[i]) {
6779            // c:5271 else if (itok(*s))
6780            // c:5272 — `s++; continue;` (skip token byte)
6781            i += 1;
6782            continue;
6783        } else {
6784            c = bytes[i]; // c:5274 else c = *s
6785        }
6786        i += 1; // c:5276 s++
6787        if stream.write_all(&[c]).is_err() {
6788            // c:5277 fputc(c, stream)
6789            return -1; // c:5278 return EOF
6790        }
6791    }
6792    0 // c:5280 return 0
6793}
6794
6795/// Port of `nicedup(char const *s, int heap)` from `Src/utils.c:5289`
6796/// (single-byte build) and `Src/utils.c:5530` (multibyte build).
6797///
6798/// C bodies:
6799/// ```c
6800/// /* Src/utils.c:5289 — !MULTIBYTE_SUPPORT */
6801/// char *retstr;
6802/// (void)sb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
6803/// return retstr;
6804///
6805/// /* Src/utils.c:5530 — MULTIBYTE_SUPPORT */
6806/// char *retstr;
6807/// (void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
6808/// return retstr;
6809/// ```
6810///
6811/// zshrs targets MULTIBYTE_SUPPORT (matches every modern zsh
6812/// build) — route through `mb_niceformat`. Previous Rust port
6813/// went through `sb_niceformat`, which is the !MULTIBYTE path —
6814/// strips wide-char handling and corrupts non-ASCII via byte-mask
6815/// `nicechar(c & 0xff)`.
6816///
6817/// C signature faithful: `(s, heap) -> String`. `heap` selects between
6818/// arena (`NICEFLAG_HEAP`) and persistent (default `ztrdup`) allocation
6819/// in C; Rust has a single allocator so both paths produce the same
6820/// owned `String`, but the `heap` parameter is preserved per Rule S1.
6821pub fn nicedup(s: &str, heap: i32) -> String {
6822    // c:5530
6823    // c:5532 — `char *retstr;`
6824    let retstr: Option<String>;
6825    // c:5534 — `(void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);`
6826    let mut slot: Option<String> = None;
6827    let _ = mb_niceformat(
6828        s,
6829        None,
6830        Some(&mut slot),
6831        if heap != 0 { NICEFLAG_HEAP } else { 0 },
6832    );
6833    retstr = slot;
6834    // c:5536 — `return retstr;`
6835    retstr.unwrap_or_default()
6836}
6837
6838/// Nice-format and duplicate string.
6839/// Port of `nicedupstring(char const *s)` from `Src/utils.c:5301`.
6840/// C body: `return nicedup(s, 1);` — heap-arena allocation form.
6841pub fn nicedupstring(s: &str) -> String {
6842    // c:5301
6843    nicedup(s, 1) // c:5303
6844}
6845
6846/// Nicely format a string
6847/// Port of `nicezputs(char const *s, FILE *stream)` from `Src/utils.c`.
6848///
6849/// Under `MULTIBYTE_SUPPORT` (the daily-driver path), C defines
6850/// `nicezputs(str, outs)` as a macro at zsh.h:3274:
6851///   `(void)mb_niceformat((str), (outs), NULL, 0)`.
6852/// Without MULTIBYTE (c:5313): `sb_niceformat(s, stream, NULL, 0)`.
6853///
6854/// The previous Rust impl was `s.chars().map(nicechar).collect()` —
6855/// wrong on every front:
6856///   1. No unmetafy step (Meta-byte pairs surfaced as `\M-…` mangled).
6857///   2. `nicechar` is byte-based (`c & 0xff`), corrupting non-ASCII
6858///      multibyte codepoints into spurious `\M-X` escapes.
6859///   3. No `itok` ZSH-token-byte handling.
6860///
6861/// Route through `mb_niceformat` to match the C macro under
6862/// MULTIBYTE — which itself unmetafies and uses `wcs_nicechar` for
6863/// proper wide-char handling.
6864pub fn nicezputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
6865    // c:5313 (non-MULTIBYTE) / zsh.h:3274 (MULTIBYTE macro:
6866    //   `(void)mb_niceformat((str), (outs), NULL, 0)`).
6867    let _ = mb_niceformat(s, Some(stream), None, 0);
6868    0 // c:5316 return 0
6869}
6870
6871/// Port of `niceztrlen(char const *s)` from `Src/utils.c:5324`.
6872///
6873/// Returns the length (in bytes) of the visible representation of
6874/// the metafied string `s`. C body walks each char via `nicechar`
6875/// and accumulates `strlen(nicechar(c))`; the Rust port mirrors
6876/// this via [`sb_niceformat`] which is the same render-then-measure
6877/// path.
6878pub fn niceztrlen(s: &str) -> usize {
6879    // c equivalent: l = sb_niceformat(s, NULL, NULL, 0); return l;
6880    sb_niceformat(s, None, None, 0)
6881}
6882
6883/// Multibyte-aware nice-format of a string.
6884/// Port of `mb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from Src/utils.c:5366. Walks the
6885/// (un-metafied) string char-by-char; for each control byte or
6886/// invalid sequence emits a `^X`/`\\xNN` representation, otherwise
6887/// passes the char through. The C source threads an
6888/// `mbstate_t` through `mbrtowc()` and falls back to single-byte
6889/// `\M-` notation on `MB_INVALID`; the Rust port uses Rust's
6890/// chars iterator which already produces valid scalar values, so
6891/// invalid-byte fallback collapses to the control-char branch.
6892/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream, outstrp, flags)
6893// Rust idiom replacement: `chars()` + `wcs_nicechar` covers the C
6894// mbrtowc loop with `MB_INVALID` fallback (Rust UTF-8 guarantees
6895// valid scalars, so the invalid-byte arm collapses).
6896/// `mb_niceformat` — see implementation.
6897pub fn mb_niceformat(
6898    s: &str,
6899    mut stream: Option<&mut dyn std::io::Write>,
6900    outstrp: Option<&mut Option<String>>,
6901    flags: i32,
6902) -> usize {
6903    // c:5366
6904    let mut l: usize = 0; // c:5368 size_t l = 0
6905    let mut newl: usize; // c:5368 size_t newl
6906                         // c:5369 — `int umlen, outalloc, outleft, eol = 0;`. outalloc/outleft
6907                         // model C's buffer-growth math; Rust String auto-grows so the realloc
6908                         // loop at c:5430-5440 collapses to push_str. umlen and eol carry.
6909    let mut umlen: usize; // c:5369
6910    let mut eol: bool = false; // c:5369 int eol = 0
6911                               // c:5370 — `wchar_t c;` (carried inside loop in Rust)
6912                               // c:5371 — `char *ums, *ptr, *fmt, *outstr, *outptr;`
6913    let mut ums: Vec<u8>; // c:5371 char *ums
6914    let mut ptr: usize; // c:5371 char *ptr
6915    let mut fmt: String; // c:5371 char *fmt
6916    let mut outstr: Option<String>; // c:5371 char *outstr
6917                                    // c:5372 — `mbstate_t mbs;` (Rust UTF-8 has no shift state; tracked
6918                                    // by the `valid_up_to` / `error_len` logic below).
6919
6920    // c:5374-5380 — `if (outstrp) outptr = outstr = zalloc(5*strlen(s));`
6921    if outstrp.is_some() {
6922        // c:5374
6923        outstr = Some(String::with_capacity(5 * s.len())); // c:5376
6924    } else {
6925        // c:5377
6926        outstr = None; // c:5379 outstr = NULL
6927    }
6928
6929    // c:5382 — `ums = ztrdup(s);`
6930    // c:5383-5387 — comment-only block carried verbatim:
6931    /*
6932     * is this necessary at this point? niceztrlen does this
6933     * but it's used in lots of places.  however, one day this may
6934     * be, too.
6935     */                                                                      // c:5383-5387
6936    // c:5388 — `untokenize(ums);` — Rust port uses the char-based
6937    // `lex::untokenize` (only fires on codepoints in the 0x84..=0xa1
6938    // ITOK range, never on UTF-8 continuation bytes); the byte-level
6939    // shape of C's untokenize is unsafe on raw UTF-8 input because a
6940    // continuation byte like 0x97 (part of `字` = 0xE5 0xAD 0x97) sits
6941    // inside the ITOK range and would be misclassified as a token.
6942    // c:5389 — `ptr = unmetafy(ums, &umlen);` — `unmeta` is the safe
6943    // UTF-8 wrapper that runs unmetafy only when Meta bytes are
6944    // present and otherwise no-ops.
6945    //
6946    // `unmetafy_str` (not `unmeta`) is what produces the raw byte buffer C
6947    // expects here. zshrs metafies at the CHARACTER level — an undecodable byte
6948    // is stored as U+0083 followed by `char::from(byte ^ 32)` — and U+0083
6949    // itself encodes as the two UTF-8 bytes C2 83. `unmeta` scans for a 0x83
6950    // BYTE, so it finds the trailing byte of that pair and mangles the string:
6951    // `$'\M-a'` came back out as `\M-^C\M-A` instead of `\M-a`.
6952    let detok = untokenize(s);
6953    ums = unmetafy_str(&detok);
6954    umlen = ums.len(); // c:5389 *umlen
6955    ptr = 0; // c:5389 ptr starts at 0 in ums
6956
6957    // c:5391 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
6958    while umlen > 0 {
6959        // c:5392
6960        // c:5393 — `cnt = eol ? MB_INVALID : mbrtowc(&c, ptr, umlen, &mbs);`
6961        // Rust equivalent: try to decode one UTF-8 scalar from ums[ptr..],
6962        // honoring `eol` (force the invalid arm when set).
6963        let cnt: usize;
6964        let decoded_c: Option<char>;
6965        if eol {
6966            // c:5396 — MB_INVALID arm via `eol = 1`.
6967            decoded_c = None;
6968            cnt = 1;
6969        } else {
6970            let remaining = &ums[ptr..ptr + umlen];
6971            match std::str::from_utf8(remaining) {
6972                Ok(s_slice) => {
6973                    // Valid UTF-8 throughout remaining; consume one char.
6974                    if let Some(ch) = s_slice.chars().next() {
6975                        decoded_c = Some(ch);
6976                        cnt = ch.len_utf8();
6977                    } else {
6978                        // Empty? Shouldn't happen given umlen > 0, but
6979                        // mirror MB_INVALID.
6980                        decoded_c = None;
6981                        cnt = 1;
6982                    }
6983                }
6984                Err(e) => {
6985                    let valid_up_to = e.valid_up_to();
6986                    if valid_up_to > 0 {
6987                        // Decode the first valid char then continue.
6988                        let valid_slice =
6989                            unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
6990                        let ch = valid_slice.chars().next().unwrap();
6991                        decoded_c = Some(ch);
6992                        cnt = ch.len_utf8();
6993                    } else if e.error_len().is_none() {
6994                        // Incomplete sequence at end of input — MB_INCOMPLETE.
6995                        // c:5394 — `case MB_INCOMPLETE: eol = 1; FALL THROUGH`
6996                        eol = true;
6997                        decoded_c = None; // MB_INVALID
6998                        cnt = 1;
6999                    } else {
7000                        // Definite invalid byte — MB_INVALID.
7001                        decoded_c = None;
7002                        cnt = 1;
7003                    }
7004                }
7005            }
7006        }
7007
7008        // c:5396-5403 / c:5404-5424 switch dispatch on cnt/decoded_c.
7009        let cnt_used: usize;
7010        match decoded_c {
7011            None => {
7012                // c:5397 — `case MB_INVALID:` (or fall-through from
7013                // MB_INCOMPLETE at c:5394).
7014                // c:5400 — `fmt = nicechar_sel(*ptr, flags & NICEFLAG_QUOTE);`
7015                fmt = nicechar_sel(ums[ptr] as char, (flags & NICEFLAG_QUOTE) != 0);
7016                newl = fmt.len(); // c:5401 newl = strlen(fmt)
7017                cnt_used = 1; // c:5402 cnt = 1
7018                              // c:5403 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless)
7019            }
7020            Some(c) if c == '\0' => {
7021                // c:5406 — `case 0:` — '\0' decodes to 0; consume 1 byte
7022                // and fall through to default.
7023                cnt_used = 1; // c:5409
7024                              // c:5411-5421 default arm (FALL THROUGH from case 0)
7025                if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
7026                    // c:5413
7027                    fmt = "\\'".to_string(); // c:5414
7028                    newl = 2; // c:5415
7029                } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
7030                    // c:5417
7031                    fmt = "\\\\".to_string(); // c:5418
7032                    newl = 2; // c:5419
7033                } else {
7034                    // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
7035                    //                                 flags & NICEFLAG_QUOTE);`
7036                    let mut width: usize = 0;
7037                    fmt =
7038                        wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
7039                    newl = width;
7040                }
7041            }
7042            Some(c) => {
7043                // c:5411 — `default:` arm (cnt > 0, valid char).
7044                cnt_used = cnt;
7045                if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
7046                    // c:5413
7047                    fmt = "\\'".to_string(); // c:5414
7048                    newl = 2; // c:5415
7049                } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
7050                    // c:5417
7051                    fmt = "\\\\".to_string(); // c:5418
7052                    newl = 2; // c:5419
7053                } else {
7054                    // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
7055                    //                                 flags & NICEFLAG_QUOTE);`
7056                    let mut width: usize = 0;
7057                    fmt =
7058                        wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
7059                    newl = width;
7060                }
7061            }
7062        }
7063
7064        umlen -= cnt_used; // c:5427 umlen -= cnt
7065        ptr += cnt_used; // c:5428 ptr += cnt
7066        l += newl; // c:5429 l += newl
7067
7068        if let Some(ref mut w) = stream {
7069            // c:5431 if (stream)
7070            // c:5432 — `zputs(fmt, stream);`
7071            let _ = w.write_all(fmt.as_bytes());
7072        }
7073        if let Some(ref mut buf) = outstr {
7074            // c:5433 if (outstr)
7075            // c:5434-5446 — append fmt to outstr, growing on demand. Rust
7076            // String auto-grows; the realloc loop collapses to push_str.
7077            buf.push_str(&fmt); // c:5446 memcpy(outptr, fmt, outlen)
7078        }
7079        let _ = fmt;
7080    }
7081
7082    // c:5451 — `free(ums);` (Rust drop at scope exit)
7083    drop(ums);
7084    if let Some(slot) = outstrp {
7085        // c:5452 if (outstrp)
7086        // c:5453 — `*outptr = '\0';` (no-op for Rust String)
7087        // c:5455-5460 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping. Rust has
7088        // a single allocator so all three paths produce identical owned
7089        // String contents; transfer ownership into caller's slot.
7090        *slot = outstr.take();
7091    }
7092
7093    l // c:5462 return l
7094}
7095
7096/// Port of `is_mb_niceformat(const char *s)` from `Src/utils.c:5474`.
7097///
7098/// Predicate: would any character in `s` need representation by
7099/// `mb_niceformat` / `nicedup`? C body:
7100/// ```c
7101/// ums = ztrdup(s);
7102/// untokenize(ums);
7103/// ptr = unmetafy(ums, &umlen);
7104/// while (umlen > 0) {
7105///     cnt = mbrtowc(&c, ptr, umlen, &mbs);
7106///     switch (cnt) {
7107///     case MB_INCOMPLETE: case MB_INVALID:
7108///         if (is_nicechar(*ptr)) { ret = 1; break; }
7109///         cnt = 1;
7110///         memset(&mbs, 0, sizeof mbs);
7111///         break;
7112///     case 0: cnt = 1;  /* FALLTHROUGH */
7113///     default:
7114///         if (is_wcs_nicechar(c)) ret = 1;
7115///         break;
7116///     }
7117///     if (ret) break;
7118///     umlen -= cnt; ptr += cnt;
7119/// }
7120/// ```
7121///
7122/// Rust port: unmetafy in place via [`unmetafy`], then walk the
7123/// resulting bytes. For valid UTF-8 sequences, check
7124/// `is_wcs_nicechar(scalar)`; for invalid bytes, check
7125/// `is_nicechar(byte)`. Either path bailing positive returns true.
7126pub fn is_mb_niceformat(s: &str) -> i32 {
7127    // c:5474
7128    // c:5476 — `int umlen, ret = 0;`
7129    let umlen: usize; // c:5476
7130    let mut ret: i32 = 0; // c:5476
7131                          // c:5477 — `char *ums, *ptr;` (eptr modelled by bytes.len())
7132    let mut ums: Vec<u8>; // c:5477
7133    let mut ptr: usize; // c:5477
7134
7135    // c:5481-5483 — `ums = ztrdup(s); untokenize(ums); ptr =
7136    //                unmetafy(ums, &umlen);`. Use BYTE-level `unmetafy_str`,
7137    // not char-level `unmeta`: `unmeta` decodes a metafied eight-bit byte
7138    // (Meta 0x83 + 0xC1 for `$'\M-a'`) into the valid Rust char U+00E1
7139    // (`á`), which the UTF-8 scan below reads as a PRINTABLE Latin-1 letter
7140    // → is_mb_niceformat=0 → `(q+)`/quotedzputs left the raw byte unquoted
7141    // (`\341`) where zsh emits `$'\M-a'`. `unmetafy_str` preserves the raw
7142    // 0xE1 byte (C's `unmetafy`), so the MB_INVALID arm's is_nicechar(0xE1)
7143    // fires (high-bit → nice) exactly as C does.
7144    let detok = untokenize(s);
7145    ums = unmetafy_str(&detok);
7146    umlen = ums.len(); // c:5483 *umlen
7147    ptr = 0; // c:5483 ptr starts at 0
7148
7149    // c:5485 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
7150    while ret == 0 && ptr < ums.len() {
7151        // c:5486 while (umlen > 0)
7152        let remaining = &ums[ptr..];
7153        match std::str::from_utf8(remaining) {
7154            Ok(s_slice) => {
7155                // c:5503-5511 — `default: if (is_wcs_nicechar(c)) ret = 1;`
7156                for ch in s_slice.chars() {
7157                    if is_wcs_nicechar(ch) {
7158                        // c:5508
7159                        ret = 1; // c:5509
7160                        break;
7161                    }
7162                }
7163                break;
7164            }
7165            Err(e) => {
7166                let valid_up_to = e.valid_up_to();
7167                if valid_up_to > 0 {
7168                    let valid = unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
7169                    for ch in valid.chars() {
7170                        if is_wcs_nicechar(ch) {
7171                            // c:5508
7172                            ret = 1; // c:5509
7173                            break;
7174                        }
7175                    }
7176                    if ret != 0 {
7177                        break;
7178                    }
7179                    ptr += valid_up_to;
7180                    continue;
7181                }
7182                // c:5493-5498 — `case MB_INVALID: if (is_nicechar(*ptr))
7183                //                ret = 1; break;`
7184                if is_nicechar(remaining[0] as char) {
7185                    // c:5494
7186                    ret = 1; // c:5495
7187                    break;
7188                }
7189                ptr += 1;
7190            }
7191        }
7192    }
7193
7194    drop(ums); // c:5519 free(ums)
7195    ret // c:5523 return ret
7196}
7197
7198/// Multibyte metachar length with conversion (from utils.c mb_metacharlenconv_r)
7199/// Port of `mb_metacharlenconv_r(const char *s, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5548`.
7200/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, wcp, mbsp)
7201// Rust idiom replacement: Rust strings are UTF-8 with valid scalar
7202// values, so `chars().next()` covers the mbrtowc state machine; no
7203// mbstate_t threading required.
7204/// `mb_metacharlenconv_r` — see implementation.
7205pub fn mb_metacharlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
7206    if let Some(c) = s[pos..].chars().next() {
7207        (c.len_utf8(), Some(c))
7208    } else {
7209        (0, None)
7210    }
7211}
7212
7213/// Multibyte-aware metafied-string char advance.
7214/// Port of `mb_metacharlenconv(const char *s, wint_t *wcp)` from Src/utils.c:5611. Returns
7215/// `(bytes_consumed, scalar_char)` for the next char in `s`. C
7216/// source dispatches to `mb_metacharlenconv_r()` for true
7217/// multibyte; we use Rust's UTF-8 char iterator which already
7218/// handles multi-byte correctly. ASCII fast-path: `Meta+X` is 2
7219/// bytes consumed → `(2, X^32)`; bare ASCII is `(1, c)`.
7220/// WARNING: param names don't match C — Rust=(s) vs C=(s, wcp)
7221pub fn mb_metacharlenconv(s: &str) -> (usize, Option<char>) {
7222    let bytes = s.as_bytes();
7223    if bytes.is_empty() {
7224        return (0, None);
7225    }
7226    if bytes[0] == 0x83 && bytes.len() >= 2 {
7227        // Meta+X pair → unescape.
7228        let raw = bytes[1] as u32 ^ 32;
7229        return (2, char::from_u32(raw));
7230    }
7231    if bytes[0] <= 0x7f {
7232        return (1, Some(bytes[0] as char));
7233    }
7234    // Multi-byte UTF-8 — let Rust decode.
7235    if let Some(c) = s.chars().next() {
7236        return (c.len_utf8(), Some(c));
7237    }
7238    (1, None)
7239}
7240
7241/// Multibyte metastring length to end (from utils.c mb_metastrlenend)
7242/// Port of `mb_metastrlenend(char *ptr, int width, char *eptr)` from `Src/utils.c:5655`.
7243///
7244/// Counts characters (`width == false`) or display columns
7245/// (`width == true`) in a metafied string. C walks the metafied
7246/// byte stream, un-metafies inline (`if (*ptr == Meta) inchar =
7247/// *++ptr ^ 32`, c:5672), and feeds one byte at a time to
7248/// `mbrtowc` to assemble each multibyte character (c:5691); a
7249/// complete character counts 1 (or its `WCWIDTH`), an invalid byte
7250/// counts as a single character (c:5712-5714).
7251///
7252/// Rust-port note: zshrs stores both metafied byte-pairs (`Meta` +
7253/// `byte ^ 32`, produced by `getkeystring` for `$'\xNN'` escapes)
7254/// AND real multibyte `char`s (the literal `$'é'` path) in one
7255/// `String`, whereas C only ever holds metafied bytes. So the port
7256/// first reduces the slice to its raw byte stream via `unmetafy_str`
7257/// (metafied pair → its byte; real `char` → its UTF-8 bytes — the
7258/// same bytes C's inline demetafy would yield), then runs C's
7259/// `mbrtowc` character-assembly loop over those bytes. Rust's UTF-8
7260/// validation is the `mbrtowc` analogue: the shortest valid prefix
7261/// is one character; a byte that begins no valid sequence counts as
7262/// one (c:5712).
7263pub fn mb_metastrlenend(ptr: &str, width: bool, eptr: usize) -> usize {
7264    // c:5672 — un-metafy the (optionally end-bounded) slice to raw bytes.
7265    let bytes = unmetafy_str(&ptr[..eptr.min(ptr.len())]);
7266    let mut num: usize = 0; // c:5658 size_t ret / counter
7267    let mut i: usize = 0;
7268    while i < bytes.len() {
7269        // c:5678-5687 — 7-bit US-ASCII subset: one character, skip mbrtowc.
7270        if bytes[i] <= 0x7f {
7271            num += 1;
7272            i += 1;
7273            continue;
7274        }
7275        // c:5691 — mbrtowc: take the shortest valid UTF-8 character.
7276        let mut num_in_char: usize = 1; // c:5701 trailing-octet span
7277        let hi = (i + 4).min(bytes.len());
7278        for end in (i + 1)..=hi {
7279            if std::str::from_utf8(&bytes[i..end]).is_ok() {
7280                num_in_char = end - i;
7281                break;
7282            }
7283        }
7284        if width {
7285            // c:5717-5723 — WCWIDTH of the assembled character (≥0).
7286            let wcw = std::str::from_utf8(&bytes[i..i + num_in_char])
7287                .ok()
7288                .and_then(|s| s.chars().next())
7289                .and_then(unicode_width::UnicodeWidthChar::width)
7290                .unwrap_or(0);
7291            num += wcw;
7292        } else {
7293            // c:5727 — one character (complete, or invalid treated as one).
7294            num += 1;
7295        }
7296        i += num_in_char;
7297    }
7298    num
7299}
7300
7301/// Multibyte char length with conversion (from utils.c mb_charlenconv_r)
7302/// Port of `mb_charlenconv_r(const char *s, int slen, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5747`.
7303/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp, mbsp)
7304pub fn mb_charlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
7305    mb_metacharlenconv_r(s, pos)
7306}
7307
7308/// Multibyte char length (from utils.c mb_charlenconv)
7309/// Port of `mb_charlenconv(const char *s, int slen, wint_t *wcp)` from `Src/utils.c:5793`.
7310/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp)
7311pub fn mb_charlenconv(s: &str, pos: usize) -> usize {
7312    s[pos..].chars().next().map(|c| c.len_utf8()).unwrap_or(0)
7313}
7314
7315/// Port of `int metacharlenconv(const char *x, int *c)` from `Src/utils.c:5810-5826`.
7316/// ```c
7317/// if (*x == Meta) {
7318///     if (c) *c = x[1] ^ 32;
7319///     return 2;
7320/// }
7321/// if (c) *c = (char)*x;
7322/// return 1;
7323/// ```
7324/// Single-byte metafied char advance — Meta+X is 2 bytes (decode
7325/// via XOR 32), plain byte is 1 byte. Previously used hardcoded
7326/// `0x83`; now routes through the canonical `Meta` const for
7327/// maintainability parity.
7328/// WARNING: param names don't match C — Rust=(s) vs C=(x, c)
7329pub fn metacharlenconv(s: &str) -> (usize, Option<char>) {
7330    // c:5811
7331    let bytes = s.as_bytes();
7332    if bytes.is_empty() {
7333        return (0, None);
7334    }
7335    if bytes[0] == Meta && bytes.len() >= 2 {
7336        // c:5818
7337        let raw = bytes[1] as u32 ^ 32; // c:5820
7338        return (2, char::from_u32(raw)); // c:5821
7339    }
7340    (1, Some(bytes[0] as char)) // c:5823-5825
7341}
7342
7343/// Plain (non-metafy) char advance.
7344/// Port of `charlenconv(const char *x, int len, int *c)` from Src/utils.c:5832 — the
7345/// non-MULTIBYTE_SUPPORT branch. Single-byte read with
7346/// `len`-bound check; matches the C source's `if (!len)` early
7347/// exit.
7348/// WARNING: param names don't match C — Rust=(s, len) vs C=(x, len, c)
7349pub fn charlenconv(s: &str, len: usize) -> (usize, Option<char>) {
7350    if len == 0 {
7351        return (0, None);
7352    }
7353    let bytes = s.as_bytes();
7354    if bytes.is_empty() {
7355        return (0, None);
7356    }
7357    (1, Some(bytes[0] as char))
7358}
7359
7360/// Port of `size_t sb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from `Src/utils.c:5849-5910`.
7361/// ```c
7362/// ums = ztrdup(s); untokenize(ums); ptr = unmetafy(ums, &umlen);
7363/// while (ptr < eptr) {
7364///     int c = (unsigned char) *ptr;
7365///     if      (c == '\''  && (flags & NICEFLAG_QUOTE)) fmt = "\\'";
7366///     else if (c == '\\'  && (flags & NICEFLAG_QUOTE)) fmt = "\\\\";
7367///     else                                              fmt = nicechar_sel(c, ...);
7368/// }
7369/// ```
7370/// Single-byte nice format. **Unmetafies the input first** (c:5872),
7371/// then calls `nicechar_sel` for EVERY byte (not just controls).
7372///
7373/// Param mapping (Rule S1, faithful to C):
7374/// - `s: &str`            ← C `const char *s`
7375/// - `stream: Option<&mut dyn Write>` ← C `FILE *stream` (`None` ≡ `NULL`)
7376/// - `outstrp: Option<&mut Option<String>>` ← C `char **outstrp`
7377///   (outer `None` ≡ `NULL`; inner `Option<String>` is the storage slot
7378///   the caller passes as `&local`, mirroring `char *retstr;
7379///   sb_niceformat(s, NULL, &retstr, ...);`).
7380/// - returns `usize` ← C `size_t l`.
7381pub fn sb_niceformat(
7382    s: &str,
7383    mut stream: Option<&mut dyn std::io::Write>,
7384    outstrp: Option<&mut Option<String>>,
7385    flags: i32,
7386) -> usize {
7387    // c:5851
7388    let mut l: usize = 0; // c:5853 size_t l = 0
7389    let mut newl: usize; // c:5853 size_t newl
7390                         // c:5854 — `int umlen, outalloc, outleft;`. outalloc/outleft model
7391                         // C's buffer-growth math; Rust String auto-grows so the realloc
7392                         // loop at c:5897-5906 collapses to push_str. umlen carries through.
7393    let umlen: usize; // c:5854
7394                      // c:5855 — `char *ums, *ptr, *eptr, *fmt, *outstr, *outptr;`
7395    let mut ums: Vec<u8>; // c:5855 char *ums
7396    let mut ptr: usize; // c:5855 char *ptr
7397    let eptr: usize; // c:5855 char *eptr
7398    let mut fmt: String; // c:5855 char *fmt
7399    let mut outstr: Option<String>; // c:5855 char *outstr
7400
7401    // c:5857-5863 — `if (outstrp) outptr = outstr = zalloc(2*strlen(s));`
7402    if outstrp.is_some() {
7403        // c:5857
7404        outstr = Some(String::with_capacity(2 * s.len())); // c:5859
7405    } else {
7406        // c:5860
7407        outstr = None; // c:5862 outstr = NULL
7408    }
7409
7410    // c:5865 — `ums = ztrdup(s);`
7411    ums = s.as_bytes().to_vec();
7412    // c:5866-5870 — comment-only block carried verbatim:
7413    /*
7414     * is this necessary at this point? niceztrlen does this
7415     * but it's used in lots of places.  however, one day this may
7416     * be, too.
7417     */                                                                      // c:5866-5870
7418    // c:5871 — `untokenize(ums);` inlined byte-level (lex::untokenize
7419    // is char-based; C does byte-walks). Mirrors `Src/exec.c:2077-2099`
7420    // exec.c untokenize().
7421    let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; // ZTOKENS — Src/lex.c:38
7422    let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
7423    for &c in &ums {
7424        // exec.c:2082
7425        if (0x84u8..=0xa1u8).contains(&c) {
7426            // exec.c:2083 itok(c)
7427            if c != 0xa1u8 {
7428                // exec.c:2086 c != Nularg
7429                let idx = (c - 0x84) as usize;
7430                if idx < ztokens_table.len() {
7431                    detok.push(ztokens_table[idx]);
7432                }
7433            }
7434        } else {
7435            detok.push(c); // exec.c:2094
7436        }
7437    }
7438    ums = detok;
7439    // c:5872 — `ptr = unmetafy(ums, &umlen);`
7440    umlen = unmetafy(&mut ums);
7441    ums.truncate(umlen);
7442    eptr = umlen; // c:5873 eptr = ptr + umlen
7443    ptr = 0; // c:5872 ptr starts at 0 in ums
7444
7445    while ptr < eptr {
7446        // c:5875
7447        let c: i32 = ums[ptr] as i32; // c:5876 int c = (unsigned char) *ptr
7448        if c == b'\'' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
7449            // c:5877
7450            fmt = "\\'".to_string(); // c:5878
7451            newl = 2; // c:5879
7452        } else if c == b'\\' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
7453            // c:5881
7454            fmt = "\\\\".to_string(); // c:5882
7455            newl = 2; // c:5883
7456        } else {
7457            // c:5885
7458            fmt = nicechar_sel(c as u8 as char, (flags & NICEFLAG_QUOTE) != 0); // c:5886
7459            newl = 1; // c:5887
7460        }
7461
7462        ptr += 1; // c:5890 ++ptr
7463        l += newl; // c:5891 l += newl
7464
7465        if let Some(ref mut w) = stream {
7466            // c:5893 if (stream)
7467            // c:5894 — `zputs(fmt, stream);`
7468            let _ = w.write_all(fmt.as_bytes());
7469        }
7470        if let Some(ref mut buf) = outstr {
7471            // c:5895 if (outstr)
7472            // c:5896-5912 — append fmt to outstr, growing on demand. Rust
7473            // String auto-grows; the realloc loop at c:5897-5906 collapses
7474            // to push_str (memcpy + outptr/outleft bookkeeping unneeded).
7475            buf.push_str(&fmt); // c:5907 memcpy(outptr, fmt, outlen)
7476        }
7477        // `fmt` is consumed at next iter assignment (C reuses pointer).
7478        let _ = fmt;
7479    }
7480
7481    // c:5915 — `free(ums);` (Rust drop at scope exit)
7482    drop(ums);
7483    if let Some(slot) = outstrp {
7484        // c:5916 if (outstrp)
7485        // c:5917 — `*outptr = '\0';` (no-op for String — push_str leaves
7486        // no embedded NUL, and Rust String is length-prefixed).
7487        // c:5919-5925 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping: in C this
7488        // selects between ztrdup (perm) / dupstring (heap arena) / direct
7489        // ownership-transfer (NODUP). Rust has a single allocator so all
7490        // three paths produce identical owned String contents; transfer
7491        // ownership of `outstr` into the caller's slot.
7492        *slot = outstr.take();
7493    }
7494
7495    l // c:5928 return l
7496}
7497
7498/// Port of `int is_sb_niceformat(const char *s)` from `Src/utils.c:5937-5959`.
7499///
7500/// Predicate: would `sb_niceformat` change the input? Walks each byte
7501/// after unmetafy, returns `1` if any byte is "nice" per `is_nicechar`,
7502/// else `0` (C `int` return faithful to Rule S1).
7503pub fn is_sb_niceformat(s: &str) -> i32 {
7504    // c:5937
7505    // c:5939 — `int umlen, ret = 0;`
7506    let umlen: usize; // c:5939
7507    let mut ret: i32 = 0; // c:5939
7508                          // c:5940 — `char *ums, *ptr, *eptr;`
7509    let mut ums: Vec<u8>; // c:5940 char *ums
7510    let mut ptr: usize; // c:5940 char *ptr
7511    let eptr: usize; // c:5940 char *eptr
7512
7513    ums = s.as_bytes().to_vec(); // c:5942 ums = ztrdup(s)
7514                                 // c:5943 — `untokenize(ums);` inlined byte-level (lex::untokenize is
7515                                 // char-based; C does byte-walks). Mirrors `Src/exec.c:2077-2099`.
7516    let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; // ZTOKENS — Src/lex.c:38
7517    let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
7518    for &c in &ums {
7519        // exec.c:2082
7520        if (0x84u8..=0xa1u8).contains(&c) {
7521            // exec.c:2083 itok(c)
7522            if c != 0xa1u8 {
7523                // exec.c:2086 c != Nularg
7524                let idx = (c - 0x84) as usize;
7525                if idx < ztokens_table.len() {
7526                    detok.push(ztokens_table[idx]);
7527                }
7528            }
7529        } else {
7530            detok.push(c); // exec.c:2094
7531        }
7532    }
7533    ums = detok;
7534    umlen = unmetafy(&mut ums); // c:5944 ptr = unmetafy(ums, &umlen)
7535    ums.truncate(umlen);
7536    eptr = umlen; // c:5945 eptr = ptr + umlen
7537    ptr = 0; // c:5944 ptr starts at 0 in ums
7538
7539    while ptr < eptr {
7540        // c:5947
7541        if is_nicechar(ums[ptr] as char) {
7542            // c:5948 is_nicechar(*ptr)
7543            ret = 1; // c:5949
7544            break; // c:5950
7545        }
7546        ptr += 1; // c:5952 ++ptr
7547    }
7548
7549    drop(ums); // c:5955 free(ums)
7550    ret // c:5957 return ret
7551}
7552
7553/// 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.
7554/// Writes `s` into `out` with TAB characters expanded to spaces against
7555/// a tabstop of `width`. `startpos` carries the cumulative emitted
7556/// column from previous calls (used by `print -X` which preserves
7557/// alignment across args). When `all_tabs` is false, only leading TABs
7558/// (those at the start of a line) are expanded; embedded TABs are
7559/// emitted verbatim and `startpos` is advanced by one tabstop. When
7560/// `all_tabs` is true, every TAB expands. Returns the new `startpos`.
7561pub(crate) fn zexpandtabs(
7562    s: &str,
7563    width: i32,
7564    startpos: i32,
7565    all_tabs: bool,
7566    out: &mut String,
7567) -> i32 {
7568    let mut startpos = startpos;
7569    let mut at_start = true;
7570    for c in s.chars() {
7571        if c == '\t' {
7572            if all_tabs || at_start {
7573                if width <= 0 || startpos % width == 0 {
7574                    out.push(' ');
7575                    startpos += 1;
7576                }
7577                if width > 0 {
7578                    while startpos % width != 0 {
7579                        out.push(' ');
7580                        startpos += 1;
7581                    }
7582                }
7583            } else {
7584                let rem = startpos % width;
7585                startpos += width - rem;
7586                out.push('\t');
7587            }
7588            continue;
7589        } else if c == '\n' || c == '\r' {
7590            out.push(c);
7591            startpos = 0;
7592            at_start = true;
7593            continue;
7594        }
7595        at_start = false;
7596        out.push(c);
7597        startpos += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as i32;
7598    }
7599    startpos
7600}
7601
7602/// Port of `int hasspecial(char const *s)` from `Src/utils.c:6072-6082`.
7603/// ```c
7604/// while (*s) {
7605///     if (ispecial(*s == Meta ? *++s ^ 32 : *s)) return 1;
7606///     s++;
7607/// }
7608/// return 0;
7609/// ```
7610/// Predicate: does `s` contain any byte that needs shell-quoting?
7611/// Routes through the canonical typtab-driven `ztype_h::ispecial`
7612/// which respects the `ZTF_SP_COMMA` (set by `makecommaspecial`)
7613/// and `ZTF_BANGCHAR` (BANGHIST + interact) flags. Previously used
7614/// a hardcoded char list — diverged from C for `,` (KSH_GLOB
7615/// extended-glob) and for the dynamic bangchar (`!` by default but
7616/// user-rewritable via `$HISTCHARS`).
7617pub fn hasspecial(s: &str) -> bool {
7618    // c:6072
7619    let bytes = s.as_bytes();
7620    let mut i = 0;
7621    while i < bytes.len() {
7622        let c = if bytes[i] == Meta && i + 1 < bytes.len() {
7623            // c:6075 — `*s == Meta ? *++s ^ 32 : *s`.
7624            let v = bytes[i + 1] ^ 32;
7625            i += 2;
7626            v
7627        } else {
7628            let v = bytes[i];
7629            i += 1;
7630            v
7631        };
7632        if crate::ported::ztype_h::ispecial(c) {
7633            // c:6075
7634            return true;
7635        }
7636    }
7637    false
7638}
7639
7640/// Port of `static char *addunprintable(char *v, const char *u, const char *uend)`
7641/// from `Src/utils.c:6082-6124`. Renders unprintable bytes using
7642/// **shell-compatible C-string escapes**:
7643///   - `\0` (NUL, with `\000` form if next byte is octal-digit)
7644///   - `\a` (0x07 BEL), `\b` (BS), `\f` (FF), `\n` (LF),
7645///     `\r` (CR), `\t` (TAB), `\v` (VT)
7646///   - `\nnn` 3-digit octal fallback for any other control byte
7647///
7648/// Previously the Rust port emitted ZLE-style caret notation (`^X`,
7649/// `\u{:04x}`) — completely different convention. C semantics target
7650/// `printf %q` / `$'...'` reuse where the output must round-trip
7651/// through `printf %b` to reconstruct the original byte. Caret
7652/// notation is what ZLE *displays* but not what `addunprintable`
7653/// emits.
7654/// WARNING: param names don't match C — Rust=(c) vs C=(v, u, uend)
7655pub fn addunprintable(c: char) -> String {
7656    // c:6082
7657    let b = c as u32 & 0xff;
7658    match b as u8 {
7659        // c:6097-6103 — `\0`. C peeks next byte and uses `\000` form
7660        // when followed by an octal digit. Rust port works on a single
7661        // char so emits just `\0`; callers needing the disambiguation
7662        // form can pass the lookahead byte separately.
7663        0x00 => "\\0".to_string(),
7664        0x07 => "\\a".to_string(), // c:6106
7665        0x08 => "\\b".to_string(), // c:6107
7666        0x0c => "\\f".to_string(), // c:6108
7667        0x0a => "\\n".to_string(), // c:6109
7668        0x0d => "\\r".to_string(), // c:6110
7669        0x09 => "\\t".to_string(), // c:6111
7670        0x0b => "\\v".to_string(), // c:6112
7671        // c:6114-6119 — `\nnn` 3-digit octal default.
7672        _ => format!("\\{:o}{:o}{:o}", (b >> 6) & 7, (b >> 3) & 7, b & 7),
7673    }
7674}
7675
7676/// One element of a metafied string, as C's `MB_METACHARLENCONV` loop sees it:
7677/// either a decoded character, or a raw byte that is not one.
7678///
7679/// A byte that cannot appear in a Rust `String` (an invalid-UTF-8 byte such as
7680/// the 0xE1 produced by `$'\M-a'`) is stored metafied — `Meta` (0x83) followed
7681/// by `byte ^ 32` — so a plain `.chars()` walk sees two innocuous characters
7682/// where the shell means one raw byte, and passes them straight through. C hits
7683/// exactly this case as `WEOF` out of `MB_METACHARLENCONV` (c:6210) and routes
7684/// it to `addunprintable()`.
7685#[derive(Clone, Copy, PartialEq)]
7686enum MetaChar {
7687    Ch(char),
7688    Raw(u8),
7689}
7690
7691/// Quote a string according to the specified type
7692/// Port from zsh/Src/utils.c quotestring() (lines 6141-6452)
7693/// Quote a string per the requested bslashquote style.
7694/// Port of `quotestring(const char *s, int instring)` from Src/utils.c — used by `print
7695/// -%q`, `${(q)var}`, completion-output escaping, history
7696/// re-emission.
7697/// WARNING: param names don't match C — Rust=(s, quote_type) vs C=(s, instring)
7698pub fn quotestring(s: &str, quote_type: i32) -> String {
7699    // c:6207-6210 — C walks the string with MB_METACHARINIT/MB_METACHARLENCONV,
7700    // which yields one decoded character at a time, or WEOF for a byte that does
7701    // not decode. These three closures are that walk, kept inline exactly as C
7702    // keeps it inline in each arm below.
7703    //
7704    // `meta_chars` is the decoder: a byte that cannot live in a Rust `String`
7705    // (an invalid-UTF-8 byte such as the 0xE1 from `$'\M-a'`) is stored metafied
7706    // — Meta (0x83) then `byte ^ 32` — so a naive `.chars()` walk sees two
7707    // innocuous characters where the shell means one raw byte and passes them
7708    // straight through. That is C's WEOF case, and it must reach addunprintable.
7709    let meta_chars = |s: &str| -> Vec<MetaChar> {
7710        let mut out = Vec::with_capacity(s.len());
7711        let mut it = s.chars().peekable();
7712        while let Some(c) = it.next() {
7713            if c as u32 == Meta as u32 {
7714                if let Some(&n) = it.peek() {
7715                    let nu = n as u32;
7716                    if (0x80..=0xff).contains(&nu) {
7717                        it.next();
7718                        out.push(MetaChar::Raw((nu as u8) ^ 32));
7719                        continue;
7720                    }
7721                }
7722            }
7723            out.push(MetaChar::Ch(c));
7724        }
7725        out
7726    };
7727    // c:6212 — `cc != WEOF && WC_ISPRINT(cc)`. A raw byte is never printable: it
7728    // IS the WEOF case.
7729    let mc_printable = |mc: MetaChar| -> bool {
7730        match mc {
7731            MetaChar::Ch(c) => !c.is_control(),
7732            MetaChar::Raw(_) => false,
7733        }
7734    };
7735    // c:6082-6124 addunprintable — emit a non-printable element BYTE by byte, so
7736    // a non-printable multibyte character becomes one escape per UTF-8 byte.
7737    // `\0` widens to `\000` when an octal digit follows so the escape cannot
7738    // swallow it (c:6097-6103). There is deliberately no `\e` case: C has none,
7739    // so ESC comes out as the octal `\033`.
7740    let push_unprintable = |out: &mut String, mc: MetaChar, next: Option<MetaChar>| {
7741        let mut buf = [0u8; 4];
7742        let bytes: &[u8] = match mc {
7743            MetaChar::Ch(c) => c.encode_utf8(&mut buf).as_bytes(),
7744            MetaChar::Raw(b) => {
7745                buf[0] = b;
7746                &buf[0..1]
7747            }
7748        };
7749        for i in 0..bytes.len() {
7750            let b = bytes[i];
7751            if b == 0 {
7752                out.push_str("\\0");
7753                let follows = if i + 1 < bytes.len() {
7754                    Some(bytes[i + 1])
7755                } else {
7756                    next.map(|n| match n {
7757                        MetaChar::Ch(c) => {
7758                            let mut nb = [0u8; 4];
7759                            c.encode_utf8(&mut nb).as_bytes()[0]
7760                        }
7761                        MetaChar::Raw(rb) => rb,
7762                    })
7763                };
7764                if matches!(follows, Some(b'0'..=b'7')) {
7765                    out.push_str("00");
7766                }
7767            } else {
7768                out.push_str(&addunprintable(b as char));
7769            }
7770        }
7771    };
7772
7773    // c:6141
7774    if s.is_empty() {
7775        return if quote_type == QT_NONE {
7776            String::new()
7777        } else if quote_type == QT_BACKSLASH {
7778            // c:6194 `if (!*s && shownull)` — shownull is 0 for plain
7779            // QT_BACKSLASH, so an empty string produces NO quotes.
7780            String::new()
7781        } else if quote_type == QT_BACKSLASH_SHOWNULL {
7782            // c:6165 sets shownull=1, so empty → '' (c:6194 adds the pair).
7783            "''".to_string()
7784        } else if quote_type == QT_SINGLE || quote_type == QT_SINGLE_OPTIONAL {
7785            "''".to_string()
7786        } else if quote_type == QT_DOUBLE {
7787            "\"\"".to_string()
7788        } else if quote_type == QT_DOLLARS {
7789            "$''".to_string()
7790        } else {
7791            String::new()
7792        };
7793    }
7794
7795    if quote_type == QT_NONE {
7796        s.to_string()
7797    } else if quote_type == QT_BACKSLASH_PATTERN {
7798        // Only bslashquote pattern characters (lines 6242-6247)
7799        let mut result = String::with_capacity(s.len() * 2);
7800        for c in s.chars() {
7801            if matches!(
7802                c,
7803                '*' | '?' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '#' | '^' | '~'
7804            ) {
7805                result.push('\\');
7806            }
7807            result.push(c);
7808        }
7809        result
7810    } else if quote_type == QT_BACKSLASH || quote_type == QT_BACKSLASH_SHOWNULL {
7811        // c:Src/utils.c:6260-6452 QT_BACKSLASH. Three sub-cases per
7812        // input char (after the special-syntax handling above):
7813        //   - `\n` → emit `$'\n'` literal 5 bytes (c:6366-6371)
7814        //   - special-and-printable → emit `\<char>` (c:6385-6395)
7815        //   - non-printable (ctrl, multibyte outside printable
7816        //     range) → emit `$'<addunprintable>'` (c:6412-6422)
7817        // Bug #144/#149 in docs/BUGS.md: previous port mapped ALL
7818        // ispecial chars to `\<char>`, so `\n`/`\t`/etc. came out
7819        // as `\<actual-byte>` instead of `$'\n'`/`$'\t'`.
7820        //
7821        // c:6301-6306 — `=` and `~` are NOT escaped unless one of:
7822        //   (B) the char is at position 0 (u == s)
7823        //   (C) MAGICEQUALSUBST is set AND the previous byte is `=`
7824        //       or `:` (covers `var==val`, `var:=val`)
7825        //   (D) the char is `~` AND EXTENDEDGLOB is set
7826        // Without these conditions, mid-word `=`/`~` falls through
7827        // to the literal-emit path (c:6418-6434).
7828        let mut result = String::with_capacity(s.len() * 2);
7829        let mut prev: char = '\0'; // would-be u[-1]
7830        let mcs = meta_chars(s);
7831        for i in 0..mcs.len() {
7832            let mc = mcs[i];
7833            let c = match mc {
7834                MetaChar::Ch(c) => c,
7835                // A raw byte is never `\n` and never ispecial — it is the
7836                // not-printable case below. Stand in with its byte value so the
7837                // `=`/`~` lookbehind still advances correctly.
7838                MetaChar::Raw(b) => b as char,
7839            };
7840            // c:6301-6306 gate for `=`/`~` (condition A/B/C/D).
7841            let eq_tilde_gate = if c == '=' || c == '~' {
7842                i == 0 // c:6303 u == s
7843                    || (isset(MAGICEQUALSUBST) && (prev == '=' || prev == ':')) // c:6304-6305
7844                    || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
7845            } else {
7846                true // c:6302 *u != '=' && *u != '~'
7847            };
7848            if mc == MetaChar::Ch('\n') {
7849                // c:6366-6371 — newline gets dedicated `$'\n'` form.
7850                result.push_str("$'\\n'");
7851            } else if matches!(mc, MetaChar::Ch(_))
7852                && ispecial(c)
7853                && eq_tilde_gate
7854                && c.is_ascii()
7855                && !c.is_ascii_control()
7856            {
7857                // c:6385-6395 — printable special → `\<char>`.
7858                result.push('\\');
7859                result.push(c);
7860            } else if !mc_printable(mc) {
7861                // c:6412-6422 — anything not printable (a control char, a
7862                // non-printable multibyte char, or a raw undecodable byte) is
7863                // wrapped one element at a time as `$'<addunprintable>'`.
7864                result.push_str("$'");
7865                push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
7866                result.push('\'');
7867            } else {
7868                result.push(c);
7869            }
7870            prev = c;
7871        }
7872        result
7873    } else if quote_type == QT_SINGLE {
7874        // c:Src/utils.c:6300-6395 QT_SINGLE — `(qq)` flag.
7875        //   The outer ispecial-gated branch at c:6301-6313 only
7876        //   triggers special handling when the QT-specific
7877        //   subcondition matches; for QT_SINGLE that's `*u == '\''`.
7878        //   Newlines do NOT match, so they fall through to the
7879        //   pass-through path at c:6394 and are emitted LITERALLY
7880        //   inside the surrounding `'…'`. Bug #199 in docs/BUGS.md:
7881        //   previous port emitted `'$'\n''` (split-quote with
7882        //   `$'\n'` escape between segments) for newline, diverging
7883        //   from zsh's literal-newline-in-single-quotes output.
7884        let mut result = String::with_capacity(s.len() + 4);
7885        result.push('\'');
7886        for c in s.chars() {
7887            if c == '\'' {
7888                // c:6364-6379 — apostrophe close-reopen sequence.
7889                result.push_str("'\\''");
7890            } else {
7891                // Newline + everything else: pass through literally.
7892                result.push(c);
7893            }
7894        }
7895        result.push('\'');
7896        result
7897    } else if quote_type == QT_SINGLE_OPTIONAL {
7898        // c:Src/utils.c:6314-6385 QT_SINGLE_OPTIONAL — minimum
7899        // quoting. Walks the string with two states:
7900        //   quotesub=1 — not currently inside a quote span. Bare
7901        //     apostrophes get `\'` (backslash form). Any OTHER
7902        //     special char triggers back-filling: insert `'` at
7903        //     `quotestart` (start of unquoted prefix), shifting
7904        //     subsequent chars right, then push char, transition
7905        //     to quotesub=2.
7906        //   quotesub=2 — currently inside a `'…'` span. Bare
7907        //     apostrophes break the span: push `'\\'`, transition
7908        //     back to quotesub=1 with quotestart=position-after.
7909        //     Other specials append in-place.
7910        // End: if quotesub=2, close with `'`.
7911        // For "hello world" this yields `'hello world'` (back-
7912        // filled at start). For "it's" it yields `it\'s` (no quote
7913        // span ever opens). The naive per-char approach without
7914        // back-filling produced `hello' 'world` — parity bug.
7915        //
7916        // c:6301-6306 — the SAME `=`/`~` gate the QT_BACKSLASH arm applies.
7917        // `ispecial` alone would open a quote span on any mid-word `=` or `~`,
7918        // so `a=b` came out as `'a=b'` where zsh leaves it bare.
7919        let chars: Vec<char> = s.chars().collect();
7920        let forces_quote = |i: usize, c: char| -> bool {
7921            if !ispecial(c) {
7922                return false; // c:6301
7923            }
7924            if c == '=' || c == '~' {
7925                i == 0 // c:6303 u == s
7926                    || (isset(MAGICEQUALSUBST) && (chars[i - 1] == '=' || chars[i - 1] == ':')) // c:6304-6305
7927                    || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
7928            } else {
7929                true // c:6302
7930            }
7931        };
7932        let needs_quoting = chars.iter().enumerate().any(|(i, &c)| forces_quote(i, c));
7933        if !needs_quoting {
7934            return s.to_string();
7935        }
7936        let mut result: Vec<char> = Vec::with_capacity(s.len() + 4);
7937        let mut quotestart: usize = 0; // index in `result` where the next `'` would go
7938        let mut quotesub: u8 = 1; // 1 = not quoting, 2 = inside `'…'`
7939        for (i, &c) in chars.iter().enumerate() {
7940            if c == '\'' {
7941                if quotesub == 2 {
7942                    // close current quote span, then `\'`, then
7943                    // mark that we may need to reopen on next special
7944                    result.push('\'');
7945                    result.push('\\');
7946                    result.push('\'');
7947                    quotesub = 1;
7948                    quotestart = result.len();
7949                } else {
7950                    result.push('\\');
7951                    result.push('\'');
7952                    quotestart = result.len();
7953                }
7954            } else if forces_quote(i, c) {
7955                if quotesub == 1 {
7956                    // Back-fill: insert `'` at quotestart, shifting
7957                    // everything after right by 1.
7958                    result.insert(quotestart, '\'');
7959                    quotesub = 2;
7960                }
7961                result.push(c);
7962            } else {
7963                result.push(c);
7964            }
7965        }
7966        if quotesub == 2 {
7967            result.push('\'');
7968        }
7969        result.into_iter().collect()
7970    } else if quote_type == QT_DOUBLE {
7971        // Double quote: "string" (lines 6272-6280, 6311-6312)
7972        let mut result = String::with_capacity(s.len() + 4);
7973        result.push('"');
7974        for c in s.chars() {
7975            if matches!(c, '$' | '`' | '"' | '\\') {
7976                result.push('\\');
7977            }
7978            result.push(c);
7979        }
7980        result.push('"');
7981        result
7982    } else if quote_type == QT_DOLLARS {
7983        // c:6203-6241 — `$'…'` quoting. Each element is either printable, in
7984        // which case it is emitted verbatim (backslashing `\`, `'`, and — under
7985        // BANGHIST — the history character), or it is NOT, in which case C hands
7986        // its raw bytes to addunprintable(): a named escape (`\t`, `\n`, `\a`, …)
7987        // or a 3-digit octal one.
7988        //
7989        // Two things this must NOT do, both of which the previous port did:
7990        //   * pass an undecodable byte through raw. `$'\M-a'` is the byte 0xE1,
7991        //     which is not valid UTF-8 and is stored metafied; walking `.chars()`
7992        //     re-emitted it verbatim instead of `\341`, so the output could not
7993        //     be re-parsed.
7994        //   * emit `\e` for ESC. C's addunprintable has no `\e` case — ESC comes
7995        //     out as the octal `\033`.
7996        let mut result = String::with_capacity(s.len() + 4);
7997        result.push_str("$'");
7998        let mcs = meta_chars(s);
7999        let bang = crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
8000        for i in 0..mcs.len() {
8001            let mc = mcs[i];
8002            if let (true, MetaChar::Ch(c)) = (mc_printable(mc), mc) {
8003                // c:6214-6224
8004                if c == '\\' || c == '\'' || (isset(BANGHIST) && bang != 0 && c as u32 == bang as u32)
8005                {
8006                    result.push('\\');
8007                }
8008                result.push(c);
8009            } else {
8010                // c:6226-6229
8011                push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
8012            }
8013        }
8014        result.push('\'');
8015        result
8016    } else if quote_type == QT_BACKTICK {
8017        // Backtick quoting (minimal - just escape backticks)
8018        s.replace('`', "\\`")
8019    } else {
8020        // Unknown quote_type — treat as no-op to match C's `default:` arm.
8021        s.to_string()
8022    }
8023}
8024
8025// ===========================================================
8026// xtrace helpers moved from src/ported/vm_helper.
8027// printprompt4 is a direct port of utils.c:1718-1735; quotedzputs
8028// is its argument-formatter companion (zsh formats `set -x` lines
8029// via the same utils.c path).
8030// ===========================================================
8031
8032/// Port of `quotedzputs(char const *s, FILE *stream)` from `Src/utils.c:6464`.
8033///
8034/// Quote a string for re-readable output (`set -x`, `typeset -p`,
8035/// `set` listing, etc.). zsh's algorithm under MULTIBYTE_SUPPORT
8036/// (c:6464-6543):
8037///   1. empty input → `''`                                     (c:6470-6475)
8038///   2. needs nice-format (controls / non-printables) → emit
8039///      `$'<mb_niceformat output>'`                            (c:6478-6492)
8040///   3. no SPECCHARS member → return string unchanged          (c:6511-6517)
8041///   4. otherwise wrap in `'…'` (Bourne) or RCQUOTES form,
8042///      with embedded `'` rewritten as `'\''` / `''`           (c:6533-6587)
8043///
8044/// Previous Rust port omitted step 2 (the `$'…'` branch).
8045/// Result: strings containing control bytes (`\n`, `\t`, escape
8046/// sequences) were single-quoted instead of `$'…'`-quoted,
8047/// breaking round-trip through `typeset -p` / `set` / `set -x`
8048/// because POSIX single-quotes are *strong* — embedded `\n` would
8049/// be re-fed as a literal newline rather than the C-escape.
8050///
8051/// The C signature is `char *quotedzputs(char const *s, FILE *stream)`
8052/// — when `stream` is non-NULL it writes there and returns NULL,
8053/// otherwise it returns the quoted string. Rust's variant covers
8054/// only the `stream==NULL` form (the `set -x` callers all want the
8055/// string back, not direct stdout writing). The `stream==Some` form
8056/// is fputs/fputc-direct in C; Rust callers that need writer-based
8057/// output should compose `print!`/`write!` with this fn's return.
8058/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream)
8059pub(crate) fn quotedzputs(s: &str) -> String {
8060    // c:6464
8061    // c:6469-6475 — `if (!*s)` empty string emits `''` literal.
8062    if s.is_empty() {
8063        return "''".to_string(); // c:6472
8064    }
8065    // c:6477-6508 — `is_mb_niceformat(s)` / `is_sb_niceformat(s)` arm:
8066    // if the string contains nice-formatted chars (controls,
8067    // non-printables), wrap in `$'…'` using sb/mb_niceformat with
8068    // NICEFLAG_QUOTE so embedded `'`/`\` get backslash-escaped.
8069    if is_mb_niceformat(s) != 0 {
8070        // c:6478-6492 (MULTIBYTE_SUPPORT branch): use mb_niceformat
8071        //   with NICEFLAG_QUOTE so multi-byte chars round-trip through
8072        //   wcs_nicechar (raw UTF-8 for printable wides, `\u`/`\U` for
8073        //   large codepoints) AND `'`/`\\` get backslash-escaped.
8074        // Under !MULTIBYTE_SUPPORT (c:6494-6508) C would use
8075        //   sb_niceformat instead. Static-link path: MULTIBYTE is
8076        //   always available in Rust.
8077        // c:6485-6492 — `mb_niceformat(s, NULL, &substr,
8078        //                              NICEFLAG_QUOTE|NICEFLAG_NODUP);`
8079        let mut substr: Option<String> = None;
8080        let _ = mb_niceformat(s, None, Some(&mut substr), NICEFLAG_QUOTE | NICEFLAG_NODUP);
8081        return format!("$'{}'", substr.unwrap_or_default()); // c:6488
8082    }
8083    // c:6511-6518 — `if (!hasspecial(s)) return dupstring(s);`.
8084    if !hasspecial(s) {
8085        // c:6511
8086        return s.to_string(); // c:6516
8087    }
8088    // c:6520-6529 — outstr buffer alloc (zhalloc). Rust uses growable
8089    // String; the C `l = strlen(s) + 2 + (per-' overhead)` size hint
8090    // is just allocation tuning and doesn't affect output.
8091    let mut out = String::with_capacity(s.len() + 2);
8092    let bytes = s.as_bytes();
8093    let csh_junkie = isset(CSHJUNKIEQUOTES); // c:6554 / c:6612
8094
8095    if isset(RCQUOTES) {
8096        // c:6533 — RCQUOTES: wrap entire string in `'…'`; each
8097        // embedded `'` becomes `''` (the rc-style doubled quote).
8098        out.push('\''); // c:6539
8099                        // c:6540-6547 — C walks METAFIED bytes; the Rust String is
8100                        // UTF-8, so the faithful transposition walks CHARS (a byte
8101                        // walk Latin-1-casts every multibyte char: em-dash E2 80 94
8102                        // became "â" + a token byte that downstream passes ate).
8103                        // Token chars (Dash U+009B, Meta U+0083) are codepoints here.
8104        let chars_v: Vec<char> = s.chars().collect();
8105        let mut i = 0;
8106        while i < chars_v.len() {
8107            let c = if chars_v[i] == Dash {
8108                // c:6541 — `if (*s == Dash) c = '-';`
8109                i += 1;
8110                '-'
8111            } else if chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len() {
8112                // c:6543 — `else if (*s == Meta) c = *++s ^ 32;`
8113                let n = chars_v[i + 1] as u32;
8114                i += 2;
8115                char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
8116            } else {
8117                // c:6546 — `else c = *s;`
8118                let dec = chars_v[i];
8119                i += 1;
8120                dec
8121            };
8122            if c == '\'' {
8123                // c:6548-6553 — `if (c == '\'') *ptr++ = '\'';` (the
8124                // rc-quote doubling).
8125                out.push('\''); // c:6553
8126            } else if c == '\n' && csh_junkie {
8127                // c:6554-6560 — `if (c == '\n' && isset(CSHJUNKIEQUOTES))
8128                //                 *ptr++ = '\\';`
8129                out.push('\\'); // c:6559
8130            }
8131            // c:6561-6570 — emit c (metafy on imeta).
8132            out.push(c); // c:6569 (non-stream branch always re-metafies;
8133                         // Rust String holds decoded chars directly)
8134        }
8135        out.push('\''); // c:6576
8136    } else {
8137        // c:6578-6637 — Bourne-style quoting, "avoiding empty quoted
8138        // strings". Tracks `inquote` so that `it's` becomes
8139        // `'it'\''s'` (no empty `''` runs).
8140        let mut inquote = false; // c:6466 (initialised at top of C fn)
8141                                 // c:6579-6586 — char-wise decode, same transposition as the
8142                                 // RCQUOTES arm above (C's byte walk is over metafied bytes).
8143        let chars_v: Vec<char> = s.chars().collect();
8144        let mut i = 0;
8145        while i < chars_v.len() {
8146            let c = if chars_v[i] == Dash {
8147                i += 1;
8148                '-' // c:6581
8149            } else if chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len() {
8150                let n = chars_v[i + 1] as u32; // c:6583
8151                i += 2;
8152                char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
8153            } else {
8154                let dec = chars_v[i]; // c:6585
8155                i += 1;
8156                dec
8157            };
8158            if c == '\'' {
8159                // c:6587-6602 — `'` closes any open inquote then emits
8160                // `\'` outside the quotes.
8161                if inquote {
8162                    out.push('\''); // c:6593
8163                    inquote = false; // c:6594
8164                }
8165                out.push('\\'); // c:6600
8166                out.push('\''); // c:6601
8167            } else {
8168                // c:6603-6629 — other chars open a quote run if not
8169                // already open, optionally backslash-escape `\n` under
8170                // CSHJUNKIEQUOTES, then emit the byte.
8171                if !inquote {
8172                    out.push('\''); // c:6609
8173                    inquote = true; // c:6610
8174                }
8175                if c == '\n' && csh_junkie {
8176                    out.push('\\'); // c:6617
8177                }
8178                out.push(c); // c:6627 (imeta-encoding handled by Rust
8179                             // String storage in the non-stream form)
8180            }
8181        }
8182        if inquote {
8183            out.push('\''); // c:6636
8184        }
8185    }
8186    // c:6639-6640 — `if (!stream) *ptr++ = '\0';` — Rust String already
8187    // NUL-terminated implicitly; no-op.
8188    out // c:6642
8189}
8190
8191/// Port of `char *dquotedztrdup(char const *s)` from `Src/utils.c:6648-6723`.
8192/// Two arms (selected by `isset(CSHJUNKIEQUOTES)` at c:6655):
8193///
8194/// **CSHJUNKIEQUOTES path** (c:6656-6686): the csh-junk-quote style
8195/// where only the non-special sections are wrapped in `"..."` and
8196/// special chars (`"`, `$`, `` ` ``) appear OUTSIDE the quotes with
8197/// backslash escape. `\n` inside the quotes gets an extra `\` so it
8198/// round-trips through history.
8199///
8200/// **Default path** (c:6687-6719): wraps the whole string in `"..."`.
8201/// `\` is doubled to `\\`. `"`, `$`, `` ` `` get backslash-escaped.
8202/// A trailing `\` gets an extra `\` appended (the `pending` quirk).
8203///
8204/// Previously the Rust port only implemented the default arm; the
8205/// CSHJUNKIEQUOTES path is now ported faithfully.
8206pub fn dquotedztrdup(s: &str) -> String {
8207    // c:6648
8208    let mut out = String::with_capacity(s.len() * 4 + 2);
8209    let bytes = s.as_bytes();
8210    // c:6655 — `if (isset(CSHJUNKIEQUOTES))`.
8211    if isset(CSHJUNKIEQUOTES) {
8212        let mut inquote = false;
8213        let mut i = 0;
8214        while i < bytes.len() {
8215            // c:6661-6662 — Meta byte decode.
8216            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
8217                i += 2;
8218                (bytes[i - 1] ^ 32) as char
8219            } else {
8220                i += 1;
8221                bytes[i - 1] as char
8222            };
8223            match c {
8224                // c:6664-6673 — `"` / `$` / `` ` `` — close quote
8225                // (if open), then `\<c>`.
8226                '"' | '$' | '`' => {
8227                    if inquote {
8228                        out.push('"');
8229                        inquote = false;
8230                    }
8231                    out.push('\\');
8232                    out.push(c);
8233                }
8234                // c:6674-6682 — default arm: open quote if needed,
8235                // backslash-escape newline, emit char.
8236                _ => {
8237                    if !inquote {
8238                        out.push('"');
8239                        inquote = true;
8240                    }
8241                    if c == '\n' {
8242                        out.push('\\');
8243                    }
8244                    out.push(c);
8245                }
8246            }
8247        }
8248        // c:6685-6686 — close trailing quote.
8249        if inquote {
8250            out.push('"');
8251        }
8252    } else {
8253        // c:6687-6718 — default (non-CSH) arm.
8254        out.push('"');
8255        let mut pending = false;
8256        let mut i = 0;
8257        while i < bytes.len() {
8258            let c = if bytes[i] == Meta && i + 1 < bytes.len() {
8259                i += 2;
8260                (bytes[i - 1] ^ 32) as char
8261            } else {
8262                i += 1;
8263                bytes[i - 1] as char
8264            };
8265            match c {
8266                '\\' => {
8267                    if pending {
8268                        out.push('\\');
8269                    }
8270                    out.push('\\');
8271                    pending = true;
8272                }
8273                '"' | '$' | '`' => {
8274                    if pending {
8275                        out.push('\\');
8276                    }
8277                    out.push('\\');
8278                    out.push(c);
8279                    pending = false;
8280                }
8281                other => {
8282                    out.push(other);
8283                    pending = false;
8284                }
8285            }
8286        }
8287        if pending {
8288            out.push('\\');
8289        }
8290        out.push('"');
8291    }
8292    // c:6720 — `ret = metafy(buf, p - buf, META_DUP);` re-metafy result.
8293    metafy(&out)
8294}
8295
8296/// Port of `dquotedzputs(char const *s, FILE *stream)` from
8297/// Src/utils.c:6729. C body (4 lines):
8298///     `char *d = dquotedztrdup(s);
8299///      int ret = zputs(d, stream);
8300///      zsfree(d);
8301///      return ret;`
8302/// Rust returns the quoted string directly (callers compose via
8303/// `format!` rather than streaming through a FILE*), so the zputs
8304/// call drops.
8305pub fn dquotedzputs(s: &str) -> String {
8306    // c:6729
8307    dquotedztrdup(s) // c:6731
8308}
8309
8310/// Convert UCS-4 to UTF-8 (from utils.c ucs4toutf8)
8311/// Port of `ucs4toutf8(char *dest, unsigned int wval)` from `Src/utils.c:6743`.
8312///
8313/// C accepts 0..=0x7FFFFFFF (legacy UCS-4 / 6-byte UTF-8) — wider
8314/// than Unicode's 0..=0x10FFFF — to match `wctomb(3)` on Linux
8315/// with UTF-8 locale. The Rust `char::from_u32` path would reject
8316/// codepoints above 0x10FFFF (and surrogates), so this port mirrors
8317/// the C bit-pattern dispatch directly.
8318///
8319/// C body (c:6743-6779):
8320/// ```c
8321/// if (wval < 0x80) len = 1;            // ASCII
8322/// else if (wval < 0x800) len = 2;
8323/// else if (wval < 0x10000) len = 3;
8324/// else if (wval < 0x200000) len = 4;
8325/// else if (wval < 0x4000000) len = 5;
8326/// else if (wval < 0x80000000) len = 6;
8327/// else { zerr("character not in range"); return -1; }
8328///
8329/// switch (len) { /* falls through except to the last case */
8330/// case 6: dest[5] = (wval & 0x3f) | 0x80; wval >>= 6;
8331/// case 5: dest[4] = (wval & 0x3f) | 0x80; wval >>= 6;
8332/// case 4: dest[3] = (wval & 0x3f) | 0x80; wval >>= 6;
8333/// case 3: dest[2] = (wval & 0x3f) | 0x80; wval >>= 6;
8334/// case 2: dest[1] = (wval & 0x3f) | 0x80; wval >>= 6;
8335///     *dest = wval | ((0xfc << (6 - len)) & 0xfc);
8336///     break;
8337/// case 1: *dest = wval;
8338/// }
8339/// return len;
8340/// ```
8341///
8342/// Returns the encoded bytes (1..=6 long) on success, None on
8343/// out-of-range. C's `zerr("character not in range")` (c:6763) is
8344/// not emitted here — the return signals the error.
8345/// WARNING: param names don't match C — Rust=(wval) vs C=(dest, wval)
8346pub fn ucs4toutf8(wval: u32) -> Option<String> {
8347    // c:6743
8348    let len: usize = if wval < 0x80 {
8349        1
8350    }
8351    // c:6750
8352    else if wval < 0x800 {
8353        2
8354    }
8355    // c:6752
8356    else if wval < 0x10000 {
8357        3
8358    }
8359    // c:6754
8360    else if wval < 0x200000 {
8361        4
8362    }
8363    // c:6756
8364    else if wval < 0x4000000 {
8365        5
8366    }
8367    // c:6758
8368    else if wval < 0x80000000 {
8369        6
8370    }
8371    // c:6760
8372    else {
8373        // c:6762
8374        zerr("character not in range"); // c:6763
8375        return None; // c:6764
8376    };
8377    let mut buf = [0u8; 6];
8378    let mut w = wval;
8379    // c:6767-6776 — fall-through switch building trailing bytes first.
8380    match len {
8381        1 => {
8382            buf[0] = w as u8;
8383        } // c:6775
8384        n => {
8385            // Trailing (len-1) bytes: each is `(w & 0x3f) | 0x80`,
8386            // shifting w right 6 between them (c:6768-6772).
8387            for i in (1..n).rev() {
8388                buf[i] = ((w & 0x3f) as u8) | 0x80;
8389                w >>= 6;
8390            }
8391            // Leading byte: `w | ((0xfc << (6 - len)) & 0xfc)` (c:6773).
8392            buf[0] = (w as u8) | (((0xfcu32 << (6 - n)) & 0xfc) as u8);
8393        }
8394    }
8395    Some(String::from_utf8_lossy(&buf[..len]).into_owned())
8396}
8397
8398/// Port of `ucs4tomb(unsigned int wval, char *buf)` from `Src/utils.c:6788`.
8399///
8400/// Encode a UCS-4 codepoint into the buffer `buf` using the current
8401/// locale's multibyte encoding. Returns the number of bytes written,
8402/// or -1 on conversion failure. C body uses `wctomb(3)` when
8403/// `__STDC_ISO_10646__` is defined (which it is on every modern
8404/// glibc / macOS libc), falls back to UTF-8 if the codeset is
8405/// `"UTF-8"`, and uses `iconv(3)` otherwise.
8406///
8407/// This Rust port mirrors the primary `wctomb` path via libc FFI;
8408/// the iconv fallback is unused on macOS/Linux modern builds.
8409/// On conversion failure, emits `zerr("character not in range")`
8410/// to match C source line 6794.
8411///
8412/// C body shape:
8413/// ```c
8414/// int count = wctomb(buf, (wchar_t)wval);
8415/// if (count == -1) zerr("character not in range");
8416/// return count;
8417/// ```
8418pub fn ucs4tomb(wval: u32, buf: &mut [u8]) -> i32 {
8419    // libc::wctomb requires at least MB_CUR_MAX bytes (typically 4
8420    // for UTF-8, 6 for some encodings). Use a stack buffer first,
8421    // then copy into the caller's buffer.
8422    // libc crate doesn't expose wctomb on all platforms; declare
8423    // the POSIX prototype directly. wchar_t is i32 on macOS/Linux
8424    // for our supported targets.
8425    extern "C" {
8426        fn wctomb(s: *mut libc::c_char, wc: libc::wchar_t) -> libc::c_int;
8427    }
8428    // libc::c_char is i8 on most targets but u8 on aarch64-linux. Use c_char
8429    // so the wctomb arg pointer type matches per-target without a cast.
8430    let mut local = [0 as libc::c_char; 16];
8431    let count = unsafe { wctomb(local.as_mut_ptr(), wval as libc::wchar_t) };
8432    if count < 0 {
8433        zerr("character not in range");
8434        return -1;
8435    }
8436    let n = count as usize;
8437    if n > buf.len() {
8438        zerr("character not in range");
8439        return -1;
8440    }
8441    for i in 0..n {
8442        buf[i] = local[i] as u8;
8443    }
8444    count
8445}
8446
8447/// Parse getkeystring escape sequences (from utils.c getkeystring)
8448/// Handles \n \t \r \e \a \b \f \v \\ \' \" \xNN \uNNNN \UNNNNNNNN \0NNN
8449/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from `Src/utils.c:6915`.
8450/// WARNING: param names don't match C — Rust=(s) vs C=(s, len, how, misc)
8451pub fn getkeystring(s: &str) -> (String, usize) {
8452    // c:6915
8453    let mut result = String::new();
8454    let mut chars = s.chars().peekable();
8455    let mut consumed = 0;
8456
8457    while let Some(c) = chars.next() {
8458        consumed += c.len_utf8();
8459        if c != '\\' {
8460            result.push(c);
8461            continue;
8462        }
8463        match chars.next() {
8464            Some('n') => {
8465                result.push('\n');
8466                consumed += 1;
8467            }
8468            Some('t') => {
8469                result.push('\t');
8470                consumed += 1;
8471            }
8472            Some('r') => {
8473                result.push('\r');
8474                consumed += 1;
8475            }
8476            Some('e') | Some('E') => {
8477                result.push('\x1b');
8478                consumed += 1;
8479            }
8480            Some('a') => {
8481                result.push('\x07');
8482                consumed += 1;
8483            }
8484            Some('b') => {
8485                result.push('\x08');
8486                consumed += 1;
8487            }
8488            Some('f') => {
8489                result.push('\x0c');
8490                consumed += 1;
8491            }
8492            Some('v') => {
8493                result.push('\x0b');
8494                consumed += 1;
8495            }
8496            Some('\\') => {
8497                result.push('\\');
8498                consumed += 1;
8499            }
8500            Some('\'') => {
8501                result.push('\'');
8502                consumed += 1;
8503            }
8504            Some('"') => {
8505                result.push('"');
8506                consumed += 1;
8507            }
8508            Some('x') => {
8509                consumed += 1;
8510                let mut hex = String::new();
8511                for _ in 0..2 {
8512                    if let Some(&c) = chars.peek() {
8513                        if c.is_ascii_hexdigit() {
8514                            hex.push(chars.next().unwrap());
8515                            consumed += 1;
8516                        } else {
8517                            break;
8518                        }
8519                    }
8520                }
8521                // c:Src/utils.c:7169-7170 — `\x` ALWAYS runs `*t++ =
8522                // zstrtol(s+1, &s, 16)`, even with no hex digit. zstrtol
8523                // over a non-hex byte parses zero digits and returns 0, so
8524                // a NUL byte is emitted (the offending char is processed
8525                // next). `$'\xg'` → NUL + 'g'; `$'\x'` → NUL. Previously
8526                // `from_str_radix("")` errored and nothing was pushed.
8527                let val: u8 = if hex.is_empty() {
8528                    0 // c:7170 zstrtol on empty reads as 0
8529                } else {
8530                    u8::from_str_radix(&hex, 16).unwrap_or(0)
8531                };
8532                // c:Src/utils.c — `\xNN` is one raw BYTE; metafied
8533                // per c:7289-7294 (vm_helper::meta_encode_byte) so
8534                // the String stays valid UTF-8. Bug #127.
8535                {
8536                    // c:Src/utils.c metafy byte-encode step:
8537                    // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8538                    let b_ = val;
8539                    if b_ < 0x80 {
8540                        result.push(b_ as char);
8541                    } else {
8542                        result.push('\u{83}');
8543                        result.push(char::from(b_ ^ 32));
8544                    }
8545                }
8546            }
8547            Some('u') => {
8548                consumed += 1;
8549                let mut hex = String::new();
8550                for _ in 0..4 {
8551                    if let Some(&c) = chars.peek() {
8552                        if c.is_ascii_hexdigit() {
8553                            hex.push(chars.next().unwrap());
8554                            consumed += 1;
8555                        } else {
8556                            break;
8557                        }
8558                    }
8559                }
8560                if let Ok(val) = u32::from_str_radix(&hex, 16) {
8561                    if let Some(c) = char::from_u32(val) {
8562                        result.push(c);
8563                    }
8564                }
8565            }
8566            Some('U') => {
8567                consumed += 1;
8568                let mut hex = String::new();
8569                for _ in 0..8 {
8570                    if let Some(&c) = chars.peek() {
8571                        if c.is_ascii_hexdigit() {
8572                            hex.push(chars.next().unwrap());
8573                            consumed += 1;
8574                        } else {
8575                            break;
8576                        }
8577                    }
8578                }
8579                if let Ok(val) = u32::from_str_radix(&hex, 16) {
8580                    if let Some(c) = char::from_u32(val) {
8581                        result.push(c);
8582                    }
8583                }
8584            }
8585            Some(c @ '0'..='7') => {
8586                consumed += 1;
8587                let mut oct = String::new();
8588                oct.push(c);
8589                for _ in 0..2 {
8590                    if let Some(&c) = chars.peek() {
8591                        if ('0'..='7').contains(&c) {
8592                            oct.push(chars.next().unwrap());
8593                            consumed += 1;
8594                        } else {
8595                            break;
8596                        }
8597                    }
8598                }
8599                if let Ok(val) = u8::from_str_radix(&oct, 8) {
8600                    // c:Src/utils.c — octal escape is one raw BYTE;
8601                    // metafied per c:7289-7294. Bug #127.
8602                    {
8603                        // c:Src/utils.c metafy byte-encode step:
8604                        // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8605                        let b_ = val;
8606                        if b_ < 0x80 {
8607                            result.push(b_ as char);
8608                        } else {
8609                            result.push('\u{83}');
8610                            result.push(char::from(b_ ^ 32));
8611                        }
8612                    }
8613                }
8614            }
8615            Some('c') => {
8616                consumed += 1;
8617                // \cX = control character
8618                if let Some(c) = chars.next() {
8619                    consumed += 1;
8620                    result.push((c as u8 & 0x1f) as char);
8621                }
8622            }
8623            Some(mod_letter @ ('C' | 'M')) => {
8624                // c:Src/utils.c:7029-7052 + c:7265-7275 — `\C` /
8625                // `\M` set control / meta flags; optional `-`
8626                // separator; then read the next char (possibly
8627                // through additional `\C`/`\M` chains) and apply
8628                // the mask: control → `& 0x9f` (or `\C-?` → 0x7f),
8629                // meta → `| 0x80`. Bug #113 in docs/BUGS.md: the
8630                // previous Rust port matched these in the default
8631                // arm and emitted literal `\C` / `\M`. This is the
8632                // second getkeystring (utils.rs); the in-tree
8633                // `getkeystring_dollar_quote` (lex.rs) also needs
8634                // the fix and gets it in the same commit.
8635                consumed += 1;
8636                let mut control = mod_letter == 'C';
8637                let mut meta = mod_letter == 'M';
8638                // Consume optional `-` separator + chained `\C`/`\M`.
8639                loop {
8640                    if chars.peek() == Some(&'-') {
8641                        chars.next();
8642                        consumed += 1;
8643                        continue;
8644                    }
8645                    // chained `\C` / `\M`?
8646                    let mut iter_clone = chars.clone();
8647                    if iter_clone.next() == Some('\\') {
8648                        if let Some(nx) = iter_clone.next() {
8649                            if nx == 'C' || nx == 'M' {
8650                                chars.next(); // consume '\'
8651                                chars.next(); // consume C/M
8652                                consumed += 2;
8653                                if nx == 'C' {
8654                                    control = true;
8655                                } else {
8656                                    meta = true;
8657                                }
8658                                continue;
8659                            }
8660                        }
8661                    }
8662                    break;
8663                }
8664                // Read one base character (allowing nested simple
8665                // escapes for common cases).
8666                let base: Option<char> = if chars.peek() == Some(&'\\') {
8667                    chars.next();
8668                    consumed += 1;
8669                    match chars.next() {
8670                        Some('n') => {
8671                            consumed += 1;
8672                            Some('\n')
8673                        }
8674                        Some('t') => {
8675                            consumed += 1;
8676                            Some('\t')
8677                        }
8678                        Some('r') => {
8679                            consumed += 1;
8680                            Some('\r')
8681                        }
8682                        Some('a') => {
8683                            consumed += 1;
8684                            Some('\x07')
8685                        }
8686                        Some('b') => {
8687                            consumed += 1;
8688                            Some('\x08')
8689                        }
8690                        Some('e') | Some('E') => {
8691                            consumed += 1;
8692                            Some('\x1b')
8693                        }
8694                        Some('f') => {
8695                            consumed += 1;
8696                            Some('\x0c')
8697                        }
8698                        Some('v') => {
8699                            consumed += 1;
8700                            Some('\x0b')
8701                        }
8702                        Some('\\') => {
8703                            consumed += 1;
8704                            Some('\\')
8705                        }
8706                        Some('\'') => {
8707                            consumed += 1;
8708                            Some('\'')
8709                        }
8710                        Some('"') => {
8711                            consumed += 1;
8712                            Some('"')
8713                        }
8714                        Some(other) => {
8715                            consumed += 1;
8716                            Some(other)
8717                        }
8718                        None => None,
8719                    }
8720                } else {
8721                    chars.next().inspect(|c| {
8722                        consumed += c.len_utf8();
8723                    })
8724                };
8725                if let Some(ch) = base {
8726                    let mut byte = ch as u32;
8727                    if control {
8728                        if byte == '?' as u32 {
8729                            byte = 0x7f;
8730                        } else {
8731                            byte &= 0x9f;
8732                        }
8733                    }
8734                    if meta {
8735                        byte |= 0x80;
8736                    }
8737                    // c:Src/utils.c:7265-7275 — masked result is one
8738                    // raw BYTE (`$'\M-i'` = 0xe9), metafied per
8739                    // c:7289-7294. Multibyte base chars (> 0xff)
8740                    // keep the codepoint form. Bug #127.
8741                    if byte <= 0xff {
8742                        {
8743                            // c:Src/utils.c metafy byte-encode step:
8744                            // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8745                            let b_ = byte as u8;
8746                            if b_ < 0x80 {
8747                                result.push(b_ as char);
8748                            } else {
8749                                result.push('\u{83}');
8750                                result.push(char::from(b_ ^ 32));
8751                            }
8752                        }
8753                    } else if let Some(c) = char::from_u32(byte) {
8754                        result.push(c);
8755                    }
8756                }
8757            }
8758            Some(c) if isset(BANGHIST)
8759                && crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst) != 0
8760                && c as u32
8761                    == crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst) as u32 =>
8762            {
8763                // The reverse of quotestring's BANGHIST escape (c:Src/utils.c:6230
8764                // — `\!` is emitted for the history char): unquoting `$'a\!b'`
8765                // must strip the backslash so `${(Q)${(qqqq)v}}` round-trips.
8766                // In zsh this happens because `(Q)` re-lexes the string and
8767                // history expansion consumes the `\<bangchar>`; here the decoder
8768                // consumes it directly.
8769                consumed += 1;
8770                result.push(c);
8771            }
8772            Some(c) => {
8773                consumed += 1;
8774                result.push('\\');
8775                result.push(c);
8776            }
8777            None => {
8778                result.push('\\');
8779            }
8780        }
8781    }
8782    (result, consumed)
8783}
8784
8785/// Check if s is a prefix of t (from utils.c strpfx)
8786// Return non-zero if s is a prefix of t.                                  // c:7345
8787/// `strpfx` — see implementation.
8788pub fn strpfx(s: &str, t: &str) -> bool {
8789    t.starts_with(s)
8790}
8791
8792/// Check if s is a suffix of t (from utils.c strsfx)
8793// Return non-zero if s is a suffix of t.                                  // c:7345
8794/// `strsfx` — see implementation.
8795pub fn strsfx(s: &str, t: &str) -> bool {
8796    t.ends_with(s)
8797}
8798
8799/// Go up n directories (from utils.c upchdir)
8800/// Port of `upchdir(int n)` from `Src/utils.c:7356`.
8801pub fn upchdir(n: usize) -> io::Result<()> {
8802    let mut path = String::new();
8803    for i in 0..n {
8804        if i > 0 {
8805            path.push('/');
8806        }
8807        path.push_str("..");
8808    }
8809    std::env::set_current_dir(&path)?;
8810    Ok(())
8811}
8812
8813/// Port of `struct dirsav` from `Src/zsh.h:1159`.
8814///
8815/// ```c
8816/// struct dirsav {
8817///     int dirfd, level;
8818///     char *dirname;
8819///     dev_t dev;
8820///     ino_t ino;
8821/// };
8822/// ```
8823///
8824/// The previous Rust port omitted `dev` and `ino` which the
8825/// `restoredir` integrity check (utils.c:7592) reads. Adding them
8826/// so callers can verify the saved-and-restored cwd matches the
8827/// captured device + inode.
8828// `struct dirsav` lives in `dirsav` per Rule C
8829// (its C definition is `Src/zsh.h:1159`, not utils.c). The previous
8830// Rust port had a `pub struct DirSav` PascalCase duplicate of the
8831// canonical lowercase struct; deleted in favour of routing through
8832// `zsh_h::dirsav` directly.
8833
8834/// Port of `init_dirsav(Dirsav d)` from `Src/utils.c:7381`. Initialize a
8835/// `dirsav` struct to its empty/default state. C body memset's the
8836/// fields to 0 (dirfd to -1).
8837///
8838/// C signature: `void init_dirsav(Dirsav d)` where
8839/// `Dirsav = struct dirsav *`. Rust port returns the initialised
8840/// struct since callers always pair-with a fresh allocation.
8841/// WARNING: param names don't match C — Rust=() vs C=(path)
8842/// C body (3 lines):
8843///   `d->ino = d->dev = 0; d->dirname = NULL; d->dirfd = d->level = -1;`
8844/// The C `dirname = NULL` becomes `dirname: None`; Rust port prefills
8845/// dirname with current_dir for legacy callers that immediately read
8846/// it (mirrors what `setpwd()` does in C right after `init_dirsav`).
8847pub fn init_dirsav() -> dirsav {
8848    // c:7381
8849    dirsav {
8850        dirfd: -1,
8851        level: 0,
8852        dev: 0,
8853        ino: 0, // c:7383-7385
8854        dirname: std::env::current_dir()
8855            .ok()
8856            .map(|p| p.to_string_lossy().to_string()),
8857    }
8858}
8859
8860/// Port of `lchdir(char const *path, struct dirsav *d, int hard)` from `Src/utils.c:7400`.
8861///
8862/// c:7388 — "Change directory, without following symlinks."  With `hard`
8863/// set, descends `path` one component at a time: `lstat`s each component
8864/// (so a symlink is never followed), `chdir`s into it, then re-`lstat`s
8865/// `.` and compares dev/ino against the component it just stat'd. A
8866/// symlink swapped under us between the `lstat` and the `chdir` is caught
8867/// by the mismatch and the saved directory is restored via `restoredir`.
8868/// `d` (when non-NULL) is filled so the caller can later restore the cwd.
8869///
8870/// Returns 0 on success, -1 on a normal descent failure, -2 if the cwd
8871/// could not even be restored afterwards.
8872///
8873/// zshrs targets macOS/Linux, so the live C arms are HAVE_LSTAT +
8874/// HAVE_FCHDIR; the no-lstat / no-fchdir fallbacks (which never compile
8875/// on our platforms) are elided. C restores `errno = err` before each
8876/// non-zero return so the caller can read the break-reason; that errno
8877/// propagation is elided here (no current caller inspects errno — the
8878/// -1/-2/0 return is the contract).
8879/// WARNING: param names match C — (path, d, hard).
8880#[cfg(unix)]
8881pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
8882    // c:7400
8883    use std::ffi::CString;
8884
8885    let mut ds = init_dirsav(); // c:7405 — local fallback `struct dirsav ds`
8886    let used_local = d.is_none(); // tracks C's `d == &ds`
8887                                  // c:7415-7418 — `if (!d) { init_dirsav(&ds); d = &ds; }`
8888    let d: &mut dirsav = d.unwrap_or(&mut ds);
8889
8890    let bytes = path.as_bytes();
8891    let mut level: i32;
8892
8893    // c:7419-7424 (HAVE_LSTAT arm) —
8894    //   `if ((*path == '/' || !hard) && (d != &ds || hard))`
8895    if (bytes.first() == Some(&b'/') || hard == 0) && (!used_local || hard != 0) {
8896        level = -1; // c:7425
8897    } else {
8898        level = 0; // c:7431
8899                   // c:7432-7435 — record dev/ino of `.` if not already captured.
8900        if d.dev == 0 && d.ino == 0 {
8901            if let Ok(meta) = fs::metadata(".") {
8902                d.dev = meta.dev(); // c:7433
8903                d.ino = meta.ino(); // c:7434
8904            }
8905        }
8906    }
8907
8908    // c:7439-7451 — soft (`!hard`) path: count components, set d->level,
8909    // and delegate to zchdir.
8910    if hard == 0 {
8911        if !used_local {
8912            // c:7443-7447 — count '/'-separated components into `level`.
8913            let mut p = 0usize;
8914            while p < bytes.len() {
8915                while p < bytes.len() && bytes[p] != b'/' {
8916                    p += 1;
8917                }
8918                while p < bytes.len() && bytes[p] == b'/' {
8919                    p += 1;
8920                }
8921                level += 1;
8922            }
8923            d.level = level; // c:7448
8924        }
8925        return zchdir(path); // c:7450
8926    }
8927
8928    // c:7453+ — hard path (HAVE_LSTAT + HAVE_FCHDIR).
8929    let mut close_dir = false; // c:7412
8930                               // c:7455-7460 — save the starting dir via an fd if we don't have one.
8931    if d.dirfd < 0 {
8932        close_dir = true; // c:7456
8933        let dot = CString::new(".").unwrap();
8934        d.dirfd = unsafe { libc::open(dot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:7457
8935        if d.dirfd < 0
8936            && zgetdir(Some(&mut *d)).is_some()
8937            && d.dirname
8938                .as_deref()
8939                .map(|s| !s.starts_with('/'))
8940                .unwrap_or(false)
8941        {
8942            // c:7458-7459 — cwd is relative; fall back to opening "..".
8943            let dotdot = CString::new("..").unwrap();
8944            d.dirfd = unsafe { libc::open(dotdot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
8945        }
8946    }
8947
8948    // c:7462-7464 — absolute path: start the descent from root.
8949    if bytes.first() == Some(&b'/') {
8950        let root = CString::new("/").unwrap();
8951        if unsafe { libc::chdir(root.as_ptr()) } < 0 {
8952            zwarn(&format!(
8953                "failed to chdir(/): {}",
8954                io::Error::last_os_error()
8955            )); // c:7464
8956        }
8957    }
8958
8959    let dot = CString::new(".").unwrap();
8960    let mut pos = 0usize; // index into `path`, replaces C `path`/`pptr`
8961    let mut err: i32 = 0; // c:7408
8962    loop {
8963        // c:7466-7467 — skip leading slashes.
8964        while pos < bytes.len() && bytes[pos] == b'/' {
8965            pos += 1;
8966        }
8967        // c:7468-7480 — end of path reached: success.
8968        if pos >= bytes.len() {
8969            if !used_local {
8970                d.level = level; // c:7472
8971            }
8972            // c:7474-7477 — close the saved-dir fd if we opened it.
8973            if d.dirfd >= 0 && close_dir {
8974                unsafe { libc::close(d.dirfd) };
8975                d.dirfd = -1;
8976            }
8977            return 0; // c:7479
8978        }
8979        // c:7481 — find the end of this path component.
8980        let start = pos;
8981        let mut end = start + 1;
8982        while end < bytes.len() && bytes[end] != b'/' {
8983            end += 1;
8984        }
8985        // c:7482-7485 — component too long.
8986        if end - start > libc::PATH_MAX as usize {
8987            err = libc::ENAMETOOLONG;
8988            break;
8989        }
8990        // c:7486-7488 — copy the component into `buf`.
8991        let comp = &bytes[start..end];
8992        pos = end;
8993        let cbuf = match CString::new(comp) {
8994            Ok(c) => c,
8995            Err(_) => {
8996                err = libc::ENOENT; // embedded NUL — lstat would fail anyway.
8997                break;
8998            }
8999        };
9000        // c:7489-7492 — lstat the component (does NOT follow a symlink).
9001        let mut st1: libc::stat = unsafe { std::mem::zeroed() };
9002        if unsafe { libc::lstat(cbuf.as_ptr(), &mut st1) } != 0 {
9003            err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9004            break;
9005        }
9006        // c:7493-7496 — must be a real directory, not a symlink/other.
9007        if st1.st_mode as u32 & libc::S_IFMT as u32 != libc::S_IFDIR as u32 {
9008            err = libc::ENOTDIR;
9009            break;
9010        }
9011        // c:7497-7500 — chdir into it.
9012        if unsafe { libc::chdir(cbuf.as_ptr()) } != 0 {
9013            err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9014            break;
9015        }
9016        if level >= 0 {
9017            level += 1; // c:7501-7502
9018        }
9019        // c:7503-7510 — re-lstat `.` and verify the dir we landed in is
9020        // the same dev/ino we lstat'd, catching a symlink swap race.
9021        let mut st2: libc::stat = unsafe { std::mem::zeroed() };
9022        if unsafe { libc::lstat(dot.as_ptr(), &mut st2) } != 0 {
9023            err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9024            break;
9025        }
9026        if st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino {
9027            err = libc::ENOTDIR; // c:7508
9028            break;
9029        }
9030    }
9031
9032    // c:7512-7548 — descent failed; restore the saved directory.
9033    if restoredir(&mut *d) != 0 {
9034        let restoreerr = io::Error::last_os_error(); // c:7513
9035                                                     // c:7519-7532 — restore failed too; force cwd to $HOME then "/".
9036        let mut reached = false;
9037        for i in 0..2 {
9038            // c:7521-7527 — destination: $HOME on pass 0, "/" on pass 1.
9039            let cdest = if i != 0 {
9040                "/".to_string()
9041            } else {
9042                match getsparam("HOME") {
9043                    Some(h) => h,     // c:7526
9044                    None => continue, // c:7525 `if (!home) continue;`
9045                }
9046            };
9047            setsparam("PWD", &cdest); // c:7528-7529 `zsfree(pwd); pwd = ztrdup(cdest);`
9048            if let Ok(cd) = CString::new(cdest.as_bytes()) {
9049                if unsafe { libc::chdir(cd.as_ptr()) } == 0 {
9050                    // c:7530
9051                    reached = true;
9052                    break;
9053                }
9054            }
9055        }
9056        if !reached {
9057            // c:7533-7534 — couldn't even reach "/".
9058            zerr(&format!(
9059                "lost current directory, failed to cd to /: {}",
9060                io::Error::last_os_error()
9061            ));
9062        } else {
9063            // c:7535-7537
9064            let pwd = getsparam("PWD").unwrap_or_default();
9065            zerr(&format!(
9066                "lost current directory: {}: changed to `{}'",
9067                restoreerr, pwd
9068            ));
9069        }
9070        // c:7540-7545 — close the saved-dir fd if we opened it.
9071        if d.dirfd >= 0 && close_dir {
9072            unsafe { libc::close(d.dirfd) };
9073            d.dirfd = -1;
9074        }
9075        let _ = err; // c:7546 `errno = err;` propagation elided (see doc).
9076        return -2; // c:7547
9077    }
9078    // c:7549-7558 — descent failed but the saved directory was restored.
9079    if d.dirfd >= 0 && close_dir {
9080        unsafe { libc::close(d.dirfd) };
9081        d.dirfd = -1;
9082    }
9083    let _ = err; // c:7557 `errno = err;` propagation elided (see doc).
9084    -1 // c:7558
9085}
9086
9087/// Non-unix stub: lchdir's symlink-safe descent is built on POSIX
9088/// `lstat`/`fchdir`/`chdir`, which have no Windows equivalent here.
9089#[cfg(not(unix))]
9090pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
9091    let _ = (path, d, hard);
9092    -1
9093}
9094
9095/// Port of `restoredir(struct dirsav *d)` from `Src/utils.c:7565`.
9096///
9097/// ```c
9098/// int restoredir(struct dirsav *d) {
9099///     if (d->dirname && *d->dirname == '/')
9100///         return chdir(d->dirname);
9101///     if (d->dirfd >= 0) {
9102///         if (!fchdir(d->dirfd)) {
9103///             if (!d->dirname) return 0;
9104///             else if (chdir(d->dirname)) {
9105///                 close(d->dirfd); d->dirfd = -1; err = -2;
9106///             }
9107///         } else {
9108///             close(d->dirfd); d->dirfd = err = -1;
9109///         }
9110///     } else if (d->level > 0)
9111///         err = upchdir(d->level);
9112///     else if (d->level < 0) err = -1;
9113///     // dev/ino integrity check ...
9114/// }
9115/// ```
9116///
9117/// Restore the cwd captured in `d`. Absolute `dirname` short-
9118/// circuits to `chdir`. Otherwise tries `fchdir(dirfd)` (when
9119/// supported) then falls through to `upchdir(level)` for the
9120/// nested-fn-exit case. Returns 0 on success, non-zero on failure
9121/// (matching C's int return).
9122///
9123/// Signature change: previous Rust port took `saved: &str` and
9124/// returned `bool` — different shape from C, missed the dirfd /
9125/// level / dev / ino fields entirely.
9126pub fn restoredir(d: &mut dirsav) -> i32 {
9127    // C: if (d->dirname && *d->dirname == '/') return chdir(d->dirname);
9128    if let Some(name) = d.dirname.as_ref() {
9129        if name.starts_with('/') {
9130            return match std::env::set_current_dir(name) {
9131                Ok(_) => 0,
9132                Err(_) => -1,
9133            };
9134        }
9135    }
9136    let mut err: i32 = 0;
9137    // C: HAVE_FCHDIR path — try fchdir(dirfd) first.
9138    #[cfg(unix)]
9139    if d.dirfd >= 0 {
9140        let rc = unsafe { libc::fchdir(d.dirfd) };
9141        if rc == 0 {
9142            if d.dirname.is_none() {
9143                return 0;
9144            }
9145            let name = d.dirname.as_ref().unwrap();
9146            if std::env::set_current_dir(name).is_err() {
9147                unsafe { libc::close(d.dirfd) };
9148                d.dirfd = -1;
9149                err = -2;
9150            }
9151        } else {
9152            unsafe { libc::close(d.dirfd) };
9153            d.dirfd = -1;
9154            err = -1;
9155        }
9156    } else if d.level > 0 {
9157        // C: err = upchdir(d->level);
9158        let _ = upchdir(d.level as usize);
9159    } else if d.level < 0 {
9160        err = -1;
9161    }
9162    // C: dev/ino integrity check after the chdir/fchdir.
9163    if (d.dev != 0 || d.ino != 0) && err == 0 {
9164        if let Ok(meta) = fs::metadata(".") {
9165            if meta.ino() != d.ino || meta.dev() != d.dev {
9166                err = -1;
9167            }
9168        } else {
9169            err = -1;
9170        }
9171    }
9172    err
9173}
9174
9175/// Port of `privasserted()` from `Src/utils.c:7607`.
9176///
9177/// "Check whether the shell is running with privileges in effect.
9178/// This is the case if EITHER the euid is zero, OR (if the system
9179/// supports POSIX.1e (POSIX.6) capability sets) the process'
9180/// Effective or Inheritable capability sets are non-empty."
9181///
9182/// ```c
9183/// if (!geteuid()) return 1;
9184/// #ifdef HAVE_CAP_GET_PROC
9185///     cap_t caps = cap_get_proc();
9186///     if (caps) {
9187///         cap_flag_value_t val;
9188///         for (cap_value_t cap = 0;
9189///              !cap_get_flag(caps, cap, CAP_EFFECTIVE, &val); cap++)
9190///             if (val && cap != CAP_WAKE_ALARM) {
9191///                 cap_free(caps);
9192///                 return 1;
9193///             }
9194///     }
9195///     cap_free(caps);
9196/// #endif
9197///     return 0;
9198/// ```
9199///
9200/// The previous Rust port checked `getuid() != geteuid()` which is
9201/// the SUID-binary detection, not the "running with privileges"
9202/// check. The capability-set inspection requires libcap (gated
9203/// behind the `libcap` feature in `crate::ported::modules::cap`);
9204/// without it, only the euid==0 path is exercised — same as the
9205/// C `#else` arm when HAVE_CAP_GET_PROC isn't defined.
9206pub fn privasserted() -> bool {
9207    #[cfg(unix)]
9208    {
9209        if unsafe { libc::geteuid() } == 0 {
9210            return true;
9211        }
9212    }
9213    // POSIX.1e capabilities check (HAVE_CAP_GET_PROC) — only
9214    // active on Linux when zshrs is built with `--features
9215    // libcap`. The cap module's `cap_get_proc` returns Ok(text)
9216    // when the process has any capability set; we treat any
9217    // non-default-empty result as "privileges asserted".
9218    #[cfg(all(target_os = "linux", feature = "libcap"))]
9219    {
9220        // Pending: walk the capability set with cap_get_flag and
9221        // skip CAP_WAKE_ALARM as the C source does. The cap.rs
9222        // port doesn't yet expose the flag-iteration FFI; until
9223        // it does, conservative-true on any non-empty cap text.
9224        if let Ok(text) = crate::ported::modules::cap::cap_get_proc() {
9225            // Empty / default-empty cap text = no privileges.
9226            if !text.is_empty() && text != "=" {
9227                return true;
9228            }
9229        }
9230    }
9231    false
9232}
9233
9234/// Port of `mode_to_octal(mode_t mode)` from `Src/utils.c:7634`.
9235///
9236/// Convert a `mode_t` into the equivalent canonical octal value
9237/// by testing each `S_I*` flag explicitly and OR-ing the matching
9238/// octal bit. This is NOT just `mode & 07777` — on systems where
9239/// the libc `S_IRUSR`/etc. constants don't match the canonical
9240/// values (e.g. when zsh runs against a non-POSIX libc),
9241/// `mode & 07777` returns the libc representation. The C version
9242/// translates to canonical bits explicitly so callers get a stable
9243/// portable result.
9244///
9245/// ```c
9246/// int mode_to_octal(mode_t mode)
9247/// {
9248///     int m = 0;
9249///     if (mode & S_ISUID) m |= 04000;
9250///     ... (12 bit-by-bit mappings)
9251///     return m;
9252/// }
9253/// ```
9254pub fn mode_to_octal(mode: u32) -> i32 {
9255    // c:7634
9256    #[cfg(not(unix))]
9257    {
9258        // No POSIX permission bits on non-Unix; fall back to canonical
9259        // octal-bit layout via the same mask values C uses on POSIX.
9260        let mut o: i32 = 0;
9261        if mode & 0o4000 != 0 {
9262            o |= 0o4000;
9263        } // c:7638-7639
9264        if mode & 0o2000 != 0 {
9265            o |= 0o2000;
9266        } // c:7640-7641
9267        if mode & 0o1000 != 0 {
9268            o |= 0o1000;
9269        } // c:7642-7643
9270        if mode & 0o0400 != 0 {
9271            o |= 0o0400;
9272        } // c:7644-7645
9273        if mode & 0o0200 != 0 {
9274            o |= 0o0200;
9275        } // c:7646-7647
9276        if mode & 0o0100 != 0 {
9277            o |= 0o0100;
9278        } // c:7648-7649
9279        if mode & 0o0040 != 0 {
9280            o |= 0o0040;
9281        } // c:7650-7651
9282        if mode & 0o0020 != 0 {
9283            o |= 0o0020;
9284        } // c:7652-7653
9285        if mode & 0o0010 != 0 {
9286            o |= 0o0010;
9287        } // c:7654-7655
9288        if mode & 0o0004 != 0 {
9289            o |= 0o0004;
9290        } // c:7656-7657
9291        if mode & 0o0002 != 0 {
9292            o |= 0o0002;
9293        } // c:7658-7659
9294        if mode & 0o0001 != 0 {
9295            o |= 0o0001;
9296        } // c:7660-7661
9297        return o; // c:7662
9298    }
9299    #[cfg(unix)]
9300    {
9301        // c:7636 — int m = 0;
9302        let mut m: i32 = 0;
9303        // c:7638-7661 — 12 bit-by-bit mappings from libc S_I* → canonical octal.
9304        if mode & S_ISUID as u32 != 0 {
9305            m |= 0o4000;
9306        } // c:7638-7639
9307        if mode & S_ISGID as u32 != 0 {
9308            m |= 0o2000;
9309        } // c:7640-7641
9310        if mode & S_ISVTX as u32 != 0 {
9311            m |= 0o1000;
9312        } // c:7642-7643
9313        if mode & S_IRUSR as u32 != 0 {
9314            m |= 0o0400;
9315        } // c:7644-7645
9316        if mode & S_IWUSR as u32 != 0 {
9317            m |= 0o0200;
9318        } // c:7646-7647
9319        if mode & S_IXUSR as u32 != 0 {
9320            m |= 0o0100;
9321        } // c:7648-7649
9322        if mode & S_IRGRP as u32 != 0 {
9323            m |= 0o0040;
9324        } // c:7650-7651
9325        if mode & S_IWGRP as u32 != 0 {
9326            m |= 0o0020;
9327        } // c:7652-7653
9328        if mode & S_IXGRP as u32 != 0 {
9329            m |= 0o0010;
9330        } // c:7654-7655
9331        if mode & S_IROTH as u32 != 0 {
9332            m |= 0o0004;
9333        } // c:7656-7657
9334        if mode & S_IWOTH as u32 != 0 {
9335            m |= 0o0002;
9336        } // c:7658-7659
9337        if mode & S_IXOTH as u32 != 0 {
9338            m |= 0o0001;
9339        } // c:7660-7661
9340        m // c:7662
9341    }
9342}
9343
9344/// Port of `mailstat(char *path, struct stat *st)` from `Src/utils.c:7685`.
9345///
9346/// C signature: `int mailstat(char *path, struct stat *st)`.
9347/// Writes maildir aggregate stats into `*st` (or the native stat for
9348/// non-directory paths) and returns the underlying `stat(2)` return
9349/// (0 on success, -1 on error — matches C's `i = stat(path, st);
9350/// return i;`).
9351///
9352/// When `path` is a maildir directory (containing `cur/`, `tmp/`,
9353/// `new/` subdirs), walks `new/` + `cur/` and aggregates into `*st`:
9354/// nlink=1, S_IFDIR→S_IFREG, size=Σ message bytes,
9355/// blocks=Σ messages, atime=newest, mtime=newest. When the path is
9356/// a plain file, leaves the native `stat(2)` result in `*st`.
9357pub fn mailstat(path: &str, st: &mut libc::stat) -> i32 {
9358    // c:7685
9359    let c_path = match CString::new(path) {
9360        Ok(c) => c,
9361        Err(_) => return -1,
9362    };
9363    // C: if ((i = stat(path, st)) != 0 || !S_ISDIR(st->st_mode)) return i;
9364    let i = unsafe { libc::stat(c_path.as_ptr(), st as *mut _) }; // c:7693
9365    if i != 0 || (st.st_mode & libc::S_IFMT) != libc::S_IFDIR {
9366        // c:7693
9367        return i; // c:7693
9368    }
9369    // C 7700-7706: nlink=1, S_IFDIR → S_IFREG, zero size/blocks.
9370    st.st_nlink = 1; // c:7701
9371    st.st_mode &= !libc::S_IFDIR; // c:7702
9372    st.st_mode |= libc::S_IFREG; // c:7703
9373    st.st_size = 0; // c:7704
9374    st.st_blocks = 0; // c:7705
9375                      // C 7707-7712: stat(path/cur). If absent or not a dir, return 0
9376                      // with the partial out (just the IFREG-coerced root).
9377    let cur_path = match CString::new(format!("{}/cur", path)) {
9378        Ok(c) => c,
9379        Err(_) => return 0,
9380    };
9381    let mut sub: libc::stat = unsafe { std::mem::zeroed() };
9382    if unsafe { libc::stat(cur_path.as_ptr(), &mut sub) } != 0               // c:7708
9383        || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9384    // c:7708
9385    {
9386        return 0; // c:7710
9387    }
9388    st.st_atime = sub.st_atime; // c:7712
9389                                // C 7715-7722: stat(path/tmp).
9390    let tmp_path = match CString::new(format!("{}/tmp", path)) {
9391        Ok(c) => c,
9392        Err(_) => return 0,
9393    };
9394    if unsafe { libc::stat(tmp_path.as_ptr(), &mut sub) } != 0               // c:7716
9395        || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9396    // c:7716
9397    {
9398        return 0; // c:7718
9399    }
9400    st.st_mtime = sub.st_mtime; // c:7720
9401                                // C 7724-7730: stat(path/new). C overwrites mtime with new/'s mtime.
9402    let new_path = match CString::new(format!("{}/new", path)) {
9403        Ok(c) => c,
9404        Err(_) => return 0,
9405    };
9406    if unsafe { libc::stat(new_path.as_ptr(), &mut sub) } != 0               // c:7724
9407        || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9408    // c:7724
9409    {
9410        return 0; // c:7726
9411    }
9412    st.st_mtime = sub.st_mtime; // c:7728
9413                                // C 7749-7778: walk new/ and cur/, sum size + blocks, track newest
9414                                // atime / mtime.
9415    let mut atime: libc::time_t = 0; // c:7748
9416    let mut mtime: libc::time_t = 0; // c:7748
9417    for sub_name in ["new", "cur"] {
9418        let dir = format!("{}/{}", path, sub_name);
9419        let entries = match fs::read_dir(&dir) {
9420            Ok(e) => e,
9421            Err(_) => return 0,
9422        };
9423        for entry in entries.flatten() {
9424            let name = entry.file_name();
9425            let name_bytes = name.as_encoded_bytes();
9426            // C: if (fn->d_name[0] == '.') continue;
9427            if name_bytes.first() == Some(&b'.') {
9428                // c:7758
9429                continue;
9430            }
9431            let entry_path = match CString::new(entry.path().to_string_lossy().as_bytes()) {
9432                Ok(c) => c,
9433                Err(_) => continue,
9434            };
9435            let mut entry_st: libc::stat = unsafe { std::mem::zeroed() };
9436            if unsafe { libc::stat(entry_path.as_ptr(), &mut entry_st) } != 0 {
9437                // c:7762
9438                continue;
9439            }
9440            st.st_size += entry_st.st_size; // c:7766
9441            st.st_blocks += 1; // c:7767
9442                               // C: if (atime != mtime && atime > atime_max) atime_max = atime;
9443            if entry_st.st_atime != entry_st.st_mtime && entry_st.st_atime > atime {
9444                // c:7769
9445                atime = entry_st.st_atime;
9446            }
9447            if entry_st.st_mtime > mtime {
9448                // c:7771
9449                mtime = entry_st.st_mtime;
9450            }
9451        }
9452    }
9453    // C 7783-7784: if (atime) st_ret.st_atime = atime;
9454    if atime != 0 {
9455        // c:7783
9456        st.st_atime = atime;
9457    }
9458    if mtime != 0 {
9459        // c:7785
9460        st.st_mtime = mtime;
9461    }
9462    0 // c:7787
9463}
9464
9465/// Script name for error messages
9466pub static mut SCRIPT_NAME: Option<String> = None;
9467/// Script filename
9468pub static mut SCRIPT_FILENAME: Option<String> = None;
9469
9470/// Print a fatal error to stderr.
9471/// Port of `zerr(VA_ALIST1(const char *fmt))` from Src/utils.c:172. C source sets `errflag`
9472/// after emitting `<prefix>: <msg>\n` so the running script aborts
9473/// at the next safe point. The Rust port currently just prints —
9474// =====================================================================
9475// Module-static state for the zerr/zwarn/zerrnam/zwarnnam family —
9476// port of the file-statics in Src/init.c + Src/exec.c that
9477// `zwarning()` (utils.c:142) reads to build the error prefix.
9478// =====================================================================
9479
9480// name of script being sourced                                             // c:33
9481/// Port of `char *scriptname` from `Src/init.c`. Set when `source`
9482/// is reading a script; cleared on return. Used by `zwarning()`
9483/// (utils.c:147) as the diagnostic prefix.
9484static SCRIPTNAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9485
9486/// Port of `char *argzero` from `Src/init.c`. The shell's argv[0].
9487/// Used by `zwarning()` (utils.c:147) as the fallback diagnostic
9488/// prefix when scriptname is unset.
9489static ARGZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9490
9491/// Port of `char *scriptfilename` from `Src/init.c` (listed in
9492/// `zsh.export:356`). The FILE that the current code was PARSED
9493/// from. Distinct from `scriptname` (which doshfunc overrides on
9494/// function entry at c:5903). `scriptfilename` stays at the outer
9495/// file across function calls; PS4's `%x` reads it at
9496/// `Src/prompt.c:937`.
9497///
9498/// Set at init.c:479 (`-c` mode → `scriptname = scriptfilename
9499/// = "zsh"`), init.c:1367 (`source` enters the named file),
9500/// init.c:1558+1592+1667 (save/install/restore around `.` /
9501/// `source` bin_dot dispatch).
9502static SCRIPTFILENAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9503
9504/// Port of `char *posixzero` from `Src/params.c:76`. The original
9505/// argv[0] preserved unchanged by later mutations. Used by
9506/// `argzerogetfn` for `$0` under `isset(POSIXARGZERO)`.
9507static POSIXZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9508
9509// error flag: bits from enum errflag_bits                                 // c:124
9510/// Port of `int errflag` from `Src/init.c`. Tracks whether an
9511/// error has been raised (`ERRFLAG_ERROR = 1`) or break/return
9512/// is in flight (`ERRFLAG_INT = 2`).
9513///
9514/// Direct global access: `errflag.load(Ordering::Relaxed)` reads
9515/// the current value, `errflag.fetch_or(ERRFLAG_ERROR, …)` matches
9516/// C's `errflag |= ERRFLAG_ERROR`, `errflag.store(0, …)` matches
9517/// C's `errflag = 0`.
9518#[allow(non_upper_case_globals)]
9519pub static errflag: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9520
9521/// Port of `int noerrs` from `Src/init.c`. Counter — when `> 0`,
9522/// suppresses error printing. `noerrs >= 2` also suppresses the
9523/// `errflag` set inside `zerr`/`zerrnam`.
9524static NOERRS: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
9525
9526/// Port of `int locallevel` from `Src/init.c`. Function-call depth
9527/// (0 = top-level, 1+ = inside a fn). `zwarning()` checks this in
9528/// the script-prefix path (utils.c:150).
9529// `LOCALLEVEL` removed — see `locallevel_lock` removal comment
9530// below. Canonical static lives in `LOCALLEVEL`
9531// (port of params.c:54).
9532
9533/// Port of `int lineno` from `Src/init.c`. Current line number;
9534/// `zerrmsg()` includes it in the diagnostic when locallevel > 0
9535/// or shinstdin is unset (utils.c:301).
9536static LINENO: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
9537
9538/// Port of the `isset(SHINSTDIN)` check from utils.c:150. C reads
9539/// the SHINSTDIN option directly; the Rust port caches it here so
9540/// callers don't pull in the whole option-table for every error.
9541static SHINSTDIN_OPT: std::sync::OnceLock<Mutex<bool>> = std::sync::OnceLock::new();
9542
9543/// `ERRFLAG_ERROR` from `Src/zsh.h`. Set on `zerr`/`zerrnam` to
9544/// signal a fatal error has occurred.
9545pub const ERRFLAG_ERROR: i32 = 1;
9546
9547/// Port of `mod_export int incompfunc` from `Src/utils.c:46`.
9548/// Set non-zero while a `comp*` builtin is dispatching from inside
9549/// a user-defined completion function — guards `comparguments` /
9550/// `compset` / `compadd` / `compdescribe` / `comptags` / `compvalues`
9551/// / `compfiles` / `compgroups` / `compquote` against being called
9552/// outside the `compfunc` shfunc context (each builtin checks
9553/// `INCOMPFUNC` early and emits "can only be called from completion
9554/// function" when zero).
9555pub static INCOMPFUNC: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:46
9556
9557/// Port of `mod_export int resetneeded` from `Src/utils.c:1821`.
9558/// Set when the editor needs a redraw — incremented by widgets +
9559/// signal handlers (e.g. SIGWINCH); the next prompt loop tick clears
9560/// it after running zrefresh.
9561pub static RESETNEEDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1821
9562
9563/// Port of `mod_export int winchanged` from `Src/utils.c:1827`.
9564/// Set by the SIGWINCH handler — the next refresh re-queries the
9565/// terminal size and re-renders, then clears the flag.
9566pub static WINCHANGED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1827
9567
9568/// Port of C's `GETKEY_*` flag enumeration from `Src/zsh.h:3143`.
9569/// Passed to `getkeystring_with` to alter escape interpretation:
9570///   - GETKEY_OCTAL_ESC: `\NNN` octal escapes are interpreted (1<<0)
9571///   - GETKEY_EMACS: unknown `\<char>` drops the backslash (1<<1)
9572///   - GETKEY_BACKSLASH_C: `\c` truncates the result (1<<3)
9573pub const GETKEY_OCTAL_ESC: u32 = 1 << 0; // c:zsh.h:3143
9574/// `GETKEY_EMACS` constant.
9575pub const GETKEY_EMACS: u32 = 1 << 1; // c:zsh.h:3150
9576/// `GETKEY_CTRL` constant.
9577pub const GETKEY_CTRL: u32 = 1 << 2; // c:zsh.h:3152
9578/// `GETKEY_BACKSLASH_C` constant.
9579pub const GETKEY_BACKSLASH_C: u32 = 1 << 3; // c:zsh.h:3154
9580
9581/// `GETKEYS_PRINT = GETKEY_OCTAL_ESC | GETKEY_BACKSLASH_C |
9582/// GETKEY_EMACS` per Src/zsh.h:3185 — the flag set `bin_print` uses
9583/// when interpreting escapes (with EMACS: unknown `\<c>` → `<c>`).
9584pub const GETKEYS_PRINT: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_BACKSLASH_C; // c:zsh.h:3185
9585
9586/// `GETKEYS_ECHO = GETKEY_BACKSLASH_C` per Src/zsh.h:3178 — the flag
9587/// set `echo` uses (and `print -e`). NOT GETKEY_EMACS: unknown
9588/// `\<c>` is preserved as literal `\<c>`. NOT GETKEY_OCTAL_ESC:
9589/// `\NNN` is NOT interpreted as octal. Only `\a \b \e \E \f \n \r
9590/// \t \v \\` (plus `\c` truncation) are handled.
9591///
9592/// Per builtin.c:4754-4760 — `bin_print` selects this for BIN_ECHO
9593/// or when `-e` is set; otherwise GETKEYS_PRINT.
9594pub const GETKEYS_ECHO: u32 = GETKEY_BACKSLASH_C; // c:zsh.h:3178
9595
9596/// `GETKEYS_BINDKEY = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL`
9597/// per Src/zsh.h:3187 — the flag set `print -b` uses. Like GETKEYS_PRINT
9598/// it has EMACS (so `\C-`/`\M-` backslash escapes are processed and
9599/// unknown `\<c>` collapses to `<c>`) but adds GETKEY_CTRL (enabling the
9600/// `^X` caret notation) and drops GETKEY_BACKSLASH_C (no `\c` truncation).
9601pub const GETKEYS_BINDKEY: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL; // c:zsh.h:3187
9602
9603/// Static `int ep` from Src/utils.c:4775 — sticky flag suppressing the
9604/// `can't set tty pgrp` warning after the first failure.
9605static ATTACHTTY_EP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:4775
9606
9607// =====================================================================
9608// !!! WARNING: RUST-ONLY HELPERS — NO DIRECT C COUNTERPART !!!
9609// =====================================================================
9610//
9611// These three ported (`fdtable_lock`, `fdtable_get`, `fdtable_set`) DO
9612// NOT EXIST as functions in `Src/utils.c`. The C source declares
9613// `fdtable` as a bare `unsigned char *` global (Src/utils.c:~63) and
9614// every call site reads/writes it as a direct array index:
9615//
9616//     if (fdtable[fd] != FDT_UNUSED) ...
9617//     fdtable[fd] = FDT_EXTERNAL;
9618//
9619// Rust can't hand out raw mutable indexes through a `Mutex<Vec<u8>>`
9620// safely without a borrow scope, so the Rust port wraps the same
9621// slot access in three `pub fn`s. Each call site to these helpers
9622// corresponds 1:1 to a `fdtable[fd] = X` or `fdtable[fd] != X`
9623// statement in the C source — they are NOT new policy, they only
9624// adapt the storage shape from `unsigned char *` to a growable
9625// `Mutex<Vec<i32>>`.
9626//
9627// Also adapts `growfdtable` (Src/utils.c:1965) by lazily growing the
9628// Vec inside `fdtable_set` instead of a separate `growfdtable`
9629// call — the C source calls `growfdtable(fd)` immediately before
9630// every `fdtable[fd] = X` write anyway.
9631//
9632// !!! Do NOT use these for any state that the C source does not
9633// already store in `fdtable[]`. Adding a new "fd kind" here is a
9634// scope expansion, not a port. !!!
9635// =====================================================================
9636
9637static FDTABLE: std::sync::OnceLock<Mutex<Vec<i32>>> = std::sync::OnceLock::new();
9638
9639/// Port of `int max_zsh_fd` global from `Src/exec.c:201`.
9640/// "The highest fd we know zsh is using" — bumped by `fdtable_set`
9641/// callers, shrunk by `zclose` when trailing slots fall to UNUSED.
9642pub static MAX_ZSH_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
9643
9644/// Port of `int fdtable_flocks` global from `Src/exec.c:204`.
9645/// Count of `FDT_FLOCK`-tagged fds; consulted by `closem()` to
9646/// decide whether to skip the flock-fd sweep.
9647pub static FDTABLE_FLOCKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9648
9649/// Setter for `scriptname`. Called from `bin_dot` / `source`
9650/// when entering a script.
9651pub fn set_scriptname(name: Option<String>) {
9652    *scriptname_lock().lock().unwrap() = name;
9653}
9654
9655/// Read `scriptname` — direct mirror of the C file-scope
9656/// `char *scriptname;` at `Src/utils.c:36`. Exposed for the
9657/// prompt expander (`%N`), `bin_dot`, and ZLE source tracking.
9658pub fn scriptname_get() -> Option<String> {
9659    scriptname_lock().lock().unwrap().clone()
9660}
9661
9662/// Setter for `scriptfilename` — direct mirror of the C
9663/// `scriptfilename` global at `Src/init.c` (zsh.export:356).
9664/// Called from `bin_dot` at init.c:1592, restored at c:1667.
9665pub fn set_scriptfilename(name: Option<String>) {
9666    *scriptfilename_lock().lock().unwrap() = name;
9667}
9668
9669/// Read `scriptfilename` — direct mirror of the C global.
9670/// Used by PS4's `%x` (`Src/prompt.c:937`) and the function-call
9671/// frame at `Src/exec.c:1613` (`fstack.filename = scriptfilename`).
9672pub fn scriptfilename_get() -> Option<String> {
9673    scriptfilename_lock().lock().unwrap().clone()
9674}
9675
9676/// Read `locallevel` — function nesting depth. Routes through the
9677/// canonical `LOCALLEVEL` static (port of
9678/// `Src/params.c:54`). The prior Rust port stored a duplicate
9679/// `Mutex<i32>` here that split state from params.rs; both copies
9680/// were never kept in sync.
9681pub fn locallevel() -> i32 {
9682    LOCALLEVEL.load(Ordering::Relaxed)
9683}
9684
9685/// Bump `locallevel` (called by `startparamscope`).
9686pub fn inc_locallevel() {
9687    LOCALLEVEL.fetch_add(1, Ordering::Relaxed);
9688}
9689
9690/// Decrement `locallevel` (called by `endparamscope`).
9691pub fn dec_locallevel() {
9692    LOCALLEVEL.fetch_sub(1, Ordering::Relaxed);
9693}
9694
9695/// Setter for `argzero`. Called once at shell init from `parseargs`.
9696pub fn set_argzero(name: Option<String>) {
9697    // c:271 (init.c) — `argv0 = argzero = posixzero = *argv++;`. At
9698    // shell init both argzero and posixzero share the same source.
9699    // posixzero is preserved unchanged after later mutations (exec -a,
9700    // function frames) — argzero is what gets rewritten. If posixzero
9701    // hasn't been independently set yet, mirror argzero so the
9702    // POSIXARGZERO branch in `argzerogetfn` returns something useful.
9703    let mut posix_lock = posixzero_lock().lock().unwrap();
9704    if posix_lock.is_none() {
9705        *posix_lock = name.clone();
9706    }
9707    drop(posix_lock);
9708    *argzero_lock().lock().unwrap() = name;
9709}
9710
9711/// Read `argzero`. Used by `argzerogetfn` for `$0`.
9712pub fn argzero() -> Option<String> {
9713    argzero_lock().lock().unwrap().clone()
9714}
9715
9716/// Port of `char *posixzero` from `Src/params.c:76`. The original
9717/// `argv[0]` preserved from shell startup, unchanged by later `exec -a`
9718/// or function-call rewrites. C's `argzerogetfn` (params.c:4958)
9719/// returns this instead of `argzero` when `isset(POSIXARGZERO)`.
9720///
9721/// Set once at init via `set_posixzero` (called from the init path
9722/// in C at init.c:271/297/321). `set_argzero` mirrors the value here
9723/// on first call so the POSIXARGZERO branch has something to read
9724/// even if the init path hasn't run yet.
9725pub fn set_posixzero(name: Option<String>) {
9726    *posixzero_lock().lock().unwrap() = name;
9727}
9728
9729/// Read `posixzero`. C: `posixzero` (params.c:76 global). Used by
9730/// `argzerogetfn` when `isset(POSIXARGZERO)`.
9731pub fn posixzero() -> Option<String> {
9732    posixzero_lock().lock().unwrap().clone()
9733}
9734
9735/// Setter for `noerrs`. Increment to suppress error output;
9736/// decrement to restore.
9737pub fn set_noerrs(v: i32) {
9738    *noerrs_lock().lock().unwrap() = v;
9739}
9740
9741/// Setter for `locallevel`. Called by function-call entry/exit.
9742pub fn set_locallevel(v: i32) {
9743    LOCALLEVEL.store(v, Ordering::Relaxed);
9744}
9745
9746/// Setter for `lineno`. Called by the parser as it advances.
9747pub fn set_lineno(v: i32) {
9748    *lineno_lock().lock().unwrap() = v;
9749}
9750
9751/// Setter for the cached SHINSTDIN flag. Called by the option
9752/// machinery whenever `setopt shinstdin` / `unsetopt shinstdin`
9753/// fires.
9754pub fn set_shinstdin(v: bool) {
9755    *shinstdin_lock().lock().unwrap() = v;
9756}
9757
9758/// Check if string is a valid identifier
9759// `isident` DELETED — fake duplicate (cited `c:params.c:1288` from
9760// utils.rs, location mismatch). Canonical port is
9761// `isident` at `params.rs:2056`, which
9762// correctly handles namespace prefix (`ns.var`) per the real C
9763// body — the utils.rs copy dropped that arm. Callers now route
9764// through `isident` directly.
9765
9766// QuoteType enum + impl deleted — the canonical quote-type values are
9767// the bare `QT_*: i32` constants at `zsh_h.rs:175` (port of anonymous
9768// `enum { QT_NONE, QT_BACKSLASH, … }` from `Src/zsh.h:253-298`).
9769// `quotestring()` takes `quote_type: i32` matching C's signature.
9770
9771/// Map a `(q)` flag count to a `QT_*` value.
9772/// Port of the q-flag dispatch in `Src/subst.c` `paramsubst()` —
9773/// `(q)`=`QT_BACKSLASH`, `(qq)`=`QT_SINGLE`, `(qqq)`=`QT_DOUBLE`,
9774/// `(qqqq+)`=`QT_DOLLARS`.
9775pub fn qflag_quotetype(count: u32) -> i32 {
9776    match count {
9777        0 => QT_NONE,
9778        1 => QT_BACKSLASH,
9779        2 => QT_SINGLE,
9780        3 => QT_DOUBLE,
9781        _ => QT_DOLLARS,
9782    }
9783}
9784
9785/// Port of `ispecial()` macro from `Src/ztype.h:59` — `zistype(X,
9786/// ISPECIAL)`. C populates the ISPECIAL typtab bit at
9787/// `Src/utils.c:4253-4262`:
9788///   - Every byte in `SPECCHARS` (`Src/zsh.h:228` =
9789///     `"#$^*()=|{}[]\`<>?~;&\\n\\t \\\\\\'\\""`)
9790///   - `,` when `typtab_flags & ZTF_SP_COMMA` (set by
9791///     `makecommaspecial(1)` per c:4271)
9792///   - `bangchar` (default `!`) when `BANGHIST` is set AND
9793///     `ZTF_INTERACT` is set (interactive shell)
9794///
9795/// The Rust port enumerates `SPECCHARS` directly because the
9796/// typtab is not lazily initialised — production `ispecial` calls
9797/// happen after `init_typtab` runs at shell startup, but
9798/// `quotestring` is unit-tested in isolation where the typtab is
9799/// all-zero. Match the C SPECCHARS set byte-for-byte; the option-
9800/// driven augments (`,` and bangchar) are still NOT wired through
9801/// (a known gap — see the typtab-routing TODO at the param level).
9802fn ispecial(c: char) -> bool {
9803    // c:59 (Src/ztype.h)
9804    // c:228 (Src/zsh.h) `SPECCHARS` — exact byte set from the C
9805    // string literal. `^` and `{`/`}` were already present; `!`
9806    // is INTENTIONALLY OMITTED here because C only ISPECIAL-tags
9807    // bangchar under BANGHIST + interactive. Including `!`
9808    // unconditionally diverges from C for non-interactive scripts
9809    // and unbang'd input.
9810    matches!(
9811        c,
9812        '#' | '$' | '^' | '*' | '(' | ')' | '='  // c:228 first half
9813            | '|' | '{' | '}' | '[' | ']' | '`'   // c:228 mid
9814            | '<' | '>' | '?' | '~' | ';' | '&'   // c:228 specials
9815            | '\n' | '\t' | ' ' | '\\' | '\'' | '"' // c:228 whitespace/backslash/quotes
9816    )
9817}
9818
9819/// Convert integer to string with specified base — re-export of
9820/// the canonical port at `convbase_param`.
9821///
9822/// The previous Rust port at this file was a SECOND, divergent
9823/// implementation of `convbase`:
9824///   - Used LOWERCASE digit chars (`"0123456789abc..."`) for
9825///     bases 11..36, while C uses UPPERCASE (`(dig - 10) + 'A'`
9826///     at Src/params.c:5621).
9827///   - Skipped CBASES + OCTALZEROES prefix handling at c:5598-5604
9828///     (`0x`/`0` prefixes when option-gated).
9829///   - Returned wrong format for bases not handled by the explicit
9830///     match arms.
9831///
9832/// The canonical port lives at `params.rs::convbase_ptr` (c:5588
9833/// faithful body) + `params.rs::convbase` (c:5634 wrapper). Route
9834/// through there so utils.rs / params.rs agree byte-for-byte and
9835/// no divergent duplicate stays alive.
9836pub fn convbase(val: i64, base: u32) -> String {
9837    // c:5634 (Src/params.c)
9838    convbase_param(val, base)
9839}
9840
9841// `checkglobqual` DELETED — fake bracket-depth scanner cited
9842// `c:glob.c:1158` from utils.rs. Canonical port is
9843// `crate::ported::glob::checkglobqual(str, sl, nobareglob, sp)` at
9844// `glob.rs:813`, which matches the real C signature
9845// `(char *str, int sl, int nobareglob, char **sp)`. The utils.rs
9846// fake reduced the signature to a single bool and discarded the
9847// `sp` glob-qualifier-start out-pointer that callers need. Zero
9848// callers used the utils.rs version.
9849
9850// `dupstrpfx` DELETED — fake duplicate cited `c:string.c:161` from
9851// utils.rs (wrong location). Canonical port is
9852// `dupstrpfx` at `string.rs:161`. Zero
9853// callers used the utils.rs version.
9854
9855/// Get username from UID (from utils.c getpwuid handling)
9856pub fn statuidprint(uid: u32) -> Option<String> {
9857    #[cfg(unix)]
9858    {
9859        let pwd = unsafe { libc::getpwuid(uid) };
9860        if pwd.is_null() {
9861            return None;
9862        }
9863        let name = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_name) };
9864        name.to_str().ok().map(|s| s.to_string())
9865    }
9866    #[cfg(not(unix))]
9867    {
9868        let _ = uid;
9869        None
9870    }
9871}
9872
9873// `ztrdup` / `dyncat` / `tricat` / `bicat` DELETED — these were
9874// fake duplicates of the canonical ports in `src/ported/string.rs`
9875// (`Src/string.c:62`, `:131`, `:98`, `:145`). The utils.rs copies
9876// admitted they were not in utils.c via `c:string.c:NNN`
9877// annotations; PORT.md Rule 1 disallows the duplicate location.
9878// Callers now route through `crate::ported::string::{ztrdup,
9879// dyncat, tricat, bicat}` directly.
9880
9881// `iwsep` / `iwsep_byte` DELETED — these were fake hardcoded
9882// `c==' '||c=='\t'||c=='\n'` checks claiming to port `iwsep` from
9883// "Src/zsh.h", but the real macro lives at `Src/ztype.h:61`
9884// (`#define iwsep(X) zistype(X, IWSEP)`) and consults the `typtab[]`
9885// lookup — which mutates when IFS is reassigned. The canonical port
9886// is `iwsep(u8) -> bool` at `ztype_h.rs:133`.
9887// Internal utils.rs callers (skipwsep / spacesplit / findword) now
9888// route through it directly.
9889
9890// `imeta(c: char)` DELETED — fake `char`-arg wrapper cited
9891// `c:ztype.h:60` from utils.rs (wrong location, wrong signature).
9892// Canonical port is `crate::ported::ztype_h::imeta(u8) -> bool` at
9893// `ztype_h.rs:130`. C `imeta(X)` takes a byte; wrapping with `char`
9894// invented an `> 0xff` early-out that C doesn't have. Migrated 1
9895// caller (`zle/computil.rs:6194`).
9896
9897/// Get hostname
9898pub fn gethostname() -> String {
9899    #[cfg(unix)]
9900    {
9901        let mut buf = vec![0u8; 256];
9902        unsafe {
9903            if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) == 0 {
9904                let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
9905                return String::from_utf8_lossy(&buf[..len]).to_string();
9906            }
9907        }
9908    }
9909    // gethostname(2) failure fallback. C consults `$HOST` /
9910    // `cached_hostname`. Read paramtab; fall back to "localhost".
9911    getsparam("HOST")
9912        .or_else(|| getsparam("HOSTNAME"))
9913        .unwrap_or_else(|| "localhost".to_string())
9914}
9915
9916/// Get current working directory — re-export of the canonical
9917/// port at `compat::zgetcwd` (Src/compat.c:559).
9918// `zgetcwd` DELETED — fake `Option<String>` wrapper around the
9919// canonical `compat::zgetcwd() -> String` port (`Src/compat.c:559`).
9920// C `zgetcwd` never returns NULL (falls back to `"."` per the
9921// `dupstring(".")` arm), so the `Option` was caller-API churn, not
9922// a port. Callers route through `crate::ported::compat::zgetcwd()`
9923// directly.
9924
9925// `zchdir` duplicate deleted — canonical port at compat.rs:253
9926// matches C's `int zchdir(char *dir)` signature (returns i32, not
9927// bool). Zero callers used the utils.rs bool variant.
9928
9929// `realpath` / `mkdir` / `symlink` / `readlink` / `getenv` DELETED —
9930// five Rust-only convenience wrappers around std::fs / std::env /
9931// std::os::unix::fs with ZERO callers across the tree. Each was a
9932// thin std-lib re-export with no zsh C counterpart:
9933//   - realpath  → std::fs::canonicalize (libc realpath(3) — not in
9934//                 utils.c; the zsh source uses chrealpath at hist.c:1971)
9935//   - mkdir     → std::fs::create_dir (libc mkdir(2) — zsh's analogue
9936//                 is Modules/files.c::bin_mkdir, not a utils.c helper)
9937//   - symlink   → std::os::unix::fs::symlink (libc symlink(2))
9938//   - readlink  → std::fs::read_link (libc readlink(2))
9939//   - getenv    → std::env::var (libc getenv(3) — zsh's analogue is
9940//                 zgetenv in compat.c)
9941// PORT.md Rule 0/A: names must exist in upstream zsh C source as a
9942// `<name>(` function. None of these qualify; with zero callers they
9943// were dead code by definition. Callers needing the same semantics
9944// route through the canonical port (chrealpath / bin_mkdir / etc.)
9945// or std::fs directly.
9946
9947/// Per-prompt callback registry.
9948/// Port of the static `prepromptfns` LinkList in Src/utils.c:1319.
9949/// Holds the bare-fn pointers `addprepromptfn`/`delprepromptfn`
9950/// register and `preprompt()` walks.
9951static PREPROMPT_FNS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
9952
9953/// Set environment variable
9954pub fn setenv(name: &str, value: &str) {
9955    std::env::set_var(name, value);
9956}
9957
9958/// Unset environment variable
9959pub fn unsetenv(name: &str) {
9960    std::env::remove_var(name);
9961}
9962
9963/// Time-ordered timed-function registry.
9964/// Port of the `timedfns` LinkList Src/utils.c:1371 keeps for the
9965/// `sched` builtin. Sorted ascending by `when` (epoch seconds);
9966/// `addtimedfn` does an insertion sort (matches the C source's
9967/// `for (;;)` walk at lines 1394-1411).
9968pub static TIMED_FNS: Mutex<Vec<(i64, fn())>> = Mutex::new(Vec::new()); // c:1371 timedfns (mod_export in C)
9969
9970/// Get current user ID
9971pub fn getuid() -> u32 {
9972    #[cfg(unix)]
9973    unsafe {
9974        libc::getuid()
9975    }
9976    #[cfg(not(unix))]
9977    0
9978}
9979
9980/// Get effective user ID
9981pub fn geteuid() -> u32 {
9982    #[cfg(unix)]
9983    unsafe {
9984        libc::geteuid()
9985    }
9986    #[cfg(not(unix))]
9987    0
9988}
9989
9990/// Get current group ID
9991pub fn getgid() -> u32 {
9992    #[cfg(unix)]
9993    unsafe {
9994        libc::getgid()
9995    }
9996    #[cfg(not(unix))]
9997    0
9998}
9999
10000/// Get effective group ID
10001pub fn getegid() -> u32 {
10002    #[cfg(unix)]
10003    unsafe {
10004        libc::getegid()
10005    }
10006    #[cfg(not(unix))]
10007    0
10008}
10009
10010/// Get process ID
10011pub fn getpid() -> i32 {
10012    std::process::id() as i32
10013}
10014
10015/// Get parent process ID
10016pub fn getppid() -> i32 {
10017    #[cfg(unix)]
10018    unsafe {
10019        libc::getppid()
10020    }
10021    #[cfg(not(unix))]
10022    0
10023}
10024
10025/// `getkeystring` with the C `how` parameter (Src/utils.c:6915).
10026/// The plain `getkeystring(s)` shim above defaults `how=0` for
10027/// non-EMACS callers (zbeep, dollar-quote — they keep unknown
10028/// `\<char>` as literal `\<char>`). Pass `GETKEYS_PRINT` to get
10029/// the print/echo behavior (drop backslash on unknown).
10030/// `getkeystring` with the C `how` parameter AND the
10031/// `GETKEY_UPDATE_OFFSET` cursor-offset out-param (Src/utils.c:6915).
10032///
10033/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from
10034/// `Src/utils.c:6915`, specifically its `GETKEY_UPDATE_OFFSET` path.
10035/// When `how` sets `GETKEY_UPDATE_OFFSET` and `misc` is `Some`, `*misc`
10036/// is the C `*misc` completion offset — a byte index into `s` — and is
10037/// updated as escapes positioned before the cursor collapse:
10038///   - `-1` per collapsed `\<char>` escape whose backslash precedes the
10039///     cursor, `s - sstart < *misc` (c:6987); undone if the backslash is
10040///     kept literal in the default (non-EMACS) arm (c:7181-7183);
10041///   - additional `-6` for `\u`, `-10` (`-4`+`-6`) for `\U` (c:7073-7084),
10042///     then `+N` for the N output bytes emitted (c:7123-7124).
10043/// Octal/hex escapes get only the base `-1`, matching C's own incomplete
10044/// `/* HERE: GETKEY_UPDATE_OFFSET? */` handling (c:7155).
10045///
10046/// The offset is a byte index. For ASCII `$'...'` content (one byte per
10047/// char) it matches the C metafied-byte semantics exactly; the byte-vs-
10048/// char divergence for non-ASCII content is the pre-existing shared-cursor
10049/// limitation documented on `set_comp_sep` (compcore.rs), not new here.
10050///
10051/// Callers with no offset to track pass `misc = None`.
10052pub fn getkeystring_with(
10053    s: &str,
10054    how: u32,
10055    mut misc: Option<&mut i32>,
10056) -> (String, usize) {
10057    // c:utils.c:6915
10058    let update_off = (how & crate::ported::zsh_h::GETKEY_UPDATE_OFFSET as u32) != 0;
10059    let mut result = String::new();
10060    let mut chars = s.chars().peekable();
10061    let mut consumed = 0;
10062    // c:7194 — C's `^` handler is `control = 1; continue;`, so the control bit
10063    // stays PENDING across loop iterations and a following `\M`/`\C` escape
10064    // chains onto it (`^\M-a` → c:7034 `meta = 1 + control` → meta==2 → 0x81).
10065    // This port's `^` arm consumes its base character inline, which cannot
10066    // express that, so the flag carries the state to the modifier arm instead.
10067    let mut pending_control = false;
10068    while let Some(c) = chars.next() {
10069        consumed += c.len_utf8();
10070        // c:utils.c:7194 — `^X` caret notation. A bare `^` (not a backslash
10071        // escape) followed by any char applies the control mask to it, but
10072        // ONLY under GETKEY_CTRL — i.e. `print -b` / bindkey. Plain print
10073        // (GETKEYS_PRINT, no GETKEY_CTRL) keeps `^A` literal.
10074        if c == '^' && !pending_control && (how & GETKEY_CTRL) != 0 {
10075            // A `\M`/`\C` escape after the caret must be handled by the
10076            // modifier arm with control already pending — C reaches it via
10077            // `continue` rather than consuming the base char here. Gated on
10078            // GETKEY_EMACS because that is what enables the modifier arm at
10079            // all (c:7031/7043); without it C's `case 'M'` emits a literal
10080            // backslash instead.
10081            let mut la = chars.clone();
10082            if (how & GETKEY_EMACS) != 0
10083                && la.next() == Some('\\')
10084                && matches!(la.next(), Some('M') | Some('C'))
10085            {
10086                pending_control = true;
10087                continue;
10088            }
10089            if let Some(base) = chars.next() {
10090                consumed += base.len_utf8();
10091                let mut byte = base as u32;
10092                // c:7261-7267 — `^?` → 0x7f (DEL), else clear bits 5-6.
10093                if byte == '?' as u32 {
10094                    byte = 0x7f;
10095                } else if byte <= 0xff {
10096                    byte &= 0x9f;
10097                }
10098                if byte <= 0xff {
10099                    let b_ = byte as u8;
10100                    if b_ < 0x80 {
10101                        result.push(b_ as char);
10102                    } else {
10103                        result.push('\u{83}');
10104                        result.push(char::from(b_ ^ 32));
10105                    }
10106                } else if let Some(ch) = char::from_u32(byte) {
10107                    result.push(ch);
10108                }
10109                continue;
10110            }
10111            // c:7194 `s[1]` guard — a trailing `^` with no char is literal.
10112            result.push(c);
10113            continue;
10114        }
10115        if c != '\\' {
10116            result.push(c);
10117            continue;
10118        }
10119        // c is a backslash; record its byte start offset for
10120        // GETKEY_UPDATE_OFFSET (`s - sstart` in C).
10121        let bs_off = consumed - c.len_utf8();
10122        // c:utils.c:6987 — base offset decrement for a collapsed `\<char>`
10123        // escape whose backslash precedes the cursor. C gates on
10124        // `*s == '\\' && s[1]`, so a following char must exist (peek).
10125        let mut miscadded = false;
10126        if update_off && chars.peek().is_some() {
10127            if let Some(m) = misc.as_deref_mut() {
10128                if (bs_off as i32) < *m {
10129                    *m -= 1;
10130                    miscadded = true;
10131                }
10132            }
10133        }
10134        match chars.next() {
10135            Some('n') => {
10136                result.push('\n');
10137                consumed += 1;
10138            }
10139            Some('t') => {
10140                result.push('\t');
10141                consumed += 1;
10142            }
10143            Some('r') => {
10144                result.push('\r');
10145                consumed += 1;
10146            }
10147            // c:utils.c:7018-7024 — `\E` is EMACS-gated:
10148            //     case 'E':
10149            //         if (!(how & GETKEY_EMACS)) {
10150            //             *t++ = '\\', s--;
10151            //             if (miscadded) (*misc)++;
10152            //             continue;
10153            //         }
10154            //         /* FALL THROUGH */
10155            //     case 'e':
10156            //         *t++ = '\033';
10157            // Without EMACS `\E` stays a literal backslash + `E`; only `\e`
10158            // is unconditional. Folding `E` in with `e` made `${(g::)v}` on
10159            // `\E` yield ESC where zsh yields `\E`. Unlike the octal
10160            // `continue` arm (c:7159), C restores the miscadded decrement
10161            // here, since the backslash is kept.
10162            Some('E') if (how & GETKEY_EMACS) == 0 => {
10163                consumed += 1;
10164                if miscadded {
10165                    if let Some(m) = misc.as_deref_mut() {
10166                        *m += 1;
10167                    }
10168                }
10169                result.push('\\');
10170                result.push('E');
10171            }
10172            Some('e') | Some('E') => {
10173                result.push('\x1b');
10174                consumed += 1;
10175            }
10176            Some('a') => {
10177                result.push('\x07');
10178                consumed += 1;
10179            }
10180            Some('b') => {
10181                result.push('\x08');
10182                consumed += 1;
10183            }
10184            Some('f') => {
10185                result.push('\x0c');
10186                consumed += 1;
10187            }
10188            Some('v') => {
10189                result.push('\x0b');
10190                consumed += 1;
10191            }
10192            // c:utils.c:7140-7152 — `\\` (and `\'` under
10193            // GETKEY_DOLLAR_QUOTE) explicitly emit the trailing byte
10194            // bare. Outside DOLLAR_QUOTE, `\'` FALLTHROUGHs to default.
10195            // `\\` stays special because the default arm (c:7180-7184)
10196            // skips the leading-backslash emission iff `*s == '\\'`,
10197            // so `\\` reduces to `\` regardless of GETKEY_EMACS.
10198            //
10199            // Previous Rust port also had `Some('\'')` and `Some('"')`
10200            // arms that unconditionally dropped the backslash. That
10201            // contradicted C: under GETKEYS_ECHO (no GETKEY_EMACS),
10202            // `\'` must remain `\'` because echo doesn't strip
10203            // unknown escapes. Removed those arms — `\'` and `\"`
10204            // now flow through the default arm and honour GETKEY_EMACS.
10205            //
10206            // Root cause of `echo "${(qq)s}"` for `s="a'b"` emitting
10207            // `'a'''b'` instead of zsh's `'a'\''b'`.
10208            Some('\\') => {
10209                result.push('\\');
10210                consumed += 1;
10211            }
10212            // c:utils.c:7072-7138 — `\u` (4-hex) / `\U` (8-hex)
10213            // Unicode codepoint escapes. Always interpreted; the C
10214            // source's `case 'U':` / `case 'u':` arms have no flag
10215            // gating (only the GETKEY_UPDATE_OFFSET bookkeeping). C
10216            // calls `ucs4tomb(wval, t)` which writes UTF-8 bytes.
10217            // Previously these were absent from `getkeystring_with`,
10218            // so `echo -e "\U00000041"` emitted literal `\U00000041`
10219            // instead of 'A'.
10220            Some('u') => {
10221                consumed += 1;
10222                // c:utils.c:7077-7084 — extra `-6` when the escape precedes
10223                // the cursor (checked at the 'u', i.e. offset bs_off+1).
10224                if update_off {
10225                    if let Some(m) = misc.as_deref_mut() {
10226                        if ((bs_off + 1) as i32) < *m {
10227                            *m -= 6;
10228                        }
10229                    }
10230                }
10231                let mut hex = String::new();
10232                for _ in 0..4 {
10233                    if let Some(&c) = chars.peek() {
10234                        if c.is_ascii_hexdigit() {
10235                            hex.push(chars.next().unwrap());
10236                            consumed += 1;
10237                        } else {
10238                            break;
10239                        }
10240                    }
10241                }
10242                // c:utils.c:7085 `wval = 0;` BEFORE the digit loop, and
10243                // c:7087-7095 leaves it untouched when the first char is not
10244                // a hex digit (`s--; break;`). So ZERO digits is not "not an
10245                // escape" — it emits codepoint 0 via `ucs4tomb(wval, t)`
10246                // (c:7101). `\u` → one NUL byte, `\uzz` → NUL + "zz". An Err
10247                // on the empty parse pushed nothing at all.
10248                let val = u32::from_str_radix(&hex, 16).unwrap_or(0);
10249                if let Some(ch) = char::from_u32(val) {
10250                    result.push(ch);
10251                    // c:utils.c:7123-7124 — add one per output byte,
10252                    // checked at the last consumed input byte.
10253                    if update_off {
10254                        if let Some(m) = misc.as_deref_mut() {
10255                            if ((consumed - 1) as i32) < *m {
10256                                *m += ch.len_utf8() as i32;
10257                            }
10258                        }
10259                    }
10260                }
10261            }
10262            Some('U') => {
10263                consumed += 1;
10264                // c:utils.c:7073-7084 — `\U` extra `-4` (c:7073) then the
10265                // shared `\u` `-6` (c:7077), both gated at offset bs_off+1.
10266                if update_off {
10267                    if let Some(m) = misc.as_deref_mut() {
10268                        if ((bs_off + 1) as i32) < *m {
10269                            *m -= 10;
10270                        }
10271                    }
10272                }
10273                let mut hex = String::new();
10274                for _ in 0..8 {
10275                    if let Some(&c) = chars.peek() {
10276                        if c.is_ascii_hexdigit() {
10277                            hex.push(chars.next().unwrap());
10278                            consumed += 1;
10279                        } else {
10280                            break;
10281                        }
10282                    }
10283                }
10284                // c:utils.c:7085 — same `wval = 0` default as the `\u` arm
10285                // above: `\U` with no hex digits emits codepoint 0, not
10286                // nothing.
10287                let val = u32::from_str_radix(&hex, 16).unwrap_or(0);
10288                if let Some(ch) = char::from_u32(val) {
10289                    result.push(ch);
10290                    // c:utils.c:7123-7124 — add one per output byte.
10291                    if update_off {
10292                        if let Some(m) = misc.as_deref_mut() {
10293                            if ((consumed - 1) as i32) < *m {
10294                                *m += ch.len_utf8() as i32;
10295                            }
10296                        }
10297                    }
10298                }
10299            }
10300            // c:utils.c:7156-7178 — the NUMERIC-ESCAPE branch. C handles
10301            // `\x`, `\NNN` and `\0NNN` in ONE arm, and the shared tail is
10302            // what makes them agree, so this port keeps them together too:
10303            //
10304            //     if ((idigit(*s) && *s < '8') || *s == 'x') {
10305            //         if (!(how & GETKEY_OCTAL_ESC)) {
10306            //             if (*s == '0')
10307            //                 s++;
10308            //             else if (*s != 'x') {
10309            //                 *t++ = '\\', s--;
10310            //                 continue;
10311            //             }
10312            //         }
10313            //         if (s[1] && s[2] && s[3]) {
10314            //             svchar = s[3]; s[3] = '\0'; u = s;
10315            //         }
10316            //         *t++ = zstrtol(s + (*s == 'x'), &s,
10317            //                        (*s == 'x') ? 16 : 8);
10318            //         ...
10319            //         s--;
10320            //     }
10321            //
10322            // Three behaviours here were each missing when these were three
10323            // separate arms:
10324            //
10325            //  1. Without GETKEY_OCTAL_ESC a NON-ZERO digit is not an escape
10326            //     at all (c:7159): C emits a literal backslash and backs up so
10327            //     the digit is re-read as an ordinary char. This is INSIDE the
10328            //     numeric branch, so — unlike the default arm — GETKEY_EMACS
10329            //     does not suppress the backslash: `${(g:e:)v}` on `\101` is
10330            //     `\101`, not `101`.
10331            //  2. The value goes through `zstrtol`, which SKIPS leading blanks
10332            //     and accepts a `+`/`-` sign (c:2444-2450), and whose end
10333            //     pointer lands past everything it consumed — blanks included.
10334            //     So `\0 1` is byte 0o1, `\x 41` is 0x04 then `1`, `\x y` is
10335            //     NUL then `y`, and `\0-1` is 0xff (a NEGATIVE parse truncated
10336            //     to a byte). Peeking for "is the next char a digit" stopped at
10337            //     the blank and left it in the output.
10338            //  3. The base is chosen from the char AT `s` AFTER the `\0`
10339            //     introducer was skipped, so `\0x41` is HEX (`A`), not NUL
10340            //     followed by `x41`.
10341            //
10342            // c:7166-7168 windows the parse by NUL-ing `s[3]` when three chars
10343            // follow `s`, capping it at 3 significant chars (2 after an `x`),
10344            // then restores the byte — so `\1234` is 0o123 then `4`.
10345            //
10346            // `*t++ = zstrtol(...)` assigns a zlong to a char, truncating to
10347            // the low byte: `\777` (511) is 0xff, `\400` (256) is NUL. Parsing
10348            // straight into u8 made those an Err and emitted nothing at all.
10349            Some(d) if d.is_digit(8) || d == 'x' => {
10350                consumed += 1;
10351                // c:7157-7161 — the pre-checks that run only without
10352                // GETKEY_OCTAL_ESC. `\0` is the octal introducer (`s++`);
10353                // any other bare digit is not an escape.
10354                let zero_intro = (how & GETKEY_OCTAL_ESC) == 0 && d == '0';
10355                if (how & GETKEY_OCTAL_ESC) == 0 && d != '0' && d != 'x' {
10356                    // c:7159 `*t++ = '\\', s--; continue;`
10357                    result.push('\\');
10358                    result.push(d);
10359                    continue;
10360                }
10361                // C's `s` after the pre-checks: the char AFTER the `\0`
10362                // introducer, else the matched char itself.
10363                //
10364                // Four chars of lookahead is all C can read: the window test
10365                // reaches `s[3]`, and the parse never sees past it. Bounding
10366                // the clone keeps this O(1) per escape — collecting the whole
10367                // tail here would make decoding an escape-dense string O(n²).
10368                let rest: Vec<char> = chars.clone().take(4).collect();
10369                let (p, after): (Option<char>, &[char]) = if zero_intro {
10370                    (rest.first().copied(), rest.get(1..).unwrap_or(&[]))
10371                } else {
10372                    (Some(d), &rest[..])
10373                };
10374                // c:7166 `if (s[1] && s[2] && s[3])` — needs three chars after
10375                // `s`; the window then spans `s` plus two more.
10376                let win: &[char] = if after.len() >= 3 { &after[..2] } else { after };
10377                let mut vis: Vec<char> = Vec::with_capacity(3);
10378                if let Some(pc) = p {
10379                    vis.push(pc);
10380                }
10381                vis.extend_from_slice(win);
10382                // c:7170 `zstrtol(s + (*s == 'x'), &s, (*s == 'x') ? 16 : 8)`.
10383                let (base, skip_p) = if p == Some('x') { (16, 1) } else { (8, 0) };
10384                let parse_src: String = vis[skip_p.min(vis.len())..].iter().collect();
10385                let (num, tail) = zstrtol(&parse_src, base);
10386                let used_chars = parse_src[..parse_src.len() - tail.len()].chars().count();
10387                // Advance past what C consumed. `vis[0]` is the already-taken
10388                // match char unless the `\0` introducer moved `s` forward.
10389                let vis_used = skip_p + used_chars;
10390                let advance = if zero_intro { vis_used } else { vis_used.saturating_sub(1) };
10391                for _ in 0..advance {
10392                    chars.next();
10393                    consumed += 1;
10394                }
10395                let val = (num as u64 & 0xff) as u8;
10396                // c:Src/utils.c — the escape is one raw BYTE; metafy high
10397                // bytes (c:7289-7294) so `\377` unmetafies to a single 0xff,
10398                // not the UTF-8 pair c3 bf.
10399                if val < 0x80 {
10400                    result.push(val as char);
10401                } else {
10402                    result.push('\u{83}');
10403                    result.push(char::from(val ^ 32));
10404                }
10405                // c:7172-7173 — under GETKEY_PRINTF_PERCENT a numeric escape
10406                // producing `%` gets a second `%`.
10407                if (how & crate::ported::zsh_h::GETKEY_PRINTF_PERCENT as u32) != 0 && val == b'%' {
10408                    result.push('%');
10409                }
10410            }
10411            // c:utils.c:7029-7052 + c:7255-7275 — `\C` / `\M` set the
10412            // control / meta modifiers (bindkey-style key escapes), an
10413            // optional `-` separator, then the next base char (possibly
10414            // through chained `\C`/`\M`) gets the mask applied:
10415            // control → `& 0x9f` (or `\C-?` → 0x7f), meta → `| 0x80`.
10416            // Gated on GETKEY_EMACS (c:7031/7043 `if (how & GETKEY_EMACS)`):
10417            // print / print -b / $'…' have it, echo does not (so echo keeps
10418            // `\C-a` literal via the default arm). Mirrors the same handler
10419            // already present in `getkeystring` (utils.rs) and
10420            // `getkeystring_dollar_quote` (lex.rs); print's `getkeystring_with`
10421            // path lacked it, so `print "\C-a"` emitted literal `C-a`.
10422            Some(mod_letter @ ('C' | 'M')) if (how & GETKEY_EMACS) != 0 => {
10423                consumed += 1;
10424                // c:7034 `meta = 1 + control;  /* preserve the order of ^ and
10425                // meta */` — meta is a COUNT, not a flag: 2 when `\M` was seen
10426                // while control was already pending, which makes c:7261-7264
10427                // apply `|0x80` BEFORE the control mask instead of after. The
10428                // two orders differ for `?`: `\M-\C-?` is 0xff, `\C-\M-?` is
10429                // 0x9f. Booleans could not express that.
10430                // A `^` already seen (c:7194 `control = 1; continue;`) arrives
10431                // here as pending state, so `^\M-a` gives meta = 1 + 1 = 2.
10432                let mut control: u32 = if pending_control {
10433                    pending_control = false;
10434                    1
10435                } else {
10436                    0
10437                };
10438                let mut meta: u32 = 0;
10439                if mod_letter == 'C' {
10440                    control = 1; // c:7046
10441                } else {
10442                    meta = 1 + control; // c:7034
10443                }
10444                // c:7032-7033 / 7044-7045 — `if (s[1] == '-') s++;` consumes at
10445                // most ONE separator per modifier. Looping over a RUN of dashes
10446                // ate the base character of `\M--` (meta applied to `-`, 0xad),
10447                // leaving an empty binding.
10448                if chars.peek() == Some(&'-') {
10449                    chars.next();
10450                    consumed += 1;
10451                }
10452                // Chained `\C`/`\M`, and the caret form after a modifier.
10453                loop {
10454                    let mut iter_clone = chars.clone();
10455                    if iter_clone.next() == Some('\\') {
10456                        if let Some(nx) = iter_clone.next() {
10457                            if nx == 'C' || nx == 'M' {
10458                                chars.next(); // consume '\'
10459                                chars.next(); // consume C/M
10460                                consumed += 2;
10461                                if nx == 'C' {
10462                                    control = 1; // c:7046
10463                                } else {
10464                                    meta = 1 + control; // c:7034
10465                                }
10466                                if chars.peek() == Some(&'-') {
10467                                    chars.next();
10468                                    consumed += 1;
10469                                }
10470                                continue;
10471                            }
10472                        }
10473                    }
10474                    // c:7194 — `else if (*s == '^' && !control && (how &
10475                    // GETKEY_CTRL) && s[1]) { control = 1; continue; }`. C's
10476                    // `case 'M'` only sets `meta` and `continue`s, so the MAIN
10477                    // loop reaches this `^` handler with meta still pending:
10478                    // `\M-^?` is control+meta applied to `?` (0xff). This arm
10479                    // consumed the base char itself, so `^` became the base and
10480                    // `\M-^?` bound TWO bytes (0xde 0x3f) — breaking the very
10481                    // common `bindkey '\M-^?' backward-kill-word`.
10482                    if control == 0 && (how & GETKEY_CTRL) != 0 && chars.peek() == Some(&'^') {
10483                        let mut it2 = chars.clone();
10484                        it2.next();
10485                        if it2.next().is_some() {
10486                            // c:7194 `s[1]` guard
10487                            chars.next();
10488                            consumed += 1;
10489                            control = 1;
10490                            continue;
10491                        }
10492                    }
10493                    break;
10494                }
10495                // Read one base character (allowing nested simple escapes).
10496                let base: Option<char> = if chars.peek() == Some(&'\\') {
10497                    chars.next();
10498                    consumed += 1;
10499                    match chars.next() {
10500                        Some('n') => {
10501                            consumed += 1;
10502                            Some('\n')
10503                        }
10504                        Some('t') => {
10505                            consumed += 1;
10506                            Some('\t')
10507                        }
10508                        Some('r') => {
10509                            consumed += 1;
10510                            Some('\r')
10511                        }
10512                        Some('a') => {
10513                            consumed += 1;
10514                            Some('\x07')
10515                        }
10516                        Some('b') => {
10517                            consumed += 1;
10518                            Some('\x08')
10519                        }
10520                        Some('e') | Some('E') => {
10521                            consumed += 1;
10522                            Some('\x1b')
10523                        }
10524                        Some('f') => {
10525                            consumed += 1;
10526                            Some('\x0c')
10527                        }
10528                        Some('v') => {
10529                            consumed += 1;
10530                            Some('\x0b')
10531                        }
10532                        Some('\\') => {
10533                            consumed += 1;
10534                            Some('\\')
10535                        }
10536                        Some('\'') => {
10537                            consumed += 1;
10538                            Some('\'')
10539                        }
10540                        Some('"') => {
10541                            consumed += 1;
10542                            Some('"')
10543                        }
10544                        Some(other) => {
10545                            consumed += 1;
10546                            Some(other)
10547                        }
10548                        None => None,
10549                    }
10550                } else {
10551                    chars.next().inspect(|c| {
10552                        consumed += c.len_utf8();
10553                    })
10554                };
10555                if let Some(ch) = base {
10556                    let mut byte = ch as u32;
10557                    // c:7261-7264 — `if (meta == 2) { t[-1] |= 0x80; meta = 0; }`
10558                    // runs BEFORE the control mask: `\M` seen while control was
10559                    // already pending sets the high bit first.
10560                    if meta == 2 {
10561                        byte |= 0x80;
10562                    }
10563                    // c:7265-7271 — control mask (`\C-?` → 0x7f, else & 0x9f).
10564                    if control == 1 {
10565                        if byte == '?' as u32 {
10566                            byte = 0x7f;
10567                        } else {
10568                            byte &= 0x9f;
10569                        }
10570                    }
10571                    // c:7272-7275 — `if (meta) { t[-1] |= 0x80; }` — the
10572                    // meta==1 order, applied AFTER the mask.
10573                    if meta == 1 {
10574                        byte |= 0x80;
10575                    }
10576                    // c:7289-7294 — a masked byte >= 0x80 is metafied so it
10577                    // unmetafies back to the single raw byte on output.
10578                    if byte <= 0xff {
10579                        let b_ = byte as u8;
10580                        if b_ < 0x80 {
10581                            result.push(b_ as char);
10582                        } else {
10583                            result.push('\u{83}');
10584                            result.push(char::from(b_ ^ 32));
10585                        }
10586                    } else if let Some(c) = char::from_u32(byte) {
10587                        result.push(c);
10588                    }
10589                }
10590            }
10591            // c:utils.c:7180-7184 — default arm. With GETKEY_EMACS
10592            // set, drop the backslash; otherwise keep `\<char>`.
10593            Some(c) => {
10594                consumed += 1;
10595                // c:utils.c:7045 — `\c` under GETKEY_BACKSLASH_C
10596                // means TRUNCATE: drop the rest of the input,
10597                // suppress the trailing newline. Used by `echo`
10598                // (and `print` without -r). Set TLS flag so the
10599                // caller can detect + suppress the newline.
10600                if c == 'c' && (how & GETKEY_BACKSLASH_C) != 0 {
10601                    GETKEY_TRUNCATED.with(|cell| cell.set(true));
10602                    break;
10603                }
10604                if (how & GETKEY_EMACS) == 0 {
10605                    // c:utils.c:7181-7183 — backslash kept literal (no EMACS),
10606                    // so the string does not collapse here: undo the base
10607                    // GETKEY_UPDATE_OFFSET decrement applied above.
10608                    if miscadded {
10609                        if let Some(m) = misc.as_deref_mut() {
10610                            *m += 1;
10611                        }
10612                    }
10613                    result.push('\\');
10614                }
10615                result.push(c);
10616            }
10617            None => {
10618                result.push('\\');
10619            }
10620        }
10621    }
10622    (result, consumed)
10623}
10624
10625thread_local! {
10626    /// Sticky flag set by `getkeystring_with` when a `\c` escape
10627    /// truncates the input under `GETKEY_BACKSLASH_C`. The caller
10628    /// (bin_print / bin_echo) reads + clears via `getkey_truncated_take()`
10629    /// to suppress the trailing newline and skip any subsequent args
10630    /// in the print pass.
10631    pub static GETKEY_TRUNCATED: std::cell::Cell<bool> =
10632        const { std::cell::Cell::new(false) };
10633}
10634
10635/// Read + clear the `\c`-truncated flag set by the previous
10636/// `getkeystring_with` call. Returns true once after a truncation
10637/// fires, false otherwise.
10638pub fn getkey_truncated_take() -> bool {
10639    GETKEY_TRUNCATED.with(|c| {
10640        let v = c.get();
10641        c.set(false);
10642        v
10643    })
10644}
10645
10646/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
10647/// the C expression `fdtable[fd]` (read). Returns `FDT_UNUSED` for
10648/// any fd that has not been explicitly set, matching the C source's
10649/// post-`growfdtable` zero-fill behaviour at Src/utils.c:1979.
10650pub fn fdtable_get(fd: i32) -> i32 {
10651    // c:utils.c:fdtable[fd]
10652    if fd < 0 {
10653        return FDT_UNUSED;
10654    }
10655    let g = fdtable_lock().lock().unwrap();
10656    g.get(fd as usize).copied().unwrap_or(FDT_UNUSED)
10657}
10658
10659/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
10660/// the C statement `fdtable[fd] = kind;`. Inlines the `growfdtable`
10661/// call from Src/utils.c:1965 since C always invokes it immediately
10662/// before the assignment anyway. Also updates `MAX_ZSH_FD` to track
10663/// the highest assigned slot — C does this inside `check_fd_table`
10664/// at `Src/utils.c:1982` (`max_zsh_fd = fd;`). Previously the Rust
10665/// `fdtable_set` skipped this, leaving `max_zsh_fd = 0` after any
10666/// `addlockfd` / `addmodulefd` call, which broke `zcloselockfd`'s
10667/// `if (fd > max_zsh_fd)` guard.
10668pub fn fdtable_set(fd: i32, kind: i32) {
10669    // c:utils.c:fdtable[fd]
10670    if fd < 0 {
10671        return;
10672    }
10673    let mut g = fdtable_lock().lock().unwrap();
10674    if (fd as usize) >= g.len() {
10675        g.resize((fd as usize) + 1, FDT_UNUSED);
10676    }
10677    g[fd as usize] = kind;
10678    // c:1982 — `max_zsh_fd = fd;` (with `if (fd <= max_zsh_fd) return;`
10679    // guard at c:1971). Inline equivalent: bump only when this fd
10680    // exceeds the current max.
10681    let cur = MAX_ZSH_FD.load(Ordering::Relaxed);
10682    if fd > cur {
10683        MAX_ZSH_FD.store(fd, Ordering::Relaxed);
10684    }
10685}
10686
10687/// Port of `imeta()` macro from `Src/ztype.h:60` — `zistype(X, IMETA)`.
10688///
10689/// IMETA is set in the typtab at `Src/utils.c:4195-4201` for:
10690///   - `'\0'` (c:4195)
10691///   - `Meta` (0x83) (c:4196)
10692///   - `Marker` (0xa2) (c:4197)
10693///   - `Pound..=LAST_NORMAL_TOK` = `0x84..=0x9c` (c:4198)
10694///   - `Snull..=Nularg` = `0x9d..=0xa1` (c:4200)
10695///
10696/// The CANONICAL set is `{0x00, 0x83..=0xa2}`. The previous Rust
10697/// port used `b >= 0x83` (every byte 0x83..=0xff) which was WRONG
10698/// for bytes `0xa3..=0xff`: C `imeta()` returns false for those,
10699/// so metafy passes them through unchanged. Rust's broader range
10700/// caused `metafy` to corrupt UTF-8 multibyte content (e.g.,
10701/// `é` = `0xc3 0xa9`: both bytes are NOT IMETA in C, but Rust
10702/// escaped both as `Meta + (byte ^ 32)`, mangling the encoding
10703/// and breaking every downstream consumer that expects the raw
10704/// UTF-8 bytes to round-trip).
10705///
10706/// Use the closed-range form rather than the typtab lookup so
10707/// `metafy` / `imeta_byte` work in test contexts where the typtab
10708/// isn't initialised, AND so the fast-path doesn't go through a
10709/// Mutex lock per byte (`metafy` is called per-character on every
10710/// shell input line).
10711#[inline]
10712pub fn imeta_byte(b: u8) -> bool {
10713    // c:4195-4201 — canonical IMETA range.
10714    b == 0 || (0x83..=0xa2).contains(&b)
10715}
10716
10717// `pub fn convfloat` and `pub fn convfloat_underscore` re-export
10718// wrappers DELETED per PORT.md Rule C. C defines both in
10719// `Src/params.c:5690` and `:5765` — the canonical Rust home is
10720// `src/ported/params.rs` (`convfloat` at line 7356,
10721// `convfloat_underscore` matching). Caller convenience cost is
10722// `use crate::ported::params::convfloat;` instead of `utils::`;
10723// no Rust-side re-export needed.
10724
10725// ===========================================================
10726// Methods moved verbatim from src/ported/vm_helper because their
10727// C counterpart's source file maps 1:1 to this Rust module.
10728// Phase: drift
10729// ===========================================================
10730
10731// BEGIN moved-from-exec-rs
10732// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
10733
10734// END moved-from-exec-rs
10735
10736// ===========================================================
10737// Free ported moved verbatim from src/ported/vm_helper.
10738// ===========================================================
10739// BEGIN moved-from-exec-rs (free ported)
10740pub(crate) fn base64_decode(s: &str) -> Vec<u8> {
10741    let decode_char = |c: u8| -> Option<u8> {
10742        match c {
10743            b'A'..=b'Z' => Some(c - b'A'),
10744            b'a'..=b'z' => Some(c - b'a' + 26),
10745            b'0'..=b'9' => Some(c - b'0' + 52),
10746            b'+' => Some(62),
10747            b'/' => Some(63),
10748            _ => None,
10749        }
10750    };
10751    let bytes = s.as_bytes();
10752    let mut out = Vec::with_capacity(s.len() / 4 * 3);
10753    let mut i = 0;
10754    while i + 4 <= bytes.len() {
10755        let chunk = &bytes[i..i + 4];
10756        let pad = chunk.iter().filter(|&&c| c == b'=').count();
10757        let v0 = decode_char(chunk[0]).unwrap_or(0) as u32;
10758        let v1 = decode_char(chunk[1]).unwrap_or(0) as u32;
10759        let v2 = decode_char(chunk[2]).unwrap_or(0) as u32;
10760        let v3 = decode_char(chunk[3]).unwrap_or(0) as u32;
10761        let n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
10762        out.push(((n >> 16) & 0xff) as u8);
10763        if pad < 2 {
10764            out.push(((n >> 8) & 0xff) as u8);
10765        }
10766        if pad < 1 {
10767            out.push((n & 0xff) as u8);
10768        }
10769        i += 4;
10770    }
10771    out
10772}
10773// END moved-from-exec-rs (free ported)
10774
10775// ===========================================================
10776// Utility helpers moved from src/ported/vm_helper.
10777// All correspond to Src/utils.c logic (path/string/bslashquote helpers).
10778// ===========================================================
10779
10780// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10781// ─── RUST-ONLY ACCESSORS ───
10782//
10783// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10784// RwLock<T>>` globals declared above. C zsh uses direct global
10785// access; Rust needs these wrappers because `OnceLock::get_or_init`
10786// is the only way to lazily construct shared state. These ported sit
10787// here so the body of this file reads in C source order without
10788// the accessor wrappers interleaved between real port ported.
10789// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10790
10791// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10792// ─── RUST-ONLY ACCESSORS ───
10793//
10794// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10795// RwLock<T>>` globals declared above. C zsh uses direct global
10796// access; Rust needs these wrappers because `OnceLock::get_or_init`
10797// is the only way to lazily construct shared state. These ported sit
10798// here so the body of this file reads in C source order without
10799// the accessor wrappers interleaved between real port ported.
10800// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10801
10802// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10803// ─── RUST-ONLY ACCESSORS ───
10804//
10805// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10806// RwLock<T>>` globals declared above. C zsh uses direct global
10807// access; Rust needs these wrappers because `OnceLock::get_or_init`
10808// is the only way to lazily construct shared state. These ported sit
10809// here so the body of this file reads in C source order without
10810// the accessor wrappers interleaved between real port ported.
10811// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10812
10813// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10814// ─── RUST-ONLY ACCESSORS ───
10815//
10816// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10817// RwLock<T>>` globals declared above. C zsh uses direct global
10818// access; Rust needs these wrappers because `OnceLock::get_or_init`
10819// is the only way to lazily construct shared state. These ported sit
10820// here so the body of this file reads in C source order without
10821// the accessor wrappers interleaved between real port ported.
10822// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10823
10824// WARNING: NOT IN UTILS.C — Rust-only OnceLock get-or-init
10825// helpers. C dereferences each global directly.
10826fn scriptname_lock() -> &'static Mutex<Option<String>> {
10827    SCRIPTNAME.get_or_init(|| Mutex::new(None))
10828}
10829
10830// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10831fn argzero_lock() -> &'static Mutex<Option<String>> {
10832    ARGZERO.get_or_init(|| Mutex::new(None))
10833}
10834
10835// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10836fn scriptfilename_lock() -> &'static Mutex<Option<String>> {
10837    SCRIPTFILENAME.get_or_init(|| Mutex::new(None))
10838}
10839
10840// WARNING: NOT IN UTILS.C — Rust-only OnceLock accessor for `posixzero`.
10841// C's `posixzero` lives in params.c:76; the canonical storage lives
10842// here for OnceLock initialisation parity with `argzero`.
10843fn posixzero_lock() -> &'static Mutex<Option<String>> {
10844    POSIXZERO.get_or_init(|| Mutex::new(None))
10845}
10846
10847// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10848pub fn noerrs_lock() -> &'static Mutex<i32> {
10849    NOERRS.get_or_init(|| Mutex::new(0))
10850}
10851
10852// `locallevel_lock` removed — duplicate of canonical
10853// `LOCALLEVEL` (port of params.c:54). All
10854// accessors here now route through the canonical AtomicI32.
10855// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10856fn lineno_lock() -> &'static Mutex<i32> {
10857    LINENO.get_or_init(|| Mutex::new(0))
10858}
10859
10860// WARNING: NOT IN UTILS.C — Rust-only cache for the SHINSTDIN
10861// option flag so error-emission doesn't pull in the option-table.
10862fn shinstdin_lock() -> &'static Mutex<bool> {
10863    SHINSTDIN_OPT.get_or_init(|| Mutex::new(false))
10864}
10865
10866/// !!! RUST-ONLY HELPER — see WARNING block above. C source uses bare
10867/// `unsigned char *fdtable` global from Src/utils.c:~63.
10868fn fdtable_lock() -> &'static Mutex<Vec<i32>> {
10869    FDTABLE.get_or_init(|| Mutex::new(Vec::new()))
10870}
10871
10872#[cfg(test)]
10873mod tests {
10874    use super::*;
10875
10876    /// checkmailpath port (utils.c:1620). Drives mtime/atime/size through
10877    /// `libc::utimes` so the new-mail condition is deterministic in headless CI.
10878    #[test]
10879    fn checkmailpath_new_mail_conditions() {
10880        use std::io::Write as _;
10881        let _g = crate::test_util::global_state_lock();
10882
10883        // Unique temp file with content (size != 0).
10884        let dir = std::env::temp_dir();
10885        let path = dir.join(format!("zshrs_mailtest_{}", std::process::id()));
10886        {
10887            let mut f = std::fs::File::create(&path).unwrap();
10888            f.write_all(b"new message\n").unwrap();
10889        }
10890        let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
10891        // atime (times[0]) <= mtime (times[1]); both well past 0.
10892        let set_times = |atime: i64, mtime: i64| {
10893            let tv = [
10894                libc::timeval {
10895                    tv_sec: atime as libc::time_t,
10896                    tv_usec: 0,
10897                },
10898                libc::timeval {
10899                    tv_sec: mtime as libc::time_t,
10900                    tv_usec: 0,
10901                },
10902            ];
10903            assert_eq!(unsafe { libc::utimes(cpath.as_ptr(), tv.as_ptr()) }, 0);
10904        };
10905
10906        let saved_shout = *crate::ported::init::shout.lock().unwrap();
10907        let saved_lmc = LAST_MAILCHECK.load(Ordering::Relaxed);
10908        *crate::ported::init::shout.lock().unwrap() = 1; // interactive
10909        LAST_MAILCHECK.store(0, Ordering::Relaxed);
10910
10911        let p = path.to_str().unwrap().to_string();
10912
10913        // c:1671-1675 — unread + newer than last check → "You have new mail."
10914        set_times(1_000_000, 2_000_000);
10915        assert_eq!(
10916            checkmailpath(&[p.clone()]),
10917            vec!["You have new mail.".to_string()]
10918        );
10919
10920        // c:1676-1698 — `PATH?message`: the message (here a literal) is emitted.
10921        assert_eq!(
10922            checkmailpath(&[format!("{}?custom note", p)]),
10923            vec!["custom note".to_string()]
10924        );
10925
10926        // c:1672 — mtime older than the last check → nothing.
10927        LAST_MAILCHECK.store(9_000_000, Ordering::Relaxed);
10928        assert!(checkmailpath(&[p.clone()]).is_empty());
10929
10930        // c:1670 — non-interactive (no shout) → nothing, even when fresh.
10931        LAST_MAILCHECK.store(0, Ordering::Relaxed);
10932        *crate::ported::init::shout.lock().unwrap() = 0;
10933        assert!(checkmailpath(&[p.clone()]).is_empty());
10934
10935        // c:1634-1636 — empty component is reported (stderr), yields no message.
10936        *crate::ported::init::shout.lock().unwrap() = 1;
10937        assert!(checkmailpath(&["?orphan".to_string()]).is_empty());
10938
10939        *crate::ported::init::shout.lock().unwrap() = saved_shout;
10940        LAST_MAILCHECK.store(saved_lmc, Ordering::Relaxed);
10941        let _ = std::fs::remove_file(&path);
10942    }
10943
10944    /// zsh_errno_msg / zerrmsg `%e` rendering (utils.c:348-365). EINTR is the
10945    /// literal "interrupt"; EIO is verbatim strerror (keeps its capital); every
10946    /// other errno lowercases the first letter (`tulower`). The old
10947    /// io::Error-based path kept the capital and appended " (os error N)".
10948    #[test]
10949    fn zsh_errno_msg_matches_c_percent_e() {
10950        // c:355-358 — EINTR → "interrupt".
10951        assert_eq!(zsh_errno_msg(libc::EINTR), "interrupt");
10952
10953        // c:366 — non-EIO errno lowercases the first letter and never carries
10954        // the Rust "(os error N)" suffix.
10955        let acc = zsh_errno_msg(libc::EACCES);
10956        assert!(
10957            acc.chars().next().is_some_and(|c| c.is_lowercase()),
10958            "EACCES first letter must be lowercased: {acc:?}"
10959        );
10960        assert!(
10961            !acc.contains("(os error"),
10962            "must be strerror, not io::Error: {acc:?}"
10963        );
10964
10965        // c:359-364 — EIO is emitted verbatim (its message reads wrong
10966        // lowercased), so its first letter stays capitalized.
10967        let eio = zsh_errno_msg(libc::EIO);
10968        assert!(
10969            eio.chars().next().is_some_and(|c| c.is_uppercase()),
10970            "EIO must be verbatim strerror (capitalized): {eio:?}"
10971        );
10972    }
10973
10974    /// c:351-354 — zerrmsg's `%e` arm sets errflag |= ERRFLAG_ERROR on EINTR.
10975    #[test]
10976    fn zerrmsg_eintr_sets_errflag() {
10977        let _g = crate::test_util::global_state_lock();
10978        let saved = errflag.load(Ordering::Relaxed);
10979        errflag.store(0, Ordering::Relaxed);
10980        zerrmsg("test", Some(libc::EINTR)); // writes to stderr; side-effect is the flag
10981        assert_ne!(
10982            errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
10983            0,
10984            "EINTR must set ERRFLAG_ERROR"
10985        );
10986        // A non-EINTR errno must NOT set the flag.
10987        errflag.store(0, Ordering::Relaxed);
10988        zerrmsg("test", Some(libc::EACCES));
10989        assert_eq!(errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR, 0);
10990        errflag.store(saved, Ordering::Relaxed);
10991    }
10992
10993    /// zjoin port (utils.c:3622). ASCII delimiters join byte-for-byte; a meta
10994    /// delimiter (NUL is `imeta` per inittyptab c:4195) must be written in
10995    /// metafied form (`Meta` + `delim^32`), never as a raw byte.
10996    #[test]
10997    fn zjoin_ascii_and_meta_delims() {
10998        let v = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
10999
11000        // c:3632-3641 — ordinary (non-meta) delimiter: plain interposition.
11001        assert_eq!(zjoin(&v(&["a", "b", "c"]), ' '), "a b c");
11002        assert_eq!(zjoin(&v(&["foo"]), ':'), "foo"); // single elem, no delim
11003                                                     // c:3629-3630 — empty array → "".
11004        assert_eq!(zjoin(&v(&[]), ' '), "");
11005
11006        // c:3634-3637 — NUL is imeta, so the separator is metafied, NOT a raw
11007        // NUL byte (the prior `arr.join("\0")` bug). The Meta byte (0x83) can't
11008        // survive the UTF-8 String boundary, but the raw NUL must be gone.
11009        let joined = zjoin(&v(&["a", "b"]), '\0');
11010        assert!(
11011            !joined.as_bytes().contains(&0u8),
11012            "NUL delim must be metafied, not emitted as a raw NUL byte: {joined:?}"
11013        );
11014        assert_ne!(joined, "a\u{0}b", "must not raw-join on a meta delimiter");
11015        assert!(joined.starts_with('a') && joined.ends_with('b'));
11016    }
11017
11018    /// findsep port (utils.c:3784). Pure function — no global/cwd state.
11019    #[test]
11020    fn findsep_default_ifs_separators() {
11021        inittyptab(); // ISEP bits are set by the type table (c:4155)
11022                      // c:3814-3817 — advance to the first ISEP char, return 1.
11023        let mut s = "foo bar".to_string();
11024        let mut pos = 0usize;
11025        assert_eq!(findsep(&mut s, &mut pos, None, false), 1);
11026        assert_eq!(pos, 3); // points at the space
11027        assert_eq!(&s[..pos], "foo");
11028
11029        // c:3821 — already sitting on a separator: return 0, no advance.
11030        let mut s = " foo".to_string();
11031        let mut pos = 0usize;
11032        assert_eq!(findsep(&mut s, &mut pos, None, false), 0);
11033        assert_eq!(pos, 0);
11034    }
11035
11036    #[test]
11037    fn findsep_quote_strips_escaped_separator() {
11038        inittyptab(); // ISEP bits are set by the type table (c:4155)
11039                      // c:3795-3804 — `\<sep>` is not a separator; the backslash is
11040                      // stripped in place and the bare char is consumed into the word.
11041        let mut s = "foo\\ bar".to_string(); // foo<bslash><space>bar
11042        let mut pos = 0usize;
11043        let r = findsep(&mut s, &mut pos, None, true);
11044        assert_eq!(r, 1);
11045        assert_eq!(s, "foo bar"); // backslash removed
11046        assert_eq!(pos, s.len()); // whole thing is one word, no real sep
11047    }
11048
11049    #[test]
11050    fn findsep_quote_collapses_double_backslash() {
11051        // c:3796-3799 — `\\` collapses to a single literal backslash.
11052        let mut s = "a\\\\b".to_string(); // a <bslash><bslash> b
11053        let mut pos = 0usize;
11054        let r = findsep(&mut s, &mut pos, None, true);
11055        assert_eq!(r, 1);
11056        assert_eq!(s, "a\\b"); // one backslash left
11057        assert_eq!(pos, s.len()); // no separator present
11058    }
11059
11060    #[test]
11061    fn findsep_literal_multichar_separator() {
11062        // c:3836-3841 — explicit multi-byte literal separator.
11063        let mut s = "a::b".to_string();
11064        let mut pos = 0usize;
11065        assert_eq!(findsep(&mut s, &mut pos, Some(b"::"), false), 1);
11066        assert_eq!(pos, 1); // points at the "::"
11067        assert_eq!(&s[..pos], "a");
11068
11069        // c:3844 — separator absent → -1.
11070        let mut s = "abc".to_string();
11071        let mut pos = 0usize;
11072        assert_eq!(findsep(&mut s, &mut pos, Some(b"x"), false), -1);
11073    }
11074
11075    #[test]
11076    fn findsep_empty_separator_advances_one_char() {
11077        // c:3825-3834 — empty sep just steps past one character.
11078        let mut s = "ab".to_string();
11079        let mut pos = 0usize;
11080        assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), 1);
11081        assert_eq!(pos, 1);
11082
11083        let mut s = String::new();
11084        let mut pos = 0usize;
11085        assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), -1); // c:3833
11086    }
11087
11088    /// c:4033 — `subst_string_by_func` returns `getaparam("reply")`
11089    /// after the hook function finishes. The previous Rust port read
11090    /// `env::var("reply")` and split on NUL — wrong because `reply`
11091    /// is a shell-local PM_ARRAY in paramtab, never exported. This
11092    /// pin sets `reply` via `setaparam` (the paramtab write path)
11093    /// and exercises `subst_string_by_hook` end-to-end isn't viable
11094    /// in unit tests without a function-name hook, so we exercise
11095    /// just the getaparam plumbing.
11096    #[test]
11097    fn getaparam_reads_reply_from_paramtab_not_env() {
11098        let _g = crate::test_util::global_state_lock();
11099        // Stash any prior `reply` value.
11100        let saved = getaparam("reply");
11101
11102        // Write a known reply array via the canonical setaparam path.
11103        let payload = vec!["abbreviated".to_string(), "11".to_string()];
11104        let _ = setaparam("reply", payload.clone());
11105
11106        // The reply array must be reachable via getaparam — not env.
11107        // (env::var would return Err because setaparam never exports
11108        //  an un-flagged array to env.)
11109        assert_eq!(
11110            getaparam("reply"),
11111            Some(payload),
11112            "getaparam(\"reply\") must return paramtab array"
11113        );
11114
11115        // Restore.
11116        let _ = setaparam("reply", saved.unwrap_or_default());
11117    }
11118
11119    /// c:1133-1134 — `finddir` reads the global `home` variable (the
11120    /// canonical `$HOME` storage, not `getenv("HOME")`). The global
11121    /// is updated by `homesetfn` (Src/params.c:5118) whenever the user
11122    /// assigns `HOME=...` inside the shell.
11123    /// Regression target: a previous Rust port read `env::var("HOME")`
11124    /// so an in-shell `HOME=...` assignment would not retarget
11125    /// `~`-abbreviation until the user re-exported HOME.
11126    #[test]
11127    fn finddir_uses_paramtab_home_not_env() {
11128        let _g = crate::test_util::global_state_lock();
11129        // C's `homesetfn` (params.c:5118) UNUSED(Param pm) — the
11130        // function ignores its param-pointer argument and writes the
11131        // canonical `char *home` global directly. zshrs's port mirrors
11132        // that: `params::homesetfn(_pm, x)` ignores _pm and updates
11133        // `home_lock()`. So we pass a stack-default param; the paramtab
11134        // wiring (PM_SPECIAL dispatch) is not on the test path.
11135        let mut pm = crate::ported::zsh_h::param::default();
11136        let saved = crate::ported::params::homegetfn(&pm);
11137        let sentinel = "/tmp/zshrs-finddir-pin".to_string();
11138        homesetfn(&mut pm, sentinel.clone());
11139
11140        // `/tmp/zshrs-finddir-pin/x` must abbreviate to `~/x`.
11141        let abbrev = finddir(&format!("{}/x", sentinel));
11142        assert_eq!(
11143            abbrev.as_deref(),
11144            Some("~/x"),
11145            "finddir must consult canonical HOME (got {:?})",
11146            abbrev
11147        );
11148
11149        // Restore.
11150        homesetfn(&mut pm, saved);
11151    }
11152
11153    #[test]
11154    fn test_sepsplit() {
11155        let _g = crate::test_util::global_state_lock();
11156        assert_eq!(sepsplit("a:b:c", Some(":"), false), vec!["a", "b", "c"]);
11157        assert_eq!(sepsplit("a::b", Some(":"), false), vec!["a", "b"]);
11158        assert_eq!(sepsplit("a::b", Some(":"), true), vec!["a", "", "b"]);
11159    }
11160
11161    #[test]
11162    fn test_unmetafy_no_meta_byte_passes_through() {
11163        let _g = crate::test_util::global_state_lock();
11164        // No Meta byte → buffer unchanged, length unchanged.
11165        let mut buf = b"hello".to_vec();
11166        let n = unmetafy(&mut buf);
11167        assert_eq!(n, 5);
11168        assert_eq!(&buf, b"hello");
11169    }
11170
11171    #[test]
11172    fn test_unmetafy_collapses_meta_escapes() {
11173        let _g = crate::test_util::global_state_lock();
11174        // C: Meta byte (0x83) followed by `'a' ^ 32` (0x41 = 'A')
11175        // unmetafies to a single byte 'a' (0x61).
11176        // i.e. {0x83, 'a' ^ 32} → {'a'}.
11177        let mut buf = vec![0x83, b'a' ^ 32];
11178        let n = unmetafy(&mut buf);
11179        assert_eq!(n, 1);
11180        assert_eq!(buf, vec![b'a']);
11181    }
11182
11183    #[test]
11184    fn test_unmetafy_mixed_prefix_then_meta() {
11185        let _g = crate::test_util::global_state_lock();
11186        // Plain prefix, then Meta-escaped 0xFF (0x83, 0xFF ^ 32 = 0xDF).
11187        let mut buf = vec![b'X', b'Y', 0x83, 0xFF ^ 32, b'Z'];
11188        let n = unmetafy(&mut buf);
11189        assert_eq!(n, 4);
11190        assert_eq!(buf, vec![b'X', b'Y', 0xFF, b'Z']);
11191    }
11192
11193    #[test]
11194    fn test_unmetafy_returns_self_value() {
11195        let _g = crate::test_util::global_state_lock();
11196        // C returns `s` (the buffer) for chaining; Rust returns
11197        // the new length. Verify length matches a call that
11198        // collapses two Meta-escapes.
11199        let mut buf = vec![
11200            b'A',
11201            0x83,
11202            b'B' ^ 32, // → 'B'
11203            0x83,
11204            b'C' ^ 32, // → 'C'
11205            b'D',
11206        ];
11207        let n = unmetafy(&mut buf);
11208        assert_eq!(n, 4);
11209        assert_eq!(buf, b"ABCD".to_vec());
11210    }
11211
11212    #[test]
11213    fn test_imeta_byte_threshold() {
11214        let _g = crate::test_util::global_state_lock();
11215        // Canonical IMETA per Src/utils.c:4195-4201:
11216        //   - 0x00 (c:4195)
11217        //   - 0x83..=0xa2 (Meta through Marker — c:4196-4200)
11218        //
11219        // The previous version of this test asserted `imeta_byte(0xFF) == true`
11220        // based on the WRONG `b >= Meta` predicate the Rust port
11221        // originally used. C `imeta()` reads the typtab; 0xa3..=0xff
11222        // have NO typtab assignment so they're NOT IMETA, and
11223        // `metafy` must NOT escape them.
11224        assert!(imeta_byte(0x00), "c:4195 — NUL is IMETA");
11225        assert!(!imeta_byte(0x82), "0x82 is NOT IMETA (below Meta)");
11226        assert!(imeta_byte(Meta), "c:4196 — Meta (0x83) is IMETA");
11227        assert!(imeta_byte(0xa2), "c:4197 — Marker (0xa2) is IMETA");
11228        // c:4198-4200 upper bound — Nularg is the last IMETA byte.
11229        assert!(imeta_byte(0xa1), "c:4200 — Nularg (0xa1) is IMETA");
11230        // 0xa3..=0xff are NOT IMETA — UTF-8 multi-byte content
11231        // must pass through `metafy` unchanged.
11232        assert!(!imeta_byte(0xa3), "0xa3 NOT IMETA (above Marker)");
11233        assert!(!imeta_byte(0xc3), "0xc3 NOT IMETA (UTF-8 'é' lead)");
11234        assert!(!imeta_byte(0xFF), "0xFF NOT IMETA");
11235    }
11236
11237    #[test]
11238    fn test_meta_constant_value() {
11239        let _g = crate::test_util::global_state_lock();
11240        // Locked at 0x83 by Src/zsh.h. If this test fails, zsh
11241        // bumped the Meta sentinel and the encoding mapping needs
11242        // a full audit.
11243        assert_eq!(Meta, 0x83);
11244    }
11245
11246    #[test]
11247    #[cfg(unix)]
11248    fn test_mode_to_octal_canonical_bits() {
11249        let _g = crate::test_util::global_state_lock();
11250        // rwx for owner = 0o700.
11251        let mode = (S_IRUSR | S_IWUSR | S_IXUSR) as u32;
11252        assert_eq!(mode_to_octal(mode), 0o700);
11253        // rwx all = 0o777.
11254        let all = (S_IRUSR | S_IWUSR | S_IXUSR) as u32 * (1 + 8 + 64);
11255        // Use libc constants individually for portability.
11256        let m = (S_IRUSR
11257            | S_IWUSR
11258            | S_IXUSR
11259            | S_IRGRP
11260            | S_IWGRP
11261            | S_IXGRP
11262            | S_IROTH
11263            | S_IWOTH
11264            | S_IXOTH) as u32;
11265        assert_eq!(mode_to_octal(m), 0o777);
11266        let _ = all;
11267    }
11268
11269    #[test]
11270    #[cfg(unix)]
11271    fn test_mode_to_octal_setuid_setgid_sticky() {
11272        let _g = crate::test_util::global_state_lock();
11273        assert_eq!(mode_to_octal(S_ISUID as u32), 0o4000);
11274        assert_eq!(mode_to_octal(S_ISGID as u32), 0o2000);
11275        assert_eq!(mode_to_octal(S_ISVTX as u32), 0o1000);
11276        // All three: 0o7000.
11277        let all = (S_ISUID | S_ISGID | S_ISVTX) as u32;
11278        assert_eq!(mode_to_octal(all), 0o7000);
11279    }
11280
11281    #[test]
11282    #[cfg(unix)]
11283    fn test_mailstat_plain_file_returns_native_stat() {
11284        let _g = crate::test_util::global_state_lock();
11285        // Plain file path → *st fields mirror native stat,
11286        // not the maildir aggregation.
11287        let mut st: libc::stat = unsafe { std::mem::zeroed() };
11288        let rc = mailstat("/etc/hosts", &mut st);
11289        if rc == 0 {
11290            assert_eq!(
11291                st.st_nlink as u64,
11292                fs::metadata("/etc/hosts").unwrap().nlink()
11293            );
11294        }
11295    }
11296
11297    #[test]
11298    fn test_mailstat_nonexistent_returns_neg1() {
11299        let _g = crate::test_util::global_state_lock();
11300        let mut st: libc::stat = unsafe { std::mem::zeroed() };
11301        assert_eq!(mailstat("/nonexistent/path/does/not/exist", &mut st), -1);
11302    }
11303
11304    #[test]
11305    fn test_mailstat_directory_without_maildir_subdirs() {
11306        let _g = crate::test_util::global_state_lock();
11307        // /tmp is a directory but not a maildir (no cur/tmp/new) —
11308        // returns the partial aggregate (top dir's atime/mtime,
11309        // size=0 since cur/ wasn't found before we'd start summing).
11310        let mut st: libc::stat = unsafe { std::mem::zeroed() };
11311        let rc = mailstat("/tmp", &mut st);
11312        assert_eq!(rc, 0);
11313        assert_eq!(st.st_nlink, 1);
11314        assert_eq!(st.st_size, 0);
11315        assert_eq!(st.st_blocks, 0);
11316        // S_IFDIR bit should be cleared, S_IFREG set.
11317        #[cfg(unix)]
11318        {
11319            assert_eq!(st.st_mode & libc::S_IFDIR, 0);
11320            assert_ne!(st.st_mode & libc::S_IFREG, 0);
11321        }
11322    }
11323
11324    #[test]
11325    fn test_dupstrpfx_byte_counted() {
11326        let _g = crate::test_util::global_state_lock(); // c:161
11327                                                        // 5 bytes of ASCII = 5 chars, identical. Canonical
11328                                                        // port lives at `string.rs:161`; pin from utils.rs's test
11329                                                        // module via the qualified path.
11330        assert_eq!(dupstrpfx("hello", 3), "hel"); // c:161
11331        assert_eq!(dupstrpfx("hi", 10), "hi"); // c:161
11332        assert_eq!(dupstrpfx("anything", 0), ""); // c:161
11333    }
11334
11335    #[test]
11336    fn test_metafy_passes_through_ascii() {
11337        let _g = crate::test_util::global_state_lock();
11338        // ASCII bytes (< 0x83) stay untouched.
11339        assert_eq!(metafy("hello"), "hello");
11340        assert_eq!(metafy(""), "");
11341    }
11342
11343    #[test]
11344    fn test_metafy_imeta_predicate_matches_c_macro() {
11345        let _g = crate::test_util::global_state_lock();
11346        // Canonical C IMETA per Src/utils.c:4195-4201:
11347        //   - 0x00 (c:4195)
11348        //   - 0x83 (Meta, c:4196)
11349        //   - 0x84..=0x9c (Pound..LAST_NORMAL_TOK=Bang, c:4198)
11350        //   - 0x9d..=0xa1 (Snull..Nularg, c:4200)
11351        //   - 0xa2 (Marker, c:4197)
11352        //
11353        // The closed set is {0x00, 0x83..=0xa2}. Every other byte
11354        // (0x01..=0x82, 0xa3..=0xff) is NOT IMETA in C and so must
11355        // NOT be Meta-encoded by `metafy`. The previous Rust
11356        // hardcoded predicate `b >= 0x83` falsely marked
11357        // 0xa3..=0xff as IMETA, corrupting UTF-8 multibyte
11358        // content.
11359
11360        // NUL is IMETA (c:4195).
11361        assert!(imeta_byte(0x00), "c:4195 — '\\0' IS imeta");
11362
11363        // 0x01..=0x82 are NOT IMETA (no typtab assignment for them).
11364        for b in 0x01u8..=0x82 {
11365            assert!(!imeta_byte(b), "byte {:#x} should NOT be imeta", b);
11366        }
11367
11368        // 0x83..=0xa2 ARE IMETA (the canonical full range).
11369        for b in 0x83u8..=0xa2 {
11370            assert!(imeta_byte(b), "c:4196-4200 — byte {:#x} IS imeta", b);
11371        }
11372
11373        // 0xa3..=0xff are NOT IMETA. C `imeta()` returns false
11374        // for these so `metafy` must pass them through unchanged.
11375        // UTF-8 continuation bytes (0x80..=0xbf) and multi-byte
11376        // leads (0xc0..=0xff) live here and must round-trip.
11377        for b in 0xa3u8..=0xff {
11378            assert!(
11379                !imeta_byte(b),
11380                "byte {:#x} should NOT be imeta (no c:4195-4201 assignment)",
11381                b
11382            );
11383        }
11384    }
11385
11386    /// Pin: `metafy` passes UTF-8 multibyte bytes through unchanged
11387    /// when they fall outside the canonical IMETA range. Previously
11388    /// the broader `b >= 0x83` predicate corrupted every UTF-8
11389    /// continuation byte and lead byte that wasn't a token marker.
11390    #[test]
11391    fn metafy_preserves_utf8_high_bytes_outside_imeta_range() {
11392        let _g = crate::test_util::global_state_lock();
11393        // 'é' = U+00E9 = UTF-8 0xc3 0xa9. Both bytes are >= 0x83 BUT
11394        // both are also > 0xa2, so they're NOT IMETA per the typtab.
11395        // C `metafy` passes them through unchanged.
11396        let input = std::str::from_utf8(&[0xC3, 0xA9]).unwrap();
11397        let out = metafy(input);
11398        // Should round-trip the two bytes exactly (no Meta escape).
11399        let out_bytes = out.as_bytes();
11400        assert_eq!(
11401            out_bytes,
11402            &[0xC3, 0xA9],
11403            "c:4196-4200 — UTF-8 bytes 0xc3/0xa9 outside IMETA range must pass through"
11404        );
11405    }
11406
11407    #[test]
11408    fn test_ztrcmp_meta_aware() {
11409        let _g = crate::test_util::global_state_lock();
11410        // Two identical metafied strings → Equal.
11411        assert_eq!(ztrcmp("foo", "foo"), std::cmp::Ordering::Equal);
11412        // "foo" < "foz".
11413        assert_eq!(ztrcmp("foo", "foz"), std::cmp::Ordering::Less);
11414        // Prefix comparison: shorter < longer.
11415        assert_eq!(ztrcmp("foo", "foobar"), std::cmp::Ordering::Less);
11416        // Meta-encoded comparison: {0x83, 'a'^32} should compare as 'a'.
11417        let s_meta = unsafe { std::str::from_utf8_unchecked(&[0x83, b'a' ^ 32]) };
11418        let s_plain = "a";
11419        // Meta-encoded "a" should compare equal to plain "a".
11420        assert_eq!(ztrcmp(s_meta, s_plain), std::cmp::Ordering::Equal);
11421    }
11422
11423    #[test]
11424    fn test_skipwsep_skips_runs() {
11425        let _g = crate::test_util::global_state_lock();
11426        // 3 spaces + 'x' → returns ("x", 3).
11427        let (rest, n) = skipwsep("   x");
11428        assert_eq!(rest, "x");
11429        assert_eq!(n, 3);
11430        // No leading whitespace → 0 skipped.
11431        let (rest, n) = skipwsep("foo");
11432        assert_eq!(rest, "foo");
11433        assert_eq!(n, 0);
11434        // Mix of space/tab/newline.
11435        let (rest, n) = skipwsep(" \t\nbar");
11436        assert_eq!(rest, "bar");
11437        assert_eq!(n, 3);
11438    }
11439
11440    #[test]
11441    fn test_imeta_macro_threshold() {
11442        let _g = crate::test_util::global_state_lock(); // c:60
11443                                                        // `Src/ztype.h:60` `imeta(X) zistype(X, IMETA)` — typtab-driven
11444                                                        // predicate. Per `Src/utils.c:4195-4201`, IMETA is set on:
11445                                                        // NUL, Meta=0x83, Marker=0xa2, and the Pound..Nularg ITOK range
11446                                                        // (0x84..=0xa2). Routes through canonical
11447                                                        // `ztype_h::imeta(u8)`; init the typtab first since the
11448                                                        // canonical port reads through it.
11449        inittyptab(); // c:4148
11450        assert!(imeta(0x00), "c:4195 — NUL is IMETA"); // c:4195
11451        assert!(imeta(Meta), "c:4196 — Meta (0x83) is IMETA"); // c:4196
11452        assert!(imeta(0xa2), "c:4197 — Marker (0xa2) is IMETA"); // c:4197
11453        assert!(
11454            imeta(0x84),
11455            "c:4199-4201 — Pound (0x84) is IMETA via ITOK range"
11456        );
11457        assert!(
11458            imeta(0x9b),
11459            "c:4199-4201 — Dash sentinel within ITOK range is IMETA"
11460        );
11461        assert!(!imeta(0x82), "c:4170 — 0x82 is ICNTRL, not IMETA"); // c:4170
11462        assert!(!imeta(0xa3), "byte 0xa3 is past Marker — NOT IMETA");
11463        assert!(
11464            !imeta(0xff),
11465            "byte 0xff is NOT IMETA (the prior `>= Meta` over-report)"
11466        );
11467        assert!(!imeta(b' '), "space is not IMETA");
11468        assert!(!imeta(b'A'), "'A' is not IMETA");
11469    }
11470
11471    #[test]
11472    fn test_unmeta_routes_through_unmetafy() {
11473        let _g = crate::test_util::global_state_lock();
11474        // unmeta wraps the in-place unmetafy via a byte-vector
11475        // copy; the no-Meta fast path returns the source as-is.
11476        assert_eq!(unmeta("plain"), "plain");
11477    }
11478
11479    #[test]
11480    fn test_iwsep_includes_newline() {
11481        let _g = crate::test_util::global_state_lock(); // c:61
11482                                                        // The previous port omitted '\n' which broke wordcount on
11483                                                        // multi-line input. Routes through canonical
11484                                                        // `ztype_h::iwsep` (`Src/ztype.h:61`).
11485        assert!(iwsep(b'\n')); // c:61
11486        assert!(iwsep(b'\t')); // c:61
11487        assert!(iwsep(b' ')); // c:61
11488        assert!(!iwsep(b'a')); // c:61
11489    }
11490
11491    #[test]
11492    fn test_mailstat_aggregates_maildir() {
11493        let _g = crate::test_util::global_state_lock();
11494        // Create a temp maildir layout with 2 messages in new/ and 1
11495        // in cur/, verify the aggregate.
11496        let tmp = std::env::temp_dir().join(format!("zshrs_mailstat_test_{}", std::process::id()));
11497        let _ = fs::remove_dir_all(&tmp);
11498        fs::create_dir_all(tmp.join("cur")).unwrap();
11499        fs::create_dir_all(tmp.join("new")).unwrap();
11500        fs::create_dir_all(tmp.join("tmp")).unwrap();
11501        let mut f = fs::File::create(tmp.join("new").join("msg1")).unwrap();
11502        f.write_all(b"hello").unwrap();
11503        let mut f = fs::File::create(tmp.join("new").join("msg2")).unwrap();
11504        f.write_all(b"world!").unwrap();
11505        let mut f = fs::File::create(tmp.join("cur").join("msg3")).unwrap();
11506        f.write_all(b"third").unwrap();
11507        let mut st: libc::stat = unsafe { std::mem::zeroed() };
11508        let rc = mailstat(tmp.to_str().unwrap(), &mut st);
11509        assert_eq!(rc, 0, "maildir should stat");
11510        assert_eq!(st.st_blocks, 3, "3 messages total across new/ + cur/");
11511        assert_eq!(st.st_size, 5 + 6 + 5, "5+6+5 bytes total");
11512        let _ = fs::remove_dir_all(&tmp);
11513    }
11514
11515    #[test]
11516    fn test_spacesplit() {
11517        let _g = crate::test_util::global_state_lock();
11518        assert_eq!(spacesplit("a b c", false), vec!["a", "b", "c"]);
11519        assert_eq!(spacesplit("a  b", false), vec!["a", "b"]);
11520    }
11521
11522    #[test]
11523    fn test_sepjoin() {
11524        let _g = crate::test_util::global_state_lock();
11525        assert_eq!(
11526            sepjoin(&["a".into(), "b".into(), "c".into()], Some(":")),
11527            "a:b:c"
11528        );
11529        assert_eq!(sepjoin(&["a".into(), "b".into()], None), "a b");
11530    }
11531
11532    #[test]
11533    fn test_isident() {
11534        let _g = crate::test_util::global_state_lock(); // c:1288
11535                                                        // Canonical port lives at `params.rs:2056` (`Src/params.c:1288`).
11536        assert!(isident("foo")); // c:1288
11537        assert!(isident("_bar")); // c:1288
11538        assert!(isident("baz123")); // c:1288
11539        assert!(!isident("123abc")); // c:1288
11540        assert!(!isident("foo-bar")); // c:1288
11541    }
11542
11543    #[test]
11544    fn test_nicechar() {
11545        let _g = crate::test_util::global_state_lock();
11546        assert_eq!(nicechar('\n'), "\\n");
11547        assert_eq!(nicechar('\t'), "\\t");
11548        assert_eq!(nicechar('a'), "a");
11549    }
11550
11551    #[test]
11552    fn test_quotedzputs_single_quote_wrap() {
11553        let _g = crate::test_util::global_state_lock();
11554        assert_eq!(quotedzputs("simple"), "simple");
11555        assert_eq!(quotedzputs("has space"), "'has space'");
11556        assert_eq!(quotedzputs("it's"), "'it'\\''s'");
11557    }
11558
11559    #[test]
11560    fn test_quotestring_backslash() {
11561        let _g = crate::test_util::global_state_lock();
11562        assert_eq!(quotestring("hello", QT_BACKSLASH), "hello");
11563        assert_eq!(quotestring("has space", QT_BACKSLASH), "has\\ space");
11564        assert_eq!(quotestring("$var", QT_BACKSLASH), "\\$var");
11565    }
11566
11567    /// Pin: `ispecial(c)` matches the canonical SPECCHARS set at
11568    /// `Src/zsh.h:228` exactly: `"#$^*()=|{}[]\`<>?~;&\n\t \\'\""`.
11569    /// Previously the Rust local `ispecial` included `!` unconditionally
11570    /// which diverged from C — C only ISPECIAL-tags bangchar (default
11571    /// `!`) under BANGHIST + interactive (per `Src/utils.c:4257-4261`).
11572    ///
11573    /// This test exercises the path indirectly via `quotestring` with
11574    /// `QT_BACKSLASH` (which prepends `\` before every ispecial char).
11575    #[test]
11576    fn quotestring_backslash_only_specchars_no_bang_in_default() {
11577        let _g = crate::test_util::global_state_lock();
11578        // `!` is NOT in canonical SPECCHARS — should NOT be backslashed
11579        // in default non-interactive mode (matches C).
11580        assert_eq!(
11581            quotestring("a!b", QT_BACKSLASH),
11582            "a!b",
11583            "c:228 — `!` is not in SPECCHARS; only bangchar+BANGHIST adds it"
11584        );
11585        // `,` is NOT in canonical SPECCHARS until makecommaspecial(1).
11586        assert_eq!(
11587            quotestring("a,b", QT_BACKSLASH),
11588            "a,b",
11589            "c:228 — `,` not in SPECCHARS until makecommaspecial(1)"
11590        );
11591        // `^` IS in canonical SPECCHARS (per c:228 `"#$^..."`).
11592        assert_eq!(
11593            quotestring("a^b", QT_BACKSLASH),
11594            "a\\^b",
11595            "c:228 — `^` is in SPECCHARS"
11596        );
11597        // Open and close braces are in canonical SPECCHARS.
11598        assert_eq!(
11599            quotestring("a{b", QT_BACKSLASH),
11600            "a\\{b",
11601            "c:228 — open-brace is in SPECCHARS"
11602        );
11603        assert_eq!(
11604            quotestring("a}b", QT_BACKSLASH),
11605            "a\\}b",
11606            "c:228 — close-brace is in SPECCHARS"
11607        );
11608        // `#` is in canonical SPECCHARS (first char of c:228).
11609        assert_eq!(
11610            quotestring("a#b", QT_BACKSLASH),
11611            "a\\#b",
11612            "c:228 — `#` is the first char of SPECCHARS"
11613        );
11614        // `\\` (literal backslash) is in canonical SPECCHARS.
11615        assert_eq!(
11616            quotestring("a\\b", QT_BACKSLASH),
11617            "a\\\\b",
11618            "c:228 — `\\\\` in SPECCHARS"
11619        );
11620    }
11621
11622    #[test]
11623    fn test_quotestring_single() {
11624        let _g = crate::test_util::global_state_lock();
11625        assert_eq!(quotestring("hello", QT_SINGLE), "'hello'");
11626        assert_eq!(quotestring("it's", QT_SINGLE), "'it'\\''s'");
11627    }
11628
11629    #[test]
11630    fn test_quotestring_double() {
11631        let _g = crate::test_util::global_state_lock();
11632        assert_eq!(quotestring("hello", QT_DOUBLE), "\"hello\"");
11633        assert_eq!(quotestring("say \"hi\"", QT_DOUBLE), "\"say \\\"hi\\\"\"");
11634    }
11635
11636    #[test]
11637    fn test_quotestring_dollars() {
11638        let _g = crate::test_util::global_state_lock();
11639        assert_eq!(quotestring("hello", QT_DOLLARS), "$'hello'");
11640        assert_eq!(quotestring("line\nbreak", QT_DOLLARS), "$'line\\nbreak'");
11641        assert_eq!(quotestring("tab\there", QT_DOLLARS), "$'tab\\there'");
11642    }
11643
11644    #[test]
11645    fn test_quotestring_pattern() {
11646        let _g = crate::test_util::global_state_lock();
11647        assert_eq!(quotestring("*.txt", QT_BACKSLASH_PATTERN), "\\*.txt");
11648        assert_eq!(quotestring("file[1]", QT_BACKSLASH_PATTERN), "file\\[1\\]");
11649    }
11650
11651    #[test]
11652    fn test_quotetype_from_q_count() {
11653        let _g = crate::test_util::global_state_lock();
11654        assert_eq!(qflag_quotetype(1), QT_BACKSLASH);
11655        assert_eq!(qflag_quotetype(2), QT_SINGLE);
11656        assert_eq!(qflag_quotetype(3), QT_DOUBLE);
11657        assert_eq!(qflag_quotetype(4), QT_DOLLARS);
11658    }
11659
11660    #[test]
11661    fn test_tulower_tuupper() {
11662        let _g = crate::test_util::global_state_lock();
11663        assert_eq!(tulower('A'), 'a');
11664        assert_eq!(tuupper('a'), 'A');
11665        assert_eq!(tulower('1'), '1');
11666    }
11667
11668    #[test]
11669    fn test_wordcount_ifs_default() {
11670        let _g = crate::test_util::global_state_lock();
11671        // C: wordcount("a b c", NULL, 0) -> 3
11672        assert_eq!(wordcount("a b c", None, 0), 3);
11673        // Leading/trailing whitespace coalesced when mul <= 0.
11674        assert_eq!(wordcount("  a b  ", None, 0), 2);
11675        // Empty string with mul == 0 -> 0 words.
11676        assert_eq!(wordcount("", None, 0), 0);
11677        // Single word, no separators.
11678        assert_eq!(wordcount("foo", None, 0), 1);
11679    }
11680
11681    #[test]
11682    fn test_wordcount_with_explicit_sep() {
11683        let _g = crate::test_util::global_state_lock();
11684        // C: wordcount("a:b:c", ":", 0) -> 3 (3 fields, 2 separators)
11685        assert_eq!(wordcount("a:b:c", Some(":"), 0), 3);
11686        // Empty fields counted when mul != 0.
11687        assert_eq!(wordcount("a::b", Some(":"), 1), 3);
11688        // Without mul, consecutive empties collapse: a, b => 2... but
11689        // C's "if ((c || mul) && (sl || *(s+sl)))" — second `:` has
11690        // c=0 and mul=0 so doesn't increment. Result: a, b => 2.
11691        assert_eq!(wordcount("a::b", Some(":"), 0), 2);
11692    }
11693
11694    #[test]
11695    fn test_ucs4tomb_ascii() {
11696        let _g = crate::test_util::global_state_lock();
11697        let mut buf = [0u8; 8];
11698        // 'A' = 0x41, ASCII, single byte in any locale.
11699        let n = ucs4tomb('A' as u32, &mut buf);
11700        // wctomb may return 1 in C/POSIX locale; in UTF-8 locale also 1.
11701        assert_eq!(n, 1);
11702        assert_eq!(buf[0], b'A');
11703    }
11704
11705    #[test]
11706    fn test_is_mb_niceformat_plain_ascii() {
11707        let _g = crate::test_util::global_state_lock();
11708        // Plain printable ASCII — nothing needs nicechar escaping.
11709        assert_eq!(is_mb_niceformat("hello world"), 0);
11710    }
11711
11712    #[test]
11713    fn test_is_mb_niceformat_with_control_char() {
11714        let _g = crate::test_util::global_state_lock();
11715        // Tab is control (< 0x20) — needs nice escaping.
11716        assert_eq!(is_mb_niceformat("a\tb"), 1);
11717        // Bell character.
11718        assert_eq!(is_mb_niceformat("a\x07b"), 1);
11719    }
11720
11721    /// c:4856/4954 — `metafy` + `unmetafy` MUST round-trip for ASCII.
11722    /// A regression in the round-trip corrupts every metafied buffer
11723    /// the lexer/param-subst pipeline produces.
11724    #[test]
11725    fn metafy_unmetafy_round_trips_for_ascii() {
11726        let _g = crate::test_util::global_state_lock();
11727        let s = "hello world";
11728        let m = metafy(s);
11729        let mut buf = m.into_bytes();
11730        unmetafy(&mut buf);
11731        assert_eq!(std::str::from_utf8(&buf).unwrap(), s);
11732    }
11733
11734    /// `ztrlen` counts metafied characters (Meta-pairs as 1).
11735    /// Regression that double-counts Meta-pair bytes would break
11736    /// every fixed-width column path (printf %s).
11737    #[test]
11738    fn ztrlen_counts_ascii_one_per_byte() {
11739        let _g = crate::test_util::global_state_lock();
11740        assert_eq!(ztrlen(""), 0);
11741        assert_eq!(ztrlen("a"), 1);
11742        assert_eq!(ztrlen("hello"), 5);
11743    }
11744
11745    /// `Src/utils.c:5141-5149` — Meta-byte pair counts as 1 char.
11746    /// `*s++ == Meta` advances 2 bytes per iteration but increments
11747    /// `l` only once. Pin: a string with one Meta+X pair counts as 1.
11748    #[test]
11749    fn ztrlen_counts_meta_pair_as_one() {
11750        let _g = crate::test_util::global_state_lock();
11751        // META (0x83) + 0x20 = unmetafies to one '\0' byte (or a NUL).
11752        let meta = char::from_u32(Meta as u32).unwrap();
11753        let s: String = [meta, '\x20'].iter().collect();
11754        assert_eq!(
11755            ztrlen(&s),
11756            1,
11757            "c:5141-5148 — Meta+X pair counts as ONE char, not two"
11758        );
11759        // Mixed: "a" + META + "x" + "b" = 3 unmetafied chars.
11760        let mixed: String = ['a', meta, '\x20', 'b'].iter().collect();
11761        assert_eq!(
11762            ztrlen(&mixed),
11763            3,
11764            "c:5141 — three unmetafied chars from 'a' + Meta+X + 'b'"
11765        );
11766    }
11767
11768    /// `Src/utils.c:2579-2618` — `setblock_fd(turnonblocking, fd, modep)`.
11769    /// Signature pin: previously the Rust port had a 2-arg
11770    /// `(fd, blocking: bool) -> bool` signature, swapping the
11771    /// turnonblocking/fd argument order vs C AND collapsing the
11772    /// 3rd `*modep` out-param entirely. The fix restored canonical
11773    /// C order `(turnonblocking, fd)` with a `(bool, c_long)` tuple
11774    /// return mirroring `int return + long *modep`.
11775    ///
11776    /// Also pin the c:2599 regular-file short-circuit: C only
11777    /// operates on non-regular fds (pipes, sockets, ttys). A regular
11778    /// file returns `(false, -1)` immediately.
11779    #[cfg(unix)]
11780    #[test]
11781    fn setblock_fd_skips_regular_files_per_c_2599() {
11782        let _g = crate::test_util::global_state_lock();
11783        // Open a regular tempfile.
11784        let dir = tempfile::TempDir::new().unwrap();
11785        let f = fs::File::create(dir.path().join("regular")).unwrap();
11786        let fd = f.as_raw_fd();
11787        let (changed, mode) = setblock_fd(true, fd);
11788        assert!(
11789            !changed,
11790            "c:2599 — regular files short-circuit; setblock_fd must NOT report a change"
11791        );
11792        assert_eq!(mode, -1, "c:2614 — `*modep = -1` for regular files");
11793    }
11794
11795    /// `Src/utils.c:2606-2611` — `setblock_fd(turnonblocking=true, fd)`
11796    /// clears O_NONBLOCK on a pipe (non-regular fd). Returns
11797    /// `(true, prior_flags)` if the state was changed.
11798    #[cfg(unix)]
11799    #[test]
11800    fn setblock_fd_clears_o_nonblock_on_pipe() {
11801        let _g = crate::test_util::global_state_lock();
11802        // Create a pipe — non-regular fd.
11803        let mut pipefd: [libc::c_int; 2] = [0; 2];
11804        let r = unsafe { libc::pipe(pipefd.as_mut_ptr()) };
11805        assert_eq!(r, 0, "pipe(2) must succeed");
11806        let read_fd = pipefd[0];
11807        let write_fd = pipefd[1];
11808        // Force NONBLOCK on the read end.
11809        let cur = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11810        unsafe {
11811            libc::fcntl(read_fd, libc::F_SETFL, cur | libc::O_NONBLOCK);
11812        }
11813        // Verify NONBLOCK is set.
11814        let now = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11815        assert_ne!(
11816            now & libc::O_NONBLOCK,
11817            0,
11818            "test setup: NONBLOCK should be set"
11819        );
11820        // Call setblock_fd to ENABLE blocking (clear NONBLOCK).
11821        let (changed, _mode) = setblock_fd(true, read_fd);
11822        assert!(
11823            changed,
11824            "c:2611 — turnonblocking=true on a NONBLOCK pipe must report state change"
11825        );
11826        // Verify NONBLOCK is now cleared.
11827        let after = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11828        assert_eq!(
11829            after & libc::O_NONBLOCK,
11830            0,
11831            "c:2611 — O_NONBLOCK must be cleared after turnonblocking=true"
11832        );
11833        // Cleanup.
11834        unsafe {
11835            libc::close(read_fd);
11836            libc::close(write_fd);
11837        }
11838    }
11839
11840    /// `Src/utils.c:2620-2625` — `setblock_stdin()`. C body
11841    /// `setblock_fd(1, 0, &mode)` enables BLOCKING on fd 0 (stdin).
11842    /// Previously the Rust port called `setblock_fd(0, false)`
11843    /// which DISABLES blocking — exact opposite. Pin via real fd
11844    /// inspection.
11845    #[cfg(unix)]
11846    #[test]
11847    fn setblock_stdin_enables_blocking_on_fd_zero() {
11848        let _g = crate::test_util::global_state_lock();
11849        // Set stdin to NONBLOCKING first to verify the function
11850        // ACTUALLY switches it back to blocking. Skip if stdin is
11851        // not a normal fd (some CI configurations).
11852        let cur = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11853        if cur < 0 {
11854            return;
11855        }
11856        // Force NONBLOCK first.
11857        unsafe {
11858            libc::fcntl(0, libc::F_SETFL, cur | libc::O_NONBLOCK);
11859        }
11860        let after_set_nb = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11861        if after_set_nb & libc::O_NONBLOCK == 0 {
11862            // System rejected the change (regular file? CI tty?) — skip.
11863            unsafe {
11864                libc::fcntl(0, libc::F_SETFL, cur);
11865            }
11866            return;
11867        }
11868        // Call setblock_stdin — should CLEAR O_NONBLOCK.
11869        setblock_stdin();
11870        let after_setblock = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11871        assert_eq!(
11872            after_setblock & libc::O_NONBLOCK,
11873            0,
11874            "c:2624 — setblock_stdin must CLEAR O_NONBLOCK (enable blocking)"
11875        );
11876        // Restore original flags.
11877        unsafe {
11878            libc::fcntl(0, libc::F_SETFL, cur);
11879        }
11880    }
11881
11882    /// `Src/utils.c:2437-2519` — `zstrtol_underscore(s, base, false)`.
11883    /// Base-10 (explicit) parses canonical decimal.
11884    #[test]
11885    fn zstrtol_underscore_base_10_parses_decimal() {
11886        let _g = crate::test_util::global_state_lock();
11887        let (v, rest) = zstrtol_underscore("12345", 10, false);
11888        assert_eq!(v, 12345, "c:2471 — decimal accumulator");
11889        assert_eq!(rest, "", "rest is empty after full consumption");
11890        // With trailing non-digit.
11891        let (v, rest) = zstrtol_underscore("100abc", 10, false);
11892        assert_eq!(v, 100);
11893        assert_eq!(
11894            rest, "abc",
11895            "c:2467 — loop exits at first non-digit; rest carries on"
11896        );
11897    }
11898
11899    /// c:2452-2461 — base==0 autodetect: `0x`→16, `0b`→2, leading
11900    /// `0`→8 (always, unlike `zstrtoul_underscore` which honors
11901    /// OCTALZEROES). `zstrtol_underscore` does NOT consult
11902    /// OCTALZEROES — pure prefix detection.
11903    #[test]
11904    fn zstrtol_underscore_base_zero_autodetects_prefix() {
11905        let _g = crate::test_util::global_state_lock();
11906        // Hex.
11907        assert_eq!(
11908            zstrtol_underscore("0xff", 0, false).0,
11909            255,
11910            "c:2455 — 0x → base 16"
11911        );
11912        assert_eq!(zstrtol_underscore("0XFF", 0, false).0, 255);
11913        // Binary.
11914        assert_eq!(
11915            zstrtol_underscore("0b1010", 0, false).0,
11916            10,
11917            "c:2457 — 0b → base 2"
11918        );
11919        // Leading 0 → octal (always, no OCTALZEROES gate).
11920        assert_eq!(
11921            zstrtol_underscore("0777", 0, false).0,
11922            511,
11923            "c:2460 — leading 0 → octal (no OCTALZEROES gate for zstrtol)"
11924        );
11925        // Decimal default.
11926        assert_eq!(zstrtol_underscore("12345", 0, false).0, 12345);
11927    }
11928
11929    /// c:2447-2450 — leading `-` and `+` consumed; `-` triggers
11930    /// negation at the end (c:2497-2498).
11931    #[test]
11932    fn zstrtol_underscore_handles_sign_chars() {
11933        let _g = crate::test_util::global_state_lock();
11934        assert_eq!(
11935            zstrtol_underscore("-42", 10, false).0,
11936            -42,
11937            "c:2447 — leading `-` → negate"
11938        );
11939        assert_eq!(
11940            zstrtol_underscore("+42", 10, false).0,
11941            42,
11942            "c:2449 — leading `+` consumed, no negation"
11943        );
11944        // Mixed whitespace + sign.
11945        assert_eq!(
11946            zstrtol_underscore("  -100", 10, false).0,
11947            -100,
11948            "c:2444 — leading whitespace skipped, then sign"
11949        );
11950    }
11951
11952    /// c:2466-2492 — base 16 (hex) accumulator: digits 0-9 + letters
11953    /// a-f / A-F via `idigit(*s)` || `'a' ≤ *s < 'a'+base-10`.
11954    /// Pin both upper- and lower-case.
11955    #[test]
11956    fn zstrtol_underscore_base_16_accepts_letters() {
11957        let _g = crate::test_util::global_state_lock();
11958        assert_eq!(
11959            zstrtol_underscore("ff", 16, false).0,
11960            255,
11961            "c:2485 — base-16 'ff' → 255"
11962        );
11963        assert_eq!(
11964            zstrtol_underscore("FF", 16, false).0,
11965            255,
11966            "c:2485 — base-16 'FF' → 255 (upper case via `*s & 0x1f`)"
11967        );
11968        assert_eq!(
11969            zstrtol_underscore("DEADbeef", 16, false).0,
11970            0xDEADBEEF,
11971            "c:2485 — mixed case 32-bit hex"
11972        );
11973    }
11974
11975    /// c:2468/2482 — `underscore` flag enables digit-separator. With
11976    /// underscore=false, `_` is treated as end-of-digits. With
11977    /// underscore=true, `_` is skipped (c:2469-2470).
11978    #[test]
11979    fn zstrtol_underscore_underscore_flag() {
11980        let _g = crate::test_util::global_state_lock();
11981        // underscore=false: `_` terminates parse.
11982        let (v, rest) = zstrtol_underscore("1_000", 10, false);
11983        assert_eq!(v, 1, "c:2467 — underscore=false: `_` terminates");
11984        assert_eq!(rest, "_000");
11985        // underscore=true: `_` accepted and skipped.
11986        let (v, rest) = zstrtol_underscore("1_000_000", 10, true);
11987        assert_eq!(
11988            v, 1_000_000,
11989            "c:2469-2470 — underscore=true: `_` skipped during accumulation"
11990        );
11991        assert_eq!(rest, "", "fully consumed including `_`s");
11992    }
11993
11994    /// `Src/utils.c:2750-2774` — `timespec_diff_us(t1, t2)` returns
11995    /// the signed microsecond delta `t2 - t1`. Pin both sign
11996    /// conventions: t2 after t1 → positive; t2 before t1 → negative.
11997    #[test]
11998    fn timespec_diff_us_sign_matches_c_t2_minus_t1() {
11999        let _g = crate::test_util::global_state_lock();
12000        let t1 = std::time::Instant::now();
12001        std::thread::sleep(std::time::Duration::from_millis(2));
12002        let t2 = std::time::Instant::now();
12003        // c:2759 — `diff_sec = t2 - t1` for the t2 > t1 path. Result
12004        // positive (microseconds elapsed).
12005        let d = timespec_diff_us(&t1, &t2);
12006        assert!(d > 0, "c:2759 — t2 after t1 → positive delta");
12007        assert!(d >= 1000, "expected >= 1ms (1000us); got {}us", d);
12008        // c:2754 — reversed (t1 > t2): result negative.
12009        let r = timespec_diff_us(&t2, &t1);
12010        assert!(r < 0, "c:2770 — t1 after t2 → negative delta");
12011        assert_eq!(d, -r, "swap of args negates the result");
12012    }
12013
12014    /// c:2752 — `timespec_diff_us(t, t)` is 0 (identical Instants).
12015    #[test]
12016    fn timespec_diff_us_same_instant_returns_zero() {
12017        let _g = crate::test_util::global_state_lock();
12018        let t = std::time::Instant::now();
12019        assert_eq!(
12020            timespec_diff_us(&t, &t),
12021            0,
12022            "c:2752 — identical Instants → 0"
12023        );
12024    }
12025
12026    /// `Src/utils.c:6743-6779` — `ucs4toutf8(dest, wval)`. Encodes
12027    /// a Unicode codepoint to UTF-8. Pin canonical 1-, 2-, 3-, and
12028    /// 4-byte encodings.
12029    #[test]
12030    fn ucs4toutf8_encodes_canonical_lengths() {
12031        let _g = crate::test_util::global_state_lock();
12032        // c:6750 — 1 byte: ASCII range [0, 0x80).
12033        assert_eq!(
12034            ucs4toutf8(0x41),
12035            Some("A".to_string()),
12036            "c:6750 — 0x41 → 'A' (1 byte)"
12037        );
12038        // c:6752 — 2 bytes: [0x80, 0x800). 'é' = U+00E9.
12039        assert_eq!(
12040            ucs4toutf8(0xe9),
12041            Some("é".to_string()),
12042            "c:6752 — U+00E9 → 'é' (2 bytes)"
12043        );
12044        // c:6754 — 3 bytes: [0x800, 0x10000). '字' = U+5B57.
12045        assert_eq!(
12046            ucs4toutf8(0x5B57),
12047            Some("字".to_string()),
12048            "c:6754 — U+5B57 → '字' (3 bytes)"
12049        );
12050        // c:6756 — 4 bytes: [0x10000, 0x200000). '𝄞' = U+1D11E.
12051        assert_eq!(
12052            ucs4toutf8(0x1D11E),
12053            Some("𝄞".to_string()),
12054            "c:6756 — U+1D11E → '𝄞' (4 bytes)"
12055        );
12056    }
12057
12058    /// `Src/utils.c:6762-6764` — values `>= 0x80000000` are
12059    /// out-of-range; C emits `zerr("character not in range")` and
12060    /// returns `-1`. Surrogates (U+D800..U+DFFF) and values in
12061    /// 0x110000..0x80000000 are *encoded as raw bytes* by C
12062    /// (matches `wctomb(3)` extended UCS-4 range).
12063    #[test]
12064    fn ucs4toutf8_rejects_invalid_codepoints() {
12065        let _g = crate::test_util::global_state_lock();
12066        // c:6762-6764 — value >= 0x80000000 → None.
12067        assert_eq!(
12068            ucs4toutf8(0xFFFF_FFFE),
12069            None,
12070            "c:6763 — values >= 0x80000000 → None"
12071        );
12072        assert_eq!(
12073            ucs4toutf8(0x8000_0000),
12074            None,
12075            "c:6760-6764 — exactly 0x80000000 is out of range"
12076        );
12077        // Surrogates encode as 3-byte sequences per C bit-pattern
12078        // (c:6754 → len=3). The C source has no Unicode-validity
12079        // gate; that's a wctomb-only concern in callers.
12080        assert!(
12081            ucs4toutf8(0xD800).is_some(),
12082            "c:6754 — surrogates encode as 3-byte UTF-8 in C (no Unicode validity check)"
12083        );
12084    }
12085
12086    /// `Src/utils.c:6648-6723` — `dquotedztrdup(s)`. Default (non-
12087    /// CSHJUNKIEQUOTES) arm: wrap whole string in `"..."`. Backslash
12088    /// doubles, `"`/`$`/`` ` `` get escaped.
12089    #[test]
12090    fn dquotedztrdup_default_path_wraps_whole_string() {
12091        let _g = crate::test_util::global_state_lock();
12092        if isset(CSHJUNKIEQUOTES) {
12093            return; // Default-only test.
12094        }
12095        // c:6690 — `*p++ = '"';` then iterate.
12096        assert_eq!(
12097            dquotedztrdup("hello"),
12098            "\"hello\"",
12099            "c:6690+6711+6718 — wrap plain text in double quotes"
12100        );
12101        // c:6703-6708 — `$` gets `\$`.
12102        assert_eq!(
12103            dquotedztrdup("$var"),
12104            "\"\\$var\"",
12105            "c:6703-6708 — `$` → `\\$`"
12106        );
12107        // c:6703-6708 — `"` gets `\"`.
12108        assert_eq!(
12109            dquotedztrdup("a\"b"),
12110            "\"a\\\"b\"",
12111            "c:6703-6708 — `\"` → `\\\"`"
12112        );
12113    }
12114
12115    /// c:6697-6701 — backslash handling with `pending` state.
12116    /// Single `\` followed by an ordinary char emits just `\` (no
12117    /// doubling). The "pending" extra `\` fires only when the next
12118    /// char is itself a backslash or one of the escaped specials
12119    /// (`"`/`$`/`` ` ``), OR at end-of-string before the closing `"`.
12120    #[test]
12121    fn dquotedztrdup_pending_backslash_only_doubles_when_needed() {
12122        let _g = crate::test_util::global_state_lock();
12123        if isset(CSHJUNKIEQUOTES) {
12124            return;
12125        }
12126        // Mid-string `\` before ordinary char: stays as single `\`.
12127        assert_eq!(
12128            dquotedztrdup("a\\b"),
12129            "\"a\\b\"",
12130            "c:6711+6712 — `\\b` mid-string: no extra (pending=0 after b)"
12131        );
12132        // Trailing `\` triggers pending quirk: c:6716-6717 emits
12133        // an extra `\` before the closing `"`.
12134        let r = dquotedztrdup("a\\");
12135        assert!(
12136            r.ends_with("\\\\\""),
12137            "c:6716-6717 — trailing `\\` → emit extra `\\` before closing `\"`: got {:?}",
12138            r
12139        );
12140    }
12141
12142    /// `Src/utils.c:6464-6549` — `quotedzputs(s)`. Three branches:
12143    ///   - empty → `''`.
12144    ///   - has special chars (via `hasspecial`) → wrap in `'...'`
12145    ///     with `'\''` escape for embedded apostrophes.
12146    ///   - else → return unchanged.
12147    /// Pin all three.
12148    #[test]
12149    fn quotedzputs_empty_string_returns_double_quotes() {
12150        let _g = crate::test_util::global_state_lock();
12151        assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
12152    }
12153
12154    /// c:6514-6543 — needs-quote path: wrap in single quotes,
12155    /// escape embedded `'` as `'\''`.
12156    #[test]
12157    fn quotedzputs_wraps_specials_in_single_quotes() {
12158        let _g = crate::test_util::global_state_lock();
12159        inittyptab();
12160        // Space is special → wrap.
12161        assert_eq!(quotedzputs("hello world"), "'hello world'");
12162        // `=` is special → wrap.
12163        assert_eq!(quotedzputs("foo=bar"), "'foo=bar'");
12164        // Embedded apostrophe — `'\''` escape.
12165        assert_eq!(
12166            quotedzputs("it's"),
12167            "'it'\\''s'",
12168            "c:6517-6519 — apostrophe → '\\''"
12169        );
12170    }
12171
12172    /// c:6512 — no specials → return unchanged.
12173    #[test]
12174    fn quotedzputs_plain_alnum_returns_unchanged() {
12175        let _g = crate::test_util::global_state_lock();
12176        inittyptab();
12177        assert_eq!(
12178            quotedzputs("hello"),
12179            "hello",
12180            "c:6512 — no specials, no wrap"
12181        );
12182        assert_eq!(quotedzputs("abc123"), "abc123");
12183    }
12184
12185    /// c:6578-6637 — Bourne path "avoids empty quoted strings" by
12186    /// tracking `inquote`. Input `'x` should not produce empty `''`
12187    /// at the front; `x'` should not produce trailing `''`.
12188    #[test]
12189    fn quotedzputs_avoids_empty_quoted_runs_on_boundaries() {
12190        let _g = crate::test_util::global_state_lock();
12191        inittyptab();
12192        // Leading `'`: c:6587 with inquote=0 skips the close, emits
12193        // `\'`. Then `x`: c:6604 opens a quote, emits `x`, end emits
12194        // closing `'`. Result: `\''x'` — the visible doubled `''` is
12195        // `\'` (escaped) + `'` (opening single-quote), NOT an empty
12196        // single-quoted string.
12197        assert_eq!(
12198            quotedzputs("'x"),
12199            "\\''x'",
12200            "c:6587-6610 — leading `'` produces `\\'` + opening `'`"
12201        );
12202        // Trailing `'`: open quote at `x`, see `'` with inquote=1 → close
12203        // (`'`), emit `\'`. End-of-string: inquote=0, no trailing `'`.
12204        // Result: `'x'\\'` — NO trailing `''`.
12205        assert_eq!(
12206            quotedzputs("x'"),
12207            "'x'\\'",
12208            "c:6631-6637 — trailing `'` doesn't generate empty `''`"
12209        );
12210    }
12211
12212    /// c:6478-6492 — `is_mb_niceformat` arm of quotedzputs uses
12213    /// mb_niceformat with NICEFLAG_QUOTE so embedded `'` becomes
12214    /// `\'` inside the `$'...'` wrapper (preventing the quote from
12215    /// terminating the wrap). Previously sb_niceformat without
12216    /// NICEFLAG_QUOTE was used, which would have left `'` raw.
12217    #[test]
12218    fn quotedzputs_dollar_quote_escapes_apostrophe_inside_wrap() {
12219        let _g = crate::test_util::global_state_lock();
12220        inittyptab();
12221        // String with embedded `'` AND a control char so the
12222        // is_mb_niceformat arm is taken (controls trigger it).
12223        let r = quotedzputs("a\nb'c");
12224        // Expected: `$'a\nb\'c'` — the embedded `'` is `\'` inside
12225        // dollar-quotes, NOT terminating the wrap.
12226        assert!(
12227            r.starts_with("$'") && r.ends_with("'"),
12228            "c:6488 — must be wrapped in $'...'  got {:?}",
12229            r
12230        );
12231        assert!(
12232            r.contains("\\'"),
12233            "c:5413-5414 — embedded `'` must be `\\'` not raw `'`: got {:?}",
12234            r
12235        );
12236    }
12237
12238    /// c:6533-6576 — RCQUOTES branch: wrap everything in `'…'`,
12239    /// double embedded `'` as `''`.
12240    #[test]
12241    fn quotedzputs_rcquotes_doubles_apostrophe() {
12242        let _g = crate::test_util::global_state_lock();
12243        inittyptab();
12244        let prev = crate::ported::options::opt_state_get("rcquotes").unwrap_or(false);
12245        crate::ported::options::opt_state_set("rcquotes", true);
12246        // RCQUOTES: `it's` → `'it''s'` (doubled `'`, NOT `'\''`).
12247        let got = quotedzputs("it's");
12248        crate::ported::options::opt_state_set("rcquotes", prev);
12249        assert_eq!(
12250            got, "'it''s'",
12251            "c:6548-6553 — RCQUOTES: `'` → `''` (doubled)"
12252        );
12253    }
12254
12255    /// `Src/utils.c:2331-2337` — `strucpy(s, t)`. Appends `t` to
12256    /// `*s` and leaves `*s` pointing at the NUL terminator (Rust
12257    /// equivalent: append-in-place; `.len()` gives the new end).
12258    /// Pin: empty input → no-op; non-empty input → append.
12259    #[test]
12260    fn strucpy_appends_t_to_dest() {
12261        let _g = crate::test_util::global_state_lock();
12262        let mut dest = String::from("prefix-");
12263        strucpy(&mut dest, "suffix");
12264        assert_eq!(
12265            dest, "prefix-suffix",
12266            "c:2335 — strucpy appends t (NOT case-changes — c-name 'u' is pointer-walk, not upper)"
12267        );
12268        // Empty t — no change.
12269        let mut dest = String::from("alone");
12270        strucpy(&mut dest, "");
12271        assert_eq!(dest, "alone");
12272        // Empty dest + non-empty t.
12273        let mut dest = String::new();
12274        strucpy(&mut dest, "hello");
12275        assert_eq!(dest, "hello");
12276    }
12277
12278    /// `Src/utils.c:2341-2350` — `struncpy(s, t, n)`. Appends up to
12279    /// `n` bytes of `t` to `*s`. Pin: n=0 no-op; n < len(t) clips;
12280    /// n >= len(t) appends all.
12281    #[test]
12282    fn struncpy_appends_up_to_n_bytes() {
12283        let _g = crate::test_util::global_state_lock();
12284        // n < len(t).
12285        let mut dest = String::from("X");
12286        struncpy(&mut dest, "abcdef", 3);
12287        assert_eq!(dest, "Xabc", "c:2345 — clipped to n=3 bytes of `abcdef`");
12288        // n >= len(t).
12289        let mut dest = String::from("Y");
12290        struncpy(&mut dest, "abc", 100);
12291        assert_eq!(dest, "Yabc", "c:2345 — full copy when n >= len");
12292        // n=0 → no append.
12293        let mut dest = String::from("Z");
12294        struncpy(&mut dest, "abc", 0);
12295        assert_eq!(dest, "Z", "c:2345 — n=0 stops the copy loop immediately");
12296    }
12297
12298    /// `Src/utils.c:2280-2288` — `has_token(s)`. Returns true iff
12299    /// any byte in `s` triggers `itok()`. Pin: ASCII strings →
12300    /// false; strings containing token bytes (Pound 0x84, Bang
12301    /// 0x9c, Nularg 0xa1) → true.
12302    #[test]
12303    fn has_token_detects_typtab_token_bytes() {
12304        let _g = crate::test_util::global_state_lock();
12305        inittyptab();
12306        // c:2285 — pure ASCII has no token bytes.
12307        assert!(!has_token(""), "empty: no tokens");
12308        assert!(
12309            !has_token("hello world"),
12310            "c:2285 — ASCII text has no token bytes"
12311        );
12312        // Pound (0x84) — first token byte.
12313        let s: String = std::iter::once(0x84u8 as char).collect();
12314        assert!(
12315            has_token(&s),
12316            "c:2285 — Pound (0x84) is itok → has_token=true"
12317        );
12318        // Bang (0x9c) — last_normal_tok.
12319        let s: String = std::iter::once(0x9cu8 as char).collect();
12320        assert!(has_token(&s), "c:2285 — Bang (0x9c) is itok");
12321        // Nularg (0xa1) — upper bound.
12322        let s: String = std::iter::once(0xa1u8 as char).collect();
12323        assert!(has_token(&s), "c:2285 — Nularg (0xa1) is itok");
12324    }
12325
12326    /// `Src/utils.c:2280` — Meta byte (0x83) is NOT a token. Pin
12327    /// the regression: previously the Rust port hardcoded `0x83`
12328    /// as a token byte. Now correctly excludes it.
12329    #[test]
12330    fn has_token_excludes_meta_byte() {
12331        let _g = crate::test_util::global_state_lock();
12332        inittyptab();
12333        // 0x83 is Meta, NOT itok. Should return false.
12334        let s: String = std::iter::once(0x83u8 as char).collect();
12335        assert!(
12336            !has_token(&s),
12337            "c:2285 — Meta (0x83) is NOT itok; previous hardcoded 0x83 list misfired"
12338        );
12339    }
12340
12341    /// `Src/utils.c:6082-6124` — `addunprintable(c)`. Renders bytes
12342    /// using shell-compatible C-string escapes (`\a`/`\b`/`\f`/`\n`/
12343    /// `\r`/`\t`/`\v` for the named controls + `\nnn` octal for
12344    /// others + `\0` for NUL). Previously emitted ZLE caret form —
12345    /// wrong convention.
12346    #[test]
12347    fn addunprintable_named_control_escapes() {
12348        let _g = crate::test_util::global_state_lock();
12349        // c:6106-6112 — named-escape per byte.
12350        assert_eq!(addunprintable('\x07'), "\\a", "c:6106 — BEL → \\a");
12351        assert_eq!(addunprintable('\x08'), "\\b", "c:6107 — BS → \\b");
12352        assert_eq!(addunprintable('\x0c'), "\\f", "c:6108 — FF → \\f");
12353        assert_eq!(addunprintable('\n'), "\\n", "c:6109 — LF → \\n");
12354        assert_eq!(addunprintable('\r'), "\\r", "c:6110 — CR → \\r");
12355        assert_eq!(addunprintable('\t'), "\\t", "c:6111 — TAB → \\t");
12356        assert_eq!(addunprintable('\x0b'), "\\v", "c:6112 — VT → \\v");
12357    }
12358
12359    /// c:6097-6103 — NUL renders as `\0`. (C peeks next byte for
12360    /// `\000` disambiguation; Rust port works one char at a time
12361    /// so emits the short `\0` form.)
12362    #[test]
12363    fn addunprintable_nul_renders_as_backslash_zero() {
12364        let _g = crate::test_util::global_state_lock();
12365        assert_eq!(addunprintable('\0'), "\\0", "c:6097-6098 — NUL → \\0");
12366    }
12367
12368    /// c:6114-6119 — `\nnn` 3-digit octal fallback for un-named
12369    /// control bytes. Pin the format: each digit is one octal digit
12370    /// (0-7), zero-padded to 3 positions.
12371    #[test]
12372    fn addunprintable_octal_fallback_for_unnamed_controls() {
12373        let _g = crate::test_util::global_state_lock();
12374        // 0x01 (SOH) = 001 octal.
12375        assert_eq!(addunprintable('\x01'), "\\001", "c:6116-6118 — SOH → \\001");
12376        // 0x1b (ESC) = 033 octal.
12377        assert_eq!(addunprintable('\x1b'), "\\033", "c:6116-6118 — ESC → \\033");
12378        // 0x7f (DEL) = 177 octal.
12379        assert_eq!(addunprintable('\x7f'), "\\177", "c:6116-6118 — DEL → \\177");
12380        // 0xff (high) = 377 octal.
12381        assert_eq!(
12382            addunprintable(char::from_u32(0xff).unwrap()),
12383            "\\377",
12384            "c:6116-6118 — 0xff → \\377"
12385        );
12386    }
12387
12388    /// `Src/utils.c:6072-6082` — `hasspecial(s)`. Pin canonical
12389    /// special chars from SPECCHARS (`Src/zsh.h:228`): `#$^*()=|{}[]
12390    /// \`<>?~;&\\n\\t \\\\\\'\\"`.
12391    #[test]
12392    fn hasspecial_recognises_canonical_special_chars() {
12393        let _g = crate::test_util::global_state_lock();
12394        // typtab access is read-only here (no flag mutation); concurrent
12395        // inittyptab() rebuilds are idempotent for the default flag set.
12396        inittyptab();
12397        // c:6075 — every char in SPECCHARS triggers true.
12398        for c in "#$^*()=|{}[]<>?~;&".chars() {
12399            let s = c.to_string();
12400            assert!(
12401                hasspecial(&s),
12402                "c:6075 — '{}' is in SPECCHARS, must be special",
12403                c
12404            );
12405        }
12406        // Whitespace specials.
12407        assert!(hasspecial(" "), "space in SPECCHARS");
12408        assert!(hasspecial("\t"), "tab in SPECCHARS");
12409        assert!(hasspecial("\n"), "newline in SPECCHARS");
12410        // Non-special chars → false.
12411        assert!(
12412            !hasspecial("hello"),
12413            "c:6075 — plain alphanumerics are NOT special"
12414        );
12415        assert!(!hasspecial("ABC012"));
12416    }
12417
12418    /// `Src/utils.c:6072-6075` — Meta-byte handling: `*s == Meta ?
12419    /// *++s ^ 32 : *s`. Pin Meta+X pair gets decoded before special-
12420    /// check. A Meta+'A' decodes to 'a' (not special), so a string
12421    /// containing only Meta+'A' returns false.
12422    #[test]
12423    fn hasspecial_decodes_meta_byte_before_check() {
12424        let _g = crate::test_util::global_state_lock();
12425        // Same as above — read-only after inittyptab.
12426        inittyptab();
12427        // Meta + 0x41 ('A') → 'a' (0x61) → not special.
12428        let bytes: Vec<u8> = vec![Meta, 0x41u8];
12429        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12430        assert!(
12431            !hasspecial(s),
12432            "c:6075 — Meta+0x41 decodes to 'a' which is NOT special"
12433        );
12434    }
12435
12436    /// `Src/utils.c:5849-5910` — `sb_niceformat(s)`. Pin: ASCII
12437    /// printable passes through unchanged; controls escape.
12438    /// C-equivalent call shape: `char *out; sb_niceformat(s, NULL, &out, 0);`
12439    #[test]
12440    fn sb_niceformat_passes_printable_ascii() {
12441        let _g = crate::test_util::global_state_lock();
12442        let mut out: Option<String> = None;
12443        let l = sb_niceformat("hello", None, Some(&mut out), 0);
12444        assert_eq!(
12445            out.as_deref(),
12446            Some("hello"),
12447            "c:5886 — nicechar_sel passes printable through"
12448        );
12449        assert_eq!(l, 5, "c:5928 — return length equals output bytes");
12450
12451        let mut out: Option<String> = None;
12452        sb_niceformat("", None, Some(&mut out), 0);
12453        assert_eq!(out.as_deref(), Some(""));
12454
12455        let mut out: Option<String> = None;
12456        sb_niceformat("ABC012!?@", None, Some(&mut out), 0);
12457        assert_eq!(out.as_deref(), Some("ABC012!?@"));
12458    }
12459
12460    /// c:5886 — controls get `\n`/`\t`/`^X`/`^?` escapes via nicechar_sel.
12461    #[test]
12462    fn sb_niceformat_escapes_controls() {
12463        let _g = crate::test_util::global_state_lock();
12464        let mut out: Option<String> = None;
12465        sb_niceformat("a\nb", None, Some(&mut out), 0);
12466        assert_eq!(out.as_deref(), Some("a\\nb"), "c:5886 — newline → \\n");
12467
12468        let mut out: Option<String> = None;
12469        sb_niceformat("a\tb", None, Some(&mut out), 0);
12470        assert_eq!(out.as_deref(), Some("a\\tb"));
12471
12472        let mut out: Option<String> = None;
12473        sb_niceformat("\x01", None, Some(&mut out), 0);
12474        assert_eq!(
12475            out.as_deref(),
12476            Some("^A"),
12477            "c:5886 — control char → ^X form"
12478        );
12479
12480        let mut out: Option<String> = None;
12481        sb_niceformat("\x7f", None, Some(&mut out), 0);
12482        assert_eq!(out.as_deref(), Some("^?"));
12483    }
12484
12485    /// `Src/utils.c:5872` — unmetafy step before formatting. Pin:
12486    /// metafied Meta+X pair gets unescaped first, then run through
12487    /// nicechar_sel.
12488    #[test]
12489    fn sb_niceformat_unmetafies_before_formatting() {
12490        let _g = crate::test_util::global_state_lock();
12491        // META + 0x41 ('A') → decodes to 'a' (0x61). 'a' is printable
12492        // → passes through unchanged.
12493        let bytes: Vec<u8> = vec![Meta, 0x41u8];
12494        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12495        let mut out: Option<String> = None;
12496        sb_niceformat(s, None, Some(&mut out), 0);
12497        assert_eq!(
12498            out.as_deref(),
12499            Some("a"),
12500            "c:5872 — unmetafy first: Meta+0x41 → 'a' → printable passthrough"
12501        );
12502        // META + 0x20 → decodes to NUL → "^@" form.
12503        let bytes: Vec<u8> = vec![Meta, 0x20u8];
12504        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12505        let mut out: Option<String> = None;
12506        sb_niceformat(s, None, Some(&mut out), 0);
12507        let r = out.unwrap_or_default();
12508        assert!(
12509            !r.is_empty(),
12510            "c:5872 — Meta+0x20 → NUL → must emit some escape, not empty"
12511        );
12512    }
12513
12514    /// `Src/utils.c:5937-5959` — `is_sb_niceformat(s)`. Returns 1
12515    /// if sb_niceformat would change the input, else 0.
12516    #[test]
12517    fn is_sb_niceformat_true_for_strings_with_controls() {
12518        let _g = crate::test_util::global_state_lock();
12519        assert_eq!(is_sb_niceformat("\n"), 1, "newline is nice");
12520        assert_eq!(is_sb_niceformat("a\tb"), 1);
12521        assert_eq!(is_sb_niceformat("\x01"), 1);
12522        // Non-control ASCII → 0.
12523        assert_eq!(is_sb_niceformat("hello"), 0);
12524        assert_eq!(is_sb_niceformat(""), 0);
12525    }
12526
12527    /// `Src/utils.c:5810-5826` — `metacharlenconv(x, c)`. Returns
12528    /// `(2, decoded)` for Meta+X pair, `(1, byte)` for plain byte.
12529    /// Pin both branches.
12530    #[test]
12531    fn metacharlenconv_plain_ascii_returns_one_byte() {
12532        let _g = crate::test_util::global_state_lock();
12533        // c:5823-5825 — plain byte.
12534        let (n, c) = metacharlenconv("a");
12535        assert_eq!((n, c), (1, Some('a')), "c:5823 — plain byte → (1, byte)");
12536        let (n, c) = metacharlenconv("X");
12537        assert_eq!((n, c), (1, Some('X')));
12538    }
12539
12540    /// c:5818-5821 — Meta+X pair: `*c = x[1] ^ 32; return 2`.
12541    #[test]
12542    fn metacharlenconv_meta_pair_xor_decodes() {
12543        let _g = crate::test_util::global_state_lock();
12544        // Meta + 0x41 ('A') → 'A' ^ 32 = 'a'.
12545        let bytes: Vec<u8> = vec![Meta, 0x41u8];
12546        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12547        let (n, c) = metacharlenconv(s);
12548        assert_eq!(
12549            (n, c),
12550            (2, Some('a')),
12551            "c:5820 — Meta+0x41 → 'a' (XOR 32), consumed 2 bytes"
12552        );
12553        // Empty input.
12554        let (n, c) = metacharlenconv("");
12555        assert_eq!((n, c), (0, None));
12556    }
12557
12558    /// `Src/utils.c:5832-5843` — `charlenconv(x, len, c)`. Returns
12559    /// `(0, NUL)` when len=0, `(1, byte)` otherwise.
12560    #[test]
12561    fn charlenconv_zero_len_returns_zero() {
12562        let _g = crate::test_util::global_state_lock();
12563        // c:5834 — `if (!len) { *c = '\0'; return 0; }`.
12564        let (n, c) = charlenconv("abc", 0);
12565        assert_eq!((n, c), (0, None), "c:5834-5837 — len=0 returns 0");
12566    }
12567
12568    /// c:5840-5842 — non-zero len: read one byte, return (1, byte).
12569    #[test]
12570    fn charlenconv_returns_first_byte_for_nonzero_len() {
12571        let _g = crate::test_util::global_state_lock();
12572        let (n, c) = charlenconv("abc", 3);
12573        assert_eq!((n, c), (1, Some('a')), "c:5841 — *c = *x; return 1");
12574        let (n, c) = charlenconv("xy", 2);
12575        assert_eq!((n, c), (1, Some('x')));
12576    }
12577
12578    /// `Src/utils.c:5474-5524` — `is_mb_niceformat(s)`. Predicate:
12579    /// would mb_niceformat produce a different output? Pin canonical
12580    /// branches:
12581    ///   - Pure ASCII printable → false (no escape needed).
12582    ///   - Contains control char → true.
12583    ///   - Pure UTF-8 wide chars → false under default PRINTEIGHTBIT-off
12584    ///     since is_wcs_nicechar treats them as printable.
12585    #[test]
12586    fn is_mb_niceformat_false_for_pure_printable_ascii() {
12587        let _g = crate::test_util::global_state_lock();
12588        assert_eq!(
12589            is_mb_niceformat("hello"),
12590            0,
12591            "c:5509 — printable ASCII needs no nice-format"
12592        );
12593        assert_eq!(
12594            is_mb_niceformat(""),
12595            0,
12596            "c:5486 — empty string has no chars to flag"
12597        );
12598        assert_eq!(is_mb_niceformat("ABC012!?@"), 0);
12599    }
12600
12601    /// c:5509 + `is_wcs_nicechar` — newline/tab/control chars trigger
12602    /// the predicate.
12603    #[test]
12604    fn is_mb_niceformat_true_for_strings_with_controls() {
12605        let _g = crate::test_util::global_state_lock();
12606        assert_eq!(is_mb_niceformat("a\nb"), 1, "c:5509 — newline is nice");
12607        assert_eq!(is_mb_niceformat("\t"), 1);
12608        assert_eq!(is_mb_niceformat("\x01"), 1, "c:5509 — control char is nice");
12609        assert_eq!(is_mb_niceformat("\x7f"), 1, "c:5509 — DEL is nice");
12610    }
12611
12612    /// `Src/utils.c:5366-5460` — `mb_niceformat(s)`. Multibyte-aware
12613    /// nice-formatter. Calls `wcs_nicechar` per char. Test uses
12614    /// C-equivalent call shape: `char *out; mb_niceformat(s, NULL, &out, 0);`
12615    #[test]
12616    fn mb_niceformat_preserves_printable_wide_chars() {
12617        let _g = crate::test_util::global_state_lock();
12618        let mut out: Option<String> = None;
12619        mb_niceformat("hello", None, Some(&mut out), 0);
12620        assert_eq!(
12621            out.as_deref(),
12622            Some("hello"),
12623            "c:5407 — printable ASCII passes through"
12624        );
12625
12626        let mut out: Option<String> = None;
12627        mb_niceformat("café", None, Some(&mut out), 0);
12628        assert_eq!(
12629            out.as_deref(),
12630            Some("café"),
12631            "c:5407 — Latin-1 'é' must NOT byte-mask to \\M-X"
12632        );
12633
12634        let mut out: Option<String> = None;
12635        mb_niceformat("字", None, Some(&mut out), 0);
12636        assert_eq!(
12637            out.as_deref(),
12638            Some("字"),
12639            "c:5407 — CJK printable passes through"
12640        );
12641
12642        let mut out: Option<String> = None;
12643        mb_niceformat("abcéxyz", None, Some(&mut out), 0);
12644        assert_eq!(out.as_deref(), Some("abcéxyz"));
12645    }
12646
12647    /// `Src/utils.c:5407` + `wcs_nicechar` — controls still escape.
12648    #[test]
12649    fn mb_niceformat_escapes_controls() {
12650        let _g = crate::test_util::global_state_lock();
12651        let mut out: Option<String> = None;
12652        mb_niceformat("\n", None, Some(&mut out), 0);
12653        assert_eq!(
12654            out.as_deref(),
12655            Some("\\n"),
12656            "c:5407 → wcs_nicechar c:625 — newline escapes"
12657        );
12658
12659        let mut out: Option<String> = None;
12660        mb_niceformat("\t", None, Some(&mut out), 0);
12661        assert_eq!(
12662            out.as_deref(),
12663            Some("\\t"),
12664            "c:5407 → wcs_nicechar c:628 — tab escapes"
12665        );
12666
12667        let mut out: Option<String> = None;
12668        mb_niceformat("a\nb", None, Some(&mut out), 0);
12669        assert_eq!(out.as_deref(), Some("a\\nb"));
12670    }
12671
12672    /// `Src/utils.c:4971-4983` — `metalen(s, len)`. **Input `len` is
12673    /// the UNMETAFIED char count; output is the METAFIED byte count.**
12674    /// Pin a metafied string with one Meta+X pair: 3 unmetafied chars
12675    /// → 4 metafied bytes.
12676    #[test]
12677    fn metalen_returns_metafied_byte_count() {
12678        let _g = crate::test_util::global_state_lock();
12679        // Pure ASCII: 5 chars → 5 bytes (no Meta encountered).
12680        assert_eq!(
12681            metalen("hello", 5),
12682            5,
12683            "c:4972 — ASCII: metafied bytes == unmetafied chars"
12684        );
12685        // [a, Meta, X, b] = 4 metafied bytes representing 3 chars.
12686        let bytes: Vec<u8> = vec![b'a', Meta, 0x41, b'b'];
12687        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12688        assert_eq!(
12689            metalen(s, 3),
12690            4,
12691            "c:4978 — 3 unmetafied chars + 1 Meta = 4 metafied bytes"
12692        );
12693        // Two Meta pairs: [Meta, X, Meta, Y] = 4 bytes for 2 chars.
12694        let bytes: Vec<u8> = vec![Meta, 0x41, Meta, 0x42];
12695        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12696        assert_eq!(
12697            metalen(s, 2),
12698            4,
12699            "c:4978 — 2 unmetafied chars (both Meta+X) = 4 metafied bytes"
12700        );
12701    }
12702
12703    /// c:4974 — `mlen = len;` is the initial value (no Meta found,
12704    /// returns the input length unchanged). Pin len=0 and pure-ASCII.
12705    #[test]
12706    fn metalen_returns_input_for_no_meta_chars() {
12707        let _g = crate::test_util::global_state_lock();
12708        assert_eq!(metalen("", 0), 0, "c:4974 — empty input returns 0");
12709        assert_eq!(
12710            metalen("abc", 3),
12711            3,
12712            "c:4974 — no Meta chars: output == input len"
12713        );
12714    }
12715
12716    /// `Src/utils.c:5070` — `if (!in || !*in) return 0;`.
12717    /// Empty input returns `('\0', 0)`. Pin: empty &str → 0 bytes consumed.
12718    #[test]
12719    fn unmeta_one_empty_input_returns_zero() {
12720        let _g = crate::test_util::global_state_lock();
12721        let (c, n) = unmeta_one("");
12722        assert_eq!(c, '\0', "c:5070 — empty input returns NUL char");
12723        assert_eq!(n, 0, "c:5070 — empty input consumes 0 bytes");
12724    }
12725
12726    /// `Src/utils.c:5081-5082` — Non-Meta byte: `*sz = 1; wc = byte`.
12727    #[test]
12728    fn unmeta_one_plain_ascii_consumes_one_byte() {
12729        let _g = crate::test_util::global_state_lock();
12730        for c in "aA0!~".chars() {
12731            let s = c.to_string();
12732            let (got, n) = unmeta_one(&s);
12733            assert_eq!(got, c, "c:5082 — '{}' decodes to itself", c);
12734            assert_eq!(n, 1, "c:5081 — non-Meta byte consumes 1");
12735        }
12736    }
12737
12738    /// `Src/utils.c:5077-5079` — Meta byte: `*sz = 2; wc = in[1] ^ 32`.
12739    /// Pin via constructed Meta byte sequence.
12740    #[test]
12741    fn unmeta_one_meta_pair_decodes_xor_32() {
12742        let _g = crate::test_util::global_state_lock();
12743        // META + 0x41 ('A') → 'A' ^ 32 = 0x61 ('a'). 2 bytes consumed.
12744        let bytes: Vec<u8> = vec![Meta, 0x41u8];
12745        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12746        let (got, n) = unmeta_one(s);
12747        assert_eq!(got, 'a', "c:5079 — Meta+0x41 decodes to 0x41^32 = 'a'");
12748        assert_eq!(n, 2, "c:5078 — Meta pair consumes 2 bytes");
12749        // META + 0x20 = 0x20^32 = 0x00 (NUL).
12750        let bytes: Vec<u8> = vec![Meta, 0x20u8];
12751        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12752        let (got, n) = unmeta_one(s);
12753        assert_eq!(
12754            got, '\0',
12755            "c:5079 — Meta+0x20 decodes to NUL (the canonical metafy-NUL pattern)"
12756        );
12757        assert_eq!(n, 2);
12758    }
12759
12760    /// Edge case: bare Meta byte at end with no follower. C source
12761    /// would read past the buffer (UB); Rust port's `bytes.len() > 1`
12762    /// guard handles this by falling through to the non-Meta arm.
12763    #[test]
12764    fn unmeta_one_trailing_meta_byte_falls_through() {
12765        let _g = crate::test_util::global_state_lock();
12766        let bytes: Vec<u8> = vec![Meta];
12767        let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12768        let (_, n) = unmeta_one(s);
12769        // Defensive: Rust returns the Meta byte itself as char + 1.
12770        assert_eq!(n, 1, "trailing Meta — no panic, no read past buffer");
12771    }
12772
12773    /// `Src/utils.c:5185-5203` — `ztrsub(t, s)`. Count of unmetafied
12774    /// chars between two pointers in the same metafied string.
12775    /// Rust port takes (buf, start, end) byte-offsets since raw
12776    /// pointer subtraction across distinct &str isn't safe.
12777    /// Pin pure-ASCII subrange (no Meta) → returns end-start.
12778    #[test]
12779    fn ztrsub_ascii_no_meta_returns_byte_distance() {
12780        let _g = crate::test_util::global_state_lock();
12781        // "hello world" — substring [0..5) → "hello" → 5 chars.
12782        assert_eq!(
12783            ztrsub("hello world", 0, 5),
12784            5,
12785            "c:5189 — no Meta: returns t-s byte distance"
12786        );
12787        // [6..11) → "world" → 5 chars.
12788        assert_eq!(ztrsub("hello world", 6, 11), 5);
12789        // Empty range.
12790        assert_eq!(ztrsub("hello", 2, 2), 0);
12791    }
12792
12793    /// c:5191-5199 — each Meta+X pair decrements `l` by 1 (since the
12794    /// initial l = byte-distance counts both bytes but the pair is
12795    /// one logical char). Pin via constructed Meta string.
12796    #[test]
12797    fn ztrsub_subtracts_one_per_meta_pair() {
12798        let _g = crate::test_util::global_state_lock();
12799        // Build "a" + Meta + 0x20 + "b" = 4 bytes total.
12800        // Unmetafied: "a" + 1-char-from-meta + "b" = 3 chars.
12801        let meta_byte = Meta;
12802        let buf_bytes: Vec<u8> = vec![b'a', meta_byte, 0x20, b'b'];
12803        let buf = unsafe { std::str::from_utf8_unchecked(&buf_bytes) };
12804        // [0..4) byte distance = 4; Meta pair found → l-- → 3.
12805        assert_eq!(
12806            ztrsub(buf, 0, 4),
12807            3,
12808            "c:5192-5199 — Meta pair decrements distance by 1"
12809        );
12810    }
12811
12812    /// c:5189 — `int l = t - s;` start of count. Pin edge case
12813    /// where start > end (caller bug) doesn't underflow or panic;
12814    /// Rust port clamps via `start.min(end)`.
12815    #[test]
12816    fn ztrsub_clamps_inverted_range_to_zero() {
12817        let _g = crate::test_util::global_state_lock();
12818        assert_eq!(
12819            ztrsub("hello", 4, 2),
12820            0,
12821            "inverted range start>end → 0 (defensive; not in C contract)"
12822        );
12823        // Beyond-buffer end gets clamped.
12824        assert_eq!(
12825            ztrsub("hi", 0, 100),
12826            2,
12827            "end > buf.len() clamps to buffer length"
12828        );
12829    }
12830
12831    /// `Src/utils.c:5141` — trailing Meta byte with no following char
12832    /// is a malformed-string edge case. C has a DPUTS debug-only
12833    /// fprintf for "unexpected end" but otherwise the loop terminates
12834    /// because `*s` is now NUL. Rust port must not panic on this
12835    /// truncated input.
12836    #[test]
12837    fn ztrlen_handles_trailing_meta_byte_without_panic() {
12838        let _g = crate::test_util::global_state_lock();
12839        let meta = char::from_u32(Meta as u32).unwrap();
12840        let trailing: String = ['a', meta].iter().collect();
12841        // c:5141-5149 — increment l, then encounter Meta, then loop
12842        // condition `*s` is empty → terminate. Count: 'a' + Meta = 2
12843        // (Rust port counts Meta as 1 char when no follower).
12844        let r = ztrlen(&trailing);
12845        assert!(r >= 1, "trailing Meta must not panic; got {}", r);
12846    }
12847
12848    /// c:params.c:1288 — `isident` rejects digit-leading names.
12849    /// A regression accepting them lets `typeset 1foo=bar` install
12850    /// a poisoned param that no later expansion can address.
12851    #[test]
12852    fn isident_rejects_digit_leading_names() {
12853        let _g = crate::test_util::global_state_lock(); // c:1288
12854                                                        // c:1315-1319 — C `isident`: if first char is digit, ALL
12855                                                        // chars must be digit (all-digit names are valid positional
12856                                                        // params like $99). So `1foo` is rejected (digit-then-alpha)
12857                                                        // but `99` is accepted as a valid positional-param name.
12858                                                        // The previous utils.rs fake rejected all-digit names too,
12859                                                        // which was non-faithful — the canonical `params::isident`
12860                                                        // at `params.rs:2056` matches the C semantics.
12861        assert!(!isident("1foo")); // c:1318
12862        assert!(
12863            isident("99"), // c:1318
12864            "c:1315-1319 — all-digit names are valid positional params"
12865        );
12866        assert!(!isident("")); // c:1290
12867    }
12868
12869    /// `isident` MUST accept underscore-leading + alpha-leading names —
12870    /// `_foo`, `Foo_BAR_42` are valid POSIX shell idents.
12871    #[test]
12872    fn isident_accepts_underscore_and_alpha_leading() {
12873        let _g = crate::test_util::global_state_lock(); // c:1288
12874        assert!(isident("foo")); // c:1288
12875        assert!(isident("_foo")); // c:1288
12876        assert!(isident("Foo_BAR_42")); // c:1288
12877        assert!(isident("a")); // c:1288
12878    }
12879
12880    /// `isident` rejects whitespace/punctuation/$. A regression
12881    /// accepting them would let assignments install names no
12882    /// subsequent lookup can address (`typeset 'foo bar'=baz`).
12883    #[test]
12884    fn isident_rejects_special_chars() {
12885        let _g = crate::test_util::global_state_lock(); // c:1288
12886                                                        // c:1320-1322 — non-digit-leading names use itype_end with
12887                                                        // INAMESPC bits (alnum + `_` + `.`); other chars terminate.
12888                                                        // The previous utils.rs fake rejected `foo.` outright, which
12889                                                        // is incorrect — C accepts trailing `.` (it's an INAMESPC
12890                                                        // member). Whitespace, `-`, and `$` are NOT INAMESPC chars
12891                                                        // and remain rejected.
12892        assert!(!isident("foo bar")); // c:1322
12893        assert!(!isident("foo-bar")); // c:1322
12894        assert!(!isident("a$b")); // c:1322
12895    }
12896
12897    /// `convbase(0, 10)` MUST produce `"0"`, not `""`. The literal-zero
12898    /// edge case is a real C divergence point — regression here would
12899    /// make `$(( 0 ))` print nothing.
12900    #[test]
12901    fn convbase_zero_renders_as_zero_literal() {
12902        let _g = crate::test_util::global_state_lock();
12903        assert_eq!(convbase(0, 10), "0");
12904    }
12905
12906    /// `convbase` uses zsh's canonical `BASE#NUMBER` syntax for
12907    /// non-decimal output (matches `$(( [#16] 255 ))` → `16#FF`).
12908    /// This is the format `printf "%X"` and `$(( ... ))` both produce.
12909    #[test]
12910    fn convbase_uses_base_prefix_syntax_for_non_decimal() {
12911        let _g = crate::test_util::global_state_lock();
12912        assert_eq!(convbase(255, 16), "16#FF");
12913        assert_eq!(convbase(8, 8), "8#10");
12914        assert_eq!(convbase(5, 2), "2#101");
12915        // Base 10 has no prefix.
12916        assert_eq!(convbase(42, 10), "42");
12917    }
12918
12919    /// Negative number renders with leading `-`. Regression that drops
12920    /// the sign would silently flip arithmetic semantics in `$((...))`.
12921    #[test]
12922    fn convbase_preserves_negative_sign() {
12923        let _g = crate::test_util::global_state_lock();
12924        assert_eq!(convbase(-42, 10), "-42");
12925    }
12926
12927    /// `Src/utils.c:837` — `slashsplit` keeps an EMPTY leading segment
12928    /// when the input has a leading `/`. The caller `xsymlinks`
12929    /// (c:879) reads this empty as the signal "absolute path, start
12930    /// xbuf from /". Previous Rust test asserted filtering of all
12931    /// empties (matching the previous buggy port); that contract was
12932    /// wrong relative to C. Updated to assert the C contract.
12933    #[test]
12934    fn slashsplit_keeps_leading_empty_segment() {
12935        let _g = crate::test_util::global_state_lock();
12936        // c:851 with t==s on first iter → empty prefix segment.
12937        assert_eq!(
12938            slashsplit("/usr/local/bin"),
12939            vec![
12940                "".to_string(),
12941                "usr".to_string(),
12942                "local".to_string(),
12943                "bin".to_string()
12944            ],
12945            "c:851 — leading `/` produces empty first segment"
12946        );
12947        // Single `/`: one empty segment only.
12948        assert_eq!(
12949            slashsplit("/"),
12950            vec!["".to_string()],
12951            "c:854-857 — trailing `/` after first iter returns ['']"
12952        );
12953        // Consecutive slashes collapse (c:852-853 inner while-loop).
12954        assert_eq!(
12955            slashsplit("//foo"),
12956            vec!["".to_string(), "foo".to_string()],
12957            "c:852-853 — consecutive `/` collapse, leading empty kept"
12958        );
12959        // Empty input still empty result.
12960        assert_eq!(
12961            slashsplit(""),
12962            Vec::<String>::new(),
12963            "c:842 — empty input → empty array"
12964        );
12965    }
12966
12967    /// `Src/utils.c:837` — relative paths (no leading `/`) start
12968    /// at the first non-slash; trailing `/` doesn't add a segment
12969    /// (c:854-857 returns before tail emit).
12970    #[test]
12971    fn slashsplit_relative_path_no_trailing_empty() {
12972        let _g = crate::test_util::global_state_lock();
12973        assert_eq!(
12974            slashsplit("a/b/"),
12975            vec!["a".to_string(), "b".to_string()],
12976            "c:854-857 — trailing `/` doesn't add segment"
12977        );
12978        // Mid-slash collapse in relative path.
12979        assert_eq!(
12980            slashsplit("a//b"),
12981            vec!["a".to_string(), "b".to_string()],
12982            "c:852-853 — mid `//` collapses"
12983        );
12984    }
12985
12986    /// `equalsplit("foo=bar")` returns Some(("foo","bar")). Used by
12987    /// `typeset NAME=val` parsing. Regression returning None on a
12988    /// well-formed assignment would silently break every export/typeset.
12989    #[test]
12990    fn equalsplit_returns_first_equals_split() {
12991        let _g = crate::test_util::global_state_lock();
12992        assert_eq!(
12993            equalsplit("foo=bar"),
12994            Some(("foo".to_string(), "bar".to_string()))
12995        );
12996        assert_eq!(
12997            equalsplit("a=b=c"),
12998            Some(("a".to_string(), "b=c".to_string())),
12999            "splits on FIRST `=` only"
13000        );
13001    }
13002
13003    /// `equalsplit("foo")` returns None — no `=` to split on. Catches
13004    /// a regression returning Some(("foo","")) which would let bare
13005    /// names install with empty values silently.
13006    #[test]
13007    fn equalsplit_no_equals_returns_none() {
13008        let _g = crate::test_util::global_state_lock();
13009        assert_eq!(equalsplit("foo"), None);
13010        assert_eq!(equalsplit(""), None);
13011    }
13012
13013    /// c:5106 — `ztrcmp` is the canonical zsh string compare. Same
13014    /// inputs MUST produce same Ordering (deterministic). Regression
13015    /// to a randomised hash-order would mis-sort `${(o)array}` output.
13016    #[test]
13017    fn ztrcmp_deterministic_and_lexicographic() {
13018        let _g = crate::test_util::global_state_lock();
13019        assert_eq!(ztrcmp("abc", "abc"), std::cmp::Ordering::Equal);
13020        assert_eq!(ztrcmp("abc", "abd"), std::cmp::Ordering::Less);
13021        assert_eq!(ztrcmp("abd", "abc"), std::cmp::Ordering::Greater);
13022        // Empty strings.
13023        assert_eq!(ztrcmp("", ""), std::cmp::Ordering::Equal);
13024        assert_eq!(ztrcmp("", "a"), std::cmp::Ordering::Less);
13025    }
13026
13027    /// c:5106 — shorter-as-prefix sorts before longer (like strcmp).
13028    /// Catches a regression where length-then-content would mis-sort.
13029    #[test]
13030    fn ztrcmp_shorter_prefix_is_less() {
13031        let _g = crate::test_util::global_state_lock();
13032        assert_eq!(ztrcmp("a", "ab"), std::cmp::Ordering::Less);
13033        assert_eq!(ztrcmp("foo", "foob"), std::cmp::Ordering::Less);
13034    }
13035
13036    /// `Src/utils.c:5117-5122` — Meta+X pair decodes to `X ^ 32`
13037    /// for comparison purposes. Pin: two strings differing only in
13038    /// the metafied byte's decoded value compare correctly.
13039    #[test]
13040    fn ztrcmp_decodes_meta_byte_for_comparison() {
13041        let _g = crate::test_util::global_state_lock();
13042        // META+0x21 ('!') = NUL? Let me use safer chars.
13043        // META+'A' (0x41) decodes to 'A' ^ 32 = 'a' (0x61).
13044        // So a "META a" pair represents the byte 'a' (0x61).
13045        // Compare "Ma" (M=Meta+a-byte-pair) vs "b" (>a).
13046        let meta_byte = Meta;
13047        let s1_bytes: Vec<u8> = vec![meta_byte, 0x41u8]; // decodes to 0x61 = 'a'
13048        let s2_bytes: Vec<u8> = vec![b'b'];
13049        let s1 = unsafe { std::str::from_utf8_unchecked(&s1_bytes) };
13050        let s2 = unsafe { std::str::from_utf8_unchecked(&s2_bytes) };
13051        // 'a' (0x61) < 'b' (0x62) → Less.
13052        assert_eq!(
13053            ztrcmp(s1, s2),
13054            std::cmp::Ordering::Less,
13055            "c:5117-5118 — Meta+0x41 decodes to 'a' which < 'b'"
13056        );
13057    }
13058
13059    /// `Src/utils.c:531-539` — `is_nicechar`. Returns true for chars
13060    /// needing escape-formatting:
13061    ///   - c:534 — printable ASCII (0x20-0x7e) → false.
13062    ///   - c:536 — high-bit byte (>= 0x80) → !PRINTEIGHTBIT.
13063    ///   - c:538 — DEL/\n/\t/<0x20 → true.
13064    /// Pin the three branches.
13065    #[test]
13066    fn is_nicechar_printable_ascii_is_not_nice() {
13067        let _g = crate::test_util::global_state_lock();
13068        // c:534 — letters, digits, punctuation in 0x20-0x7e all NOT nice.
13069        for c in "abcXYZ012!?@~".chars() {
13070            assert!(
13071                !is_nicechar(c),
13072                "c:534 — '{}' (ASCII printable) must NOT be nice",
13073                c
13074            );
13075        }
13076        // c:534 — space (0x20) is printable.
13077        assert!(!is_nicechar(' '));
13078    }
13079
13080    /// c:538 — control chars (DEL, newline, tab, <0x20) ARE nice.
13081    #[test]
13082    fn is_nicechar_control_chars_are_nice() {
13083        let _g = crate::test_util::global_state_lock();
13084        assert!(is_nicechar('\n'), "c:538 — newline is nice");
13085        assert!(is_nicechar('\t'), "c:538 — tab is nice");
13086        assert!(is_nicechar('\x7f'), "c:538 — DEL is nice");
13087        assert!(is_nicechar('\x00'), "c:538 — NUL is nice (<0x20)");
13088        assert!(is_nicechar('\x07'), "c:538 — BEL is nice (<0x20)");
13089        assert!(is_nicechar('\x1b'), "c:538 — ESC is nice (<0x20)");
13090        assert!(is_nicechar('\x1f'), "c:538 — boundary 0x1f is nice");
13091    }
13092
13093    /// `Src/utils.c:2528-2570` — `zstrtoul_underscore(s)`. Pins
13094    /// the base-prefix dispatch: `0x` → 16, `0b` → 2, leading `0`
13095    /// only when OCTALZEROES is set. Tests the default state.
13096    #[test]
13097    fn zstrtoul_underscore_recognises_hex_binary_decimal() {
13098        let _g = crate::test_util::global_state_lock();
13099        // c:2538 — hex.
13100        assert_eq!(zstrtoul_underscore("0xff"), Some(255));
13101        assert_eq!(zstrtoul_underscore("0XFF"), Some(255));
13102        // c:2540 — binary.
13103        assert_eq!(zstrtoul_underscore("0b1010"), Some(10));
13104        assert_eq!(zstrtoul_underscore("0B11"), Some(3));
13105        // c:2537 — pure decimal (no leading zero).
13106        assert_eq!(zstrtoul_underscore("12345"), Some(12345));
13107        // c:2543 — leading-zero with OCTALZEROES off (default) → decimal.
13108        if !isset(OCTALZEROES) {
13109            assert_eq!(
13110                zstrtoul_underscore("0777"),
13111                Some(777),
13112                "c:2543 — OCTALZEROES off: leading-0 parses as decimal"
13113            );
13114            assert_eq!(zstrtoul_underscore("010"), Some(10));
13115        }
13116    }
13117
13118    /// `Src/utils.c:2547-2548` — underscores are stripped before
13119    /// parsing. C: `if (*s == '_') continue;`. Used to support
13120    /// human-readable big numbers like `1_000_000`.
13121    #[test]
13122    fn zstrtoul_underscore_strips_underscores() {
13123        let _g = crate::test_util::global_state_lock();
13124        assert_eq!(
13125            zstrtoul_underscore("1_000_000"),
13126            Some(1_000_000),
13127            "c:2547-2548 — `_` stripped from numeric input"
13128        );
13129        assert_eq!(zstrtoul_underscore("0xff_ff"), Some(0xffff));
13130        assert_eq!(zstrtoul_underscore("0b1010_1010"), Some(0xaa));
13131    }
13132
13133    /// `Src/utils.c:2533-2534` — leading `+` sign is consumed.
13134    /// `zulong` is unsigned so `-` is not handled here (caller
13135    /// handles negation at `zstrtol_underscore`).
13136    #[test]
13137    fn zstrtoul_underscore_consumes_leading_plus() {
13138        let _g = crate::test_util::global_state_lock();
13139        assert_eq!(zstrtoul_underscore("+42"), Some(42));
13140        assert_eq!(zstrtoul_underscore("+0xff"), Some(255));
13141    }
13142
13143    /// `Src/utils.c:467` — `if (ZISPRINT(c)) goto done;`. Printable
13144    /// ASCII passes through nicechar_sel unchanged. Pin a single-char
13145    /// printable input → single-char output.
13146    #[test]
13147    fn nicechar_sel_passes_printable_ascii_unchanged() {
13148        let _g = crate::test_util::global_state_lock();
13149        for c in "aA0!~".chars() {
13150            assert_eq!(
13151                nicechar_sel(c, false),
13152                c.to_string(),
13153                "c:467 — printable ASCII '{}' passes through",
13154                c
13155            );
13156        }
13157    }
13158
13159    /// `Src/utils.c:487-492` — `\n` becomes `\n` (backslash-n) and
13160    /// `\t` becomes `\t`. Both two-char outputs. Pin the canonical
13161    /// escapes.
13162    #[test]
13163    fn nicechar_sel_escapes_newline_and_tab() {
13164        let _g = crate::test_util::global_state_lock();
13165        assert_eq!(
13166            nicechar_sel('\n', false),
13167            "\\n",
13168            "c:487-489 — newline escape"
13169        );
13170        assert_eq!(nicechar_sel('\t', false), "\\t", "c:490-492 — tab escape");
13171    }
13172
13173    /// `Src/utils.c:493-501` — control chars (<0x20 except \n/\t)
13174    /// get `^X` prefix in non-quotable mode and `\C-X` in quotable.
13175    /// c:500 — `c += 0x40` adds 0x40 to map 0x01 → 0x41 ('A').
13176    #[test]
13177    fn nicechar_sel_control_chars_use_caret_or_c_prefix() {
13178        let _g = crate::test_util::global_state_lock();
13179        // Non-quotable: ^A
13180        assert_eq!(
13181            nicechar_sel('\x01', false),
13182            "^A",
13183            "c:499-500 — \\x01 → ^A non-quotable"
13184        );
13185        // Quotable: \C-A
13186        assert_eq!(
13187            nicechar_sel('\x01', true),
13188            "\\C-A",
13189            "c:495-500 — \\x01 → \\C-A quotable"
13190        );
13191        // ^G (BEL = 0x07 → 0x47 = 'G')
13192        assert_eq!(nicechar_sel('\x07', false), "^G");
13193        // ^[ (ESC = 0x1b → 0x5b = '[')
13194        assert_eq!(nicechar_sel('\x1b', false), "^[");
13195    }
13196
13197    /// `Src/utils.c:479-486` — DEL (0x7f) renders as `^?` or `\C-?`.
13198    #[test]
13199    fn nicechar_sel_del_renders_as_caret_question() {
13200        let _g = crate::test_util::global_state_lock();
13201        assert_eq!(nicechar_sel('\x7f', false), "^?", "c:485-486 — DEL is `^?`");
13202        assert_eq!(
13203            nicechar_sel('\x7f', true),
13204            "\\C-?",
13205            "c:481-486 — DEL is `\\C-?` quotable"
13206        );
13207    }
13208
13209    /// `Src/utils.c:469-478` — high-bit bytes (>= 0x80) under default
13210    /// PRINTEIGHTBIT-off path: write `\M-` then mask to low ASCII.
13211    /// If the masked char is printable, output is `\M-<char>`. Pin
13212    /// the canonical 0xc1 → \M-A and 0xe1 → \M-a cases.
13213    #[test]
13214    fn nicechar_sel_highbit_uses_meta_prefix_when_printeightbit_off() {
13215        let _g = crate::test_util::global_state_lock();
13216        if isset(PRINTEIGHTBIT) {
13217            return; // Test only valid when PRINTEIGHTBIT off (default).
13218        }
13219        // 0xc1 = 'A' + 0x80 → \M-A
13220        let c = char::from_u32(0xc1).unwrap();
13221        assert_eq!(
13222            nicechar_sel(c, false),
13223            "\\M-A",
13224            "c:472-477 — high-bit 0xc1 → \\M-A under PRINTEIGHTBIT-off"
13225        );
13226        // 0xe1 = 'a' + 0x80 → \M-a
13227        let c = char::from_u32(0xe1).unwrap();
13228        assert_eq!(nicechar_sel(c, false), "\\M-a");
13229    }
13230
13231    /// `Src/utils.c:593-705` — `wcs_nicechar_sel`. Wide-char variant
13232    /// of `nicechar_sel`: control chars escape, printable wides
13233    /// emit raw UTF-8, large codepoints get `\u`/`\U` hex escape.
13234    /// Pin every branch. Previously delegated to `nicechar_sel`
13235    /// which byte-masked via `c & 0xff` — every UTF-8 codepoint
13236    /// emerged mangled.
13237    #[test]
13238    fn wcs_nicechar_sel_printable_wide_emits_utf8() {
13239        let _g = crate::test_util::global_state_lock();
13240        // c:644-678 — printable wide char emits raw UTF-8.
13241        assert_eq!(
13242            wcs_nicechar_sel('a', None, None, false),
13243            "a",
13244            "ASCII printable"
13245        );
13246        assert_eq!(
13247            wcs_nicechar_sel('é', None, None, false),
13248            "é",
13249            "Latin-1 printable"
13250        );
13251        assert_eq!(
13252            wcs_nicechar_sel('字', None, None, false),
13253            "字",
13254            "CJK printable"
13255        );
13256    }
13257
13258    /// c:625-630 — `\n` and `\t` escape (same as nicechar_sel for ASCII).
13259    #[test]
13260    fn wcs_nicechar_sel_escapes_newline_and_tab() {
13261        let _g = crate::test_util::global_state_lock();
13262        assert_eq!(wcs_nicechar_sel('\n', None, None, false), "\\n");
13263        assert_eq!(wcs_nicechar_sel('\t', None, None, false), "\\t");
13264    }
13265
13266    /// c:617-623 — DEL (0x7f) renders as `^?` (non-quotable) or
13267    /// `\C-?` (quotable). Same as the byte version.
13268    #[test]
13269    fn wcs_nicechar_sel_del_uses_caret_question() {
13270        let _g = crate::test_util::global_state_lock();
13271        assert_eq!(wcs_nicechar_sel('\x7f', None, None, false), "^?");
13272        assert_eq!(wcs_nicechar_sel('\x7f', None, None, true), "\\C-?");
13273    }
13274
13275    /// c:631-638 — control chars (0x01-0x1f, except \n/\t) emit
13276    /// `^X` or `\C-X` with the +0x40 offset.
13277    #[test]
13278    fn wcs_nicechar_sel_control_chars_use_caret_prefix() {
13279        let _g = crate::test_util::global_state_lock();
13280        assert_eq!(wcs_nicechar_sel('\x01', None, None, false), "^A");
13281        assert_eq!(wcs_nicechar_sel('\x07', None, None, false), "^G");
13282        assert_eq!(wcs_nicechar_sel('\x1b', None, None, false), "^[");
13283        assert_eq!(wcs_nicechar_sel('\x01', None, None, true), "\\C-A");
13284    }
13285
13286    /// `Src/utils.c:656-663` — large non-printable codepoints get
13287    /// hex escape: `\U%.8x` (>= 0x10000), `\u%.4x` (>= 0x100).
13288    /// Both lowercase per C's `%x`. Use a non-printable wide char
13289    /// to test (most BMP chars are printable; pick a control area
13290    /// like U+0085 NEXT LINE).
13291    #[test]
13292    fn wcs_nicechar_sel_large_nonprintable_uses_hex_escape() {
13293        let _g = crate::test_util::global_state_lock();
13294        // U+0085 NEL — control in C1 range, not iswprint.
13295        let nel = char::from_u32(0x85).unwrap();
13296        // Since 0x85 < 0x100, falls into nicechar_sel fallback.
13297        // After u9_iswprint says false, cv < 0x80 is false (cv=0x85),
13298        // PRINTEIGHTBIT off → enters !is_printable && cv < 0x80 branch?
13299        // Actually 0x85 >= 0x80 → !print_eightbit true → enters non-print branch.
13300        // 0x85 not 0x7f, not \n/\t, not <0x20. Falls past all if-elses to
13301        // post-branch logic → !is_printable, cv >= 0x100 false, cv >= 0x100 false
13302        // → falls to nicechar_sel fallback.
13303        let r = wcs_nicechar_sel(nel, None, None, false);
13304        assert!(!r.is_empty(), "must emit something for U+0085");
13305        // U+200B ZERO WIDTH SPACE — non-printable, 0x100-0xffff range.
13306        let zwsp = char::from_u32(0x200B).unwrap();
13307        let r = wcs_nicechar_sel(zwsp, None, None, false);
13308        // u9_iswprint(0x200B) returns true via unicode-width 0-width.
13309        // Per the impl: u9_iswprint true → emit raw. So r should be
13310        // the raw zwsp char.
13311        assert!(!r.is_empty());
13312    }
13313
13314    /// `Src/utils.c:709-714` — `wcs_nicechar(c)` delegates to
13315    /// `wcs_nicechar_sel(c, widthp, swidep, 0)`. Pin the parity.
13316    #[test]
13317    fn wcs_nicechar_matches_wcs_nicechar_sel_with_zero() {
13318        let _g = crate::test_util::global_state_lock();
13319        for c in ['a', '\n', '\t', '\x7f', '\x01', 'é', '字'] {
13320            assert_eq!(
13321                wcs_nicechar(c, None, None),
13322                wcs_nicechar_sel(c, None, None, false),
13323                "c:711 — `wcs_nicechar(c, NULL, NULL)` must equal `wcs_nicechar_sel(c, NULL, NULL, 0)` for {:?}",
13324                c
13325            );
13326        }
13327    }
13328
13329    /// `Src/utils.c:1989-2012` — `movefd(fd)`. Three contracts:
13330    /// 1. fd == -1 → returned as-is (no syscalls).
13331    /// 2. fd >= 10 → returned unchanged (already high).
13332    /// 3. fd < 10 → dupped with F_DUPFD >= 10, original closed
13333    ///    unconditionally (even on dup failure).
13334    #[test]
13335    #[cfg(unix)]
13336    fn movefd_returns_minus_one_unchanged() {
13337        let _g = crate::test_util::global_state_lock();
13338        // c:1992 — `if (fd != -1 && fd < 10)` skipped for fd=-1.
13339        assert_eq!(movefd(-1), -1, "fd=-1 must be returned unchanged");
13340    }
13341
13342    /// c:1992 — `fd >= 10` skips the dup-and-close path.
13343    #[test]
13344    #[cfg(unix)]
13345    fn movefd_returns_high_fd_unchanged() {
13346        let _g = crate::test_util::global_state_lock();
13347        // Use a known-open fd. /dev/null open with fd guaranteed to be
13348        // <10 because new fds get the lowest free slot — but we can
13349        // dup it to 10 directly to test the early-return path.
13350        let f = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13351        if f < 0 {
13352            return;
13353        } // Skip on unusual systems.
13354        let high = unsafe { libc::fcntl(f, libc::F_DUPFD, 20) };
13355        unsafe {
13356            libc::close(f);
13357        }
13358        if high < 0 {
13359            return;
13360        }
13361        // movefd(20) must return 20 unchanged (no dup, no close).
13362        let r = movefd(high);
13363        assert_eq!(r, high, "c:1992 — fd >= 10 returned unchanged");
13364        unsafe {
13365            libc::close(high);
13366        }
13367    }
13368
13369    /// `Src/utils.c:1968-1983` — `check_fd_table(fd)`. Grows the
13370    /// fdtable Vec to `fd+1` slots (filling new entries with
13371    /// FDT_UNUSED) and bumps `MAX_ZSH_FD` to `fd`. Pin: a small
13372    /// fd grows the table to `fd+1` length; MAX_ZSH_FD reflects it.
13373    /// fd ≤ cur_max early-returns without growing (c:1971-1972).
13374    #[test]
13375    fn check_fd_table_grows_to_fd_plus_one() {
13376        let _g = crate::test_util::global_state_lock();
13377        // Reset globals so the test is deterministic regardless
13378        // of prior ordering — other tests may have grown the
13379        // table or bumped MAX_ZSH_FD.
13380        MAX_ZSH_FD.store(-1, Ordering::Relaxed);
13381        {
13382            let mut g = fdtable_lock().lock().unwrap();
13383            g.clear();
13384        }
13385
13386        // c:1971-1972 — fd ≤ cur_max is the early-return path.
13387        // With cur_max=-1, fd=-1 hits `fd <= cur_max` → true.
13388        assert!(check_fd_table(-1));
13389        assert_eq!(
13390            MAX_ZSH_FD.load(Ordering::Relaxed),
13391            -1,
13392            "fd ≤ cur_max early-returns; MAX_ZSH_FD untouched"
13393        );
13394
13395        // c:1974-1981 — fd > cur_max grows fdtable to fd+1 slots.
13396        assert!(check_fd_table(7));
13397        assert_eq!(
13398            MAX_ZSH_FD.load(Ordering::Relaxed),
13399            7,
13400            "c:1982 — MAX_ZSH_FD := fd"
13401        );
13402        let len = {
13403            let g = fdtable_lock().lock().unwrap();
13404            g.len()
13405        };
13406        assert!(
13407            len >= 8,
13408            "c:1975-1979 — fdtable grew to ≥ fd+1 = 8 slots, got {}",
13409            len
13410        );
13411
13412        // Idempotence under fd ≤ cur_max.
13413        assert!(check_fd_table(3));
13414        assert_eq!(
13415            MAX_ZSH_FD.load(Ordering::Relaxed),
13416            7,
13417            "fd ≤ cur_max keeps MAX_ZSH_FD at 7"
13418        );
13419    }
13420
13421    /// `Src/utils.c:4364,4367` — `wcsitype(c, IWORD/ISEP)` reads
13422    /// from the `wordchars`/`ifs` GLOBALS (writable by
13423    /// `wordcharssetfn`/`ifssetfn`). Previously read from
13424    /// `std::env::var` — the libc process env — which never
13425    /// reflects runtime `WORDCHARS=…` shell-side assignments.
13426    /// Pin: set wordchars via `wordcharssetfn`, verify `wcsitype`
13427    /// returns true for chars in the new set.
13428    #[test]
13429    fn wcsitype_iword_reads_from_canonical_wordchars_global() {
13430        let _g = crate::test_util::global_state_lock();
13431        // Test only meaningful in MULTIBYTE mode for non-ASCII chars;
13432        // ASCII chars (< 0x80) route through TYPTAB directly.
13433        // Skip if MULTIBYTE off.
13434        if !isset(MULTIBYTE) {
13435            return;
13436        }
13437        // Save and set WORDCHARS to a single non-ASCII char.
13438        // C dispatches `pm->gsu.s->{get,set}fn(pm, val)`; mirror via
13439        // paramtab lookup.
13440        let saved = crate::ported::params::paramtab()
13441            .read()
13442            .ok()
13443            .and_then(|t| {
13444                t.get("WORDCHARS")
13445                    .map(|pm| crate::ported::params::wordcharsgetfn(pm))
13446            })
13447            .unwrap_or_default();
13448        // wordcharssetfn ignores its `_pm` arg and re-enters paramtab's
13449        // read lock via inittyptab (sibling of the IFS deadlock above).
13450        // Use a stack-local dummy pm instead of going through paramtab's
13451        // write lock so the test never holds two paramtab locks at once.
13452        let mut dummy = crate::ported::zsh_h::param::default();
13453        let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
13454            wordcharssetfn(dummy, val);
13455        };
13456        do_set(&mut dummy, "é".to_string());
13457        // 'é' is alphanumeric per Unicode → IWORD returns true via
13458        // is_alphanumeric short-circuit at c:4353. So we can't pin
13459        // the WORDCHARS-specific path with 'é'. Use a non-alnum char.
13460        do_set(&mut dummy, ":".to_string());
13461        // ':' is ASCII so wcsitype routes through TYPTAB (which now
13462        // has IWORD on ':' because wordcharssetfn called inittyptab).
13463        assert!(
13464            wcsitype(':', IWORD as u32),
13465            "c:4364 — wordchars membership through canonical global"
13466        );
13467        // Restore.
13468        do_set(&mut dummy, saved);
13469    }
13470
13471    /// `Src/utils.c:2090-2097` — `addmodulefd(fd, fdt)`. Stores the
13472    /// provided fdt in the fdtable slot for the given fd. Previously
13473    /// Rust port hardcoded FDT_MODULE and added CLOEXEC unconditionally.
13474    /// Pin: pass FDT_EXTERNAL, verify slot stores FDT_EXTERNAL (not
13475    /// FDT_MODULE).
13476    #[cfg(unix)]
13477    #[test]
13478    fn addmodulefd_respects_fdt_parameter() {
13479        let _g = crate::test_util::global_state_lock();
13480        // Open a real fd to use.
13481        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13482        if fd < 0 {
13483            return;
13484        }
13485        addmodulefd(fd, FDT_EXTERNAL);
13486        assert_eq!(
13487            fdtable_get(fd),
13488            FDT_EXTERNAL,
13489            "c:2095 — fdt parameter must be stored verbatim"
13490        );
13491        // Switch to FDT_MODULE.
13492        addmodulefd(fd, FDT_MODULE);
13493        assert_eq!(
13494            fdtable_get(fd),
13495            FDT_MODULE,
13496            "c:2095 — second call overwrites with new fdt"
13497        );
13498        // Cleanup.
13499        unsafe {
13500            libc::close(fd);
13501        }
13502    }
13503
13504    /// `Src/utils.c:2093` — `if (fd >= 0)`. Negative fd is silently
13505    /// ignored (no fdtable update). Pin the guard.
13506    #[test]
13507    fn addmodulefd_ignores_negative_fd() {
13508        let _g = crate::test_util::global_state_lock();
13509        // Should not panic, no side effect.
13510        addmodulefd(-1, FDT_MODULE);
13511        // Nothing to assert on side-effect-free path; just verify no panic.
13512    }
13513
13514    /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns -1 for
13515    /// non-lock fds, 0 for FDT_FLOCK/FDT_FLOCK_EXEC. Previously the
13516    /// Rust port always returned 0 — broke `zsystem flock -u <fd>`
13517    /// distinguishing "fd never flocked" from "successfully released".
13518    #[cfg(unix)]
13519    #[test]
13520    fn zcloselockfd_returns_minus_one_for_non_lock_fd() {
13521        let _g = crate::test_util::global_state_lock();
13522        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13523        if fd < 0 {
13524            return;
13525        }
13526        // No addlockfd call → fd is NOT in the flock table.
13527        let r = zcloselockfd(fd);
13528        assert_eq!(r, -1, "c:2160-2161 — non-lock fd must return -1, NOT 0");
13529        unsafe {
13530            libc::close(fd);
13531        }
13532    }
13533
13534    /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns 0 for an
13535    /// fd that was registered via `addlockfd`. Pin the end-to-end
13536    /// round-trip: addlockfd → zcloselockfd returns 0.
13537    #[cfg(unix)]
13538    #[test]
13539    fn zcloselockfd_returns_zero_for_flock_fd_then_closes() {
13540        let _g = crate::test_util::global_state_lock();
13541        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13542        if fd < 0 {
13543            return;
13544        }
13545        addlockfd(fd, true);
13546        let r = zcloselockfd(fd);
13547        assert_eq!(r, 0, "c:2162-2163 — FDT_FLOCK fd → zclose + return 0");
13548        // After zcloselockfd, the underlying close() has fired.
13549        // close() returning -1 with EBADF would confirm that, but
13550        // that's a libc side-effect; just verify the return value.
13551    }
13552
13553    /// `Src/utils.c:1982` — `check_fd_table` sets `max_zsh_fd = fd`
13554    /// when fd is new. `fdtable_set` inlines this to keep the guard
13555    /// in `zcloselockfd` (`if (fd > max_zsh_fd) return -1`) working
13556    /// against fds populated by `addmodulefd`/`addlockfd`. Pin: after
13557    /// `fdtable_set(fd, FDT_FLOCK)`, MAX_ZSH_FD >= fd.
13558    #[cfg(unix)]
13559    #[test]
13560    fn fdtable_set_bumps_max_zsh_fd() {
13561        let _g = crate::test_util::global_state_lock();
13562        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13563        if fd < 0 {
13564            return;
13565        }
13566        fdtable_set(fd, FDT_FLOCK);
13567        let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
13568        assert!(
13569            max_fd >= fd,
13570            "c:1982 — max_zsh_fd ({}) must be >= newly-set fd ({})",
13571            max_fd,
13572            fd
13573        );
13574        // Cleanup.
13575        fdtable_set(fd, FDT_UNUSED);
13576        unsafe {
13577            libc::close(fd);
13578        }
13579    }
13580
13581    /// `Src/utils.c:2111-2121` — `addlockfd(fd, cloexec)`. Selects
13582    /// between FDT_FLOCK (when cloexec=true, lock dies on exec) and
13583    /// FDT_FLOCK_EXEC (when cloexec=false, lock survives exec).
13584    /// Previously the Rust port did `fcntl(F_SETFD, CLOEXEC)` —
13585    /// totally wrong semantics. Pin the FDT slot.
13586    #[cfg(unix)]
13587    #[test]
13588    fn addlockfd_selects_flock_category_per_cloexec_flag() {
13589        let _g = crate::test_util::global_state_lock();
13590        let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13591        if fd < 0 {
13592            return;
13593        }
13594        // cloexec=true → FDT_FLOCK (lock dies on exec).
13595        addlockfd(fd, true);
13596        assert_eq!(
13597            fdtable_get(fd),
13598            FDT_FLOCK,
13599            "c:2117 — cloexec=true → FDT_FLOCK"
13600        );
13601        // cloexec=false → FDT_FLOCK_EXEC (lock survives exec).
13602        addlockfd(fd, false);
13603        assert_eq!(
13604            fdtable_get(fd),
13605            FDT_FLOCK_EXEC,
13606            "c:2119 — cloexec=false → FDT_FLOCK_EXEC"
13607        );
13608        // Cleanup.
13609        unsafe {
13610            libc::close(fd);
13611        }
13612    }
13613
13614    /// `Src/utils.c:1211` — `adduserdir` rejects paths `>= PATH_MAX`
13615    /// by removing the existing entry (treating as "can't use this
13616    /// value as a directory"). Pin with a path constructed to exceed
13617    /// PATH_MAX, verify no insertion happens.
13618    #[cfg(unix)]
13619    #[test]
13620    fn adduserdir_rejects_paths_at_or_above_path_max() {
13621        let _g = crate::test_util::global_state_lock();
13622        if !interact() {
13623            // c:1193 — non-interactive shells skip the table entirely.
13624            // Test inactive in non-interactive contexts.
13625            return;
13626        }
13627        // Ensure the entry doesn't exist beforehand.
13628        let name = "ZSHRS_TEST_PATHMAX_DIR";
13629        let _ = removenameddirnode(name);
13630        // Construct an oversized path.
13631        let mut over: String = "/".to_string();
13632        over.push_str(&"a".repeat(libc::PATH_MAX as usize));
13633        // adduserdir with the oversized path + AUTONAMEDIRS-off path
13634        // via always=true (so the AUTONAMEDIRS guard doesn't reject).
13635        adduserdir(name, &over, 0, true);
13636        // c:1211 — too-long → remove path triggered, no entry added.
13637        let tab = nameddirtab().lock().unwrap();
13638        assert!(
13639            !tab.contains_key(name),
13640            "c:1211 — strlen(t) >= PATH_MAX must NOT insert entry"
13641        );
13642    }
13643
13644    /// `Src/utils.c:760-786` — `pathprog`. Finds any regular file
13645    /// (not directory, no executable-bit check) in `$PATH`.
13646    /// Previously required `mode & 0o111 != 0` — silently missed
13647    /// non-executable autoload-function plaintext scripts. Pin
13648    /// using a known-existing file in /tmp.
13649    #[test]
13650    #[cfg(unix)]
13651    fn pathprog_finds_non_executable_files() {
13652        let _g = crate::test_util::global_state_lock();
13653        // Create a non-executable file in /tmp and add /tmp to PATH.
13654        let test_name = format!("zshrs_test_pathprog_{}", unsafe { libc::getpid() });
13655        let path = PathBuf::from("/tmp").join(&test_name);
13656        // Write content, NO executable bit.
13657        if fs::write(&path, b"plain content").is_err() {
13658            return; // /tmp may be unwritable on some systems.
13659        }
13660        // Verify it's not executable.
13661        let meta = fs::metadata(&path).unwrap();
13662        assert_eq!(
13663            meta.permissions().mode() & 0o111,
13664            0,
13665            "test setup: must be non-executable"
13666        );
13667        // Set PATH to /tmp.
13668        let saved_path = getsparam("PATH");
13669        assignsparam("PATH", "/tmp", 0);
13670        // pathprog should find the file.
13671        let r = pathprog(&test_name);
13672        // Cleanup.
13673        let _ = fs::remove_file(&path);
13674        if let Some(prev) = saved_path {
13675            assignsparam("PATH", &prev, 0);
13676        }
13677        assert_eq!(
13678            r,
13679            Some(path),
13680            "c:776 — pathprog finds non-executable files (only F_OK + !S_ISDIR)"
13681        );
13682    }
13683
13684    /// `Src/utils.c:776-778` — pathprog skips directories. Pin: a
13685    /// directory in $PATH with the queried name must NOT be returned.
13686    #[test]
13687    #[cfg(unix)]
13688    fn pathprog_skips_directories() {
13689        let _g = crate::test_util::global_state_lock();
13690        let test_name = format!("zshrs_test_pathprog_dir_{}", unsafe { libc::getpid() });
13691        let path = PathBuf::from("/tmp").join(&test_name);
13692        if fs::create_dir(&path).is_err() {
13693            return;
13694        }
13695        let saved_path = getsparam("PATH");
13696        assignsparam("PATH", "/tmp", 0);
13697        let r = pathprog(&test_name);
13698        // Cleanup.
13699        let _ = fs::remove_dir(&path);
13700        if let Some(prev) = saved_path {
13701            assignsparam("PATH", &prev, 0);
13702        }
13703        assert!(
13704            r.is_none(),
13705            "c:778 — pathprog must skip directories (!S_ISDIR)"
13706        );
13707    }
13708
13709    /// `Src/utils.c:4367` — `wcsitype(c, ISEP)` reads from canonical
13710    /// `ifs` global. Pin via `ifssetfn`. ASCII path routes through
13711    /// TYPTAB which gets ISEP bit from inittyptab's IFS walk
13712    /// (added earlier this session). End-to-end pin.
13713    #[test]
13714    fn wcsitype_isep_reads_from_canonical_ifs_global() {
13715        let _g = crate::test_util::global_state_lock();
13716        if !isset(MULTIBYTE) {
13717            return;
13718        }
13719        // C: `pm->gsu.s->{get,set}fn(pm, val)`. Mirror via paramtab.
13720        let saved = crate::ported::params::paramtab()
13721            .read()
13722            .ok()
13723            .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
13724            .unwrap_or_default();
13725        // ifssetfn ignores its `_pm` arg (the C `gsu.s->setfn` receives
13726        // the param but the IFS callback only touches the global `ifs`
13727        // store + calls inittyptab). Calling through paramtab's write
13728        // lock would deadlock because inittyptab() re-takes paramtab's
13729        // read lock for the IFS walk (utils.rs:5028) — pthread rwlock
13730        // forbids same-thread write→read upgrade. Pass a stack-local
13731        // dummy pm instead so we never hold the paramtab lock during
13732        // ifssetfn.
13733        let mut dummy = crate::ported::zsh_h::param::default();
13734        let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
13735            ifssetfn(dummy, val);
13736        };
13737        do_set(&mut dummy, ":".to_string());
13738        assert!(
13739            wcsitype(':', ISEP as u32),
13740            "c:4367 — IFS membership through canonical global"
13741        );
13742        // Restore.
13743        do_set(&mut dummy, saved);
13744    }
13745
13746    /// c:536 — high-bit byte (>= 0x80) is nice when PRINTEIGHTBIT is
13747    /// OFF (the default). Pin the default case; the alternate
13748    /// behavior under PRINTEIGHTBIT-on is harder to exercise from
13749    /// a unit test due to opt-state global mutation.
13750    #[test]
13751    fn is_nicechar_highbit_is_nice_when_printeightbit_off() {
13752        let _g = crate::test_util::global_state_lock();
13753        // Default state: PRINTEIGHTBIT is off → high-bit bytes are nice.
13754        // Use char with high-bit equivalent low byte.
13755        // 0xb5 (Meta+5 territory) — masked to 0xff → still 0xb5.
13756        let c = char::from_u32(0xb5).unwrap();
13757        // ZISPRINT(0xb5) is false (>0x7e). 0xb5 & 0x80 set → check PRINTEIGHTBIT.
13758        // Default PRINTEIGHTBIT off → returns true (nice).
13759        if !isset(PRINTEIGHTBIT) {
13760            assert!(
13761                is_nicechar(c),
13762                "c:536 — high-bit byte 0x{:x} must be nice when PRINTEIGHTBIT off",
13763                0xb5_u32
13764            );
13765        }
13766    }
13767
13768    /// `Src/utils.c:1969-1983` — `check_fd_table(fd)` grows the
13769    /// fdtable so it can index `fd` and bumps `max_zsh_fd`. The
13770    /// previous Rust port was a no-op shim that always returned true;
13771    /// real behavior is "fdtable.len() must be > fd" and
13772    /// "MAX_ZSH_FD >= fd" after the call.
13773    #[test]
13774    fn check_fd_table_grows_fdtable_and_bumps_max_zsh_fd() {
13775        let _g = crate::test_util::global_state_lock();
13776        // Snapshot prior state so we don't poison other tests.
13777        let saved_max = MAX_ZSH_FD.load(Ordering::Relaxed);
13778        // Pick a target fd well above the typical 0/1/2.
13779        let target = saved_max.max(50) + 7;
13780        check_fd_table(target);
13781        let new_max = MAX_ZSH_FD.load(Ordering::Relaxed);
13782        assert!(
13783            new_max >= target,
13784            "c:1982 — max_zsh_fd must be >= target after grow (got {})",
13785            new_max
13786        );
13787        // Calling again with a lower fd MUST NOT shrink max_zsh_fd
13788        // (c:1971-1972 early return).
13789        check_fd_table(target - 3);
13790        assert_eq!(
13791            MAX_ZSH_FD.load(Ordering::Relaxed),
13792            new_max,
13793            "c:1971 — fd <= max_zsh_fd path must not change max"
13794        );
13795        // Restore approximately — best we can do without exposing
13796        // fdtable internals; subsequent fdtable_set callers cope.
13797        MAX_ZSH_FD.store(saved_max, Ordering::Relaxed);
13798    }
13799
13800    /// `Src/utils.c:1971` — `if (fd <= max_zsh_fd) return;` early
13801    /// exit. Calling with a small fd (<= current max) must be a
13802    /// no-op.
13803    #[test]
13804    fn check_fd_table_small_fd_is_noop() {
13805        let _g = crate::test_util::global_state_lock();
13806        // Ensure max_zsh_fd is non-trivial.
13807        let _ = check_fd_table(100);
13808        let max_before = MAX_ZSH_FD.load(Ordering::Relaxed);
13809        check_fd_table(5);
13810        assert_eq!(
13811            MAX_ZSH_FD.load(Ordering::Relaxed),
13812            max_before,
13813            "c:1971 — small fd path must not touch max_zsh_fd"
13814        );
13815    }
13816
13817    /// `Src/utils.c:1969-1983` — defensive: negative fd shouldn't
13818    /// panic. C-side would have indexed past the start of `fdtable`
13819    /// (UB). Rust port should fail-soft.
13820    #[test]
13821    fn check_fd_table_negative_fd_does_not_panic() {
13822        let _g = crate::test_util::global_state_lock();
13823        // Should not panic; behavior beyond "no panic" is unspecified.
13824        let _ = check_fd_table(-1);
13825        let _ = check_fd_table(-100);
13826    }
13827
13828    /// `Src/zsh.h:3274` — under MULTIBYTE_SUPPORT, the C source
13829    /// defines `nicezputs(str, outs)` as a macro:
13830    ///   `(void)mb_niceformat((str), (outs), NULL, 0)`.
13831    /// So Rust `nicezputs(s)` must equal `mb_niceformat(s)` for
13832    /// every input. Pins the macro-equivalence after fixing the
13833    /// previous `chars().map(nicechar)` impl (which corrupted
13834    /// non-ASCII multibyte codepoints into `\M-X` mangle).
13835    #[test]
13836    fn nicezputs_matches_mb_niceformat_under_multibyte() {
13837        let _g = crate::test_util::global_state_lock();
13838        // C-equivalent pattern: write to a stream, compare to
13839        // mb_niceformat outstrp-form output. Inline rather than via a
13840        // Rust-only helper.
13841        for input in ["hello", "a\nb", "é", ""] {
13842            let mut nz_buf: Vec<u8> = Vec::new();
13843            let _ = nicezputs(input, &mut nz_buf);
13844            let nz = String::from_utf8(nz_buf).expect("utf8");
13845
13846            let mut mb_out: Option<String> = None;
13847            let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
13848            assert_eq!(
13849                nz,
13850                mb_out.unwrap_or_default(),
13851                "nicezputs and mb_niceformat must agree for {:?}",
13852                input
13853            );
13854        }
13855        // \n must produce literal `\n` (backslash + n).
13856        let mut nz_buf: Vec<u8> = Vec::new();
13857        let _ = nicezputs("a\nb", &mut nz_buf);
13858        let nz = String::from_utf8(nz_buf).expect("utf8");
13859        assert!(nz.contains("\\n"), "nicechar emits `\\n`, not raw 0x0a");
13860    }
13861
13862    /// `Src/utils.c:5530` — under MULTIBYTE_SUPPORT, `nicedup(s, heap)`
13863    /// body is `(void)mb_niceformat(s, NULL, &retstr, …); return retstr;`
13864    /// so the returned string MUST equal mb_niceformat output.
13865    #[test]
13866    fn nicedup_matches_mb_niceformat_under_multibyte() {
13867        let _g = crate::test_util::global_state_lock();
13868        for input in ["hello", "a\nb", "é"] {
13869            let nd = nicedup(input, 0);
13870            let mut mb_out: Option<String> = None;
13871            let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
13872            assert_eq!(
13873                nd,
13874                mb_out.unwrap_or_default(),
13875                "nicedup must equal mb_niceformat outstrp form for {:?}",
13876                input
13877            );
13878        }
13879        // nicedupstring delegates to nicedup(s, 1).
13880        assert_eq!(nicedupstring("hé\nllo"), nicedup("hé\nllo", 1));
13881    }
13882
13883    /// `Src/utils.c:734-744` — `zwcwidth(wc)` returns 1 when MULTIBYTE
13884    /// option is unset (c:738), regardless of the codepoint's actual
13885    /// display width. The previous Rust port skipped the option gate
13886    /// and always used the Unicode width table. Pin the option-gated
13887    /// behavior end-to-end: unset MULTIBYTE → width 1 for CJK; set →
13888    /// width 2 for CJK.
13889    #[test]
13890    fn zwcwidth_returns_1_when_multibyte_unset() {
13891        let _g = crate::test_util::global_state_lock();
13892        let saved = isset(MULTIBYTE);
13893        // c:738 — unset(MULTIBYTE) path returns 1 for everything.
13894        dosetopt(MULTIBYTE, 0, 1);
13895        assert_eq!(zwcwidth('a'), 1, "c:738 — ASCII width 1 under nomultibyte");
13896        assert_eq!(
13897            zwcwidth('字'),
13898            1,
13899            "c:738 — CJK collapses to 1 under nomultibyte (was 2 via Unicode-width)"
13900        );
13901        assert_eq!(
13902            zwcwidth('\u{200B}'),
13903            1,
13904            "c:738 — zero-width space → 1 under nomultibyte (was 0 via Unicode-width)"
13905        );
13906        // c:740-744 — MULTIBYTE on: Unicode-width table applies.
13907        dosetopt(MULTIBYTE, 1, 1);
13908        assert_eq!(zwcwidth('a'), 1, "ASCII width is 1");
13909        assert_eq!(zwcwidth('字'), 2, "c:740 — CJK width 2 under multibyte");
13910        // Restore prior state.
13911        dosetopt(MULTIBYTE, if saved { 1 } else { 0 }, 1);
13912    }
13913
13914    /// `Src/utils.c:6478-6492` — `quotedzputs(s, NULL)` under
13915    /// MULTIBYTE_SUPPORT detects "needs nice-format" inputs via
13916    /// `is_mb_niceformat(s)` and emits `$'<nice-formatted body>'`
13917    /// (the dollar-quoted form so embedded `\n`/`\t`/escape sequences
13918    /// round-trip through `typeset -p`/`set`/`set -x`). The previous
13919    /// Rust port skipped this branch entirely and single-quoted such
13920    /// strings — POSIX `'…'` is *strong* (no escapes interpreted),
13921    /// breaking round-trip for control bytes.
13922    #[test]
13923    fn quotedzputs_uses_dollar_quotes_for_control_chars() {
13924        let _g = crate::test_util::global_state_lock();
13925        // Ensure typtab is initialised — hasspecial depends on ISPECIAL
13926        // bits which are populated by inittyptab. Without this the
13927        // SPECCHARS arm short-circuits to "no specials".
13928        inittyptab();
13929        // Empty stays `''`.
13930        assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
13931        // Plain alphanumeric: no specials, no controls → bare.
13932        assert_eq!(
13933            quotedzputs("hello"),
13934            "hello",
13935            "c:6511-6517 — no SPECCHARS member → return unchanged"
13936        );
13937        // Control char `\n` needs `$'…'` to round-trip.
13938        let r = quotedzputs("a\nb");
13939        assert!(
13940            r.starts_with("$'") && r.ends_with('\''),
13941            "c:6488 — control char must use $'…' form (got {:?})",
13942            r
13943        );
13944        // Tab and ESC also force the niceformat arm.
13945        let r = quotedzputs("\t");
13946        assert!(
13947            r.starts_with("$'"),
13948            "c:6488 — TAB forces $'…' arm (got {:?})",
13949            r
13950        );
13951        let r = quotedzputs("\u{1b}[31m");
13952        assert!(
13953            r.starts_with("$'"),
13954            "c:6488 — ESC sequence forces $'…' arm (got {:?})",
13955            r
13956        );
13957        // Single quote forces single-quote arm via SPECCHARS membership
13958        // (Src/zsh.h:228 — SPECCHARS includes `'`). Embedded `'`
13959        // rewrites to `'\''`.
13960        let r = quotedzputs("a'b");
13961        assert!(
13962            r.contains("'\\''"),
13963            "c:6573-6587 — embedded ' → '\\''  (got {:?})",
13964            r
13965        );
13966    }
13967
13968    /// `Src/utils.c:2923-2945` — `read_loop` returns the requested
13969    /// length on full read, or the partial count on EOF. Pin: writing
13970    /// a known buffer to a pipe and reading it back returns the same
13971    /// content + correct length. Drives the no-side-effect path that
13972    /// the diagnostic-message fix doesn't touch.
13973    #[test]
13974    #[cfg(unix)]
13975    fn read_loop_round_trips_pipe_bytes() {
13976        let _g = crate::test_util::global_state_lock();
13977        // Create a pipe; write 16 bytes; read them back.
13978        let mut fds: [libc::c_int; 2] = [0; 2];
13979        unsafe {
13980            assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
13981        }
13982        let payload = b"hello-world-1234";
13983        let written = unsafe {
13984            libc::write(
13985                fds[1],
13986                payload.as_ptr() as *const libc::c_void,
13987                payload.len(),
13988            )
13989        };
13990        assert_eq!(written, payload.len() as isize, "write all 16 bytes");
13991        unsafe {
13992            libc::close(fds[1]);
13993        }
13994        let mut buf = [0u8; 16];
13995        let got = read_loop(fds[0], &mut buf).expect("read_loop ok");
13996        assert_eq!(got, payload.len(), "c:2929 — read_loop returns full length");
13997        assert_eq!(
13998            &buf[..],
13999            &payload[..],
14000            "c:2940-2941 — buffer copied verbatim"
14001        );
14002        unsafe {
14003            libc::close(fds[0]);
14004        }
14005    }
14006
14007    /// `Src/utils.c:2949-2970` — `write_loop` returns the requested
14008    /// length when the kernel accepts all bytes (the common case for
14009    /// a pipe with room). Pin the no-side-effect happy path.
14010    #[test]
14011    #[cfg(unix)]
14012    fn write_loop_writes_all_bytes_to_pipe() {
14013        let _g = crate::test_util::global_state_lock();
14014        let mut fds: [libc::c_int; 2] = [0; 2];
14015        unsafe {
14016            assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
14017        }
14018        let payload = b"abcdef";
14019        let got = write_loop(fds[1], payload).expect("write_loop ok");
14020        assert_eq!(
14021            got,
14022            payload.len(),
14023            "c:2955-2956 — write_loop returns full length on accept"
14024        );
14025        // Read back to verify.
14026        unsafe {
14027            libc::close(fds[1]);
14028        }
14029        let mut buf = [0u8; 6];
14030        let _ = unsafe { libc::read(fds[0], buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
14031        assert_eq!(&buf[..], &payload[..]);
14032        unsafe {
14033            libc::close(fds[0]);
14034        }
14035    }
14036
14037    /// `Src/utils.c:2935-2936` — `read_loop` on a closed/invalid fd
14038    /// returns an io::Error (the C path returns `ret` and emits the
14039    /// `zwarn` to stderr). Pin the error propagation; the zwarn
14040    /// emission is a stderr side-effect tested only by inspecting
14041    /// log output (out of scope here).
14042    #[test]
14043    #[cfg(unix)]
14044    fn read_loop_returns_error_on_invalid_fd() {
14045        let _g = crate::test_util::global_state_lock();
14046        let mut buf = [0u8; 4];
14047        // fd 9999 is essentially guaranteed-not-open in a test
14048        // process; the canonical "bad fd" error path.
14049        let r = read_loop(9999, &mut buf);
14050        assert!(
14051            r.is_err(),
14052            "c:2935 — invalid fd → io::Error (and zwarn to stderr)"
14053        );
14054    }
14055
14056    /// `Src/utils.c:2972-2988` — `read1char(echo)` reads from SHTTY,
14057    /// not stdin. With SHTTY uninitialised (default -1 in test
14058    /// environment), the function MUST return -1 immediately rather
14059    /// than blocking on a stdin read. The previous Rust port read
14060    /// from stdin and returned None — which in some test runners
14061    /// would block waiting for input.
14062    #[test]
14063    #[cfg(unix)]
14064    fn read1char_returns_minus_one_when_shtty_unset() {
14065        let _g = crate::test_util::global_state_lock();
14066        // Test environment: SHTTY is -1 (no controlling tty bound
14067        // by the port). C-side would `read(-1, ...)` which fails;
14068        // Rust port should fail-fast with -1.
14069        let saved = SHTTY.load(Ordering::Relaxed);
14070        SHTTY.store(-1, Ordering::Relaxed);
14071        let got = read1char(0); // echo=0
14072        assert_eq!(got, -1, "c:2978 — SHTTY=-1 → read fails → return -1");
14073        // Restore.
14074        SHTTY.store(saved, Ordering::Relaxed);
14075    }
14076
14077    /// `Src/utils.c:1989-2012` — `movefd(fd)` dups fd to >= 10 (so it
14078    /// stays out of the user fd range 0..=9), closes the original,
14079    /// AND marks the new fd as `FDT_INTERNAL` in fdtable. Previous
14080    /// Rust port omitted the fdtable_set call — internal-fd tracking
14081    /// (`closeallelse`, forkexec) silently never saw zshrs-internal
14082    /// fds. Pin: after movefd, fdtable[new_fd] == FDT_INTERNAL.
14083    #[test]
14084    #[cfg(unix)]
14085    fn movefd_marks_fdtable_internal() {
14086        let _g = crate::test_util::global_state_lock();
14087        // Open /dev/null to get a small (< 10) fd...
14088        let dev_null = CString::new("/dev/null").unwrap();
14089        let fd = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14090        assert!(fd >= 0, "open /dev/null returned -1");
14091        let new_fd = movefd(fd);
14092        assert!(
14093            new_fd >= 10,
14094            "c:1992-1994 — movefd dups to fd >= 10 (got {})",
14095            new_fd
14096        );
14097        // c:2009 — fdtable[new_fd] := FDT_INTERNAL.
14098        let entry = fdtable_get(new_fd);
14099        assert_eq!(
14100            entry, FDT_INTERNAL,
14101            "c:2009 — movefd must mark new_fd as FDT_INTERNAL (got {})",
14102            entry
14103        );
14104        unsafe {
14105            libc::close(new_fd);
14106        }
14107    }
14108
14109    /// `Src/utils.c:1989` — `movefd(-1)` is a defensive no-op: the
14110    /// `fd != -1 && fd < 10` gate at c:1992 fails, the c:2007
14111    /// `if (fd != -1)` post-check skips, and -1 is returned
14112    /// unchanged. Pin so a refactor of the early-out doesn't
14113    /// accidentally call fcntl(-1, ...).
14114    #[test]
14115    #[cfg(unix)]
14116    fn movefd_minus_one_returns_minus_one() {
14117        let _g = crate::test_util::global_state_lock();
14118        assert_eq!(
14119            movefd(-1),
14120            -1,
14121            "c:1992 — movefd(-1) bypasses both gates and returns -1"
14122        );
14123    }
14124
14125    /// `Src/utils.c:2021-2068` — `redup(x, y)` after a successful
14126    /// `dup2(x, y)` must:
14127    ///   * Copy fdtable[x] to fdtable[y] (c:2054).
14128    ///   * Promote FDT_FLOCK/FDT_FLOCK_EXEC to FDT_INTERNAL on the
14129    ///     dup target (c:2055-2056) — the lock doesn't transfer.
14130    ///   * Then close x (c:2064).
14131    ///
14132    /// Previous Rust port skipped the fdtable updates entirely.
14133    /// Pin the ownership-transfer with two fds the test allocates.
14134    #[test]
14135    #[cfg(unix)]
14136    fn redup_copies_fdtable_ownership_to_target() {
14137        let _g = crate::test_util::global_state_lock();
14138        // Open two distinct fds — both will land in the fdtable.
14139        let dev_null = CString::new("/dev/null").unwrap();
14140        let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14141        let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14142        assert!(x >= 0 && y >= 0, "open /dev/null returned -1");
14143        assert_ne!(x, y);
14144
14145        // Mark x as FDT_INTERNAL so we can observe the copy to y.
14146        check_fd_table(x);
14147        check_fd_table(y);
14148        fdtable_set(x, FDT_INTERNAL);
14149        fdtable_set(y, FDT_UNUSED);
14150
14151        let ret = redup(x, y);
14152        assert_eq!(ret, y, "c:2067 — successful redup returns y");
14153        // c:2054 — fdtable[y] inherited from fdtable[x].
14154        assert_eq!(
14155            fdtable_get(y),
14156            FDT_INTERNAL,
14157            "c:2054 — fdtable[y] = fdtable[x] (FDT_INTERNAL)"
14158        );
14159        // x is closed by c:2064.
14160        unsafe {
14161            libc::close(y);
14162        }
14163    }
14164
14165    /// `Src/utils.c:872-908` — `xsymlinks(path)` resolves `.`/`..`
14166    /// AND follows ONE LEVEL of symlinks via `readlink(2)`. Pin the
14167    /// symlink-following behavior with a temp-dir-managed symlink
14168    /// (the actual bug-fix pin — previous Rust port just normalised
14169    /// `.`/`..` without ever calling readlink).
14170    #[test]
14171    #[cfg(unix)]
14172    fn xsymlinks_follows_one_level_of_symlinks() {
14173        let _g = crate::test_util::global_state_lock();
14174        let tmp = std::env::temp_dir();
14175        let target = tmp.join(format!("zshrs_xsymlinks_target_{}", std::process::id()));
14176        let link = tmp.join(format!("zshrs_xsymlinks_link_{}", std::process::id()));
14177        // Create target as a regular dir to symlink to.
14178        let _ = fs::create_dir(&target);
14179        let _ = fs::remove_file(&link);
14180        std::os::unix::fs::symlink(&target, &link).unwrap();
14181
14182        let got = xsymlinks(link.to_str().unwrap()).unwrap();
14183        assert_eq!(
14184            got,
14185            target.to_string_lossy(),
14186            "c:908 — xsymlinks must follow the symlink to its target"
14187        );
14188
14189        // Cleanup.
14190        let _ = fs::remove_file(&link);
14191        let _ = fs::remove_dir(&target);
14192    }
14193
14194    /// `Src/utils.c:881-882` — `.` components are skipped.
14195    /// `Src/utils.c:883-896` — `..` components walk back one
14196    /// `/`-segment of xbuf (unless xbuf is empty or `/`).
14197    ///
14198    /// Use a temp directory we create ourselves — we can't pin
14199    /// `/tmp/...` literally because the macOS sandbox symlinks
14200    /// `/tmp -> /private/tmp`, which `xsymlinks` correctly follows
14201    /// (proving the bug-fix works). Test with a directory under
14202    /// the env tempdir + a non-existent sub so readlink fails on
14203    /// every component and the test exercises ONLY the c:881-896
14204    /// `.` / `..` paths.
14205    #[test]
14206    #[cfg(unix)]
14207    fn xsymlinks_normalises_dot_and_dotdot() {
14208        let _g = crate::test_util::global_state_lock();
14209        let tmp = fs::canonicalize(std::env::temp_dir()).unwrap();
14210        let base_dir = tmp.join(format!("zshrs_xs_norm_{}", std::process::id()));
14211        let _ = fs::create_dir(&base_dir);
14212        // `<base>/.` should be `<base>` (c:881 — `.` skipped).
14213        let arg = format!("{}/./.", base_dir.display());
14214        let got = xsymlinks(&arg).unwrap();
14215        assert_eq!(
14216            got,
14217            base_dir.to_string_lossy(),
14218            "c:881 — `.` segments collapse"
14219        );
14220        // `<base>/foo/..` should be `<base>` (c:891-895 — `..` walks back).
14221        let arg = format!("{}/foo/..", base_dir.display());
14222        let got = xsymlinks(&arg).unwrap();
14223        assert_eq!(
14224            got,
14225            base_dir.to_string_lossy(),
14226            "c:891-895 — `..` walks back one segment"
14227        );
14228        let _ = fs::remove_dir(&base_dir);
14229    }
14230
14231    /// `Src/utils.c:2055-2056` — when fdtable[x] is FDT_FLOCK or
14232    /// FDT_FLOCK_EXEC, the dup'd fd y gets promoted to FDT_INTERNAL
14233    /// (the dup doesn't carry the flock). Pin the promotion.
14234    ///
14235    /// Note on fdtable_flocks: C's c:2062-2063 decrements the
14236    /// flock count in `redup` BEFORE `zclose(x)`, and zclose ALSO
14237    /// decrements when it sees fdtable[fd] == FDT_FLOCK (c:2135).
14238    /// So C double-decrements for an FDT_FLOCK source fd in redup —
14239    /// the C comment at c:2058-2061 calls this "isn't expected to
14240    /// happen" (FDT_FLOCK fds aren't normally redup'd). We mirror
14241    /// C exactly: test asserts the count drops by 2 (1 → -1) to
14242    /// pin the faithful-to-C behavior. Reporting this as a C bug
14243    /// upstream is the right path; the port preserves it verbatim.
14244    #[test]
14245    #[cfg(unix)]
14246    fn redup_promotes_flock_to_internal_on_target() {
14247        let _g = crate::test_util::global_state_lock();
14248        let dev_null = CString::new("/dev/null").unwrap();
14249        let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14250        let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14251        assert!(x >= 0 && y >= 0);
14252        assert_ne!(x, y);
14253
14254        check_fd_table(x);
14255        check_fd_table(y);
14256        // Mark x as FDT_FLOCK.
14257        fdtable_set(x, FDT_FLOCK);
14258        FDTABLE_FLOCKS.store(2, Ordering::SeqCst); // start at 2 so double-decrement lands at 0
14259
14260        let _ = redup(x, y);
14261
14262        // c:2055-2056 — promoted to FDT_INTERNAL on y, NOT carried.
14263        assert_eq!(
14264            fdtable_get(y),
14265            FDT_INTERNAL,
14266            "c:2055-2056 — FDT_FLOCK on x promotes to FDT_INTERNAL on y"
14267        );
14268        // c:2062-2063 + zclose c:2135 — double decrement.
14269        assert_eq!(
14270            FDTABLE_FLOCKS.load(Ordering::SeqCst),
14271            0,
14272            "c:2062-2063 + c:2135 — flock count double-decremented (faithful to C)"
14273        );
14274        unsafe {
14275            libc::close(y);
14276        }
14277    }
14278
14279    /// `Src/utils.c:5217-5240` — `zreaddir(dir, ignoredots)` exposes
14280    /// the dot-filter as a parameter and returns one entry per call.
14281    /// Two paths:
14282    /// * `ignoredots=1` (the common case at c:590/655/1653/2884) —
14283    ///   `.` and `..` are filtered out.
14284    /// * `ignoredots=0` (used at c:4648 for spelling correction) —
14285    ///   `.` and `..` are RETAINED as valid candidates.
14286    #[test]
14287    #[cfg(unix)]
14288    fn zreaddir_honors_ignoredots_flag() {
14289        let _g = crate::test_util::global_state_lock();
14290        let tmp = std::env::temp_dir();
14291        let base = tmp.join(format!("zshrs_zreaddir_{}", std::process::id()));
14292        let _ = fs::create_dir(&base);
14293        // Create one real entry to make the test non-trivial.
14294        fs::write(base.join("file"), "x").unwrap();
14295
14296        // c:5232 — ignoredots=1: skip `.` and `..`, keep `file`.
14297        let mut dir = fs::read_dir(&base).unwrap();
14298        let mut with_skip: Vec<String> = Vec::new();
14299        while let Some(n) = zreaddir(&mut dir, 1) {
14300            with_skip.push(n);
14301        }
14302        assert!(
14303            with_skip.contains(&"file".to_string()),
14304            "c:5232 — real entry survives ignoredots=1"
14305        );
14306        assert!(
14307            !with_skip.contains(&".".to_string()),
14308            "c:5232 — `.` filtered with ignoredots=1"
14309        );
14310        assert!(
14311            !with_skip.contains(&"..".to_string()),
14312            "c:5232 — `..` filtered with ignoredots=1"
14313        );
14314
14315        // c:4648-equivalent — ignoredots=0: KEEP `.` and `..`.
14316        // (libstd's fs::read_dir filters them before exposing on
14317        // macOS/Linux, so the without_skip set is functionally
14318        // identical here — pin only that the API path doesn't error.)
14319        let mut dir2 = fs::read_dir(&base).unwrap();
14320        let mut without_skip: Vec<String> = Vec::new();
14321        while let Some(n) = zreaddir(&mut dir2, 0) {
14322            without_skip.push(n);
14323        }
14324        assert!(
14325            without_skip.contains(&"file".to_string()),
14326            "ignoredots=0 still yields real entries"
14327        );
14328
14329        let _ = fs::remove_file(base.join("file"));
14330        let _ = fs::remove_dir(&base);
14331    }
14332
14333    /// `Src/utils.c:1075` — `get_username()` uses `ztrdup_metafy` on
14334    /// `pswd->pw_name`. Previous Rust port returned the raw pw_name
14335    /// verbatim — fine for ASCII usernames, broken for high-bit
14336    /// bytes (downstream paramtab consumers assume metafied entries).
14337    /// Pin: ASCII usernames round-trip identically through metafy,
14338    /// AND the result is a non-empty string for the current uid.
14339    /// `Src/utils.c:3445-3460` — ztrftime zsh-specific %K/%L/%f
14340    /// extensions return values WITHOUT leading zeros (vs the
14341    /// strftime %H/%I/%d which pad).
14342    #[test]
14343    #[cfg(unix)]
14344    fn ztrftime_zsh_extensions_no_leading_zero() {
14345        let _g = crate::test_util::global_state_lock();
14346        // Pick a known time: Jan 5 2024 09:07:42.123456789 UTC.
14347        // Use SystemTime + an offset since we want deterministic values
14348        // independent of TZ; we just verify that the format substitutes
14349        // the right NUMBER OF DIGITS for the leading-zero case.
14350        use std::time::Duration;
14351        // 2024-01-05 09:07:42 UTC = 1704445662
14352        let t = UNIX_EPOCH + Duration::new(1704445662, 123_456_789);
14353        // %H gives leading zero "09"; %K should give "9" (in some TZ
14354        // hour will be different but the digit-count rule still holds).
14355        let h_padded = ztrftime("%H", t, false);
14356        let k_unpadded = ztrftime("%K", t, false);
14357        // Both represent the same hour. If h_padded starts with `0`,
14358        // k_unpadded must be 1-char and not start with `0`.
14359        if h_padded.starts_with('0') && h_padded.len() == 2 {
14360            assert!(
14361                !k_unpadded.starts_with('0'),
14362                "c:3445 — %K must strip the leading 0 from %H={}, got %K={}",
14363                h_padded,
14364                k_unpadded
14365            );
14366            assert_eq!(
14367                k_unpadded.len(),
14368                1,
14369                "c:3445 — %K should be 1 digit when hour < 10"
14370            );
14371        }
14372        // %f for day-of-month: t is Jan 5 → in any reasonable TZ
14373        // day is between 4 and 6; format should be 1 digit when day < 10.
14374        let d_padded = ztrftime("%d", t, false);
14375        let f_unpadded = ztrftime("%f", t, false);
14376        if d_padded.starts_with('0') && d_padded.len() == 2 {
14377            assert!(!f_unpadded.starts_with('0'));
14378            assert_eq!(
14379                f_unpadded.len(),
14380                1,
14381                "c:3457 — %f should be 1 digit when day < 10"
14382            );
14383        }
14384        // %3. fractional seconds: input nsec is 123456789 → first 3
14385        // digits should be 123. Note zsh's syntax is `%N.` where N is
14386        // the digit count (c:3374-3384 + c:3409).
14387        let frac = ztrftime("%3.", t, false);
14388        assert_eq!(
14389            frac, "123",
14390            "c:3409-3438 — %3. must emit first 3 digits of nsec"
14391        );
14392        // %. with no digit prefix defaults to 3 digits per c:3409.
14393        let frac_default = ztrftime("%.", t, false);
14394        assert_eq!(frac_default, "123", "c:3409 — %. defaults to 3 digits");
14395    }
14396
14397    #[test]
14398    #[cfg(unix)]
14399    fn get_username_returns_metafied_non_empty_string() {
14400        let _g = crate::test_util::global_state_lock();
14401        let name = get_username();
14402        // CI environments may have a username from `whoami(1)`. In
14403        // weird sandbox-only setups it might be empty, but on every
14404        // standard build it should be set.
14405        if name.is_empty() {
14406            return;
14407        }
14408        // ASCII round-trip via metafy is identity. If the username
14409        // had high-bit bytes (the bug case), the output would be
14410        // Meta-escaped — both forms are non-empty so this pin
14411        // doesn't reject either, just ensures the fn returns
14412        // SOMETHING usable (not an empty string).
14413        assert!(
14414            !name.is_empty(),
14415            "c:1086 — getpwuid result must yield a non-empty username"
14416        );
14417        // Sanity: metafy(name) == name for ASCII input (every byte
14418        // is below Meta range). Confirms the c:1086 step preserves
14419        // ASCII paths byte-for-byte.
14420        if name.bytes().all(|b| b < 0x80) {
14421            assert_eq!(
14422                metafy(&name),
14423                name,
14424                "c:1086 — ASCII metafy is identity (so pin holds for ASCII users)"
14425            );
14426        }
14427    }
14428
14429    // ═══════════════════════════════════════════════════════════════════
14430    // Pure-utility tests — zstrtol/zstrtoul (numeric parse) and
14431    // getkeystring (key-escape decode). Each test pinned against either
14432    // C zsh semantics or a direct zsh shell invocation where applicable.
14433    // ═══════════════════════════════════════════════════════════════════
14434
14435    // ── zstrtol: numeric string → (value, unconsumed_tail) ──────────
14436    /// `zstrtol("42", 10)` → (42, "") — full consumption.
14437    #[test]
14438    fn zstrtol_decimal_full_consumption() {
14439        let (v, t) = zstrtol("42", 10);
14440        assert_eq!(v, 42);
14441        assert_eq!(t, "");
14442    }
14443
14444    /// `zstrtol("42abc", 10)` → (42, "abc") — stops at first non-digit.
14445    #[test]
14446    fn zstrtol_decimal_stops_at_non_digit() {
14447        let (v, t) = zstrtol("42abc", 10);
14448        assert_eq!(v, 42);
14449        assert_eq!(t, "abc");
14450    }
14451
14452    /// `zstrtol("-7", 10)` → (-7, "") — sign handling.
14453    #[test]
14454    fn zstrtol_negative_decimal() {
14455        let (v, t) = zstrtol("-7", 10);
14456        assert_eq!(v, -7);
14457        assert_eq!(t, "");
14458    }
14459
14460    /// `zstrtol("+12", 10)` → (12, "") — explicit `+` sign.
14461    #[test]
14462    fn zstrtol_explicit_plus_sign_consumed() {
14463        let (v, t) = zstrtol("+12", 10);
14464        assert_eq!(v, 12);
14465        assert_eq!(t, "");
14466    }
14467
14468    /// `zstrtol("ff", 16)` → (255, "") — hex base.
14469    #[test]
14470    fn zstrtol_hex_base_16() {
14471        let (v, t) = zstrtol("ff", 16);
14472        assert_eq!(v, 255);
14473        assert_eq!(t, "");
14474    }
14475
14476    /// `zstrtol("FF", 16)` → (255, "") — case-insensitive hex.
14477    #[test]
14478    fn zstrtol_hex_uppercase() {
14479        let (v, t) = zstrtol("FF", 16);
14480        assert_eq!(v, 255);
14481    }
14482
14483    /// `zstrtol("1010", 2)` → (10, "") — binary base.
14484    #[test]
14485    fn zstrtol_binary_base_2() {
14486        let (v, t) = zstrtol("1010", 2);
14487        assert_eq!(v, 10);
14488        assert_eq!(t, "");
14489    }
14490
14491    /// `zstrtol("17", 8)` → (15, "") — octal base.
14492    #[test]
14493    fn zstrtol_octal_base_8() {
14494        let (v, t) = zstrtol("17", 8);
14495        assert_eq!(v, 15);
14496    }
14497
14498    /// `zstrtol("0x1A", 0)` → base-detect picks hex from `0x` prefix → 26.
14499    #[test]
14500    fn zstrtol_base_zero_detects_hex_prefix() {
14501        let (v, _) = zstrtol("0x1A", 0);
14502        assert_eq!(v, 26, "0x1A with base=0 → hex 26");
14503    }
14504
14505    /// `zstrtol("0b101", 0)` → base-detect picks binary → 5.
14506    #[test]
14507    fn zstrtol_base_zero_detects_binary_prefix() {
14508        let (v, _) = zstrtol("0b101", 0);
14509        assert_eq!(v, 5, "0b101 with base=0 → binary 5");
14510    }
14511
14512    /// `zstrtol("017", 0)` → base-detect: leading `0` → octal → 15.
14513    #[test]
14514    fn zstrtol_base_zero_leading_zero_means_octal() {
14515        let (v, _) = zstrtol("017", 0);
14516        assert_eq!(v, 15, "017 with base=0 → octal 15");
14517    }
14518
14519    /// `zstrtol("  42", 10)` → (42, "") — leading whitespace skipped.
14520    #[test]
14521    fn zstrtol_leading_whitespace_skipped() {
14522        let (v, _) = zstrtol("  42", 10);
14523        assert_eq!(v, 42);
14524    }
14525
14526    /// `zstrtol("0", 10)` → (0, "") — zero is valid input.
14527    #[test]
14528    fn zstrtol_zero_input() {
14529        let (v, t) = zstrtol("0", 10);
14530        assert_eq!(v, 0);
14531        assert_eq!(t, "");
14532    }
14533
14534    // ── zstrtol_underscore: digit-separator support ─────────────────
14535    /// `zstrtol_underscore("1_000_000", 10, true)` → (1_000_000, "")
14536    /// — underscores skipped during digit accumulation.
14537    #[test]
14538    fn zstrtol_underscore_separator_in_decimal() {
14539        let (v, _) = zstrtol_underscore("1_000_000", 10, true);
14540        assert_eq!(v, 1_000_000);
14541    }
14542
14543    /// Without underscore flag, `_` stops parsing.
14544    #[test]
14545    fn zstrtol_underscore_disabled_stops_at_underscore() {
14546        let (v, t) = zstrtol_underscore("123_456", 10, false);
14547        assert_eq!(v, 123);
14548        assert_eq!(t, "_456");
14549    }
14550
14551    // ── zstrtoul_underscore: unsigned variant ───────────────────────
14552    /// Parses a plain decimal unsigned.
14553    #[test]
14554    fn zstrtoul_underscore_basic_decimal() {
14555        let v = zstrtoul_underscore("12345");
14556        assert_eq!(v, Some(12345));
14557    }
14558
14559    /// Empty string → None (no number to parse).
14560    #[test]
14561    fn zstrtoul_underscore_empty_returns_none() {
14562        let v = zstrtoul_underscore("");
14563        assert_eq!(v, None);
14564    }
14565
14566    // ── getkeystring: shell-escape decode ───────────────────────────
14567    /// `\n` → newline, `\t` → tab, `\r` → CR.
14568    /// Anchor: `print -r -- $'\n\t\r'` produces the three bytes.
14569    #[test]
14570    fn getkeystring_decodes_common_escapes() {
14571        assert_eq!(getkeystring("\\n").0, "\n");
14572        assert_eq!(getkeystring("\\t").0, "\t");
14573        assert_eq!(getkeystring("\\r").0, "\r");
14574    }
14575
14576    /// `\\` → single backslash.
14577    #[test]
14578    fn getkeystring_double_backslash_yields_single() {
14579        assert_eq!(getkeystring("\\\\").0, "\\");
14580    }
14581
14582    /// `\xNN` hex escape → byte value.
14583    /// Anchor: `print -r -- $'\x41'` → "A" (0x41 = 65 = 'A').
14584    #[test]
14585    fn getkeystring_hex_escape_lowercase_x() {
14586        assert_eq!(getkeystring("\\x41").0, "A");
14587        assert_eq!(getkeystring("\\x7E").0, "~");
14588    }
14589
14590    /// `\u{NNNN}` Unicode escape → corresponding char.
14591    /// Anchor: `print -r -- $'é'` → "é".
14592    #[test]
14593    fn getkeystring_unicode_escape_u_four_digits() {
14594        assert_eq!(getkeystring("\\u00E9").0, "é");
14595    }
14596
14597    /// Plain text passes through unchanged.
14598    #[test]
14599    fn getkeystring_plain_text_passes_through() {
14600        assert_eq!(getkeystring("hello").0, "hello");
14601        assert_eq!(getkeystring("").0, "");
14602    }
14603
14604    /// Mixed plain + escapes → both handled.
14605    #[test]
14606    fn getkeystring_mixed_text_and_escapes() {
14607        assert_eq!(getkeystring("a\\nb").0, "a\nb");
14608        assert_eq!(
14609            getkeystring("line1\\nline2\\tindented").0,
14610            "line1\nline2\tindented"
14611        );
14612    }
14613
14614    // ── metafy / unmetafy round-trip on ASCII (identity) ────────────
14615    /// ASCII strings: metafy is identity (no meta-bytes to escape).
14616    #[test]
14617    fn metafy_then_unmetafy_ascii_roundtrips_to_input() {
14618        let s = "hello world";
14619        let m = metafy(s);
14620        assert_eq!(m, s);
14621        let mut bytes = m.into_bytes();
14622        let _len = unmetafy(&mut bytes);
14623        assert_eq!(String::from_utf8(bytes).unwrap(), "hello world");
14624    }
14625
14626    // ═══════════════════════════════════════════════════════════════════
14627    // quotestring — emit a quoted shell-safe form of the input string.
14628    // Each QT_* mode produces a different quoting style. Tests pin
14629    // each mode for both empty and non-empty input.
14630    // ═══════════════════════════════════════════════════════════════════
14631
14632    use crate::zsh_h::{
14633        QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_DOLLARS, QT_DOUBLE, QT_NONE,
14634        QT_SINGLE, QT_SINGLE_OPTIONAL,
14635    };
14636
14637    /// QT_NONE: no quoting; passes input through unchanged.
14638    #[test]
14639    fn quotestring_qt_none_passes_through_unchanged() {
14640        let _g = crate::test_util::global_state_lock();
14641        assert_eq!(quotestring("hello world", QT_NONE), "hello world");
14642        assert_eq!(quotestring("", QT_NONE), "");
14643        assert_eq!(quotestring("a*b?c", QT_NONE), "a*b?c");
14644    }
14645
14646    /// QT_NONE empty → empty.
14647    #[test]
14648    fn quotestring_qt_none_empty_is_empty() {
14649        let _g = crate::test_util::global_state_lock();
14650        assert_eq!(quotestring("", QT_NONE), "");
14651    }
14652
14653    /// QT_BACKSLASH on empty → "''" (single-quote pair).
14654    #[test]
14655    fn quotestring_qt_backslash_empty_yields_empty() {
14656        let _g = crate::test_util::global_state_lock();
14657        // c:6194 — shownull is 0 for plain QT_BACKSLASH, so empty → "".
14658        assert_eq!(quotestring("", QT_BACKSLASH), "");
14659    }
14660
14661    /// QT_BACKSLASH_SHOWNULL on empty → "''" too.
14662    #[test]
14663    fn quotestring_qt_backslash_shownull_empty_yields_empty_single_quotes() {
14664        let _g = crate::test_util::global_state_lock();
14665        assert_eq!(quotestring("", QT_BACKSLASH_SHOWNULL), "''");
14666    }
14667
14668    /// QT_SINGLE on empty → "''" (single-quote pair).
14669    #[test]
14670    fn quotestring_qt_single_empty_yields_empty_single_quotes() {
14671        let _g = crate::test_util::global_state_lock();
14672        assert_eq!(quotestring("", QT_SINGLE), "''");
14673    }
14674
14675    /// QT_SINGLE_OPTIONAL on empty → "''" too.
14676    #[test]
14677    fn quotestring_qt_single_optional_empty_yields_empty_single_quotes() {
14678        let _g = crate::test_util::global_state_lock();
14679        assert_eq!(quotestring("", QT_SINGLE_OPTIONAL), "''");
14680    }
14681
14682    /// QT_DOUBLE on empty → "" (empty double-quote pair).
14683    #[test]
14684    fn quotestring_qt_double_empty_yields_empty_double_quotes() {
14685        let _g = crate::test_util::global_state_lock();
14686        assert_eq!(quotestring("", QT_DOUBLE), "\"\"");
14687    }
14688
14689    /// QT_DOLLARS on empty → "$''".
14690    #[test]
14691    fn quotestring_qt_dollars_empty_yields_dollar_quote_pair() {
14692        let _g = crate::test_util::global_state_lock();
14693        assert_eq!(quotestring("", QT_DOLLARS), "$''");
14694    }
14695
14696    /// QT_BACKSLASH_PATTERN escapes only pattern meta-chars.
14697    /// Input "a*b?c[d]" → "a\\*b\\?c\\[d\\]".
14698    #[test]
14699    fn quotestring_qt_backslash_pattern_escapes_glob_metas() {
14700        let _g = crate::test_util::global_state_lock();
14701        let r = quotestring("a*b?c[d]", QT_BACKSLASH_PATTERN);
14702        assert!(
14703            r.contains("\\*") && r.contains("\\?") && r.contains("\\[") && r.contains("\\]"),
14704            "all globs must be escaped; got {r:?}"
14705        );
14706    }
14707
14708    /// QT_BACKSLASH_PATTERN does NOT escape plain ASCII letters/digits.
14709    #[test]
14710    fn quotestring_qt_backslash_pattern_doesnt_escape_plain_chars() {
14711        let _g = crate::test_util::global_state_lock();
14712        let r = quotestring("plain", QT_BACKSLASH_PATTERN);
14713        assert_eq!(r, "plain", "plain text passes through");
14714    }
14715
14716    /// QT_BACKSLASH_PATTERN escapes `<`, `>`, `(`, `)`, `|`, `#`, `^`, `~`.
14717    #[test]
14718    fn quotestring_qt_backslash_pattern_escapes_all_meta_set() {
14719        let _g = crate::test_util::global_state_lock();
14720        for ch in ['<', '>', '(', ')', '|', '#', '^', '~'] {
14721            let s = format!("x{ch}y");
14722            let r = quotestring(&s, QT_BACKSLASH_PATTERN);
14723            assert!(
14724                r.contains(&format!("\\{ch}")),
14725                "{ch:?} must be escaped, got {r:?}"
14726            );
14727        }
14728    }
14729
14730    /// QT_SINGLE on simple input → "'simple'" (wrapped in single quotes).
14731    #[test]
14732    fn quotestring_qt_single_wraps_simple_input() {
14733        let _g = crate::test_util::global_state_lock();
14734        assert_eq!(quotestring("simple", QT_SINGLE), "'simple'");
14735    }
14736
14737    // ═══════════════════════════════════════════════════════════════════
14738    // metafy / unmetafy edge cases — bytes that NEED meta-encoding
14739    // (NUL, Meta byte 0x83, Nularg 0xa1, etc.). Pin that metafy escapes
14740    // them via the Meta + (byte ^ 32) scheme and unmetafy reverses it.
14741    // ═══════════════════════════════════════════════════════════════════
14742
14743    /// `metafy` is identity for plain ASCII (no meta bytes).
14744    #[test]
14745    fn metafy_ascii_is_identity_byte_for_byte() {
14746        let s = "abc 123 XYZ!@#";
14747        assert_eq!(metafy(s), s);
14748    }
14749
14750    /// Empty string round-trips.
14751    #[test]
14752    fn metafy_empty_string_returns_empty() {
14753        assert_eq!(metafy(""), "");
14754    }
14755
14756    /// metafy → unmetafy is round-trip identity for ASCII input.
14757    #[test]
14758    fn metafy_unmetafy_roundtrip_with_punctuation() {
14759        let s = "Hello, World! 123 +-=";
14760        let m = metafy(s);
14761        let mut bytes = m.into_bytes();
14762        let _ = unmetafy(&mut bytes);
14763        let result = String::from_utf8(bytes).expect("valid utf-8");
14764        assert_eq!(result, s);
14765    }
14766
14767    /// Multi-byte UTF-8 chars get meta-encoded then unmetafied. The
14768    /// round-trip MUST preserve the original bytes. Inline the byte-
14769    /// level metafy here — `metafy()`'s String return goes through
14770    /// `from_utf8_lossy` for non-UTF-8 outputs (correct for String
14771    /// consumers like the lexer or pattern compile, but breaks byte-
14772    /// round-trip because U+FFFD inserts EF BF BD where the
14773    /// original Meta-escaped bytes lived). The unmetafy step
14774    /// reverses the Meta-escape to recover the source UTF-8 verbatim.
14775    #[test]
14776    fn metafy_unmetafy_roundtrip_with_utf8_multibyte_anchored() {
14777        let s = "日本語";
14778        // Inline of metafy() byte path (no String materialisation).
14779        let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
14780        for &b in s.as_bytes() {
14781            if imeta_byte(b) {
14782                bytes.push(Meta);
14783                bytes.push(b ^ 32);
14784            } else {
14785                bytes.push(b);
14786            }
14787        }
14788        let _ = unmetafy(&mut bytes);
14789        let result = String::from_utf8(bytes).expect("valid utf-8");
14790        assert_eq!(result, s, "UTF-8 round-trip must preserve content");
14791    }
14792
14793    // ─── zsh-corpus pins for quotestring per quote_type ─────────────
14794
14795    /// `quotestring(QT_NONE)` is identity on any input.
14796    #[test]
14797    fn quotestring_corpus_qt_none_is_identity() {
14798        let s = "anything `goes` $here";
14799        assert_eq!(quotestring(s, QT_NONE), s);
14800    }
14801
14802    /// `quotestring(QT_SINGLE)` on plain word wraps in single quotes.
14803    #[test]
14804    fn quotestring_corpus_qt_single_wraps_plain_word() {
14805        let out = quotestring("hello", QT_SINGLE);
14806        assert!(
14807            out.starts_with('\'') && out.ends_with('\''),
14808            "single-quoted = wraps with ', got {out:?}"
14809        );
14810        assert!(out.contains("hello"), "content preserved");
14811    }
14812
14813    /// `quotestring(QT_DOUBLE)` on plain word wraps in double quotes.
14814    #[test]
14815    fn quotestring_corpus_qt_double_wraps_plain_word() {
14816        let out = quotestring("hello", QT_DOUBLE);
14817        assert!(
14818            out.starts_with('"') && out.ends_with('"'),
14819            "double-quoted = wraps with \", got {out:?}"
14820        );
14821    }
14822
14823    /// `quotestring(QT_SINGLE)` on string with apostrophe escapes the
14824    /// apostrophe by closing+escape+reopen: `it's` → `'it'\''s'`.
14825    #[test]
14826    fn quotestring_corpus_qt_single_escapes_apostrophe() {
14827        let out = quotestring("it's", QT_SINGLE);
14828        assert!(
14829            out.contains("\\'") || out.contains("'\\''"),
14830            "apostrophe gets escaped in single-quote form, got {out:?}",
14831        );
14832    }
14833
14834    /// `quotestring(QT_BACKSLASH)` escapes shell special chars.
14835    /// Pattern chars like `*`, `?`, `$`, `(`, `)` should be backslashed.
14836    #[test]
14837    fn quotestring_corpus_qt_backslash_escapes_glob_chars() {
14838        let out = quotestring("a*b?c", QT_BACKSLASH);
14839        assert!(out.contains("\\*"), "* gets backslashed, got {out:?}");
14840        assert!(out.contains("\\?"), "? gets backslashed, got {out:?}");
14841    }
14842
14843    /// `quotestring("", QT_DOUBLE)` returns `""` literal.
14844    #[test]
14845    fn quotestring_corpus_qt_double_empty_yields_double_quotes() {
14846        let out = quotestring("", QT_DOUBLE);
14847        assert_eq!(out, "\"\"", "empty double-quoted = \"\"");
14848    }
14849
14850    /// `quotestring("", QT_SINGLE)` returns `''` literal.
14851    #[test]
14852    fn quotestring_corpus_qt_single_empty_yields_single_quotes() {
14853        let out = quotestring("", QT_SINGLE);
14854        assert_eq!(out, "''", "empty single-quoted = ''");
14855    }
14856
14857    /// `quotestring(QT_BACKSLASH)` on plain alphanumeric is identity.
14858    /// "abc123" should round-trip with no backslashes added.
14859    #[test]
14860    fn quotestring_corpus_qt_backslash_plain_word_unchanged() {
14861        let out = quotestring("abc123", QT_BACKSLASH);
14862        assert_eq!(
14863            out, "abc123",
14864            "plain alphanumeric needs no escape, got {out:?}"
14865        );
14866    }
14867
14868    /// `ztrlen("hello")` = 5 (no meta chars).
14869    #[test]
14870    fn ztrlen_corpus_plain_ascii_byte_count() {
14871        assert_eq!(ztrlen("hello"), 5);
14872    }
14873
14874    /// `ztrlen("")` = 0.
14875    #[test]
14876    fn ztrlen_corpus_empty_is_zero() {
14877        assert_eq!(ztrlen(""), 0);
14878    }
14879
14880    // ═══════════════════════════════════════════════════════════════════
14881    // itype_end C-parity tests — pin `utils.c:4395-4495` per-itype-flag
14882    // behavior. One assertion per flag/path so a regression in the
14883    // char-class walk points at the exact branch that drifted.
14884    //
14885    // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"]
14886    // so the bug stays surfaced but CI doesn't fail until the fix.
14887    // ═══════════════════════════════════════════════════════════════════
14888
14889    /// `itype_end("abc123", IIDENT, false)` walks the full identifier.
14890    /// C utils.c:4395 — IIDENT matches letters/digits/`_` via TYPTAB.
14891    /// Requires `inittyptab()` for TYPTAB population — call at entry.
14892    #[test]
14893    fn itype_end_ident_walks_full_identifier() {
14894        let _g = crate::test_util::global_state_lock();
14895        inittyptab();
14896        use crate::ported::ztype_h::IIDENT;
14897        assert_eq!(itype_end("abc123", IIDENT as u32, false), 6);
14898    }
14899
14900    /// `itype_end("abc-def", IIDENT, false)` stops at `-` (non-IIDENT).
14901    #[test]
14902    fn itype_end_ident_stops_at_dash() {
14903        let _g = crate::test_util::global_state_lock();
14904        inittyptab();
14905        use crate::ported::ztype_h::IIDENT;
14906        assert_eq!(itype_end("abc-def", IIDENT as u32, false), 3);
14907    }
14908
14909    /// `itype_end("a", IIDENT, true)` with `once=true` stops after 1.
14910    #[test]
14911    fn itype_end_once_stops_after_first_match() {
14912        let _g = crate::test_util::global_state_lock();
14913        inittyptab();
14914        use crate::ported::ztype_h::IIDENT;
14915        assert_eq!(itype_end("abc", IIDENT as u32, true), 1);
14916    }
14917
14918    /// `itype_end("", IIDENT, false)` on empty string returns 0.
14919    #[test]
14920    fn itype_end_empty_string_returns_zero() {
14921        let _g = crate::test_util::global_state_lock();
14922        inittyptab();
14923        use crate::ported::ztype_h::IIDENT;
14924        assert_eq!(itype_end("", IIDENT as u32, false), 0);
14925    }
14926
14927    /// `itype_end(":", IIDENT, false)` on non-IIDENT first char = 0.
14928    #[test]
14929    fn itype_end_non_ident_first_char_returns_zero() {
14930        let _g = crate::test_util::global_state_lock();
14931        inittyptab();
14932        use crate::ported::ztype_h::IIDENT;
14933        assert_eq!(itype_end(":foo", IIDENT as u32, false), 0);
14934    }
14935
14936    /// `itype_end("ns.foo", INAMESPC, false)` — C utils.c:4399-4413
14937    /// INAMESPC special-cases ksh93 namespace `.` separators. With
14938    /// non-POSIXIDENTIFIERS / non-KSH emulation, dotted names should
14939    /// walk through the `.` to consume `ns.foo` as a single name.
14940    /// The Rust port may stop at `.` since dotted-namespace support
14941    /// requires POSIXIDENTIFIERS check + ksh emulation gating that
14942    /// isn't fully wired.
14943    #[test]
14944    fn itype_end_inamespc_walks_through_ksh93_dot() {
14945        let _g = crate::test_util::global_state_lock();
14946        inittyptab();
14947        use crate::ported::ztype_h::INAMESPC;
14948        // Expected: full "ns.foo" = 6 bytes walked.
14949        assert_eq!(itype_end("ns.foo", INAMESPC, false), 6);
14950    }
14951
14952    /// `itype_end("a b c", ISEP, false)` from a space char.
14953    /// ISEP matches IFS chars. Default IFS includes ` \t\n`.
14954    /// Walk should consume only the leading separator(s).
14955    #[test]
14956    fn itype_end_isep_walks_separator_run() {
14957        let _g = crate::test_util::global_state_lock();
14958        inittyptab();
14959        use crate::ported::ztype_h::ISEP;
14960        // " \t " then 'a' — 3 sep bytes.
14961        assert_eq!(itype_end(" \t a", ISEP as u32, false), 3);
14962    }
14963
14964    // ═══════════════════════════════════════════════════════════════════
14965    // unmetafy C-parity tests — pin `utils.c:4954-4983`. Meta-byte
14966    // collapse (0x83 + (b^0x20) → b) must be in-place + return new len.
14967    // ═══════════════════════════════════════════════════════════════════
14968
14969    /// `unmetafy("abc")` is a no-op for non-metafied input.
14970    #[test]
14971    fn unmetafy_pure_ascii_unchanged() {
14972        let _g = crate::test_util::global_state_lock();
14973        let mut v = b"abc".to_vec();
14974        let n = unmetafy(&mut v);
14975        assert_eq!(n, 3);
14976        assert_eq!(v, b"abc");
14977    }
14978
14979    /// `unmetafy("a\x83a")` → "a\x01" because Meta(0x83) + next ^ 0x20
14980    /// = 'a' ^ 0x20 = 0x41 ^ 0x20 = 0x61... wait, 'a'=0x61, 0x61 ^ 0x20
14981    /// = 0x41 = 'A'. So unmetafy("a\x83a") → "aA" (2 bytes).
14982    #[test]
14983    fn unmetafy_meta_pair_collapses_to_xor_byte() {
14984        let _g = crate::test_util::global_state_lock();
14985        // 'a' + Meta(0x83) + 'a' → 'a' + ('a' ^ 0x20) = 'a' + 'A' = "aA"
14986        let mut v: Vec<u8> = vec![b'a', 0x83, b'a'];
14987        let n = unmetafy(&mut v);
14988        assert_eq!(n, 2);
14989        assert_eq!(v, vec![b'a', b'A']);
14990    }
14991
14992    /// `unmetafy("")` returns 0 with empty buffer.
14993    #[test]
14994    fn unmetafy_empty_returns_zero() {
14995        let _g = crate::test_util::global_state_lock();
14996        let mut v: Vec<u8> = Vec::new();
14997        let n = unmetafy(&mut v);
14998        assert_eq!(n, 0);
14999    }
15000
15001    /// `unmetafy` with trailing lone Meta byte. C `unmetafy` checks
15002    /// `p < s.len()` before reading the byte after Meta — trailing
15003    /// Meta at EOF is preserved as a single literal Meta byte.
15004    #[test]
15005    fn unmetafy_trailing_lone_meta_preserved() {
15006        let _g = crate::test_util::global_state_lock();
15007        let mut v: Vec<u8> = vec![b'a', 0x83];
15008        let n = unmetafy(&mut v);
15009        // Rust port copies the Meta byte to t, then loop exits with
15010        // p == s.len() → no second-byte read. Final = "a\x83" (2).
15011        assert_eq!(n, 2);
15012        assert_eq!(v, vec![b'a', 0x83]);
15013    }
15014
15015    // ═══════════════════════════════════════════════════════════════════
15016    // nicechar / wcs_nicechar C-parity tests — pin display-form
15017    // conversion. Required by stradd, prompt expansion, completion etc.
15018    // ═══════════════════════════════════════════════════════════════════
15019
15020    /// `nicechar('a')` returns the printable char unchanged.
15021    #[test]
15022    fn nicechar_printable_ascii_passes_through() {
15023        let _g = crate::test_util::global_state_lock();
15024        assert_eq!(nicechar('a'), "a");
15025    }
15026
15027    /// `nicechar('\n')` returns `\n` escape sequence per C `nicechar`
15028    /// (utils.c:464+ — control chars get `^M` style or `\n` escape).
15029    /// zsh emits `\n` as literal `\n` (two chars) for non-quotable
15030    /// nicechar; the `nicechar_sel(c, false)` form is non-quotable.
15031    #[test]
15032    fn nicechar_newline_emits_escape() {
15033        let _g = crate::test_util::global_state_lock();
15034        let out = nicechar('\n');
15035        assert!(
15036            out == "\\n" || out == "\n",
15037            "newline should be escaped or literal; got {out:?}"
15038        );
15039    }
15040
15041    /// `wcs_nicechar('A', None, None)` returns "A" for printable wide
15042    /// chars per utils.c:689.
15043    #[test]
15044    fn wcs_nicechar_printable_ascii_passes_through() {
15045        let _g = crate::test_util::global_state_lock();
15046        assert_eq!(wcs_nicechar('A', None, None), "A");
15047    }
15048
15049    // ═══════════════════════════════════════════════════════════════════
15050    // Additional C-parity tests for Src/utils.c nicechar dispatch.
15051    // ═══════════════════════════════════════════════════════════════════
15052
15053    /// c:462 — nicechar for printable ASCII passes through.
15054    #[test]
15055    fn nicechar_printable_ascii_passes_through_pin() {
15056        let _g = crate::test_util::global_state_lock();
15057        assert_eq!(nicechar('a'), "a");
15058        assert_eq!(nicechar('Z'), "Z");
15059        assert_eq!(nicechar('5'), "5");
15060        assert_eq!(nicechar(' '), " ");
15061    }
15062
15063    /// c:487 — nicechar('\\n') returns '\\n' (backslash + n).
15064    #[test]
15065    fn nicechar_newline_returns_backslash_n() {
15066        let _g = crate::test_util::global_state_lock();
15067        assert_eq!(nicechar('\n'), "\\n");
15068    }
15069
15070    /// c:490 — nicechar('\\t') returns '\\t' (backslash + t).
15071    #[test]
15072    fn nicechar_tab_returns_backslash_t() {
15073        let _g = crate::test_util::global_state_lock();
15074        assert_eq!(nicechar('\t'), "\\t");
15075    }
15076
15077    /// c:479 — nicechar(0x7f) returns '^?' (DEL → ^?).
15078    #[test]
15079    fn nicechar_del_returns_caret_question() {
15080        let _g = crate::test_util::global_state_lock();
15081        assert_eq!(nicechar('\x7f'), "^?");
15082    }
15083
15084    /// c:493 — nicechar(0x01) returns '^A' (Ctrl-A).
15085    #[test]
15086    fn nicechar_ctrl_a_returns_caret_A() {
15087        let _g = crate::test_util::global_state_lock();
15088        assert_eq!(nicechar('\x01'), "^A");
15089    }
15090
15091    /// c:493 — nicechar(0x1b) returns '^[' (ESC).
15092    #[test]
15093    fn nicechar_esc_returns_caret_bracket() {
15094        let _g = crate::test_util::global_state_lock();
15095        assert_eq!(nicechar('\x1b'), "^[");
15096    }
15097
15098    /// c:462 — nicechar_sel quotable=true uses '\\C-' instead of '^'
15099    /// for control chars.
15100    #[test]
15101    fn nicechar_sel_quotable_uses_backslash_C() {
15102        let _g = crate::test_util::global_state_lock();
15103        let r = nicechar_sel('\x01', true);
15104        assert!(
15105            r.contains("\\C-") || r.contains("^"),
15106            "quotable should emit \\C- form, got {:?}",
15107            r
15108        );
15109    }
15110
15111    /// c:531 — is_nicechar('a') returns FALSE: printable chars don't
15112    /// NEED nice (escape) representation, they pass through verbatim.
15113    /// Per the C comment in utils.c:531: "Return whether the char needs
15114    /// nice representation".
15115    #[test]
15116    fn is_nicechar_printable_returns_false() {
15117        let _g = crate::test_util::global_state_lock();
15118        assert!(!is_nicechar('a'), "printable doesn't NEED nice repr");
15119        assert!(!is_nicechar('Z'));
15120        assert!(!is_nicechar('5'));
15121    }
15122
15123    /// c:531 — is_nicechar for any byte 0..127 is safe (no panic).
15124    #[test]
15125    fn is_nicechar_all_ascii_safe() {
15126        let _g = crate::test_util::global_state_lock();
15127        for c in 0u8..=127 {
15128            let _ = is_nicechar(c as char); // no panic
15129        }
15130    }
15131
15132    /// c:462 — nicechar deterministic.
15133    #[test]
15134    fn nicechar_is_deterministic() {
15135        let _g = crate::test_util::global_state_lock();
15136        for c in ['a', '\n', '\t', '\x7f', '\x01', '\x1b'] {
15137            let first = nicechar(c);
15138            for _ in 0..5 {
15139                assert_eq!(nicechar(c), first, "{:?} must be pure", c);
15140            }
15141        }
15142    }
15143
15144    /// c:706 — `pathprog("")` returns None (empty path invalid).
15145    #[test]
15146    fn pathprog_empty_returns_none() {
15147        let _g = crate::test_util::global_state_lock();
15148        assert!(pathprog("").is_none());
15149    }
15150
15151    /// c:776 — `pathprog("/nonexistent/zshrs_xyz")` returns None.
15152    #[test]
15153    fn pathprog_nonexistent_returns_none() {
15154        let _g = crate::test_util::global_state_lock();
15155        let r = pathprog("/__never_exists_zshrs_pathprog_xyz");
15156        assert!(r.is_none());
15157    }
15158
15159    // ═══════════════════════════════════════════════════════════════════
15160    // Additional C-parity tests for Src/utils.c
15161    // c:99 set_widearray / c:447 nicechar_sel / c:521 is_nicechar /
15162    // c:742 zwcwidth / c:836 findpwd / c:928 slashsplit /
15163    // c:1258 get_username / c:1426 getnameddir / c:1475 dircmp /
15164    // c:1576 callhookfunc
15165    // ═══════════════════════════════════════════════════════════════════
15166
15167    /// c:99 — `set_widearray("")` empty returns empty Vec.
15168    #[test]
15169    fn set_widearray_empty_returns_empty() {
15170        let _g = crate::test_util::global_state_lock();
15171        assert!(set_widearray("").is_empty());
15172    }
15173
15174    /// c:99 — `set_widearray` returns Vec<char>.
15175    #[test]
15176    fn set_widearray_returns_vec_char_type() {
15177        let _g = crate::test_util::global_state_lock();
15178        let _: Vec<char> = set_widearray("a");
15179    }
15180
15181    /// c:447 — `nicechar_sel('a', false)` returns "a" (printable pass-through).
15182    #[test]
15183    fn nicechar_sel_ascii_letter_passthrough() {
15184        assert_eq!(nicechar_sel('a', false), "a", "printable letter as-is");
15185    }
15186
15187    /// c:447 — `nicechar_sel` is pure.
15188    #[test]
15189    fn nicechar_sel_is_pure() {
15190        for c in ['a', '\t', '\n', '\x1b', '\x7f', '日'] {
15191            let first = nicechar_sel(c, false);
15192            for _ in 0..3 {
15193                assert_eq!(
15194                    nicechar_sel(c, false),
15195                    first,
15196                    "nicechar_sel({:?}, false) must be pure",
15197                    c
15198                );
15199            }
15200        }
15201    }
15202
15203    /// c:742 — `zwcwidth` returns usize (compile-time type pin).
15204    #[test]
15205    fn zwcwidth_returns_usize_type() {
15206        let _: usize = zwcwidth('a');
15207    }
15208
15209    /// c:742 — `zwcwidth('a')` ASCII letter returns 1.
15210    #[test]
15211    fn zwcwidth_ascii_returns_one() {
15212        assert_eq!(zwcwidth('a'), 1, "ASCII letter width = 1");
15213    }
15214
15215    /// c:836 — `findpwd("")` empty returns Option<String>.
15216    #[test]
15217    fn findpwd_empty_returns_option_string_type() {
15218        let _g = crate::test_util::global_state_lock();
15219        let _: Option<String> = findpwd("");
15220    }
15221
15222    /// c:928 — `slashsplit("")` empty returns empty Vec.
15223    #[test]
15224    fn slashsplit_empty_returns_empty() {
15225        assert!(slashsplit("").is_empty());
15226    }
15227
15228    /// c:928 — `slashsplit("a/b/c")` splits to 3 elements.
15229    #[test]
15230    fn slashsplit_simple_three_components() {
15231        let r = slashsplit("a/b/c");
15232        assert_eq!(r, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
15233    }
15234
15235    /// c:1475 — `dircmp("", "")` both empty returns bool (type pin + safe).
15236    #[test]
15237    fn dircmp_both_empty_returns_bool_type() {
15238        let _g = crate::test_util::global_state_lock();
15239        let _: bool = dircmp("", "");
15240    }
15241
15242    /// c:1426 — `getnameddir("")` empty returns None.
15243    #[test]
15244    fn getnameddir_empty_returns_none() {
15245        let _g = crate::test_util::global_state_lock();
15246        assert!(getnameddir("").is_none(), "empty → None");
15247    }
15248
15249    /// c:1258 — `get_username` returns String (compile-time type pin).
15250    #[test]
15251    fn get_username_returns_string_type() {
15252        let _g = crate::test_util::global_state_lock();
15253        let _: String = get_username();
15254    }
15255
15256    /// c:1921-1935 — adjustwinsize(1) seeds `$LINES`/`$COLUMNS` from the
15257    /// TIOCGWINSZ probe of SHTTY. The port gated the setiparam calls on
15258    /// LINES/COLUMNS being in the OS environment (a misread of C's
15259    /// zgetenv guard, which only covers env re-publication — the C param
15260    /// is always live via its zterm_lines valptr alias), so every
15261    /// interactive shell ran with LINES=0 → `$(( LINES / 3 ))` division
15262    /// by zero in history-search-multi-word pagination (_hsmw_main:81).
15263    #[test]
15264    fn adjustwinsize_seeds_lines_columns_from_shtty() {
15265        let _g = crate::test_util::global_state_lock();
15266        unsafe {
15267            let mut master: libc::c_int = -1;
15268            let mut slave: libc::c_int = -1;
15269            let mut ws: libc::winsize = std::mem::zeroed();
15270            ws.ws_row = 33;
15271            ws.ws_col = 77;
15272            if libc::openpty(
15273                &mut master,
15274                &mut slave,
15275                std::ptr::null_mut(),
15276                std::ptr::null_mut(),
15277                &mut ws,
15278            ) != 0
15279            {
15280                return; // no pty available (sandboxed CI) — skip
15281            }
15282            let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
15283            crate::ported::init::SHTTY.store(slave, Ordering::Relaxed);
15284            let _ = adjustwinsize(1);
15285            crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
15286            libc::close(slave);
15287            libc::close(master);
15288        }
15289        assert_eq!(
15290            crate::ported::params::getiparam("LINES"),
15291            33,
15292            "LINES must reflect the SHTTY winsize probe"
15293        );
15294        assert_eq!(
15295            crate::ported::params::getiparam("COLUMNS"),
15296            77,
15297            "COLUMNS must reflect the SHTTY winsize probe"
15298        );
15299    }
15300}
15301
15302// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
15303// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
15304// pipeline. Extracted body of zerrmsg's `%e` arm (Src/utils.c:355-366):
15305// EINTR → "interrupt", EIO verbatim, else lowercased strerror.
15306// zerrmsg's errno branch routes through this so the prefix
15307// formatting matches C; module callers (zsh/system) embed the
15308// string in their own zwarnnam messages where C uses `%e`.
15309/// Render an errno the way zsh's `%e` warning format does —
15310/// `zerrmsg` case `'e'` at `Src/utils.c:352-368`:
15311///
15312/// - `EINTR` → the literal string `interrupt` (C also sets
15313///   `errflag |= ERRFLAG_ERROR` and truncates the rest of the
15314///   format; every zshrs call site uses `%e` as the final
15315///   conversion so the truncation is a no-op here).
15316/// - `EIO` → `strerror(EIO)` verbatim ("I/O" reads wrong
15317///   lowercased, per the c:361-364 comment).
15318/// - everything else → `strerror(num)` with the first letter
15319///   lowercased (`tulower(errmsg[0])`, c:366).
15320///
15321/// `std::io::Error::from_raw_os_error(n)` Display is NOT this
15322/// format — it appends " (os error N)" and keeps the capital.
15323/// That mismatch was the residual byte-diff in zsh/system's
15324/// `sysopen`/`zsystem flock` warnings (docs/BUGS.md #316).
15325pub fn zsh_errno_msg(num: i32) -> String {
15326    if num == libc::EINTR {
15327        return "interrupt".to_string(); // c:355-358
15328    }
15329    let msg = unsafe {
15330        let p = libc::strerror(num); // c:360
15331        if p.is_null() {
15332            return String::new();
15333        }
15334        std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
15335    };
15336    if num == libc::EIO {
15337        return msg; // c:363-364
15338    }
15339    // c:366 — `fputc(tulower(errmsg[0]), file);`
15340    let mut chars = msg.chars();
15341    match chars.next() {
15342        Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
15343        None => msg,
15344    }
15345}
15346
15347// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
15348// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
15349// pipeline. Byte-exact inverse of metafy (Src/utils.c:5022 unmetafy):
15350// decodes Meta (\u{83}) + c^32 pairs back to raw bytes. C
15351// unmetafy works in place on char*; Rust strings are UTF-8 so
15352// the decode returns Vec<u8> for byte-exact writes.
15353/// Decode a char-level metafied String back to RAW bytes for a write
15354/// or exec boundary. c:Src/utils.c:4954 unmetafy — `if (*t++ == Meta
15355/// && *p) t[-1] = *p++ ^ 32;` — transposed onto the char-level
15356/// encoding `meta_encode_byte` produces: U+0083 followed by a scalar
15357/// in U+0080..=U+00FF yields the single raw byte `payload ^ 32`;
15358/// everything else emits its UTF-8 bytes verbatim. A U+0083 followed
15359/// by anything outside that range is not our encoding and passes
15360/// through untouched. Bug #127.
15361pub fn unmetafy_str(s: &str) -> Vec<u8> {
15362    let mut out = Vec::with_capacity(s.len());
15363    let mut it = s.chars().peekable();
15364    let mut buf = [0u8; 4];
15365    while let Some(c) = it.next() {
15366        if c == '\u{83}' {
15367            if let Some(&n) = it.peek() {
15368                let nu = n as u32;
15369                if (0x80..=0xff).contains(&nu) {
15370                    out.push((nu as u8) ^ 32);
15371                    it.next();
15372                    continue;
15373                }
15374            }
15375        }
15376        out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
15377    }
15378    out
15379}