zsh/ported/utils.rs
1//! Utility functions for zshrs
2//!
3//! Port from zsh/Src/utils.c
4//!
5//! Provides miscellaneous utilities: error handling, file operations,
6//! string utilities, and character classification.
7
8use std::ffi::CString;
9use std::fs;
10use std::io::{self, Read, Write};
11use std::os::unix::fs::{MetadataExt, PermissionsExt};
12use std::os::unix::io::{AsRawFd, RawFd};
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicI64, Ordering};
15use std::sync::Mutex;
16use std::time::UNIX_EPOCH;
17
18use crate::init::zleentry;
19use crate::params::getsparam_u;
20use crate::ported::builtin::{BUILTINS, SFCONTEXT, STOPMSG};
21use crate::ported::compat::{u9_iswprint, zchdir, zgetdir};
22use crate::ported::hashnameddir::{nameddirtab, removenameddirnode};
23use crate::ported::hashtable::shfunctab_lock;
24use crate::ported::hist::{bangchar, chrealpath};
25use crate::ported::init::SHTTY;
26use crate::ported::modules::clone::{coprocin, coprocout, mypgrp};
27use crate::{DPUTS, DPUTS1};
28use libc::{
29 S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP,
30 S_IXOTH, S_IXUSR,
31};
32// SHTTY imported under an alias to avoid collision with the
33// `SHTTY: i32` function parameters at fdsettyinfo/fdgettyinfo
34// (Rule E — C uses SHTTY as both the global and the parameter name).
35use crate::ported::lex::lineno;
36use crate::ported::options::{dosetopt, opt_state_set};
37use crate::ported::params::{
38 assignsparam, convbase as convbase_param, getaparam, getsparam, homesetfn, ifsgetfn, ifssetfn,
39 isident, locallevel as LOCALLEVEL, setaparam, setiparam, setsparam, wordcharsgetfn,
40 wordcharssetfn,
41};
42use crate::ported::signals::{queue_signals, unqueue_signals};
43use crate::ported::string::dupstrpfx;
44use crate::ported::zsh_h::{
45 dirsav, hashnode, interact, isset, jobbing, nameddir, opt_name, shfunc, unset, Dash, Marker,
46 Meta, Nularg, Pound, Snull, AUTONAMEDIRS, BANGHIST, BEEP, CHASELINKS, CSHJUNKIEQUOTES,
47 DEFAULT_IFS, DVORAK, EMULATE_KSH, EMULATE_SH, EMULATION, EXTENDEDGLOB, FDT_EXTERNAL, FDT_FLOCK,
48 FDT_FLOCK_EXEC, FDT_INTERNAL, FDT_MODULE, FDT_UNUSED, GLOBDOTS, HISTFLAG_NOEXEC,
49 LAST_NORMAL_TOK, MAGICEQUALSUBST, MULTIBYTE, ND_NOABBREV, ND_USERNAME, NICEFLAG_HEAP,
50 NICEFLAG_NODUP, NICEFLAG_QUOTE, OCTALZEROES, PATCHARS, POSIXIDENTIFIERS, PRINTEIGHTBIT,
51 QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_BACKTICK, QT_DOLLARS, QT_DOUBLE,
52 QT_NONE, QT_SINGLE, QT_SINGLE_OPTIONAL, RCQUOTES, RMSTARWAIT, SFC_SUBST, SHINSTDIN, SPECCHARS,
53 XTRACE, ZLE_CMD_TRASH,
54};
55use crate::ported::zsh_h::{
56 Inbrace, Inbrack, Inpar, Inparmath, Outbrace, Outbrack, Outpar, Outparmath, Qstring, Qtick,
57 Stringg, Tick,
58};
59use crate::ported::zsh_system_h::DEFAULT_WORDCHARS;
60use crate::ported::ztype_h::{
61 imeta, itok, iwsep, IALNUM, IALPHA, IBLANK, ICNTRL, IDIGIT, IIDENT, IMETA, INBLANK, INULL,
62 IPATTERN, ISEP, ISPECIAL, ITOK, IUSER, IWORD, IWSEP, TYPTAB, TYPTAB_FLAGS, ZISPRINT,
63 ZTF_BANGCHAR, ZTF_INIT, ZTF_INTERACT, ZTF_SP_COMMA,
64};
65
66/// Set a wide-char array from a multibyte source string.
67/// Port of `set_widearray(char *mb_array, Widechar_array wca)` from `Src/utils.c:69`.
68///
69/// ```c
70/// static void set_widearray(char *mb_array, Widechar_array wca) {
71/// if (wca->chars) free(wca->chars);
72/// wca->len = 0;
73/// if (mb_array) {
74/// while (*mb_array) {
75/// if (unsigned char *mb_array <= 0x7f) {
76/// *wcptr++ = (wchar_t)*mb_array++;
77/// continue;
78/// }
79/// mblen = mb_metacharlenconv(mb_array, &wci);
80/// if (!mblen) break;
81/// if (wci == WEOF) return; // any non-convertible aborts
82/// *wcptr++ = (wchar_t)wci;
83/// mb_array += mblen;
84/// }
85/// wca->chars = malloc(...); wca->len = wcptr - tmpwcs;
86/// }
87/// }
88/// ```
89///
90/// Build a wide-char array from a metafied multibyte source string.
91/// C uses `mb_metacharlenconv()` to walk Meta-encoded sequences;
92/// Rust port unmetafies first, then collects chars (the
93/// equivalent: walk Unicode codepoints).
94///
95/// Returns the new vec; caller assigns to the appropriate slot
96/// (`WORDCHARS_w`, `IFS_w`, etc.). C aborts on non-convertible
97/// chars (returns without setting `wca->chars`); Rust port mirrors
98/// by returning empty Vec when conversion fails.
99/// WARNING: param names don't match C — Rust=(mb_array) vs C=(mb_array, wca)
100// Rust idiom replacement: `unmetafy` + `str::chars` covers the C
101// `mbsrtowcs`+`mbstate_t` conversion; the C `wca` out-param drops
102// since the Vec is returned by value.
103/// Port of `set_widearray()` from `Src/utils.c:69`.
104///
105/// `set_widearray` — see implementation.
106/// `static struct widechar_array ifs_wide;` (c:Src/utils.c:64) — `$IFS` as
107/// wide characters, rebuilt by `inittyptab` under MULTIBYTE (c:4207-4213).
108/// `wcsitype(c, ISEP)` matches a non-ASCII character against it (c:4367);
109/// ASCII characters use the typtab ISEP bit.
110pub static IFS_WIDE: std::sync::RwLock<Vec<char>> = std::sync::RwLock::new(Vec::new());
111
112pub fn set_widearray(mb_array: &str) -> Vec<char> {
113 let mut bytes = mb_array.as_bytes().to_vec();
114 unmetafy(&mut bytes);
115 match std::str::from_utf8(&bytes) {
116 Ok(s) => s.chars().collect(),
117 Err(_) => Vec::new(),
118 }
119}
120
121// =====================================================================
122// Port of `zwarning(const char *cmd, const char *fmt, va_list ap)` from `Src/utils.c:142`.
123// =====================================================================
124
125/// Port of `zwarning()` from `Src/utils.c:142` — C decl `zwarning(const char *cmd, const char *fmt, va_list ap)`.
126///
127/// Internal helper that builds the diagnostic prefix and emits it +
128/// the formatted message to stderr. Direct C body translation:
129///
130/// ```c
131/// if (isatty(2)) zleentry(ZLE_CMD_TRASH);
132/// char *prefix = scriptname ? scriptname : (argzero ? argzero : "");
133/// if (cmd) {
134/// if (unset(SHINSTDIN) || locallevel) {
135/// nicezputs(prefix, stderr);
136/// fputc(':', stderr);
137/// }
138/// nicezputs(cmd, stderr);
139/// fputc(':', stderr);
140/// } else {
141/// nicezputs((isset(SHINSTDIN) && !locallevel) ? "zsh" : prefix, stderr);
142/// fputc(':', stderr);
143/// }
144/// zerrmsg(stderr, fmt, ap);
145/// ```
146/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd, fmt, ap)
147fn zwarning(cmd: Option<&str>, msg: &str) {
148 // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
149 // C zsh has one thread, so every diagnostic here belongs to the
150 // command the user just ran. zshrs parses/compiles shell bodies on
151 // worker-pool threads (compinit's bytecode backfill), and a body that
152 // fails to parse there is BACKGROUND noise, not a user-visible error:
153 // printing it stomps the live ZLE line (`zsh: unmatched "` landing in
154 // the middle of the prompt) and the ZLE_CMD_TRASH repaint below runs
155 // off-thread. Route worker-thread diagnostics to the log instead.
156 if crate::worker::in_worker_thread() {
157 tracing::debug!(cmd = ?cmd, msg = %msg, "background-thread diagnostic (not shown)");
158 return;
159 }
160 // c:96 — `if (isatty(2)) zleentry(ZLE_CMD_TRASH);`
161 // Flush any in-flight ZLE redraw state before the warning lands
162 // on stderr — without this, half-painted edit lines bleed into
163 // the diagnostic. Previously: `let _ = isatty(2);` (the result
164 // was discarded; the canonical zleentry port at init.rs:905 was
165 // never actually called from the warning path).
166 if unsafe { libc::isatty(2) } != 0 {
167 // c:96
168 let _ = zleentry(
169 // c:96
170 ZLE_CMD_TRASH, // c:96
171 );
172 }
173 let scriptname = scriptname_lock().lock().unwrap().clone();
174 let argzero = argzero_lock().lock().unwrap().clone();
175 // c:150 reads the `locallevel` global. `here()` rather than `load()`:
176 // C's counter belongs to its single thread of execution, so it can only
177 // ever describe the frame that raised THIS diagnostic. zshrs runs
178 // `async_precmd` hook functions on a pool worker against the shared
179 // counter, and a hook's scope made a top-level error print with a
180 // function-name prefix and a line number zsh does not print —
181 // spin_hook:4: parse error near `done' vs zsh: parse error near `done'
182 // for a line typed while the hook ran. See
183 // `crate::thread_shell_state::LocalLevelCell`.
184 let locallevel = LOCALLEVEL.here();
185 let prefix: String = scriptname.or(argzero).unwrap_or_default();
186 let stderr_handle = io::stderr();
187 let mut stderr_lock = stderr_handle.lock();
188 if let Some(cmd) = cmd {
189 // c:107-110 — `if (unset(SHINSTDIN) || locallevel) {
190 // nicezputs(prefix, stderr); fputc(':', stderr); }`
191 if unset(SHINSTDIN) || locallevel != 0 {
192 let _ = nicezputs(&prefix, &mut stderr_lock); // c:108
193 let _ = stderr_lock.write_all(b":");
194 }
195 let _ = nicezputs(cmd, &mut stderr_lock); // c:111
196 let _ = stderr_lock.write_all(b":");
197 } else {
198 // c:114 — `nicezputs((isset(SHINSTDIN) && !locallevel) ? "zsh" : prefix, stderr);`
199 let to_emit = if isset(SHINSTDIN) && locallevel == 0 {
200 "zsh"
201 } else {
202 prefix.as_str()
203 };
204 let _ = nicezputs(to_emit, &mut stderr_lock); // c:114
205 let _ = stderr_lock.write_all(b":");
206 }
207 // c:116 — `zerrmsg(stderr, fmt, ap)` — lineno prefix + message.
208 // Pre-built `msg: &str` covers C's va_list; zerrmsg port hasn't
209 // had its `(file, fmt, ap)` signature wired yet so the lineno
210 // prefix + write is inlined here against the same `unset(SHINSTDIN)`
211 // gate C uses at c:301.
212 let lineno = lineno() as i32;
213 if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
214 let _ = stderr_lock.write_all(format!("{}: ", lineno).as_bytes());
215 } else {
216 let _ = stderr_lock.write_all(b" ");
217 }
218 let _ = stderr_lock.write_all(msg.as_bytes());
219 let _ = stderr_lock.write_all(b"\n");
220 let _ = stderr_lock.flush();
221}
222
223// =====================================================================
224// Port of `zerr(VA_ALIST1(const char *fmt))` / `zerrnam` / `zwarn` / `zwarnnam` from utils.c:173
225// onward. Each is a thin wrapper: check errflag/noerrs guards, set
226// `ERRFLAG_ERROR` on the fatal variants, call `zwarning`.
227// =====================================================================
228
229/// Port of `zerr()` from `Src/utils.c:173` — C decl `zerr(VA_ALIST1(const char *fmt))`.
230///
231/// ```c
232/// if (errflag || noerrs) {
233/// if (noerrs < 2) errflag |= ERRFLAG_ERROR;
234/// return;
235/// }
236/// errflag |= ERRFLAG_ERROR;
237/// zwarning(NULL, fmt, ap);
238/// ```
239/// WARNING: param names don't match C — Rust=(msg) vs C=()
240pub fn zerr(msg: &str) {
241 // c:173
242 let noerrs = *noerrs_lock().lock().unwrap();
243 if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
244 // c:175
245 if noerrs < 2 {
246 // c:176
247 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:176
248 }
249 return; // c:177
250 }
251 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:194
252 zwarning(None, msg); // c:194
253}
254
255/// Port of `zerrnam()` from `Src/utils.c:194` — C decl `zerrnam(VA_ALIST2(const char *cmd, const char *fmt))`.
256///
257/// ```c
258/// if (errflag || noerrs) return;
259/// errflag |= ERRFLAG_ERROR;
260/// zwarning(cmd, fmt, ap);
261/// ```
262/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd)
263pub fn zerrnam(cmd: &str, msg: &str) {
264 // c:194
265 let noerrs = *noerrs_lock().lock().unwrap();
266 if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
267 // c:196
268 return;
269 }
270 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:214
271 zwarning(Some(cmd), msg); // c:214
272}
273
274/// Port of `zwarn()` from `Src/utils.c:214` — C decl `zwarn(VA_ALIST1(const char *fmt))`.
275///
276/// ```c
277/// if (errflag || noerrs) return;
278/// zwarning(NULL, fmt, ap);
279/// ```
280/// WARNING: param names don't match C — Rust=(msg) vs C=()
281pub fn zwarn(msg: &str) {
282 // c:214
283 let noerrs = *noerrs_lock().lock().unwrap();
284 if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
285 // c:216
286 return;
287 }
288 zwarning(None, msg); // c:231
289}
290
291/// Port of `zwarnnam()` from `Src/utils.c:231` — C decl `zwarnnam(VA_ALIST2(const char *cmd, const char *fmt))`.
292///
293/// ```c
294/// if (errflag || noerrs) return;
295/// zwarning(cmd, fmt, ap);
296/// ```
297/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd)
298pub fn zwarnnam(cmd: &str, msg: &str) {
299 // c:231
300 let noerrs = *noerrs_lock().lock().unwrap();
301 if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
302 // c:233
303 return;
304 }
305 zwarning(Some(cmd), msg); // c:235
306}
307
308/// Port of `dputs()` from `Src/utils.c:253` — C decl `dputs(VA_ALIST1(const char *message))`.
309///
310/// C body (c:253-270):
311/// ```c
312/// mod_export void dputs(const char *message)
313/// {
314/// char *filename;
315/// FILE *file;
316/// if ((filename = getsparam_u("ZSH_DEBUG_LOG")) != NULL &&
317/// (file = fopen(filename, "a")) != NULL) {
318/// zerrmsg(file, message, ap);
319/// fclose(file);
320/// } else
321/// zerrmsg(stderr, message, ap);
322/// }
323/// ```
324///
325/// Previous Rust port was a FAKE — `eprintln!("BUG: {}", msg)` ignored
326/// `$ZSH_DEBUG_LOG` entirely (which is the whole point of dputs — to
327/// route debug output to a user-specified log file). Re-port now
328/// consults paramtab via `getsparam_u`, opens the log in append mode,
329/// and routes the message there; falls back to stderr per c:268.
330///
331/// Rust signature drift: takes `&str` instead of va_args; callers
332/// pre-format via Rust's `format!` (same pattern as `zerrmsg`).
333pub fn dputs(msg: &str) {
334 // c:253
335 // c:263 — `getsparam_u("ZSH_DEBUG_LOG")`. The `_u` variant
336 // unmetafies the result (utils.rs's getsparam_u port at
337 // params.rs:3831 wraps getsparam + unmeta).
338 let log_file = getsparam_u("ZSH_DEBUG_LOG"); // c:263
339 // c:264 — `fopen(filename, "a")` — append mode.
340 let opened = log_file.as_ref().and_then(|p| {
341 // c:264
342 fs::OpenOptions::new()
343 .create(true)
344 .append(true)
345 .open(p)
346 .ok()
347 });
348 // Shared format logic: lineno prefix + msg + newline, matching
349 // zerrmsg at c:296-308. Built once, written to file or stderr.
350 let lineno = lineno() as i32;
351 let shinstdin = isset(SHINSTDIN);
352 // `here()` not `load()` — the depth of the thread raising this message;
353 // see the matching note in `zwarning`.
354 let locallevel = LOCALLEVEL.here();
355 let prefix = if (!shinstdin || locallevel != 0) && lineno != 0 {
356 format!("{}: ", lineno)
357 } else {
358 String::new()
359 };
360 let line = format!("{}{}\n", prefix, msg);
361 if let Some(mut f) = opened {
362 // c:265 zerrmsg(file, ...)
363 let _ = f.write_all(line.as_bytes()); // c:265
364 // c:266 — `fclose(file)` — handled by Drop when `f` goes out of scope.
365 } else {
366 // c:267 else stderr
367 let _ = io::stderr().write_all(line.as_bytes()); // c:268 zerrmsg(stderr, ...)
368 }
369}
370
371// ---------------------------------------------------------------------------
372// Remaining 33 missing utils.c functions
373// ---------------------------------------------------------------------------
374
375// `zwarning` is defined earlier in this file as the real port of
376// utils.c:142 (private helper invoked by `zerr`/`zerrnam`/`zwarn`/
377// `zwarnnam`). The duplicate stub previously here has been deleted
378// — callers use the four public entry points instead.
379
380/// Port of `zz_plural_z_alpha()` from `Src/utils.c:282` — C decl `void zz_plural_z_alpha(void)`.
381///
382/// Cygwin-only no-op symbol the C source emits to work around a
383/// dllwrap bug that drops the last alphabetically-sorted exported
384/// symbol (zwarnnam). The Rust port has no equivalent linker
385/// problem; this is preserved as a no-op for symbol-table parity.
386pub fn zz_plural_z_alpha() {} // c:282
387
388/// Port of `zerrmsg(FILE *file, const char *fmt, va_list ap)` from `Src/utils.c:289`.
389///
390/// C body emits the formatted message + (when locallevel > 0 or
391/// SHINSTDIN unset) the line number prefix + "\n". The Rust port
392/// is invoked indirectly through `zwarning` — direct callers pass
393/// pre-formatted strings.
394/// WARNING: param names don't match C — Rust=(msg, errno) vs C=(file, fmt, ap)
395// Rust idiom replacement: pre-formatted `msg: &str` covers the C
396// va_list expansion; the C `file`+`fmt`+`ap` triplet collapses
397// because callers (zerr/zwarning) pre-format via Rust's `format!`.
398/// Port of `zerrmsg()` from `Src/utils.c:289`.
399///
400/// `zerrmsg` — see implementation.
401pub fn zerrmsg(msg: &str, errno: Option<i32>) {
402 // c:289
403 // c:296 — `lineno` is the parser-advanced line counter. The
404 // previous Rust port read `lineno_lock` (a Mutex<i32> in utils.rs)
405 // that ONLY fusevm_bridge::set_lineno wrote to; the actual
406 // parser uses lex::LEX_LINENO (thread_local Cell<u64>) updated
407 // by every newline scan. Result: error messages emitted from
408 // parse-time always printed line 0 (or last fusevm_bridge value)
409 // rather than the actual line of the syntax error.
410 //
411 // Route through lex::lineno() so the parser-advanced counter
412 // drives the error prefix.
413 let lineno = lineno() as i32;
414 // `here()` not `load()` — the depth of the thread raising this message;
415 // see the matching note in `zwarning`.
416 let locallevel = LOCALLEVEL.here();
417 // c:301-308 — `if ((unset(SHINSTDIN) || locallevel) && lineno)
418 // fprintf(file, "%d: ", lineno); else fputc(' ', file);`
419 if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
420 eprint!("{}: ", lineno);
421 } else {
422 eprint!(" ");
423 }
424 if let Some(e) = errno {
425 // c:348-365 — `%e`: render the errno exactly as C's zerrmsg does,
426 // through `zsh_errno_msg` (EINTR→"interrupt", EIO verbatim, else
427 // strerror with a lowercased first letter). The previous
428 // `io::Error::from_raw_os_error` kept the capital and appended
429 // " (os error N)" — the byte-diff tracked as docs/BUGS.md #316.
430 if e == libc::EINTR {
431 // c:351-354 — EINTR sets the error flag and stops formatting.
432 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
433 }
434 eprintln!("{}: {}", msg, zsh_errno_msg(e));
435 } else {
436 eprintln!("{}", msg);
437 }
438}
439
440/// Port of `zsetupterm()` from `Src/utils.c:390` — C decl `void zsetupterm(void)`.
441///
442/// Reference-counts terminfo's `cur_term` setup. The C source
443/// guards with `#ifdef HAVE_SETUPTERM`; without terminfo this is a
444/// pure no-op. Rust's `term`-style state lives elsewhere (modules
445/// like `zle/termcap` initialize on demand), so this is a no-op
446/// counter for symbol-table parity.
447pub fn zsetupterm() {
448 // c:390
449 static TERM_COUNT: std::sync::atomic::AtomicI32 = // c:402
450 std::sync::atomic::AtomicI32::new(0);
451 let tc = TERM_COUNT.load(Ordering::Relaxed);
452 // c:395-396 — DPUTS(term_count < 0 || (term_count > 0 && !cur_term),
453 // "inconsistent term_count and/or cur_term");
454 // The Rust port has no `cur_term` analogue (terminfo state lives
455 // in zle/termcap module), so the second condition reduces to
456 // `term_count > 0 && true` if we treat cur_term as the count
457 // itself. Simplified to the bare `term_count < 0` invariant.
458 DPUTS!(tc < 0, "inconsistent term_count and/or cur_term"); // c:395-396
459 TERM_COUNT.fetch_add(1, Ordering::Relaxed); // c:402
460}
461
462/// Port of `zdeleteterm()` from `Src/utils.c:409` — C decl `void zdeleteterm(void)`.
463pub fn zdeleteterm() {
464 // c:409
465 static TERM_COUNT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
466 let tc = TERM_COUNT.load(Ordering::Relaxed);
467 // c:412-413 — DPUTS(term_count < 1 || !cur_term,
468 // "inconsistent term_count and/or cur_term");
469 DPUTS!(tc < 1, "inconsistent term_count and/or cur_term"); // c:412-413
470 if tc > 0 {
471 TERM_COUNT.fetch_sub(1, Ordering::Relaxed); // c:414
472 }
473}
474
475/// Port of `putraw()` from `Src/utils.c:424` — C decl `int putraw(int c)`. Writes a
476/// single byte to stdout for the termcap library, returning 0.
477///
478/// Declared `extern "C"` with C's exact `int (int)` signature because
479/// it is handed to `tputs(3)` as a function pointer — that is the only
480/// thing C uses it for (`Src/Modules/termcap.c:132`,
481/// `tputs(t, 1, putraw)`), and a Rust-ABI `fn(char) -> i32` cannot be
482/// passed there. `src/ported/modules/termcap.rs` used to carry its own
483/// copy under this name for exactly that reason; the copy is gone and
484/// termcap now passes this one.
485///
486/// !!! RUST-ONLY DEVIATION !!! C is `putc(c, stdout)` — buffered through
487/// the FILE*. This writes the byte straight to fd 1, so the caller
488/// controls interleaving with Rust's own buffered stdout by flushing
489/// (termcap.rs does, right after the `tputs` call).
490pub extern "C" fn putraw(c: libc::c_int) -> libc::c_int {
491 // c:424
492 let byte = c as u8; // c:426 — `putc(c, stdout);`
493 unsafe {
494 libc::write(1, &byte as *const u8 as *const libc::c_void, 1);
495 }
496 0 // c:427
497}
498
499/// Port of `putshout()` from `Src/utils.c:434` — C decl `int putshout(int c)`. Writes a
500/// single byte to `shout` (zsh's interactive stdout — falls back
501/// to stdout in zshrs's static-link path), returning 0.
502pub fn putshout(c: char) -> i32 {
503 // c:434
504 print!("{}", c); // c:434
505 0 // c:437
506}
507
508/// Port of `nicechar_sel()` from `Src/utils.c:462` — C decl `nicechar_sel(int c, int quotable)`.
509/// Renders one byte as its `^X` / `M-X` / `\\n` / `\\t` display form;
510/// `quotable=true` emits `\\C-X` instead of `^X` so the result is
511/// shell-quotable.
512pub fn nicechar_sel(c: char, quotable: bool) -> String {
513 // c:462
514 // c:466 — `c &= 0xff;` mask to byte before any classification.
515 let mut c = (c as u32) & 0xff;
516 let mut out = String::new();
517 // c:467 — `if (ZISPRINT(c)) goto done;`. Use the canonical
518 // `ZISPRINT` predicate (port of ztype.h:89 — IPRINT typtab bit
519 // AND != 0x7f). Previously used a custom `is_print` closure that
520 // wrongly accepted 0xa0+ as printable — diverged from C ZISPRINT
521 // for every high-bit byte under PRINTEIGHTBIT-off, breaking
522 // `\M-X` escape generation.
523 if !ZISPRINT(c as u8) {
524 // c:467
525 if c & 0x80 != 0 {
526 // c:469
527 if isset(PRINTEIGHTBIT) { // c:470
528 // c:471 — goto done (output raw); c unchanged.
529 } else {
530 out.push_str("\\M-"); // c:472-474
531 c &= 0x7f; // c:475
532 if ZISPRINT(c as u8) {
533 // c:476-477
534 // c:477 — goto done after writing \M- + ASCII char.
535 if let Some(ch) = char::from_u32(c) {
536 out.push(ch);
537 }
538 return out;
539 }
540 }
541 }
542 if c == 0x7f {
543 // c:479
544 out.push_str(if quotable { "\\C-" } else { "^" }); // c:481-486
545 c = b'?' as u32;
546 } else if c == b'\n' as u32 {
547 // c:487
548 out.push('\\');
549 c = b'n' as u32;
550 } else if c == b'\t' as u32 {
551 // c:490
552 out.push('\\');
553 c = b't' as u32;
554 } else if c < 0x20 {
555 // c:493
556 out.push_str(if quotable { "\\C-" } else { "^" }); // c:494-499
557 c += 0x40;
558 }
559 }
560 if let Some(ch) = char::from_u32(c) {
561 // c:511
562 out.push(ch);
563 }
564 out
565}
566
567/// Port of `nicechar()` from `Src/utils.c:520` — C decl `nicechar(int c)`. C body:
568/// `return nicechar_sel(c, 0);`
569pub fn nicechar(c: char) -> String {
570 // c:520
571 nicechar_sel(c, false) // c:523
572}
573
574/// Port of `is_nicechar()` from `Src/utils.c:531` — C decl `is_nicechar(int c)`.
575/// ```c
576/// c &= 0xff;
577/// if (ZISPRINT(c)) return 0;
578/// if (c & 0x80) return !isset(PRINTEIGHTBIT);
579/// return (c == 0x7f || c == '\n' || c == '\t' || c < 0x20);
580/// ```
581/// "Nice" means "needs escape-formatting when printed" — so
582/// returns true for control chars + (under PRINTEIGHTBIT off)
583/// high-bit bytes. The previous Rust port treated all `!c.is_ascii()`
584/// as nice unconditionally — divergent for users running with
585/// `setopt printeightbit` (very common for non-ASCII filenames).
586pub fn is_nicechar(c: char) -> bool {
587 let cu = (c as u32) & 0xff;
588 // c:534 — `if (ZISPRINT(c)) return 0;` — printable ASCII is not nice.
589 if ZISPRINT(cu as u8) {
590 return false;
591 }
592 // c:536 — high-bit byte path.
593 if (cu & 0x80) != 0 {
594 return !isset(PRINTEIGHTBIT);
595 }
596 // c:538 — ASCII control chars (DEL/\n/\t/<0x20).
597 cu == 0x7f || cu == b'\n' as u32 || cu == b'\t' as u32 || cu < 0x20
598}
599
600/// Initialize multibyte state (from utils.c mb_charinit) - no-op in Rust
601/// Port of `mb_charinit` from `Src/utils.c:553`.
602pub fn mb_charinit() {
603 // Rust handles UTF-8 natively
604}
605
606/// Scratch space for a libc `mbstate_t`.
607///
608/// C declares `mbstate_t mbs;` on the stack wherever it converts
609/// between the multibyte and wide representations (`Src/utils.c:543`
610/// `mb_shiftstate`, `Src/Zle/zle_utils.c:381`, `Src/Zle/zle_utils.c:202`,
611/// `Src/Zle/zle_refresh.c:625`, `Src/Zle/zle_main.c:995`). POSIX leaves
612/// the struct opaque and its size is platform-specific — 8 bytes on
613/// glibc/musl, 128 bytes on macOS and the BSDs — so the port hands libc
614/// an over-sized, 8-byte-aligned, zeroed buffer instead of restating the
615/// layout. `MBSTATE_ZERO` is the `memset(&mbs, 0, sizeof mbs)` those C
616/// sites perform before their first conversion.
617pub type MbStateBuf = [u64; 16];
618
619/// A zeroed [`MbStateBuf`] — the `memset(&mbs, '\0', sizeof mbs)` that
620/// precedes every C conversion loop (e.g. `Src/Zle/zle_utils.c:444`).
621pub const MBSTATE_ZERO: MbStateBuf = [0u64; 16];
622
623// !!! WARNING: RUST-ONLY HELPER !!!
624//
625// C has no counterpart: `zsh_main` calls `setlocale(LC_ALL, "")` at
626// `Src/init.c:1861` before anything else runs, so by the time any
627// conversion happens the process locale is already the environment's.
628// A Rust binary never calls `setlocale`, and a library consumer of this
629// crate (unit tests, embedded entry points, the daemon) may reach the
630// `mbrtowc`/`wcrtomb` wrappers above without ever going through
631// `zsh_main`. Under the startup default ("C") those are single-byte
632// codecs, so a UTF-8 terminal line would be shredded into raw bytes.
633//
634// Forcing the locale once, lazily, from the environment restores the C
635// precondition for those consumers. `zsh_main` touches it right after
636// its own `setlocale` (`Src/init.c:1861`) so the lazy path can never
637// fire later and undo an `LC_*` parameter assignment made from an rc
638// file.
639pub static MB_LOCALE_READY: std::sync::LazyLock<()> = std::sync::LazyLock::new(|| {
640 #[cfg(unix)]
641 unsafe {
642 let empty = std::ffi::CString::new("").unwrap();
643 libc::setlocale(libc::LC_CTYPE, empty.as_ptr());
644 }
645});
646
647/// `(size_t)-2` — `mbrtowc` "the bytes so far are a valid but incomplete
648/// character". zsh spells this `MB_INCOMPLETE` (`Src/zsh.h:3313`).
649pub const MB_INCOMPLETE: usize = usize::MAX - 1;
650
651/// `(size_t)-1` — `mbrtowc` "invalid multibyte sequence". zsh spells this
652/// `MB_INVALID` (`Src/zsh.h:3314`).
653pub const MB_INVALID: usize = usize::MAX;
654
655// The libc conversion primitives themselves. The whole point of routing
656// through libc rather than Rust's native UTF-8 codecs is that these are
657// LOCALE-DRIVEN: in a single-byte locale (`MB_CUR_MAX == 1`) `mbrtowc`
658// consumes exactly one byte and yields that byte's value as the wide
659// character (under the ASCII C locale only for bytes up to 0x7f, below),
660// while under a UTF-8 locale it decodes the full sequence. zsh's
661// byte-vs-multibyte behaviour is entirely this distinction — no C caller
662// in `stringaszleline`/`zlelineasstring`/`getrestchar`/`zwcputc` tests
663// `MB_CUR_MAX` or `isset(MULTIBYTE)` itself. The `mbstate_t` argument is
664// typed as an opaque pointer; pass `&mut MBSTATE_ZERO`-initialised
665// [`MbStateBuf`]. The libc crate does not re-export these on unix.
666//
667// `mbrtowc`/`wcrtomb` come through `crate::c_locale`, which gives the C
668// locale (ASCII codeset) glibc's answer on every platform: a byte or wide
669// character above 0x7f does not convert. macOS libc would convert it, and
670// zsh's own tests expect the glibc output (E02xtrace.ztst `\M-c`). Every
671// other locale is libc unchanged. Returns: bytes consumed / written, `0` for
672// a NUL, [`MB_INCOMPLETE`] or [`MB_INVALID`].
673pub use crate::c_locale::{mbrtowc, wcrtomb};
674extern "C" {
675 /// libc `MB_CUR_MAX`: the maximum number of bytes in a multibyte
676 /// character in the CURRENT locale — 1 in a single-byte codeset
677 /// (`LC_ALL=C`), 4 under UTF-8, 2 under `zh_CN.GB2312`. It is a
678 /// MACRO in C (`<stdlib.h>`), so the libc crate cannot re-export
679 /// it; each platform's underlying function is declared here, the
680 /// same way `crate::c_locale` declares `mbrtowc`/`wcrtomb`. Darwin spells it
681 /// `___mb_cur_max()` (SDK `_stdlib.h:133`), glibc
682 /// `__ctype_get_mb_cur_max()`.
683 #[cfg(target_vendor = "apple")]
684 #[link_name = "___mb_cur_max"]
685 pub fn mb_cur_max_raw() -> libc::c_int;
686
687 /// glibc/musl spelling of the same accessor — see the Darwin arm.
688 #[cfg(not(target_vendor = "apple"))]
689 #[link_name = "__ctype_get_mb_cur_max"]
690 pub fn mb_cur_max_raw() -> libc::size_t;
691}
692
693/// Port of `wcs_nicechar_sel()` from `Src/utils.c:593`.
694///
695/// Port of `wcs_nicechar_sel(wchar_t c, size_t *widthp, char **swidep,
696/// int quotable)` from `Src/utils.c:593-705`. Four branches per C:
697/// 1. c < 0x80 and not printable: control-char escape (\n, \t, ^X, \C-X)
698/// — same as `nicechar_sel`.
699/// 2. c >= 0x80 printable AND PRINTEIGHTBIT set: emit raw UTF-8 bytes.
700/// 3. c >= 0x80 fits in UTF-8: emit UTF-8 bytes (default-on for MULTIBYTE).
701/// 4. c >= 0x10000: `\U%.8x` 8-digit hex; c >= 0x100: `\u%.4x` 4-digit hex.
702///
703/// Param mapping (Rule S1, faithful to C):
704/// - `widthp: Option<&mut usize>` ← C `size_t *widthp` (`None` ≡ `NULL`).
705/// Set to the display column width of the produced sequence.
706/// - `swidep: Option<&mut usize>` ← C `char **swidep` (`None` ≡ `NULL`).
707/// Set to the byte index in the returned string where the post-wide
708/// meta-prefixed bytes begin (i.e. boundary between display-width
709/// characters and trailing Meta+X encoding bytes that don't add to
710/// column position).
711pub fn wcs_nicechar_sel(
712 c: char,
713 widthp: Option<&mut usize>,
714 swidep: Option<&mut usize>,
715 quotable: bool,
716) -> String {
717 // c:593
718 let cv = c as u32;
719 // c:616 — `if (!WC_ISPRINT(c) && (c < 0x80 || !isset(PRINTEIGHTBIT)))`.
720 // The non-printable + (low-ASCII or PRINTEIGHTBIT-off) branch.
721 let print_eightbit = isset(PRINTEIGHTBIT);
722 let is_printable = u9_iswprint(c);
723 let buf: String;
724 if !is_printable && (cv < 0x80 || !print_eightbit) {
725 if cv == 0x7f {
726 // c:617-624 — DEL: `^?` / `\C-?`
727 buf = if quotable {
728 "\\C-?".to_string()
729 } else {
730 "^?".to_string()
731 };
732 // c:686 widthp = (s - buf) + wcw (wcw for '?' = 1).
733 if let Some(wp) = widthp {
734 *wp = buf.chars().count();
735 }
736 if let Some(sp) = swidep {
737 *sp = buf.len();
738 }
739 return buf;
740 } else if c == '\n' {
741 // c:625-627 — `\n` literal.
742 buf = "\\n".to_string();
743 if let Some(wp) = widthp {
744 *wp = 2;
745 }
746 if let Some(sp) = swidep {
747 *sp = buf.len();
748 }
749 return buf;
750 } else if c == '\t' {
751 // c:628-630 — `\t` literal.
752 buf = "\\t".to_string();
753 if let Some(wp) = widthp {
754 *wp = 2;
755 }
756 if let Some(sp) = swidep {
757 *sp = buf.len();
758 }
759 return buf;
760 } else if cv < 0x20 {
761 // c:631-638 — ^X / \C-X for controls (excluding \n, \t).
762 let cc = (cv + 0x40) as u8 as char;
763 buf = if quotable {
764 format!("\\C-{}", cc)
765 } else {
766 format!("^{}", cc)
767 };
768 if let Some(wp) = widthp {
769 *wp = buf.chars().count();
770 }
771 if let Some(sp) = swidep {
772 *sp = buf.len();
773 }
774 return buf;
775 }
776 // c:639-641 — c >= 0x80 non-printable falls through to ret=-1
777 // path (hex escape).
778 } else if cv < 0x80 {
779 // c:644-704 — printable ASCII: emit raw char. wcw=1.
780 buf = c.to_string();
781 if let Some(wp) = widthp {
782 *wp = 1;
783 }
784 if let Some(sp) = swidep {
785 *sp = buf.len();
786 }
787 return buf;
788 }
789 // c:644-678 — high-bit char: try UTF-8 encode first.
790 if u9_iswprint(c) {
791 // c:681-693 — printable wide char: emit raw UTF-8.
792 buf = c.to_string();
793 let wcw = zwcwidth(c) as usize;
794 if let Some(wp) = widthp {
795 *wp = wcw;
796 }
797 if let Some(sp) = swidep {
798 *sp = buf.len();
799 }
800 return buf;
801 }
802 // c:656-678 — non-printable wide: hex escape (or fall back to byte
803 // nicechar for c < 0x100).
804 if cv >= 0x10000 {
805 // c:656-659 — `\U%.8x` (10 chars).
806 buf = format!("\\U{:08x}", cv);
807 if let Some(wp) = widthp {
808 *wp = 10;
809 }
810 if let Some(sp) = swidep {
811 *sp = buf.len();
812 }
813 buf
814 } else if cv >= 0x100 {
815 // c:660-663 — `\u%.4x` (6 chars).
816 buf = format!("\\u{:04x}", cv);
817 if let Some(wp) = widthp {
818 *wp = 6;
819 }
820 if let Some(sp) = swidep {
821 *sp = buf.len();
822 }
823 buf
824 } else {
825 // c:664-674 — fall back to byte nicechar_sel.
826 buf = nicechar_sel(c, quotable);
827 // c:670-671 — `*widthp = ztrlen(buf);` (display width respects
828 // metafied chars). `ztrlen` counts visible cells.
829 if let Some(wp) = widthp {
830 *wp = ztrlen(&buf);
831 }
832 // c:672-673 — `*swidep = buf + strlen(buf);` (no trailing meta
833 // since nicechar_sel ASCII output).
834 if let Some(sp) = swidep {
835 *sp = buf.len();
836 }
837 buf
838 }
839}
840
841/// Port of `wcs_nicechar()` from `Src/utils.c:709` — C decl `wcs_nicechar(wchar_t c, size_t *widthp, char **swidep)`.
842/// C body: `return wcs_nicechar_sel(c, widthp, swidep, 0);`
843pub fn wcs_nicechar(c: char, widthp: Option<&mut usize>, swidep: Option<&mut usize>) -> String {
844 // c:709
845 wcs_nicechar_sel(c, widthp, swidep, false) // c:711
846}
847
848/// Port of `is_wcs_nicechar()` from `Src/utils.c:720` — C decl `int is_wcs_nicechar(wchar_t c)`.
849///
850/// "Return 1 if wcs_nicechar() would reformat this character for
851/// display." Mirrors the C condition: non-printable AND (low ASCII
852/// OR PRINTEIGHTBIT unset) for control chars; for high bytes, true
853/// when ≥0x100 or `is_nicechar` says so.
854pub fn is_wcs_nicechar(c: char) -> bool {
855 // c:720
856 let cv = c as u32;
857 // c:722 `WC_ISPRINT(c)` — u9_iswprint in a --enable-unicode9 build
858 // (Src/ztype.h:77), as wcs_nicechar_sel uses. Rust's is_control() is
859 // only Cc, so U+200B / U+00AD / U+FEFF read as printable and
860 // `${(q+)v}` left them bare where zsh writes $'\u200b'.
861 let printable = crate::ported::compat::u9_iswprint(c);
862 let print_eight = isset(PRINTEIGHTBIT); // c:722
863 if !printable && (cv < 0x80 || !print_eight) {
864 if cv == 0x7f || c == '\n' || c == '\t' || cv < 0x20 {
865 // c:734
866 return true;
867 }
868 if cv >= 0x80 {
869 // c:734
870 return cv >= 0x100 || is_nicechar(c); // c:734
871 }
872 }
873 false // c:734
874}
875
876/// Port of `zwcwidth()` from `Src/utils.c:734` — C decl `int zwcwidth(wint_t wc)`.
877///
878/// Get wide character width (from utils.c zwcwidth)
879///
880/// C body (c:734-745):
881/// ```c
882/// int wcw;
883/// /* assume a single-byte character if not valid */
884/// if (wc == WEOF || unset(MULTIBYTE)) // c:738
885/// return 1;
886/// wcw = WCWIDTH(wc); // c:740
887/// /* if not printable, assume width 1 */
888/// if (wcw < 0) // c:742
889/// return 1;
890/// return wcw; // c:744
891/// ```
892///
893/// The previous Rust port skipped the `unset(MULTIBYTE)` early-
894/// return (c:738). When the MULTIBYTE option is OFF (set via
895/// `setopt nomultibyte` / `set +o multibyte`), C zsh treats every
896/// codepoint as a single-byte char and returns width 1 unconditionally.
897/// Without the option check, the Rust port would still report the
898/// Unicode-width-table answer (2 for CJK, 0 for combining marks)
899/// in single-byte mode — diverging from prompt/refresh layout that
900/// relies on the option as the source of truth.
901pub fn zwcwidth(wc: char) -> usize {
902 // c:734
903 // c:738 — `if (wc == WEOF || unset(MULTIBYTE)) return 1;`. WEOF
904 // path is Rust-impossible (char is always a valid scalar). The
905 // MULTIBYTE option gate maps to the canonical option register.
906 if !isset(MULTIBYTE) {
907 // c:738
908 return 1; // c:739
909 }
910 // c:740-744 — WCWIDTH(wc); negative result → width 1.
911 unicode_width::UnicodeWidthChar::width(wc).unwrap_or(1) // c:740-744
912}
913
914/// Port of `pathprog()` from `Src/utils.c:761` — C decl `pathprog(char *prog, char **namep)`.
915/// ```c
916/// for (pp = path; *pp; pp++) {
917/// sprintf(buf, "%s/%s", *pp, prog);
918/// funmeta = unmeta(buf);
919/// if (access(funmeta, F_OK) == 0 && stat(funmeta, &st) >= 0 &&
920/// !S_ISDIR(st.st_mode)) {
921/// return funmeta;
922/// }
923/// }
924/// return NULL;
925/// ```
926/// C checks: (1) F_OK = exists, (2) stat succeeds, (3) NOT a directory.
927/// NO executable-bit check. Used by autoload / `which` paths that
928/// need to find any file in PATH, not just executables.
929///
930/// Previously the Rust port added a `mode & 0o111 != 0` executable
931/// check — divergent, made every `pathprog` lookup miss non-executable
932/// files (e.g. autoload-function plaintext scripts that don't have
933/// +x set).
934/// WARNING: param names don't match C — Rust=(prog) vs C=(prog, namep)
935pub fn pathprog(prog: &str) -> Option<PathBuf> {
936 // c:760
937 // The early-return on `prog` containing '/' is a Rust-port
938 // convenience NOT in C's pathprog. C unconditionally walks $PATH
939 // and prefixes each entry. The convenience is harmless since
940 // C's caller (`findcmd`) handles slashes separately before
941 // calling pathprog.
942 if prog.contains('/') {
943 let p = PathBuf::from(prog);
944 return if p.exists() { Some(p) } else { None };
945 }
946 if let Some(path_var) = getsparam("PATH") {
947 for dir in path_var.split(':') {
948 // c:773
949 let full_path = PathBuf::from(dir).join(prog);
950 // c:776 — `funmeta = unmeta(buf)`. The previous Rust port
951 // passed the raw composed path to `fs::metadata`, missing
952 // the unmeta step. Paths containing Meta-encoded bytes
953 // (from PATH entries or prog name with metafy lead bytes)
954 // would silently miss valid executables.
955 let unmeta_path = unmeta(full_path.to_str().unwrap_or("")); // c:776 unmeta(buf)
956 // c:777-779 — `access(F_OK) == 0 && stat >= 0 && !S_ISDIR`.
957 // is_file() folds existence + stat + not-dir into one.
958 if let Ok(meta) = fs::metadata(&unmeta_path) {
959 if meta.is_file() {
960 // Return the unmeta'd path since that's what
961 // C does (funmeta is the returned value).
962 return Some(PathBuf::from(unmeta_path));
963 }
964 }
965 }
966 }
967 None
968}
969
970/// Port of `findpwd(char *s)` from `Src/utils.c:792`.
971///
972/// ```c
973/// char *findpwd(char *s)
974/// {
975/// char *t;
976/// if (*s == '/')
977/// return xsymlink(s, 0);
978/// s = tricat((pwd[1]) ? pwd : "", "/", s);
979/// t = xsymlink(s, 0);
980/// zsfree(s);
981/// return t;
982/// }
983/// ```
984///
985/// Resolve `s` to its canonical form. Absolute paths route through
986/// `xsymlink` directly; relative paths get prefixed with the
987/// current `pwd` first.
988///
989/// Signature note: C takes `s: char *`. The previous Rust port had
990/// no parameter (returning the cwd) — completely wrong. New port
991/// matches C: takes `&str`, returns `Option<String>` (xsymlink can
992/// return NULL).
993// get a symlink-free pathname for s relative to PWD // c:792
994/// Port of `findpwd()` from `Src/utils.c:792`.
995///
996/// `findpwd` — see implementation.
997pub fn findpwd(s: &str) -> Option<String> {
998 // c:792
999 if s.starts_with('/') {
1000 // c:792
1001 return xsymlink(s); // c:797
1002 }
1003 // C: tricat((pwd[1]) ? pwd : "", "/", s) — uses the global
1004 // `pwd` (logical cwd; differs from realpath when chasing
1005 // symlinks is disabled). The Rust port reads `$PWD` since
1006 // shell-set `PWD` mirrors C's `pwd` global; falls back to
1007 // `getcwd()` when unset.
1008 let pwd = getsparam("PWD")
1009 .or_else(|| {
1010 std::env::current_dir()
1011 .ok()
1012 .map(|p| p.to_string_lossy().into_owned())
1013 })
1014 .unwrap_or_default(); // c:798
1015 let prefix: &str = if pwd.len() > 1 { &pwd } else { "" }; // c:798 pwd[1]
1016 let combined = format!("{}/{}", prefix, s); // c:798
1017 xsymlink(&combined) // c:799
1018}
1019
1020/// Port of `ispwd()` from `Src/utils.c:809`.
1021///
1022/// Validate an inherited `$PWD` exactly like zsh's ispwd() at
1023/// src/zsh/Src/utils.c:809: PWD must be absolute, must stat to the
1024/// same dev+inode as ".", and must contain no `.` or `..` components.
1025/// When this returns false, callers should fall back to `getcwd()`.
1026pub(crate) fn ispwd(pwd: &str) -> bool {
1027 if !pwd.starts_with('/') {
1028 return false;
1029 }
1030 let pwd_meta = match fs::metadata(pwd) {
1031 Ok(m) => m,
1032 Err(_) => return false,
1033 };
1034 let dot_meta = match fs::metadata(".") {
1035 Ok(m) => m,
1036 Err(_) => return false,
1037 };
1038 if pwd_meta.dev() != dot_meta.dev() || pwd_meta.ino() != dot_meta.ino() {
1039 return false;
1040 }
1041 // Reject any component that is exactly `.` or `..` — the same loop
1042 // zsh runs after the dev/ino check.
1043 for comp in pwd.split('/') {
1044 if comp == "." || comp == ".." {
1045 return false;
1046 }
1047 }
1048 true
1049}
1050
1051// ---------------------------------------------------------------------------
1052// Missing utility functions ported from utils.c
1053// ---------------------------------------------------------------------------
1054
1055/// Port of `slashsplit()` from `Src/utils.c:837` — C decl `static char **slashsplit(char *s)`.
1056///
1057/// Split path into components (from utils.c slashsplit).
1058///
1059///
1060/// C body (c:837-863):
1061/// ```c
1062/// if (!*s) return zshcalloc(...); // c:842 — empty input → empty
1063/// for (t = s, t0 = 0; *t; t++) // c:845 — count slashes
1064/// if (*t == '/') t0++;
1065/// q = r = zalloc(sizeof(char*) * (t0 + 2));
1066/// while ((t = strchr(s, '/'))) { // c:850
1067/// *q++ = ztrduppfx(s, t - s); // c:851 — emit prefix
1068/// while (*t == '/') t++; // c:852-853 — collapse runs
1069/// if (!*t) { *q = NULL; return r; } // c:854-857 — trailing `/` ends
1070/// s = t;
1071/// }
1072/// *q++ = ztrdup(s); // c:860 — final tail
1073/// ```
1074///
1075/// Three behaviors that the previous `split('/').filter(non_empty)`
1076/// Rust port got WRONG:
1077/// 1. **Leading `/` keeps an empty segment** (c:851 with `t == s`).
1078/// C: `slashsplit("/usr")` → `["", "usr"]`; previous Rust dropped
1079/// the empty, returning `["usr"]`. Caller `xsymlinks` (c:879)
1080/// iterates the result building `xbuf` byte-by-byte — the
1081/// empty leading segment is how it knows to start from `/`.
1082/// Without it, absolute-path resolution silently became relative.
1083/// 2. **Consecutive slashes collapse** (c:852-853 inner while-loop).
1084/// C: `"a//b"` → `["a", "b"]` (drop empty between). Filter-on-
1085/// empty Rust gets this right by coincidence.
1086/// 3. **Trailing `/` drops** (c:854-857). C: `"a/b/"` → `["a", "b"]`.
1087/// Filter-on-empty Rust gets this right by coincidence.
1088///
1089/// Pin: the leading-empty-segment behavior IS the C contract, not
1090/// an oversight to filter out. Matches `xsymlinks` and any future
1091/// path-walker port that reads slashsplit output.
1092pub fn slashsplit(s: &str) -> Vec<String> {
1093 // c:837
1094 if s.is_empty() {
1095 // c:842
1096 return Vec::new();
1097 }
1098 let mut result = Vec::new();
1099 let mut rest = s;
1100 // c:850 — `while ((t = strchr(s, '/')))`.
1101 while let Some(pos) = rest.find('/') {
1102 result.push(rest[..pos].to_string()); // c:851 ztrduppfx
1103 rest = &rest[pos..];
1104 // c:852-853 — `while (*t == '/') t++;` collapse run.
1105 while let Some(rest_after) = rest.strip_prefix('/') {
1106 rest = rest_after;
1107 }
1108 // c:854-857 — if walked off end, return without emitting tail.
1109 if rest.is_empty() {
1110 return result;
1111 }
1112 }
1113 // c:860 — `*q++ = ztrdup(s);` emit final tail.
1114 result.push(rest.to_string());
1115 result
1116}
1117
1118/// Port of `xsymlinks()` from `Src/utils.c:872` — C decl `static int xsymlinks(char *s)`.
1119///
1120/// Expands `.` and `..` components AND follows ONE LEVEL of
1121/// symlinks (per C source comment at c:865-867: "expands .. or .
1122/// expressions and one level of symlinks"). Used by the `:A`/`:P`
1123/// modifier paths via `subst.rs:6896`.
1124///
1125/// The previous Rust port did `.`/`..` normalization ONLY, with a
1126/// stale doc-comment claiming "Does NOT follow symlinks (matches
1127/// the `physical = 0` mode in C)." That claim is FALSE: C
1128/// `xsymlinks` IS the symlink-following form (it calls `readlink(2)`
1129/// at c:908). The `physical = 0` no-symlink path is a different
1130/// fn (`xsymlink` at utils.c:971, without the trailing `s`). Result:
1131/// `:A` modifier output never resolved actual symlinks — `:A` on
1132/// `/tmp/link -> /usr` returned `/tmp/link` instead of `/usr`.
1133///
1134/// Rewrite: walk components; for each non-`.`/`..` component, try
1135/// `readlink` on the accumulated path. If it succeeds, the target
1136/// replaces (or prepends to) the accumulator; otherwise the
1137/// component appends as-is. C handles re-rooting (absolute symlink
1138/// target replaces buf) and component-level concat.
1139pub fn xsymlinks(s: &str) -> io::Result<String> {
1140 // c:872
1141 if s.is_empty() {
1142 return Ok(String::new());
1143 }
1144
1145 let path = if !s.starts_with('/') {
1146 // c:879 — slashsplit
1147 let cwd = std::env::current_dir()?;
1148 format!("{}/{}", cwd.display(), s)
1149 } else {
1150 s.to_string()
1151 };
1152
1153 let components: Vec<&str> = path.split('/').collect();
1154 // c:877 — `xbuflen = strlen(xbuf)`. Start with empty xbuf for
1155 // absolute paths (the leading "" from slashsplit handles that).
1156 let mut xbuf = String::new();
1157 for comp in components {
1158 match comp {
1159 "" | "." => continue, // c:881
1160 ".." => {
1161 // c:883
1162 if xbuf == "/" || xbuf.is_empty() {
1163 // c:886-889
1164 continue;
1165 }
1166 // c:891-895 — walk back one `/`-delimited component.
1167 if let Some(pos) = xbuf.rfind('/') {
1168 xbuf.truncate(pos);
1169 }
1170 }
1171 c => {
1172 // c:905-907 — `memcpy xbuf2, xbuf` then append `/comp`.
1173 let candidate = format!("{}/{}", xbuf, c); // c:907
1174 // c:908 — `readlink(unmeta(xbuf2), xbuf3, PATH_MAX)`.
1175 #[cfg(unix)]
1176 {
1177 match fs::read_link(&candidate) {
1178 // c:908
1179 Ok(target) => {
1180 // c:918-933 — successful readlink: target is
1181 // either absolute (replaces xbuf wholesale) or
1182 // relative (appends with `/`).
1183 let t = target.to_string_lossy().into_owned();
1184 if t.starts_with('/') {
1185 // c:927
1186 xbuf = t; // c:928
1187 } else {
1188 xbuf = format!("{}/{}", xbuf, t); // c:930-931
1189 }
1190 continue;
1191 }
1192 Err(_) => {
1193 // c:909-916 — readlink failed (not a symlink),
1194 // append the component verbatim.
1195 xbuf = candidate; // c:910-912
1196 }
1197 }
1198 }
1199 #[cfg(not(unix))]
1200 {
1201 xbuf = candidate;
1202 }
1203 }
1204 }
1205 }
1206
1207 if xbuf.is_empty() {
1208 Ok(if path.starts_with('/') {
1209 "/".to_string()
1210 } else {
1211 ".".to_string()
1212 })
1213 } else {
1214 Ok(xbuf)
1215 }
1216}
1217
1218/// Port of `xsymlink()` from `Src/utils.c:971` — C decl `xsymlink(char *s, int heap)`.
1219///
1220/// ```c
1221/// mod_export char *
1222/// xsymlink(char *s, int heap)
1223/// {
1224/// if (*s != '/')
1225/// return NULL;
1226/// *xbuf = '\0';
1227/// if (!chrealpath(&s, 'P', heap)) {
1228/// zwarn("path expansion failed, using root directory");
1229/// return heap ? dupstring("/") : ztrdup("/");
1230/// }
1231/// return s;
1232/// }
1233/// ```
1234///
1235/// Returns `Some(resolved)` on success, `None` if the path isn't
1236/// absolute (C: returns NULL). On resolve failure emits the same
1237/// "path expansion failed, using root directory" warning and
1238/// returns `Some("/")`.
1239///
1240/// C body (c:971-980):
1241/// ```c
1242/// char *xsymlink(char *s, int heap)
1243/// {
1244/// if (*s != '/') return NULL;
1245/// *xbuf = '\0';
1246/// if (!chrealpath(&s, 'P', heap)) {
1247/// zwarn("path expansion failed, using root directory");
1248/// return heap ? dupstring("/") : ztrdup("/");
1249/// }
1250/// return s;
1251/// }
1252/// ```
1253///
1254/// Previous Rust port was a FAKE — called `std::fs::canonicalize`
1255/// directly with the rationale "same semantics for symlink-
1256/// resolution". That bypassed the canonical `chrealpath` port
1257/// (hist.rs:2311) which handles the partial-prefix walk + xbuf
1258/// state that `fs::canonicalize` doesn't replicate. Re-port now
1259/// matches C line-by-line.
1260///
1261/// Rust signature drift: `heap` param dropped — Rust strings always
1262/// live on the heap; the C `heap` flag toggles between zhalloc
1263/// (heap arena) and ztrdup (process-wide) allocation which has no
1264/// distinction in Rust. Always defaults to `false` (ztrdup arm) at
1265/// the chrealpath call.
1266pub fn xsymlink(path: &str) -> Option<String> {
1267 // c:971
1268 // c:973 — `if (*s != '/') return NULL;`
1269 if !path.starts_with('/') {
1270 // c:973
1271 return None;
1272 }
1273 // c:974 — `*xbuf = '\0';` Reset the xbuf cursor; no-op in Rust
1274 // (xbuf is an internal chrealpath buffer not exposed at this level).
1275 // c:975 — `if (!chrealpath(&s, 'P', heap))`
1276 match chrealpath(path, b'P', false) {
1277 // c:975
1278 Some(r) => Some(r), // c:979 return s
1279 None => {
1280 // c:976 failure arm
1281 // c:977 — `zwarn("path expansion failed, using root directory");`
1282 zwarn("path expansion failed, using root directory"); // c:977
1283 // c:978 — `return heap ? dupstring("/") : ztrdup("/");`
1284 Some("/".to_string()) // c:978
1285 }
1286 }
1287}
1288
1289/// Port of `print_if_link()` from `Src/utils.c:985` — C decl `void print_if_link(char *s, int all)`.
1290///
1291/// "Print arrow + symlink target(s) iff `s` is an absolute path
1292/// pointing through symlinks." When `all` is set, follows the
1293/// chain (`xsymlinks` loop, c:992); otherwise emits just the final
1294/// realpath if it differs from the input. Always relative to
1295/// stdout. The Rust port writes via `print!` to mirror C's
1296/// `printf`/`zputs(stdout)` calls.
1297pub fn print_if_link(s: &str, all: bool) {
1298 // c:985
1299 if !s.starts_with('/') {
1300 // c:987
1301 return;
1302 }
1303 if all {
1304 // c:988
1305 let mut start = s.to_string();
1306 loop {
1307 // c:992
1308 match xsymlinks(&start) {
1309 Ok(target) if !target.is_empty() && target != start => {
1310 print!(" -> "); // c:994
1311 print!("{}", if target.is_empty() { "/" } else { &target }); // c:995
1312 start = target; // c:998-999
1313 }
1314 _ => break, // c:1002
1315 }
1316 }
1317 } else {
1318 // c:1006
1319 // c:1007-1011 — HAVE_MEMCCPY arm: copy s into a PATH_MAX-1 buffer
1320 // and DPUTS1 if the input overflows. Rust's String has no fixed
1321 // PATH_MAX limit but the C parity-check still fires when a
1322 // pathological caller passes a path that wouldn't fit C's
1323 // s_at_entry[PATH_MAX+1] buffer.
1324 DPUTS1!(
1325 // c:1009
1326 s.len() >= libc::PATH_MAX as usize, // c:1008 memccpy returns NULL
1327 "path longer than PATH_MAX: {}",
1328 s // c:1009
1329 );
1330 let s_at_entry = s.to_string(); // c:1013
1331 // c:1015 — `if (chrealpath(&s, 'P', 0) && strcmp(s, s_at_entry))`
1332 // The previous Rust port called std::fs::canonicalize directly —
1333 // a fake that bypassed the canonical chrealpath port (hist.rs:2311).
1334 let mut resolved = s.to_string(); // c:1015 &s in/out
1335 if let Some(r) = chrealpath(&resolved, b'P', false) {
1336 // c:1015
1337 resolved = r;
1338 if resolved != s_at_entry {
1339 // c:1015 strcmp(s, s_at_entry)
1340 print!(" -> "); // c:1016
1341 print!("{}", if resolved.is_empty() { "/" } else { &resolved });
1342 // c:1017
1343 }
1344 }
1345 }
1346 let _ = io::stdout().flush();
1347}
1348
1349/// Port of `fprintdir()` from `Src/utils.c:1031` — C decl `void fprintdir(char *s, FILE *f)`.
1350///
1351/// "print a directory" — abbreviates `s` via `finddir` (so a path
1352/// matching `$HOME` or a `nameddirtab` entry shows as `~name/...`)
1353/// and returns the rendering. The C source writes to a FILE*; Rust
1354/// returns the string for the caller to print.
1355pub fn fprintdir(s: &str) -> String {
1356 // c:1031
1357 match finddir(s) {
1358 // c:1031
1359 None => unmeta(s), // c:1036
1360 Some(rendered) => rendered, // c:1038-1040
1361 }
1362}
1363
1364/// Port of `substnamedir()` from `Src/utils.c:1053` — C decl `char *substnamedir(char *s)`.
1365///
1366/// C body (c:1053-1061):
1367/// ```c
1368/// Nameddir d = finddir(s);
1369/// if (!d)
1370/// return quotestring(s, QT_BACKSLASH);
1371/// return zhtricat("~", d->node.nam, quotestring(s + strlen(d->dir),
1372/// QT_BACKSLASH));
1373/// ```
1374///
1375/// The previous Rust port was a FAKE — it called `finddir(s)` (Rust
1376/// signature returns `Option<String>` of the already-formatted
1377/// `~name/rest`) and returned that string unchanged in the Some-arm,
1378/// missing the `quotestring(..., QT_BACKSLASH)` C applies to the
1379/// residue. C's `finddir` returns a `Nameddir` pointer; the
1380/// `~name` + quoted-residue join lives here in `substnamedir`.
1381///
1382/// This re-port duplicates the HOME-first + nameddirtab-scan logic
1383/// (finddir_scan at c:1106) so it can take the (name, dir_prefix)
1384/// split BEFORE pre-joining, then apply quotestring to the residue
1385/// per c:1059.
1386pub fn substnamedir(s: &str) -> String {
1387 // c:1053
1388 // C `finddir` at c:1127 runs the homenode through the SAME
1389 // best-diff scan as the nameddirtab (c:1164-1165) — home COMPETES
1390 // with `hash -d` entries by `diff`, it does not preempt them.
1391 // Duplicate that competition here without the pre-format, so we
1392 // can apply quotestring on just the residue (c:1059).
1393 let home = getsparam("HOME").unwrap_or_default(); // c:1133
1394 let mut finddir_best: i32 = 0; // c:1163
1395 let mut best: Option<(String, String)> = None; // finddir_last
1396 let home_diff = if home.len() == 1 {
1397 0
1398 } else {
1399 home.len() as i32
1400 }; // c:1140-1141
1401 if home_diff > 0 {
1402 if let Some(rest) = s.strip_prefix(&home) {
1403 if rest.is_empty() || rest.starts_with('/') {
1404 finddir_best = home_diff;
1405 // HOME is the implicit homenode (no name).
1406 best = Some((String::new(), rest.to_string()));
1407 }
1408 }
1409 }
1410 // c:1106 finddir_scan — best-diff walk over nameddirtab.
1411 if let Some((name, rest, diff)) = finddir_scan(s) {
1412 if diff > finddir_best {
1413 best = Some((name, rest));
1414 }
1415 }
1416 match best {
1417 // c:1059 — `zhtricat("~", d->node.nam, quotestring(s + strlen(d->dir), QT_BACKSLASH))`
1418 Some((name, rest)) => format!("~{}{}", name, quotestring(&rest, QT_BACKSLASH)),
1419 None => quotestring(s, QT_BACKSLASH), // c:1058
1420 }
1421}
1422
1423// ===========================================================
1424// Direct ports of utility entries from Src/utils.c.
1425// ===========================================================
1426
1427/// Cached current-user lookup.
1428/// Port of `get_username()` from Src/utils.c:1075 — `getpwuid(3)`
1429/// against the current real uid, with the result cached so a
1430/// re-call after setuid sees the new identity. The C source uses
1431/// a `cached_uid`+`cached_username` pair guarded by a uid match;
1432/// the Rust port uses an `OnceLock` keyed on uid for the same
1433/// invalidate-on-uid-change behaviour.
1434pub fn get_username() -> String {
1435 static CACHE: Mutex<Option<(u32, String)>> = Mutex::new(None);
1436
1437 let current_uid = unsafe { libc::getuid() };
1438 let mut guard = CACHE.lock().unwrap();
1439 if let Some((uid, name)) = &*guard {
1440 if *uid == current_uid {
1441 return name.clone();
1442 }
1443 }
1444 let name = unsafe {
1445 let pw = libc::getpwuid(current_uid);
1446 if pw.is_null() {
1447 String::new() // c:1088 — `ztrdup("")` fallback
1448 } else {
1449 // c:1086 — `cached_username = ztrdup_metafy(pswd->pw_name);`.
1450 // `ztrdup_metafy` calls `metafy((char *)s, -1, META_DUP)`
1451 // (utils.c:4929). The previous Rust port returned the raw
1452 // pw_name verbatim — fine for ASCII usernames, but high-bit
1453 // bytes (e.g. an `émile` username on some systems) would
1454 // surface to callers un-metafied. zsh's downstream pipeline
1455 // (param expansion, prompt rendering) assumes paramtab
1456 // entries are metafied; an un-metafied high-bit byte breaks
1457 // the Meta-escape contract.
1458 let raw = std::ffi::CStr::from_ptr((*pw).pw_name)
1459 .to_string_lossy()
1460 .into_owned();
1461 metafy(&raw) // c:1086 ztrdup_metafy
1462 }
1463 };
1464 *guard = Some((current_uid, name.clone()));
1465 name
1466}
1467
1468/// Port of `finddir_scan()` from `Src/utils.c:1106`.
1469///
1470/// Port of `finddir_scan(HashNode hn, UNUSED(int flags))` from Src/utils.c:1106-1112 — ScanFunc the
1471/// C source registers with `scanhashtable(nameddirtab, …)`. Keeps the
1472/// entry with the LARGEST `diff` (zsh.h:2152 — `strlen(dir) -
1473/// strlen(nam)`, the abbreviation savings — NOT the raw dir length)
1474/// whose dir is a boundary-prefix of the path (`!dircmp`, c:1075-1086)
1475/// and which is not ND_NOABBREV (PWD/OLDPWD entries, c:1108).
1476/// WARNING: param names don't match C — Rust=(path) vs C=(hn, flags);
1477/// the C statics finddir_full/finddir_last/finddir_best are threaded
1478/// as the argument and the `(name, rest, diff)` return instead.
1479pub fn finddir_scan(path: &str) -> Option<(String, String, i32)> {
1480 // c:1106
1481 let table = nameddirtab().lock().ok()?;
1482 let mut best: Option<(String, String, i32)> = None;
1483 for (name, nd) in table.iter() {
1484 // c:1107-1108 — `nd->diff > finddir_best && !dircmp(nd->dir,
1485 // finddir_full) && !(nd->node.flags & ND_NOABBREV)`.
1486 if (nd.node.flags & crate::ported::zsh_h::ND_NOABBREV) != 0 {
1487 continue;
1488 }
1489 if best.as_ref().is_some_and(|b| nd.diff <= b.2) {
1490 continue;
1491 }
1492 if let Some(rest) = path.strip_prefix(nd.dir.as_str()) {
1493 if rest.is_empty() || rest.starts_with('/') {
1494 best = Some((name.clone(), rest.to_string(), nd.diff));
1495 }
1496 }
1497 }
1498 best
1499}
1500
1501/// Port of `finddir()` from `Src/utils.c:1127` — C decl `Nameddir finddir(char *s)`.
1502///
1503/// "See if a path has a named directory as its prefix." Compares
1504/// `s` against `$HOME` first (longest implicit named dir), then
1505/// scans the global `nameddirtab`, then falls back to
1506/// `subst_string_by_hook("zsh_directory_name", "d", s)`.
1507///
1508/// Rust signature returns `Option<String>` (the abbreviated path
1509/// `~name/rest`) instead of the C `Nameddir` pointer.
1510pub fn finddir(path: &str) -> Option<String> {
1511 // c:1127
1512 // c:1138 — `homenode.dir = home ? home : "";`. Reads the C global
1513 // `char *home` (params.c:91) DIRECTLY — not via paramtab. zshrs
1514 // ports the global as `params::home_lock()` accessed by
1515 // `homegetfn` (which ignores its ¶m arg per
1516 // `c:5109 UNUSED(Param pm)`). Pass a default param so the read
1517 // works even before paramtab["HOME"] has been hydrated (during
1518 // early init / unit-test environments).
1519 let _default_pm = crate::ported::zsh_h::param::default();
1520 let home = crate::ported::params::homegetfn(&_default_pm); // c:1138 home
1521 // c:1139-1141 + 1164-1165 — the home node (nam="", diff =
1522 // strlen(home), root's 1 → 0) runs through the SAME best-diff scan
1523 // as the table (`finddir_scan(&homenode.node, 0);
1524 // scanhashtable(nameddirtab, …)`): it COMPETES with `hash -d`
1525 // entries instead of preempting them, so a named dir with a larger
1526 // diff wins (e.g. ~ZPWR over ~/.zpwr). The previous early return
1527 // on the $HOME prefix hid every named dir under $HOME.
1528 let mut finddir_best: i32 = 0; // c:1163
1529 let mut best: Option<(String, String)> = None; // finddir_last
1530 let home_diff = if home.len() == 1 {
1531 0
1532 } else {
1533 home.len() as i32
1534 }; // c:1140-1141
1535 if home_diff > 0 {
1536 if let Some(rest) = path.strip_prefix(&home) {
1537 if rest.is_empty() || rest.starts_with('/') {
1538 finddir_best = home_diff;
1539 best = Some((String::new(), rest.to_string()));
1540 }
1541 }
1542 }
1543 if let Some((name, rest, diff)) = finddir_scan(path) {
1544 // c:1165 — scanhashtable: only a strictly larger diff replaces
1545 // the home candidate (finddir_scan's `nd->diff > finddir_best`).
1546 if diff > finddir_best {
1547 finddir_best = diff;
1548 best = Some((name, rest));
1549 }
1550 }
1551 // c:1167-1175 — zsh_directory_name hook — returns ["name", "len"];
1552 // wins only when its match length beats finddir_best (c:1169).
1553 if let Some(reply) = subst_string_by_hook("zsh_directory_name", Some("d"), path) {
1554 if reply.len() >= 2 {
1555 // c:1170
1556 if let Ok(len) = reply[1].parse::<i32>() {
1557 if len > finddir_best && (len as usize) <= path.len() {
1558 return Some(format!("~[{}]{}", reply[0], &path[len as usize..]));
1559 }
1560 }
1561 }
1562 }
1563 best.map(|(name, rest)| format!("~{}{}", name, rest)) // c:1177
1564}
1565
1566/// Port of `adduserdir()` from `Src/utils.c:1187`.
1567///
1568/// Port of `void adduserdir(char *s, char *t, int flags, int always)`
1569/// from Src/utils.c:1187.
1570///
1571/// Adds (or removes when `t` is empty / non-absolute) an entry in
1572/// the global `nameddirtab`. ND_USERNAME entries from `getpwnam`
1573/// don't override explicit assignments. AUTONAMEDIRS gating, the
1574/// trailing-slash trim, and PWD/OLDPWD ND_NOABBREV stamp are all
1575/// preserved. Routes through `crate::ported::hashnameddir`.
1576pub fn adduserdir(name: &str, dir: &str, flags: i32, always: bool) {
1577 // c:1187
1578
1579 if !interact() {
1580 return;
1581 } // c:1193
1582 if let Ok(t) = nameddirtab().lock() {
1583 if (flags & ND_USERNAME) != 0 && t.contains_key(name) {
1584 // c:1199
1585 return;
1586 }
1587 if !always && !isset(AUTONAMEDIRS) && !t.contains_key(name) {
1588 // c:1207
1589 return;
1590 }
1591 }
1592 // c:1211 — `if (!t || *t != '/' || strlen(t) >= PATH_MAX)`. C
1593 // rejects paths >= PATH_MAX as too long (would overflow path-
1594 // expansion buffers downstream). PATH_MAX is platform-dependent
1595 // (4096 on Linux, 1024 on macOS); libc::PATH_MAX exposes it.
1596 if dir.is_empty() || !dir.starts_with('/') || dir.len() >= libc::PATH_MAX as usize
1597 // c:1211 strlen(t) >= PATH_MAX
1598 {
1599 let _ = removenameddirnode(name); // c:1214
1600 return;
1601 }
1602 let mut trimmed = dir.trim_end_matches('/').to_string(); // c:1224-1226
1603 if trimmed.is_empty() {
1604 trimmed = dir.to_string(); // c:1227-1233
1605 }
1606 let final_flags = if name == "PWD" || name == "OLDPWD" {
1607 // c:1237
1608 flags | ND_NOABBREV
1609 } else {
1610 flags
1611 };
1612 // c:1239 — `nd = (Nameddir) zshcalloc(sizeof *nd); nd->node.flags = …;
1613 // nd->dir = ztrdup(t); addnode(nameddirtab, ztrdup(s), nd);`
1614 let nd = nameddir {
1615 node: hashnode {
1616 next: None,
1617 nam: name.to_string(),
1618 flags: final_flags,
1619 },
1620 dir: trimmed,
1621 diff: 0,
1622 };
1623 crate::ported::hashnameddir::addnameddirnode(name, nd);
1624}
1625
1626/// Port of `getnameddir()` from `Src/utils.c:1247` — C decl `char *getnameddir(char *name)`.
1627///
1628/// Looks up `name` in `nameddirtab`; if absent, checks for a
1629/// scalar parameter whose value starts with `/` and registers it
1630/// via `adduserdir`; finally falls back to `getpwnam(name)` for
1631/// `~user`-style lookups when USE_GETPWNAM is enabled.
1632pub fn getnameddir(name: &str) -> Option<String> {
1633 // c:1247
1634 if let Ok(t) = nameddirtab().lock() {
1635 if let Some(nd) = t.get(name) {
1636 // c:1254
1637 return Some(nd.dir.clone());
1638 }
1639 }
1640 // c:1257-1264 — `if ((pm = realparamtab->getnode2(realparamtab, name)) &&
1641 // !pm->level && (PM_TYPE(pm->node.flags) == PM_SCALAR) &&
1642 // (str = getsparam(name)) && *str == '/') { pm->node.flags |=
1643 // PM_NAMEDDIR; adduserdir(name, str, 0, 1); return str; }`. Only a
1644 // GLOBAL string parameter names a directory (54328); a reference is
1645 // PM_NAMEREF, not PM_SCALAR, so `~ref` does not resolve through it.
1646 let global_scalar = crate::ported::params::paramtab()
1647 .read()
1648 .ok()
1649 .and_then(|t| {
1650 t.get(name).map(|pm| {
1651 pm.level == 0
1652 && crate::ported::zsh_h::PM_TYPE(pm.node.flags as u32)
1653 == crate::ported::zsh_h::PM_SCALAR
1654 })
1655 })
1656 .unwrap_or(false);
1657 if global_scalar {
1658 if let Some(s) = getsparam(name) {
1659 if s.starts_with('/') {
1660 if let Ok(mut t) = crate::ported::params::paramtab().write() {
1661 if let Some(pm) = t.get_mut(name) {
1662 pm.node.flags |= crate::ported::zsh_h::PM_NAMEDDIR as i32; // c:1261
1663 }
1664 }
1665 adduserdir(name, &s, 0, true); // c:1262
1666 return Some(s); // c:1263
1667 }
1668 }
1669 }
1670 #[cfg(unix)]
1671 {
1672 // c:1268 — getpwnam fallback.
1673 let cn = CString::new(name).ok()?;
1674 let pw = unsafe { libc::getpwnam(cn.as_ptr()) };
1675 if !pw.is_null() {
1676 let raw_dir = unsafe {
1677 std::ffi::CStr::from_ptr((*pw).pw_dir)
1678 .to_string_lossy()
1679 .into_owned()
1680 };
1681 // c:1273-1274 — `isset(CHASELINKS) ? xsymlink(pw->pw_dir, 0)
1682 // : ztrdup(pw->pw_dir);`. Resolve symlinks when the option
1683 // is set. Previously omitted in the Rust port — silently
1684 // returned the raw passwd-db dir even when the user had
1685 // `setopt chaselinks`.
1686 let dir = if isset(CHASELINKS) {
1687 xsymlink(&raw_dir).unwrap_or(raw_dir)
1688 } else {
1689 raw_dir
1690 };
1691 // c:1276 — `adduserdir(name, dir, ND_USERNAME, 1);`
1692 // Cache the lookup so subsequent `~user` expansions hit the
1693 // nameddirtab fast-path at c:1254 instead of round-tripping
1694 // through getpwnam every time.
1695 adduserdir(name, &dir, ND_USERNAME, true); // c:1276
1696 return Some(dir);
1697 }
1698 }
1699 None
1700}
1701
1702/// Port of `dircmp()` from `Src/utils.c:1296` — C decl `dircmp(char *s, char *t)`.
1703///
1704/// Compare directory paths (from utils.c dircmp)
1705pub fn dircmp(s: &str, t: &str) -> bool {
1706 let s = s.trim_end_matches('/');
1707 let t = t.trim_end_matches('/');
1708 s == t
1709}
1710
1711// Add a function to the list of pre-prompt functions. // c:1332
1712/// Port of `addprepromptfn()` from `Src/utils.c:1319` — C decl `addprepromptfn(voidvoidfnptr_t func)`.
1713///
1714/// Register a callback to run before each prompt.
1715pub fn addprepromptfn(func: fn()) {
1716 // c:1319
1717 PREPROMPT_FNS.lock().unwrap().push(func);
1718}
1719
1720// Remove a function from the list of pre-prompt functions. // c:1332
1721/// Port of `delprepromptfn()` from `Src/utils.c:1332` — C decl `delprepromptfn(voidvoidfnptr_t func)`.
1722///
1723/// Remove a previously-registered pre-prompt callback.
1724pub fn delprepromptfn(func: fn()) {
1725 // c:1332
1726 let mut list = PREPROMPT_FNS.lock().unwrap();
1727 if let Some(pos) = list.iter().position(|f| *f as usize == func as usize) {
1728 list.remove(pos);
1729 }
1730}
1731
1732// Add a function to the list of timed functions. // c:1367
1733/// Port of `addtimedfn()` from `Src/utils.c:1371`.
1734///
1735/// Port of `void addtimedfn(voidvoidfnptr_t func, time_t when)` from
1736/// `Src/utils.c:1371`. Faithful walk of the timedfns LinkList:
1737/// allocate a `Timedfn`, lazy-init the list when empty, otherwise scan
1738/// from `firstnode` and insert BEFORE the first node whose `when` is
1739/// greater than ours. The standard linklist API only inserts AFTER a
1740/// node, so the C loop carries `ln` as the previous node and inserts
1741/// before `next` once `when < next->when`. Note: zsh's `time_t` is
1742/// signed and historically negative-`when` was supported, so we keep
1743/// `i64`.
1744pub fn addtimedfn(func: fn(), when: i64) {
1745 // c:1371
1746 let mut list = TIMED_FNS.lock().unwrap(); // c:1365 timedfns
1747 let tfdat: (i64, fn()) = (when, func); // c:1373-1375 Timedfn tfdat
1748 if list.is_empty() {
1749 // c:1377 !timedfns
1750 list.push(tfdat); // c:1378-1379 znewlinklist + zaddlinknode
1751 return;
1752 }
1753 if list.is_empty() {
1754 // c:1394 !ln (firstnode of empty list)
1755 list.push(tfdat); // c:1395 zaddlinknode
1756 return; // c:1396
1757 }
1758 let mut idx: usize = 0; // c:1381 LinkNode ln = firstnode(timedfns)
1759 loop {
1760 // c:1398 for(;;)
1761 let next = idx + 1; // c:1400 LinkNode next = nextnode(ln)
1762 if next >= list.len() {
1763 // c:1401 !next
1764 list.push(tfdat); // c:1402 zaddlinknode
1765 return; // c:1403
1766 }
1767 let tfdat2_when = list[next].0; // c:1405 tfdat2 = getdata(next)
1768 if when < tfdat2_when {
1769 // c:1406 when < tfdat2->when
1770 list.insert(next, tfdat); // c:1407 zinsertlinknode(timedfns, ln, tfdat)
1771 return; // c:1408
1772 }
1773 idx = next; // c:1410 ln = next
1774 }
1775}
1776
1777/// Port of `deltimedfn()` from `Src/utils.c:1430` — C decl `deltimedfn(voidvoidfnptr_t func)`.
1778///
1779/// Remove a registered timed function (first occurrence only).
1780pub fn deltimedfn(func: fn()) {
1781 // c:1430
1782 let mut list = TIMED_FNS.lock().unwrap();
1783 if let Some(pos) = list.iter().position(|(_, f)| *f as usize == func as usize) {
1784 list.remove(pos);
1785 }
1786}
1787
1788/// Port of `callhookfunc()` from `Src/utils.c:1469`.
1789///
1790/// Invoke a hook function by name plus any `<name>_functions` array.
1791/// Port of `callhookfunc(char *name, LinkList lnklst, int arrayp, int *retval)` from Src/utils.c:1469. Returns 0 if at
1792/// least one hook ran, 1 otherwise — the C source uses the same
1793/// stat semantics so the prompt machinery can detect "did periodic
1794/// fire". Hook dispatch goes through the executor singleton (which
1795/// owns the function table); we look up `name` and then walk the
1796/// `<name>_functions` array exactly as the C source does at
1797/// Src/utils.c:1469.
1798/// Direct port of
1799/// `int callhookfunc(char *name, LinkList lnklst, int arrayp, int *retval)`
1800/// at `Src/utils.c:1469`. C signature mapping:
1801/// - `char *name` → `name: &str`
1802/// - `LinkList lnklst` → `lnklst: Option<&[String]>` (Rust-shape
1803/// stand-in for the C-shape `LinkList` — both are an ordered list
1804/// of meta-fied strings; the LinkList port itself diverges)
1805/// - `int arrayp` → `arrayp: i32`
1806/// - `int *retval` → `retval: *mut i32` (out-param; NULL is fine
1807/// when the caller doesn't need the doshfunc return value)
1808///
1809/// Returns `stat` — 0 if at least one shfunc fired, 1 otherwise
1810/// (matches `c:1495 stat = 0` after every dispatch). When `retval` is
1811/// non-null, `*retval` is set to the most recent `doshfunc`-returned
1812/// status, mirroring the C body's `*retval = ret` semantics.
1813pub fn callhookfunc(name: &str, lnklst: Option<&[String]>, arrayp: i32, retval: *mut i32) -> i32 {
1814 let mut stat: i32 = 1; // c:1475
1815 let mut ret: i32 = 0; // c:1475
1816
1817 // Build the doshfunc arg list `[fname ($0), $1, $2, …]`. A provided
1818 // `lnklst` already carries the ORIGINAL hook name at [0] and the real
1819 // positional args at [1..] (matching C, where the caller does
1820 // `addlinknode(args, name)` before the args). So substitute `fname` for
1821 // [0] — `fname` is the hook name on the direct path (c:1487, identity)
1822 // and a `${name}_functions` member on the array path (c:1504-1508,
1823 // `arg0 = [*arrptr] + argarr[1..]`) — and KEEP [1..].
1824 //
1825 // The prior code prepended `fname` to the WHOLE list (name included),
1826 // so every hook invoked with args saw `$1` == its own name (e.g.
1827 // `preexec`'s `$1` was "preexec" instead of the command line). c:1483.
1828 let mk_args = |fname: &str| -> Vec<String> {
1829 let mut v: Vec<String> = vec![fname.to_string()];
1830 if let Some(extra) = lnklst {
1831 if extra.len() > 1 {
1832 v.extend_from_slice(&extra[1..]);
1833 }
1834 }
1835 v
1836 };
1837
1838 // c:1495 — `if ((shfunc = getshfunc(name))) { doshfunc(...); stat = 0; }`
1839 let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
1840 .read()
1841 .ok()
1842 .and_then(|t| t.get(name).cloned());
1843 if let Some(mut shf) = shf_clone {
1844 let args = mk_args(name);
1845 // c:1487 — `ret = doshfunc(shfunc, lnklst, 1);`. Direct
1846 // doshfunc invocation; body_runner routes through the host
1847 // body-only entry to avoid double-wrapping the scope.
1848 let name_for_body = name.to_string();
1849 let body_args = args.clone();
1850 let body_runner = move || -> i32 {
1851 crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
1852 };
1853 ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
1854 stat = 0; // c:1504
1855 }
1856
1857 if arrayp != 0 {
1858 // c:1507-1525 — fire every `${name}_functions` hook in order.
1859 let arr_name = format!("{}_functions", name); // c:1511
1860 let arr = crate::ported::params::paramtab()
1861 .read()
1862 .ok()
1863 .and_then(|t| t.get(&arr_name).and_then(|p| p.u_arr.clone()))
1864 .unwrap_or_default(); // c:1512 getaparam
1865 for fn_name in arr {
1866 // c:1514
1867 let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
1868 .read()
1869 .ok()
1870 .and_then(|t| t.get(&fn_name).cloned());
1871 if let Some(mut shf) = shf_clone {
1872 // c:1518
1873 let args = mk_args(&fn_name);
1874 // c:1508 — `newret = doshfunc(shfunc, arg0, 1);`.
1875 let name_for_body = fn_name.clone();
1876 let body_args = args.clone();
1877 let body_runner = move || -> i32 {
1878 crate::ported::exec::run_function_body(&name_for_body, &body_args[1..])
1879 .unwrap_or(0)
1880 };
1881 ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
1882 stat = 0; // c:1520
1883 }
1884 }
1885 }
1886
1887 // c:1528 — `if (retval) *retval = ret;`
1888 if !retval.is_null() {
1889 unsafe {
1890 *retval = ret;
1891 }
1892 }
1893 let _ = ret;
1894
1895 stat // c:1530
1896}
1897
1898// do pre-prompt stuff // c:1530
1899/// Run pre-prompt machinery: precmd, periodic, prepromptfns.
1900/// Port of `preprompt()` from Src/utils.c:1530: the `PROMPT_SP` heuristic, the
1901/// pending-job scan, the `precmd` hook + `precmd_functions` array, the
1902/// `periodic` hook on its PERIOD-second cadence, the mail check and the
1903/// prepromptfns registry.
1904pub fn preprompt() {
1905 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1906 // Stamp duration + exit status onto the SQLite history row hend()
1907 // added for the just-finished command (no-op when nothing is
1908 // pending). preprompt runs immediately after execode returns and
1909 // before the next ZLE read, so the elapsed time is command
1910 // wall-time, not prompt idle.
1911 crate::history::history_sqlite_finish(crate::ported::builtin::LASTVAL.load(Ordering::Relaxed));
1912 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1913 // Flush the autoload bytecode cache. `put_one` buffers rather than
1914 // rewriting the whole shard per autoload, and this is the batch
1915 // boundary: every function the just-finished command (or a <TAB>
1916 // completion) autoloaded goes out in ONE write. Cheap when nothing
1917 // is buffered, which is the common case.
1918 crate::autoload_cache::try_flush_pending();
1919 crate::deparse_cache::try_flush_pending();
1920 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1921 // Native p10k engine (src/extensions/p10k): snapshot `$?` and close
1922 // the command timer BEFORE the precmd hook runs, so precmd's own
1923 // commands can't clobber what the status / command_execution_time
1924 // segments show. Mirrors p10k's `_p9k_save_status` precmd ordering.
1925 crate::p10k::note_command_finished(
1926 crate::ported::builtin::LASTVAL.load(Ordering::Relaxed) as i64
1927 );
1928 // c:1532 `static time_t lastperiodic;` — periodic-hook last-fire timestamp.
1929 static LAST_PERIODIC: AtomicI64 = AtomicI64::new(0);
1930 // `lastmailcheck` is module-scoped (LAST_MAILCHECK below) because C makes it
1931 // a file-static shared between checkmail() and checkmailpath() (c:1447).
1932
1933 // c:1535-1536 — `zlong period = getiparam("PERIOD"); zlong mailcheck
1934 // = getiparam("MAILCHECK");`
1935 let period = crate::ported::params::getiparam("PERIOD");
1936 let mailcheck = crate::ported::params::getiparam("MAILCHECK");
1937
1938 // c:1538-1543 — `winch_unblock(); ... winch_block();` — let any
1939 // pending SIGWINCH fire so the prompt picks up the latest column
1940 // count, then re-block to prevent the resize-handler from
1941 // interrupting prompt rendering mid-paint.
1942 crate::ported::signals_h::winch_unblock();
1943 crate::ported::signals_h::winch_block();
1944
1945 // c:1543-1565 — PROMPT_SP heuristic: when the last command left
1946 // dangling output (no trailing newline), print `$PROMPT_EOL_MARK`,
1947 // pad to the right margin, then `\r` + w spaces + `\r`. On a line that
1948 // already ended cleanly the marker is overwritten by the padding and
1949 // nothing shows; on a partial line the pad wraps to the next row, so
1950 // the prompt starts fresh and the truncated output stays visible with
1951 // the inverse `%` marker.
1952 //
1953 // Was previously left as a no-op on the grounds that `countprompt`
1954 // could not measure zshrs's expanded prompt (which carries readline
1955 // `\x01`/`\x02` ignore markers rather than C's Inpar/Outpar). That is
1956 // stale: countprompt accepts BOTH conventions (prompt.rs:3458).
1957 //
1958 // Without this, `printf 100` (or any output whose last line is
1959 // partial) had its tail overwritten by the next prompt — zsh shows
1960 // `100%` then a fresh prompt line, zshrs showed the prompt painted
1961 // over the `100`.
1962 // !!! WARNING: RUST-ONLY GUARD — NO C COUNTERPART !!!
1963 // In C, `preprompt` runs only from loop() between commands, with the
1964 // cursor at a known baseline. zshrs also reaches it while ZLE owns the
1965 // screen — e.g. with a completion listing still displayed — and the mark
1966 // plus its right-margin padding then lands INSIDE that listing, blanking
1967 // the row from the cursor rightwards (`-z -- push arguments%` … `stack`).
1968 // Only write the mark when ZLE is not holding the display.
1969 let zle_owns_screen = crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0
1970 || crate::ported::zle::zle_refresh::LISTSHOWN.load(Ordering::Relaxed) != 0;
1971 if isset(crate::ported::zsh_h::PROMPTSP)
1972 && isset(crate::ported::zsh_h::PROMPTCR)
1973 && crate::ported::init::use_exit_printed.load(Ordering::SeqCst) == 0
1974 // c:1543 `&& shout` — init_shout (c:Src/init.c:735-739) points
1975 // `shout` at stderr for an interactive shell without a tty
1976 // (SHTTY == -1), and init_io only calls it when interactive
1977 // (c:Src/init.c:705-706).
1978 && (crate::ported::init::SHTTY.load(Ordering::Relaxed) >= 0
1979 || isset(crate::ported::zsh_h::INTERACTIVE))
1980 && !zle_owns_screen
1981 {
1982 // c:1550-1554 — `$PROMPT_EOL_MARK`, default `%B%S%#%s%b`.
1983 let eolmark = crate::ported::params::getsparam("PROMPT_EOL_MARK")
1984 .filter(|s| !s.is_empty())
1985 .unwrap_or_else(|| "%B%S%#%s%b".to_string());
1986 // c:1555 — the mark is always `%`-expanded, whatever PROMPT_PERCENT
1987 // is set to; restore the option afterwards (c:1559).
1988 let percents = crate::ported::options::opt_state_get("promptpercent");
1989 crate::ported::options::opt_state_set("promptpercent", true);
1990 // c:1556 — `txtunknownattrs = TXT_ATTR_ALL;`
1991 crate::ported::prompt::txtunknownattrs
1992 .store(crate::ported::zsh_h::TXT_ATTR_ALL, Ordering::Relaxed);
1993 let (mark, _, _) = crate::ported::prompt::promptexpand(&eolmark, 1, None); // c:1557
1994 let mut w: i32 = 0; // c:1552
1995 let mut h: i32 = 1;
1996 crate::ported::prompt::countprompt(&mark, &mut w, &mut h, -1); // c:1558
1997 if let Some(p) = percents {
1998 crate::ported::options::opt_state_set("promptpercent", p);
1999 }
2000 // c:1560 `zputs(str, shout)` — the tty, or stderr (c:Src/init.c:738).
2001 let fd = match crate::ported::init::SHTTY.load(Ordering::Relaxed) {
2002 -1 => 2,
2003 tty => tty,
2004 };
2005 // c:1561 reads the `zterm_columns` global as it stands. With no tty
2006 // it is never resampled (adjustwinsize returns early on SHTTY == -1,
2007 // c:Src/utils.c:1900), so it can be 0; `adjustcolumns()` would
2008 // instead seed 80 into it, changing $COLUMNS as a side effect.
2009 let columns = ZTERM_COLUMNS.load(Ordering::SeqCst);
2010 // c:1561-1562 — `fprintf(shout, "%*s\r%*s\r",
2011 // zterm_columns - w - !hasxn, "", w, "")`. A negative `%*s`
2012 // width means left-justify in a field of its magnitude (C99
2013 // 7.21.6.1p5), so the pad is |n| spaces, never clamped to zero.
2014 let pad = (columns
2015 - w
2016 - if crate::ported::init::hasxn.load(Ordering::SeqCst) != 0 {
2017 0
2018 } else {
2019 1
2020 })
2021 .unsigned_abs() as usize;
2022 // zshrs's `promptexpand` keeps C's Inpar/Outpar non-printing spans as
2023 // readline RL_PROMPT_*_IGNORE bytes (\x01/\x02) — `countprompt` reads
2024 // them (prompt.rs:3458) but the TERMINAL must never see them, exactly
2025 // as zle_refresh filters them before every write (zle_refresh.rs:2274).
2026 let mut out: String = mark
2027 .chars()
2028 .filter(|&c| c != '\u{1}' && c != '\u{2}')
2029 .collect();
2030 out.push_str(&" ".repeat(pad));
2031 out.push('\r');
2032 out.push_str(&" ".repeat(w.max(0) as usize));
2033 out.push('\r');
2034 let _ = write_loop(fd, out.as_bytes()); // c:1560-1563
2035 }
2036
2037 // Reap any children that exited while the shell sat idle. zsh's
2038 // SIGCHLD handler reaps asynchronously during the ZLE read (signals
2039 // are unqueued there), but zshrs runs the interactive event loop
2040 // inside a queued-signal window (init.rs `queue_signals()` at loop
2041 // top), so zhandler QUEUES each SIGCHLD instead of reaping it
2042 // (signals.rs:373). Disowned / background jobs (`&!`, `&`, and
2043 // `<(…)` process substitutions) therefore piled up as zombies
2044 // between prompts — every zpwr/zinit/p10k async `&!` per prompt
2045 // leaked a zombie, slowing startup and eventually exhausting the
2046 // process table. Mirror zhandler's reap+route here, right before
2047 // the scanjobs notification pass, so `waitpid(-1)` clears the
2048 // zombies and the DONE bits land for scanjobs to print/delete. No
2049 // foreground job is live at preprompt, so reaping -1 can't steal a
2050 // fg wait's status.
2051 {
2052 let reaped = crate::ported::signals::wait_for_processes();
2053 if !reaped.is_empty() {
2054 if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
2055 if let Ok(mut guard) = jt.lock() {
2056 for (pid, status) in reaped {
2057 let _ = crate::ported::jobs::update_bg_job(&mut guard, pid, status);
2058 }
2059 }
2060 }
2061 }
2062 // c:Src/jobs.c:651-652 — the `dotrap(SIGCHLD)` update_job owes for a
2063 // child that is not `thisjob`, run now that JOBTAB is unlocked.
2064 for _ in 0..crate::ported::jobs::CHLD_TRAP_PENDING.swap(0, std::sync::atomic::Ordering::SeqCst) {
2065 crate::ported::signals::dotrap(libc::SIGCHLD);
2066 }
2067 // c:Src/jobs.c:658-678 — the trapped SIGINT/SIGQUIT pseudo-delivery
2068 // update_job parked in PSEUDO_SIG_TRAP_PENDING while it held the
2069 // JOBTAB guard: `dotrap(sig); if (errflag) breaks = loops;
2070 // check_cursh_sig(sig);`, run now that JOBTAB is unlocked.
2071 let pseudo = crate::ported::jobs::PSEUDO_SIG_TRAP_PENDING.swap(0, std::sync::atomic::Ordering::SeqCst);
2072 if pseudo != 0 {
2073 crate::ported::signals::dotrap(pseudo); // c:663
2074 if errflag.load(Ordering::Relaxed) != 0 {
2075 crate::ported::builtin::BREAKS.store(
2076 crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
2077 Ordering::Relaxed,
2078 ); // c:671-672
2079 }
2080 if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
2081 crate::ported::jobs::check_cursh_sig(&jt.lock().unwrap_or_else(|e| e.into_inner()), pseudo); // c:677
2082 }
2083 }
2084 }
2085
2086 // c:1569-1572 — `if (unset(NOTIFY)) scanjobs();` — sync job-status
2087 // print before prompt, via the canonical scanjobs port
2088 // (Src/jobs.c:1993).
2089 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::NOTIFY) {
2090 if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
2091 let mut guard = jt.lock().unwrap();
2092 crate::ported::jobs::scanjobs(&mut guard);
2093 }
2094 }
2095
2096 // c:1573-1574 — `if (errflag) return;` — bail if a previous error
2097 // already set the global flag.
2098 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
2099 // the C line cited above tests the WHOLE errflag, so masking here let an
2100 // interrupted shell keep going where zsh stops.
2101 if errflag.load(Ordering::Relaxed) != 0 {
2102 return;
2103 }
2104
2105 // c:1576-1580 — `callhookfunc("precmd", NULL, 1, NULL);` + errflag bail.
2106 callhookfunc("precmd", None, 1, std::ptr::null_mut());
2107 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
2108 return;
2109 }
2110
2111 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2112 // Native p10k engine (src/extensions/p10k): rebuild PROMPT/RPROMPT
2113 // after the precmd hook so user precmd state is fresh. Equivalent
2114 // to the zsh theme's `_p9k_precmd` running as the last precmd
2115 // hook (p10k.zsh registers _p9k_precmd via add-zsh-hook). No-op
2116 // unless the powerlevel10k theme source was intercepted.
2117 crate::p10k::preprompt_render();
2118
2119 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
2120 // Fire the `async_precmd` hooks on the worker pool AFTER the prompt is
2121 // built/rendered, so they never block the paint. Non-blocking: submits to
2122 // the pool and returns immediately; the hooks write into the shared param
2123 // table which the NEXT prompt render reads. (Same call shape as the p10k
2124 // render above — the ShellExecutor-using logic lives in the extension.)
2125 crate::async_precmd::fire_async_precmd();
2126
2127 // c:1582-1589 — periodic-hook dispatch on PERIOD cadence.
2128 if period > 0 {
2129 let now = std::time::SystemTime::now()
2130 .duration_since(UNIX_EPOCH)
2131 .map(|d| d.as_secs() as i64)
2132 .unwrap_or(0);
2133 if now > LAST_PERIODIC.load(Ordering::Relaxed) + period
2134 && callhookfunc("periodic", None, 1, std::ptr::null_mut()) == 0
2135 {
2136 LAST_PERIODIC.store(now, Ordering::Relaxed);
2137 }
2138 }
2139 // c:1588-1589 — `if (errflag) return;` post-periodic bail.
2140 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
2141 // the C line cited above tests the WHOLE errflag, so masking here let an
2142 // interrupted shell keep going where zsh stops.
2143 if errflag.load(Ordering::Relaxed) != 0 {
2144 return;
2145 }
2146
2147 // c:1591-1611 — mail check: `if (mailcheck && difftime(now,
2148 // lastmailcheck) > mailcheck)` walk MAILPATH (or scalar MAIL) via
2149 // checkmailpath. Faithful port — checkmailpath already exists at
2150 // utils.rs:1623, getaparam exists for the MAILPATH array.
2151 let currentmailcheck = std::time::SystemTime::now()
2152 .duration_since(UNIX_EPOCH)
2153 .map(|d| d.as_secs() as i64)
2154 .unwrap_or(0);
2155 if mailcheck > 0 && (currentmailcheck - LAST_MAILCHECK.load(Ordering::Relaxed)) > mailcheck {
2156 // c:1597 — `if (mailpath && *mailpath && **mailpath)`
2157 let mailpath = crate::ported::params::getaparam("MAILPATH");
2158 let has_mailpath = mailpath
2159 .as_ref()
2160 .map(|p| !p.is_empty() && p.first().map(|s| !s.is_empty()).unwrap_or(false))
2161 .unwrap_or(false);
2162 if has_mailpath {
2163 // c:1598 `checkmailpath(mailpath);` — emit each message to the
2164 // terminal (C writes directly to `shout`).
2165 for m in checkmailpath(mailpath.as_deref().unwrap()) {
2166 println!("{}", m);
2167 }
2168 } else {
2169 // c:1600-1608 — `if ((mailfile = getsparam("MAIL")) && *mailfile)
2170 // { x[0]=mailfile; x[1]=NULL; checkmailpath(x); }`
2171 crate::ported::signals::queue_signals(); // c:1600
2172 if let Some(mailfile) = crate::ported::params::getsparam("MAIL") {
2173 if !mailfile.is_empty() {
2174 let x = vec![mailfile]; // c:1604-1605
2175 for m in checkmailpath(&x) {
2176 println!("{}", m); // c:1606
2177 }
2178 }
2179 }
2180 crate::ported::signals::unqueue_signals(); // c:1608
2181 }
2182 LAST_MAILCHECK.store(currentmailcheck, Ordering::Relaxed); // c:1610
2183 }
2184
2185 // c:1613-1618 — `if (prepromptfns) for (...) ppnode->func();`
2186 let snapshot: Vec<fn()> = PREPROMPT_FNS.lock().unwrap().clone();
2187 for f in snapshot {
2188 f();
2189 }
2190}
2191
2192/// `static time_t lastmailcheck;` (Src/utils.c:1447) — the timestamp of the
2193/// previous mail check. `checkmail()` reads it (mtime/atime comparisons happen
2194/// against the PREVIOUS check) and writes it after walking the paths.
2195pub static LAST_MAILCHECK: AtomicI64 = AtomicI64::new(0);
2196
2197/// Port of `checkmailpath()` from `Src/utils.c:1623` — C decl `void checkmailpath(char **s)`.
2198/// Walks each `PATH` / `PATH?message` MAILPATH component:
2199/// - empty component → `zerr` (c:1634-1636)
2200/// - `mailstat` failure → `zerr` unless ENOENT (c:1637-1639)
2201/// - directory → list entries (`PATH/entry[?message]`) and recurse
2202/// (c:1640-1669)
2203/// - file, interactive (`shout`) only: "You have new mail." or the
2204/// `?`-suffixed message expanded with `$_` bound to the file
2205/// (`parsestr`+`singsub`, c:1670-1699); and, under `MAILWARNING`,
2206/// "The mail in PATH has been read." (c:1700-1704)
2207///
2208/// **Signature note:** C is `void` and writes to `shout`; the zshrs caller
2209/// drives output, so this returns the messages (in C emission order) instead.
2210/// The new-mail / read-warning conditions are now ported faithfully — the
2211/// previous adhoc port used an unfaithful "mtime within 60s" heuristic and its
2212/// result was discarded by the caller, so the feature did nothing.
2213pub fn checkmailpath(s: &[String]) -> Vec<String> {
2214 // c:1620
2215 let mut messages = Vec::new();
2216 let interactive = *crate::ported::init::shout.lock().unwrap() != 0; // c:1670 `else if (shout)`
2217 let lastmailcheck = LAST_MAILCHECK.load(Ordering::Relaxed);
2218
2219 for path in s {
2220 // c:1627-1633 — split on the first '?': `file` before, `u` after (or None).
2221 let (file, u) = match path.find('?') {
2222 Some(p) => (&path[..p], Some(&path[p + 1..])),
2223 None => (path.as_str(), None),
2224 };
2225
2226 if file.is_empty() {
2227 // c:1634-1636 — `if (**s == 0) zerr("empty MAILPATH component: %s", *s);`
2228 zerr(&format!("empty MAILPATH component: {}", path));
2229 continue;
2230 }
2231
2232 let mut st: libc::stat = unsafe { std::mem::zeroed() };
2233 let real = unmeta(file); // c:1637 — mailstat(unmeta(*s), &st)
2234 if mailstat(&real, &mut st) == -1 {
2235 // c:1638-1639 — report anything other than "no such file".
2236 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2237 if e != libc::ENOENT {
2238 zerr(&format!("{}: {}", zsh_errno_msg(e), path));
2239 }
2240 } else if (st.st_mode as libc::mode_t & libc::S_IFMT) == libc::S_IFDIR {
2241 // c:1640-1669 — a directory: enumerate entries and recurse.
2242 match fs::read_dir(&real) {
2243 Err(_) => {
2244 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2245 zerr(&format!("{}: {}", zsh_errno_msg(e), path)); // c:1647
2246 }
2247 Ok(mut rd) => {
2248 let mut arr = Vec::new();
2249 // c:1653-1662 — `while ((fn = zreaddir(lock, 1)) && !errflag)`.
2250 while let Some(fname) = zreaddir(&mut rd, 1) {
2251 if errflag.load(Ordering::Relaxed) != 0 {
2252 break;
2253 }
2254 // c:1654-1657 — keep the `?message` suffix on each entry.
2255 arr.push(match u {
2256 Some(u) => format!("{}/{}?{}", path, fname, u),
2257 None => format!("{}/{}", path, fname),
2258 });
2259 }
2260 messages.extend(checkmailpath(&arr)); // c:1667 — recurse
2261 }
2262 }
2263 } else if interactive {
2264 // c:1671-1672 — new mail: non-empty, not yet read, newer than the
2265 // last check.
2266 if st.st_size != 0 && st.st_atime <= st.st_mtime && st.st_mtime >= lastmailcheck {
2267 match u {
2268 // c:1673-1675 — `fprintf(shout, "You have new mail.\n");`
2269 None => messages.push("You have new mail.".to_string()),
2270 // c:1676-1698 — custom message: bind `$_` to the file, then
2271 // parsestr + singsub the message (prompt-style expansion).
2272 Some(u) => {
2273 let usav = crate::ported::init::zunderscore.lock().unwrap().clone();
2274 crate::ported::exec::setunderscore(path); // c:1685 setunderscore(*s)
2275 if let Ok(parsed) = crate::ported::lex::parsestr(u) {
2276 // c:1688-1691 — parse ok → singsub then emit.
2277 messages.push(crate::ported::subst::singsub(&parsed));
2278 }
2279 crate::ported::exec::setunderscore(&usav); // c:1694-1697 restore $_
2280 }
2281 }
2282 }
2283 // c:1700-1704 — MAILWARNING: file read since the last check.
2284 if crate::ported::zsh_h::isset(crate::ported::zsh_h::MAILWARNING)
2285 && st.st_atime > st.st_mtime
2286 && st.st_atime > lastmailcheck
2287 && st.st_size != 0
2288 {
2289 messages.push(format!("The mail in {} has been read.", unmeta(file)));
2290 }
2291 }
2292 }
2293 messages
2294}
2295
2296/// Port of `FILE *xtrerr` from `Src/utils.c:1714` — the stream xtrace
2297/// output goes to. Held as the fd behind the FILE: `2` is `stderr`
2298/// (c:Src/init.c:637 `xtrerr = stderr`). `execcmd_exec` points it at a
2299/// private copy of stderr for the duration of a simple command
2300/// (c:Src/exec.c:3766-3772), so the trace
2301/// of `print x 2>file` — and of whatever a redirected `.` runs — reaches
2302/// the stderr the command was started with, not the file.
2303#[allow(non_upper_case_globals)]
2304pub static xtrerr: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(2); // c:1714
2305
2306/// Port of `printprompt4()` from `Src/utils.c:1718`.
2307///
2308/// Render the PS4 / PROMPT4 prefix and write it to stderr. zsh's
2309/// implementation reads `prompt4` global, suppresses XTRACE during
2310/// promptexpand (so subshells inside `%(?…)` don't recursively trace),
2311/// then fprintf's the expanded prefix to xtrerr. zshrs uses the same
2312/// suppress-XTRACE-around-expand pattern; ksh/sh emulation defaults
2313/// to `+ ` per Src/init.c:1192, zsh default to `+%N:%i> `.
2314///
2315/// The C source's caller (Src/exec.c::tracingcond etc.) follows this
2316/// with the per-line/per-arg fprintf — same shape mirrored at the two
2317/// zshrs call sites in fusevm_bridge.rs (BUILTIN_XTRACE_LINE / ARGS).
2318pub(crate) fn printprompt4() {
2319 // c:1720-1721 — `if (!xtrerr) xtrerr = stderr;`. There is NO XTRACE
2320 // test here: every XTRACE caller gates itself, and source()'s
2321 // SOURCE_TRACE line (c:Src/init.c:1627-1630) prints PS4 with XTRACE off.
2322 // c:utils.c:1722-1724 — `if (prompt4) { ... s = dupstring(prompt4);`
2323 // C `prompt4` is a global initialized in init.c:1192 from the
2324 // emulation bits; PS4/PROMPT4 paramtab entries alias it via
2325 // IPDEF7R/IPDEF7 (params.c:381, 421). Read from paramtab to
2326 // honour user-set values, fall back to the same emulation
2327 // default C uses at init.c:1192.
2328 let posix = EMULATION(EMULATE_KSH | EMULATE_SH); // c:init.c:1192
2329 let prefix_template = getsparam("PS4")
2330 .or_else(|| getsparam("PROMPT4"))
2331 .unwrap_or_else(|| {
2332 if posix {
2333 "+ ".to_string() // c:init.c:1192
2334 } else {
2335 "+%N:%i> ".to_string() // c:init.c:1193
2336 }
2337 });
2338 // c:utils.c:1723,1726,1730 — `t = opts[XTRACE]; opts[XTRACE] = 0;
2339 // promptexpand(...); opts[XTRACE] = t;`
2340 let saved = isset(XTRACE);
2341 opt_state_set(&opt_name(XTRACE), false);
2342 let prefix = crate::prompt::expand_prompt(&prefix_template);
2343 opt_state_set(&opt_name(XTRACE), saved);
2344 // c:utils.c:1736 — `fprintf(xtrerr, "%s", s)`. Append to the xtrerr
2345 // line buffer rather than writing straight to stderr; the emitter
2346 // appends the command/args and flushes the whole line in one write,
2347 // matching C's single `fflush(xtrerr)` (so concurrent pipeline-stage
2348 // trace lines never interleave).
2349 crate::fusevm_bridge::xtrerr_fputs(&prefix);
2350}
2351
2352/// Port of `freestr()` from `Src/utils.c:1739` — C decl `freestr(void *a)`.
2353///
2354/// C body:
2355/// ```c
2356/// void freestr(void *a) { zsfree(a); }
2357/// ```
2358/// The C function is registered as the `freenode` callback for
2359/// hashtables holding plain string values. The Rust port consumes
2360/// `a` by value; Rust's `Drop` runs the equivalent of `zsfree` when
2361/// the `String` is moved into this fn and falls out of scope at the
2362/// closing brace — the no-op body is the correct port. Param name
2363/// `a` matches C exactly per Rule E.
2364pub fn freestr(_a: String) {}
2365
2366/// Port of `gettempfile()` from `Src/utils.c:2231`.
2367///
2368/// Port of `int gettempfile(const char *prefix, int use_heap, char **tempname)`
2369/// from Src/utils.c:2231. Creates a fresh tempfile with `O_RDWR|O_CREAT|O_EXCL`
2370/// and mode 0600 under umask 0177; returns `(fd, path)` on success.
2371///
2372/// C signature uses an out-param for the filename and returns the fd; Rust
2373/// returns both as a tuple. Matches the C control-flow: open with O_EXCL,
2374/// retry up to 16 times on EEXIST (the non-mkstemp branch, c:2255-2269).
2375pub fn gettempfile(prefix: Option<&str>) -> Option<(i32, String)> {
2376 // c:2231
2377 #[cfg(unix)]
2378 {
2379 queue_signals(); // c:2239
2380 let old_umask = unsafe { libc::umask(0o177) }; // c:2240
2381 let mut failures = 0; // c:2255
2382 let mut result: Option<(i32, String)> = None;
2383 loop {
2384 let fn_ = match gettempname(prefix, false) {
2385 // c:2260
2386 Some(n) => n,
2387 None => break,
2388 };
2389 let cn = match CString::new(fn_.clone()) {
2390 Ok(c) => c,
2391 Err(_) => break,
2392 };
2393 let fd = unsafe {
2394 libc::open(
2395 cn.as_ptr(),
2396 libc::O_RDWR | libc::O_CREAT | libc::O_EXCL, // c:2264
2397 0o600 as libc::c_int,
2398 )
2399 };
2400 if fd >= 0 {
2401 result = Some((fd, fn_));
2402 break;
2403 }
2404 let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2405 if err != libc::EEXIST {
2406 // c:2269
2407 break;
2408 }
2409 failures += 1;
2410 if failures >= 16 {
2411 // c:2269
2412 break;
2413 }
2414 }
2415 unsafe {
2416 libc::umask(old_umask);
2417 } // c:2273
2418 unqueue_signals(); // c:2274
2419 result
2420 }
2421 #[cfg(not(unix))]
2422 {
2423 let _ = prefix;
2424 None
2425 }
2426}
2427
2428/// Port of `void gettyinfo(struct ttyinfo *ti)` from Src/utils.c:1746.
2429///
2430/// The saved baseline terminal mode — C's `struct ttyinfo shttyinfo`
2431/// (Src/init.c). Captured ONCE while the terminal is still in its
2432/// original (cooked / canonical) mode, before ZLE ever puts it into
2433/// raw mode via `zsetterm()`. `trashzle()` restores this baseline via
2434/// `settyinfo(&shttyinfo)` so that external commands the user runs
2435/// (`cat`, `less`, …) execute with a cooked terminal — where `^D` is
2436/// EOF, `^C` is SIGINT, and line editing works. Without it the tty
2437/// stayed in ZLE raw mode across command execution, so `cat` never
2438/// saw EOF on `^D`.
2439#[cfg(unix)]
2440pub static SHTTYINFO: std::sync::Mutex<Option<libc::termios>> = std::sync::Mutex::new(None);
2441
2442/// Port of `gettyinfo()` from `Src/utils.c:1746`.
2443///
2444/// Reads the current termios from the global `SHTTY`. Returns
2445/// `None` when SHTTY is closed or the call fails (matching C's
2446/// silent return when SHTTY == -1).
2447/// C body (single statement): `fdgettyinfo(SHTTY, ti);`
2448/// Rust returns `Option<termios>` instead of taking an out-ptr;
2449/// the SHTTY=-1 case naturally returns Err from fdgettyinfo → None.
2450#[cfg(unix)]
2451pub fn gettyinfo() -> Option<libc::termios> {
2452 // c:1746
2453 fdgettyinfo(SHTTY.load(Ordering::Relaxed)).ok() // c:1748
2454}
2455
2456/// Port of `fdgettyinfo()` from `Src/utils.c:1753`.
2457///
2458/// Emit the `$PS4` xtrace prefix to stderr.
2459/// Read terminal mode from a file descriptor.
2460/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1753. C source uses
2461/// `tcgetattr(SHTTY, &ti->tio)`; we return the populated termios
2462/// or an io::Error on failure (caller equivalent to zsh's `zerr`).
2463#[cfg(unix)]
2464/// WARNING: param names don't match C — Rust=(fd) vs C=(SHTTY, ti)
2465pub fn fdgettyinfo(fd: i32) -> io::Result<libc::termios> {
2466 let mut tio: libc::termios = unsafe { std::mem::zeroed() };
2467 if unsafe { libc::tcgetattr(fd, &mut tio) } == -1 {
2468 Err(io::Error::last_os_error())
2469 } else {
2470 Ok(tio)
2471 }
2472}
2473
2474/// Port of `fdgettyinfo()` from `Src/utils.c:1753` — C decl `fdgettyinfo(int SHTTY, struct ttyinfo *ti)`.
2475#[cfg(not(unix))]
2476/// WARNING: param names don't match C — Rust=(_fd) vs C=(SHTTY, ti)
2477pub fn fdgettyinfo(_fd: i32) -> std::io::Result<()> {
2478 Err(std::io::Error::new(
2479 std::io::ErrorKind::Unsupported,
2480 "no termios",
2481 ))
2482}
2483
2484/// Port of `settyinfo()` from `Src/utils.c:1778` — C decl `void settyinfo(struct ttyinfo *ti)`.
2485///
2486/// Restores the termios state on the global `SHTTY` with EINTR
2487/// retry; no-op when SHTTY is closed.
2488/// C body (single statement): `fdsettyinfo(SHTTY, ti);`
2489/// Rust returns bool; SHTTY=-1 yields Err → false from fdsettyinfo.
2490#[cfg(unix)]
2491pub fn settyinfo(ti: &libc::termios) -> bool {
2492 // c:1778
2493 fdsettyinfo(SHTTY.load(Ordering::Relaxed), ti).is_ok() // c:1780
2494}
2495
2496/// Port of `fdsettyinfo()` from `Src/utils.c:1785`.
2497///
2498/// Apply terminal mode to a file descriptor, with EINTR retry.
2499/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1785. C source loops
2500/// `while (tcsetattr(SHTTY, TCSADRAIN, &ti->tio) == -1 && errno
2501/// == EINTR)` — same retry shape here.
2502#[cfg(unix)]
2503pub fn fdsettyinfo(SHTTY2: i32, ti: &libc::termios) -> io::Result<()> {
2504 loop {
2505 if unsafe { libc::tcsetattr(SHTTY2, libc::TCSADRAIN, ti) } != -1 {
2506 return Ok(());
2507 }
2508 let err = io::Error::last_os_error();
2509 if err.kind() != io::ErrorKind::Interrupted {
2510 return Err(err);
2511 }
2512 }
2513}
2514
2515/// Port of `fdsettyinfo()` from `Src/utils.c:1785` — C decl `fdsettyinfo(int SHTTY, struct ttyinfo *ti)`.
2516#[cfg(not(unix))]
2517pub fn fdsettyinfo(SHTTY: i32, ti: &()) -> std::io::Result<()> {
2518 Err(std::io::Error::new(
2519 std::io::ErrorKind::Unsupported,
2520 "no termios",
2521 ))
2522}
2523
2524/// Port of the `zterm_lines` global declared at `Src/params.c:105`
2525/// (`zterm_lines, /* $LINES */`).
2526///
2527/// C aliases the `$LINES` parameter to this very storage —
2528/// `Src/params.c:363` `IPDEF5("LINES", &zterm_lines, zlevar_gsu)` — so
2529/// a shell script's `$LINES` and every C reader of `zterm_lines` are
2530/// one number, and it changes only where `adjustwinsize` writes it.
2531pub static ZTERM_LINES: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
2532
2533/// Port of the `zterm_columns` global declared at `Src/params.c:104`
2534/// (`zterm_columns, /* $COLUMNS */`), aliased to `$COLUMNS` by
2535/// `Src/params.c:362` `IPDEF5("COLUMNS", &zterm_columns, zlevar_gsu)`.
2536/// Same single-storage contract as `ZTERM_LINES` above.
2537pub static ZTERM_COLUMNS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
2538
2539// window size changed // c:1831
2540/// Reads the `zterm_lines` global — `Src/utils.c:1833` `int oldlines =
2541/// zterm_lines;` is the shape every C caller uses.
2542///
2543/// C's `adjustlines(int signalled)` (c:1831) is a FILE-STATIC helper
2544/// that only `adjustwinsize` calls; it refreshes `zterm_lines` from the
2545/// already-probed `shttyinfo.winsize` and returns whether the value
2546/// changed. Every other C site — `Src/Zle/compresult.c:1932`,
2547/// `Src/loop.c:375`, `Src/Zle/zle_refresh.c` — just reads the bare
2548/// global. This function stands for that read, which is why it returns
2549/// the row count rather than C's changed-flag.
2550///
2551/// The TIOCGWINSZ probe lives in `adjustwinsize` (c:1902) and NOWHERE
2552/// else. It used to live here, re-sampling the tty on every call, and
2553/// that broke C's single-storage contract: `$LINES`/`$COLUMNS` (updated
2554/// only when the SIGWINCH handler runs) and this function (live) gave
2555/// two different answers for the whole window between a resize and the
2556/// handler — which C blocks SIGWINCH across by default
2557/// (`Src/init.c:1458` `winch_block()`). A completion listing built its
2558/// display strings from `$COLUMNS` and then had `calclist` count them
2559/// against the live width: `git <TAB>` across a 24x80 -> 24x60 resize
2560/// asked "see all 164 possibilities (251 lines)?" for 164 one-line
2561/// matches.
2562///
2563/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2564pub fn adjustlines() -> usize {
2565 // c:1833
2566 let cached = ZTERM_LINES.load(Ordering::SeqCst);
2567 if cached > 0 {
2568 return cached as usize;
2569 }
2570 // c:1841-1845 — `if (zterm_lines <= 0) zterm_lines = tclines > 0 ?
2571 // tclines : 24;`
2572 //
2573 // !!! RUST-ONLY SEEDING !!! C can reach this with `shttyinfo.winsize`
2574 // already filled by `setupvals`'s `adjustwinsize(0)` (Src/init.c:1276);
2575 // zshrs has callers that run before that (see prompt.rs:3385), and
2576 // for them a one-shot probe is the only source of a real geometry.
2577 // Once seeded the value is cached like C's global, so the split
2578 // between "what the shell says" and "what the tty says" cannot
2579 // reopen.
2580 #[cfg(unix)]
2581 {
2582 unsafe {
2583 // c:1902 probes SHTTY, not fd 1; fall back to fd 1 only while
2584 // SHTTY is still unset (-1), which is where the previous
2585 // always-probe port read from.
2586 let shtty = SHTTY.load(Ordering::Relaxed);
2587 let fd = if shtty >= 0 { shtty } else { 1 };
2588 let mut ws: libc::winsize = std::mem::zeroed();
2589 if libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_row > 0 {
2590 ZTERM_LINES.store(ws.ws_row as i32, Ordering::SeqCst);
2591 return ws.ws_row as usize;
2592 }
2593 }
2594 }
2595 // paramtab `$LINES`, not OS env. See adjustcolumns for why the `> 0`
2596 // filter is load-bearing.
2597 let n = getsparam("LINES")
2598 .and_then(|s| s.parse().ok())
2599 .filter(|&n: &usize| n > 0)
2600 .unwrap_or(24); // c:1844
2601 ZTERM_LINES.store(n as i32, Ordering::SeqCst);
2602 n
2603}
2604
2605/// Reads the `zterm_columns` global — `Src/utils.c:1858` `int
2606/// oldcolumns = zterm_columns;`.
2607///
2608/// See `adjustlines` above for why this is a cached read and not the
2609/// TIOCGWINSZ probe it used to be; C's `adjustcolumns(int signalled)`
2610/// (c:1856) is the file-static half that only `adjustwinsize` calls.
2611///
2612/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2613pub fn adjustcolumns() -> usize {
2614 // c:1858
2615 let cached = ZTERM_COLUMNS.load(Ordering::SeqCst);
2616 if cached > 0 {
2617 return cached as usize;
2618 }
2619 // c:1866-1870 — `if (zterm_columns <= 0) zterm_columns = tccolumns >
2620 // 0 ? tccolumns : 80;`. Same Rust-only seeding note as
2621 // `adjustlines`.
2622 #[cfg(unix)]
2623 {
2624 unsafe {
2625 let shtty = SHTTY.load(Ordering::Relaxed);
2626 let fd = if shtty >= 0 { shtty } else { 1 };
2627 let mut ws: libc::winsize = std::mem::zeroed();
2628 if libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
2629 ZTERM_COLUMNS.store(ws.ws_col as i32, Ordering::SeqCst);
2630 return ws.ws_col as usize;
2631 }
2632 }
2633 }
2634 // C consults `getsparam("COLUMNS")` (paramtab), not OS env.
2635 // The `> 0` filter is C's `<= 0` clamp: a literal `COLUMNS=0` (what a
2636 // TERM=dumb pty leaves behind) parses fine, so without it callers that
2637 // divide by the column count panic instead of falling back to 80.
2638 let n = getsparam("COLUMNS")
2639 .and_then(|s| s.parse().ok())
2640 .filter(|&n: &usize| n > 0)
2641 .unwrap_or(80); // c:1869
2642 ZTERM_COLUMNS.store(n as i32, Ordering::SeqCst);
2643 n
2644}
2645
2646// window size changed // c:1824
2647/// Port of `adjustwinsize()` from `Src/utils.c:1889` — C decl `void adjustwinsize(int from)`.
2648/// SIGWINCH handler + LINES/COLUMNS-update entry. Reads the tty's
2649/// current geometry via `TIOCGWINSZ` ioctl, updates the cached
2650/// `zterm_lines`/`zterm_columns`, and writes `$LINES` / `$COLUMNS`
2651/// when they're already set in the environment.
2652/// ```c
2653/// void
2654/// adjustwinsize(int from)
2655/// {
2656/// static int getwinsz = 1;
2657/// int ttyrows = shttyinfo.winsize.ws_row;
2658/// int ttycols = shttyinfo.winsize.ws_col;
2659/// int resetzle = 0;
2660/// if (getwinsz || from == 1) {
2661/// if (SHTTY == -1) return;
2662/// if (ioctl(SHTTY, TIOCGWINSZ, &shttyinfo.winsize) == 0) {
2663/// resetzle = (ttyrows != ... || ttycols != ...);
2664/// ...
2665/// } else {
2666/// shttyinfo.winsize.ws_row = zterm_lines;
2667/// shttyinfo.winsize.ws_col = zterm_columns;
2668/// }
2669/// }
2670/// switch (from) {
2671/// case 0: case 1:
2672/// getwinsz = 0;
2673/// if (adjustlines(from) && zgetenv("LINES")) setiparam("LINES", zterm_lines);
2674/// if (adjustcolumns(from) && zgetenv("COLUMNS")) setiparam("COLUMNS", zterm_columns);
2675/// getwinsz = 1;
2676/// break;
2677/// case 2: resetzle = adjustlines(0); break;
2678/// case 3: resetzle = adjustcolumns(0); break;
2679/// }
2680/// if (interact && resetzle) zleentry(ZLE_CMD_REFRESH);
2681/// }
2682/// ```
2683pub fn adjustwinsize(from: i32) {
2684 // c:1889
2685 //
2686 // Returns nothing, like C's `void adjustwinsize(int from)` (c:1888-1889).
2687 // This used to return `(usize, usize)`, and it built that pair by calling
2688 // `adjustcolumns()` / `adjustlines()` again on the way out — a Rust-only
2689 // return value that no caller ever read (every call site is `let _ =`).
2690 // Those trailing re-entries DEADLOCKED the shell: `assignnparam` takes the
2691 // paramtab WRITE guard and still holds it when it dispatches the c:2874
2692 // gsu setfn (`intsetfn` on its PM_INTEGER arm), and the setfn behind
2693 // `$LINES` is `zlevarsetfn`, which calls back in here with from=2
2694 // (c:4232). Exiting through `adjustcolumns()` then reached
2695 // `getsparam("COLUMNS")` — its last-resort fallback, below — which wants a
2696 // READ lock on the very table the caller still holds for writing. std's
2697 // `RwLock` is not reentrant, so the shell
2698 // parked in `lock_contended` forever. C cannot hit this: `adjustcolumns`
2699 // (c:1856-1878) reads `shttyinfo.winsize` and `tccolumns` and never once
2700 // touches the parameter table.
2701 //
2702 // The window it needed was a terminal reporting 0 columns, because only
2703 // then does `adjustcolumns` fall past its ioctl to the parameter fallback.
2704 // A `zsh/zpty` slave is exactly that whenever the shell that spawned it is
2705 // NOT interactive: the stamp that would give the slave a real geometry,
2706 // `Src/Modules/zpty.c:374-377`, sits inside an `if (interact)` at
2707 // zpty.c:371, so a pty opened from a script keeps the 0x0 the kernel gave
2708 // it. Measured: both shells report 24x80 (their fallbacks, not a real
2709 // window) inside a zpty spawned from `zsh -f <script>`, and giving the
2710 // OUTER shell a 40x100 terminal changes nothing, because it is still not
2711 // interactive. That is every pty the test harness opens, which is why this
2712 // took out the zpty parity suite wholesale while leaving hand-run
2713 // interactive shells alone.
2714
2715 // c:1891 — `static int getwinsz = 1;`
2716 let getwinsz = ADJUSTWINSIZE_GETWINSZ.load(Ordering::SeqCst);
2717
2718 // c:1893-1894 — `int ttyrows = shttyinfo.winsize.ws_row;` /
2719 // `ttycols = shttyinfo.winsize.ws_col;` — the PREVIOUSLY cached
2720 // geometry, read before the ioctl overwrites it, so c:1903 can tell
2721 // whether the window actually changed size.
2722 //
2723 // !!! RUST-ONLY SOURCING !!! zshrs's `SHTTYINFO` (utils.rs) holds only
2724 // the `termios` half of C's `struct ttyinfo`; there is no cached
2725 // `winsize` to read. The last geometry this function published lives
2726 // in `ZTERM_LINES` / `ZTERM_COLUMNS`, which the c:1837/c:1862 stores
2727 // below keep in lock-step with `shttyinfo.winsize` — C's c:1839/c:1864
2728 // else-arms copy the globals straight back into that struct, so the
2729 // two are the same number. Same values, same comparison.
2730 let mut ttyrows: i32 = ZTERM_LINES.load(Ordering::SeqCst); // c:1893
2731 let mut ttycols: i32 = ZTERM_COLUMNS.load(Ordering::SeqCst); // c:1894
2732 let mut resetzle = 0i32; // c:1896
2733
2734 // c:1898-1917 — TIOCGWINSZ probe.
2735 if getwinsz != 0 || from == 1 {
2736 // c:1898
2737 let shtty = SHTTY.load(Ordering::Relaxed);
2738 if shtty == -1 {
2739 // c:1900 — no tty: `zterm_lines` / `zterm_columns` keep what
2740 // setupvals imported from the environment (c:Src/init.c:1294-1305),
2741 // or 0. `zsh -fc 'print $COLUMNS $LINES' </dev/null` with no
2742 // controlling tty prints `0 0`.
2743 return; // c:1901
2744 }
2745 #[cfg(unix)]
2746 unsafe {
2747 let mut ws: libc::winsize = std::mem::zeroed();
2748 if libc::ioctl(shtty, libc::TIOCGWINSZ, &mut ws as *mut _) == 0 {
2749 // c:1902
2750 // c:1903-1904 — `resetzle = (ttyrows != ws_row ||
2751 // ttycols != ws_col);`
2752 // This assignment was MISSING: `resetzle` was left at 0 on
2753 // the `from == 1` (SIGWINCH) path, so the c:1954 redraw
2754 // never fired and a resize left the screen as the terminal
2755 // had mangled it. With a completion listing displayed and
2756 // the window SHRUNK, the terminal drops the top rows and
2757 // nothing repaints them — zsh kept all 3 non-blank rows on
2758 // a 24x80 -> 14x80 shrink, zshrs kept 0.
2759 resetzle = (ttyrows != ws.ws_row as i32 || ttycols != ws.ws_col as i32) as i32;
2760 ttyrows = ws.ws_row as i32; // c:1907
2761 ttycols = ws.ws_col as i32; // c:1908
2762 }
2763 }
2764 }
2765
2766 match from {
2767 // c:1921
2768 0 | 1 => {
2769 // c:1922-1923
2770 ADJUSTWINSIZE_GETWINSZ.store(0, Ordering::SeqCst); // c:1924
2771 // c:1931-1934 — C: `if (adjustlines(from) && zgetenv("LINES"))
2772 // setiparam("LINES", zterm_lines);` (same for COLUMNS). The
2773 // zgetenv gate only guards re-publishing to an ENV-exported
2774 // LINES — in C the param itself is ALWAYS live because its
2775 // valptr aliases the `zterm_lines` global that adjustlines
2776 // just wrote (createparamtable IPDEF5 → zlevarsetfn/intvargetfn
2777 // on &zterm_lines). zshrs params store their own u_val copy
2778 // (no valptr aliasing), so the probe result must be written
2779 // through setiparam unconditionally — gating on the env left
2780 // `$LINES`/`$COLUMNS` at 0 for every interactive shell
2781 // (LINES is not in the environment; zsh doesn't export it).
2782 // Effect: `$(( LINES / 3 ))` = 0 → division-by-zero in
2783 // history-search-multi-word pagination (_hsmw_main:81), and
2784 // every LINES/COLUMNS-derived layout was wrong. Recursion is
2785 // safe: setiparam → zlevarsetfn → adjustwinsize(2|3) never
2786 // re-enters this arm.
2787 let lines = if ttyrows > 0 {
2788 ttyrows as i64 // c:1837 — zterm_lines = shttyinfo.winsize.ws_row
2789 } else {
2790 adjustlines() as i64 // c:1844 — tclines/24 fallback chain
2791 };
2792 // c:1837 — `zterm_lines = shttyinfo.winsize.ws_row;`, the ONE
2793 // place the row count is published. It has to land in the
2794 // global BEFORE setiparam, because setiparam's zlevarsetfn
2795 // recursion (c:1932 → params.c:4232 → this function, from=2)
2796 // reads it back.
2797 ZTERM_LINES.store(lines as i32, Ordering::SeqCst); // c:1837
2798 // c:1847-1850 — `if (zterm_lines > 2) termflags &= ~TERM_SHORT;
2799 // else termflags |= TERM_SHORT;`
2800 if lines > 2 {
2801 crate::ported::params::TERMFLAGS
2802 .fetch_and(!crate::ported::zsh_h::TERM_SHORT, Ordering::SeqCst); // c:1848
2803 } else {
2804 crate::ported::params::TERMFLAGS
2805 .fetch_or(crate::ported::zsh_h::TERM_SHORT, Ordering::SeqCst); // c:1850
2806 }
2807 setiparam("LINES", lines); // c:1932
2808 let cols = if ttycols > 0 {
2809 ttycols as i64 // c:1862 — zterm_columns = ws_col
2810 } else {
2811 adjustcolumns() as i64 // c:1869 — tccolumns/80 fallback chain
2812 };
2813 ZTERM_COLUMNS.store(cols as i32, Ordering::SeqCst); // c:1862
2814 // c:1872-1875 — the TERM_NARROW half of the same clamp.
2815 if cols > 2 {
2816 crate::ported::params::TERMFLAGS
2817 .fetch_and(!crate::ported::zsh_h::TERM_NARROW, Ordering::SeqCst); // c:1873
2818 } else {
2819 crate::ported::params::TERMFLAGS
2820 .fetch_or(crate::ported::zsh_h::TERM_NARROW, Ordering::SeqCst); // c:1875
2821 }
2822 setiparam("COLUMNS", cols); // c:1934
2823 ADJUSTWINSIZE_GETWINSZ.store(1, Ordering::SeqCst); // c:1935
2824 }
2825 2 => {
2826 // c:1937-1938 — `resetzle = adjustlines(0);`
2827 //
2828 // C arrives here from `zlevarsetfn` (Src/params.c:4232), which
2829 // has ALREADY done `*p = x` with `p == &zterm_lines`
2830 // (Src/params.c:363 IPDEF5) — that write is mirrored into
2831 // `ZTERM_LINES` by the zshrs `zlevarsetfn` port. So `oldlines`
2832 // here is the value the assignment just published, and
2833 // c:1836's `(signalled && …) || zterm_lines <= 0` is false for
2834 // any positive value: the else arm copies the global into
2835 // shttyinfo and leaves it alone, making c:1852's
2836 // `zterm_lines != oldlines` zero. Only an explicit `LINES=0`
2837 // falls through to the ws_row refresh.
2838 let oldlines = ZTERM_LINES.load(Ordering::SeqCst); // c:1833
2839 if oldlines <= 0 && ttyrows > 0 {
2840 ZTERM_LINES.store(ttyrows, Ordering::SeqCst); // c:1837
2841 }
2842 resetzle = (ZTERM_LINES.load(Ordering::SeqCst) != oldlines) as i32; // c:1852
2843 }
2844 3 => {
2845 // c:1940-1941 — `resetzle = adjustcolumns(0);` — the COLUMNS
2846 // half of the same shape (Src/utils.c:1858-1877).
2847 let oldcolumns = ZTERM_COLUMNS.load(Ordering::SeqCst); // c:1858
2848 if oldcolumns <= 0 && ttycols > 0 {
2849 ZTERM_COLUMNS.store(ttycols, Ordering::SeqCst); // c:1862
2850 }
2851 resetzle = (ZTERM_COLUMNS.load(Ordering::SeqCst) != oldcolumns) as i32; // c:1877
2852 }
2853 _ => {}
2854 }
2855
2856 // c:1945-1952 — the `interact && from >= 2` block is entirely commented
2857 // out in the C (the TIOCSWINSZ write-back is disabled), so nothing to
2858 // port. Keep the reads alive for the c:1954 gate below.
2859 let _ = ttyrows;
2860 let _ = ttycols;
2861
2862 // c:1954-1961 —
2863 // ```c
2864 // if (zleactive && resetzle) {
2865 // winchanged = resetneeded = 1;
2866 // zleentry(ZLE_CMD_RESET_PROMPT);
2867 // zleentry(ZLE_CMD_REFRESH);
2868 // }
2869 // ```
2870 // The whole block was absent, which is why a SIGWINCH never repainted
2871 // anything.
2872 if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0 && resetzle != 0 {
2873 // !!! RUST-ONLY GUARD !!! C protects this re-entry with
2874 // `queue_signals()` around every critical section (Src/signals.h:90),
2875 // so a SIGWINCH arriving inside zrefresh is deferred rather than
2876 // recursing. zshrs has no equivalent coverage over the ZLE refresh
2877 // path and its video state is behind non-reentrant `std::sync::Mutex`
2878 // handles, so a signal landing mid-refresh would DEADLOCK rather than
2879 // recurse. Serialise instead: a resize that arrives while this
2880 // repaint is in flight is dropped, exactly as C's queued signal is
2881 // superseded by the pending full redraw.
2882 if !ADJUSTWINSIZE_IN_ZLE.swap(true, Ordering::SeqCst) {
2883 WINCHANGED.store(1, Ordering::SeqCst); // c:1956
2884 RESETNEEDED.store(1, Ordering::SeqCst); // c:1958
2885 crate::ported::zle::zle_refresh::RESETNEEDED.store(1, Ordering::SeqCst); // c:1958
2886 crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_RESET_PROMPT); // c:1959
2887 crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_REFRESH); // c:1960
2888 ADJUSTWINSIZE_IN_ZLE.store(false, Ordering::SeqCst);
2889 }
2890 }
2891 // c:1962 — C falls off the end here. The `(adjustcolumns(), adjustlines())`
2892 // that used to stand in this spot was the deadlock described on the
2893 // signature above; nothing needs it, because every arm that can leave a
2894 // geometry unpublished is followed by a caller that seeds it (the from=0/1
2895 // arm stores both globals itself, and the SHTTY==-1 branch seeds both
2896 // before returning).
2897}
2898
2899/// Port of `static int getwinsz` from `Src/utils.c:1891`. Local
2900/// reentry guard inside adjustwinsize — bumped to 0 around the
2901/// setiparam recursion so the recursive call short-circuits.
2902pub static ADJUSTWINSIZE_GETWINSZ: std::sync::atomic::AtomicI32 =
2903 std::sync::atomic::AtomicI32::new(1); // c:1891
2904
2905/// !!! WARNING: RUST-ONLY GUARD !!! No C counterpart.
2906///
2907/// Set while `adjustwinsize`'s c:1954 branch is inside
2908/// `zleentry(ZLE_CMD_RESET_PROMPT)` / `zleentry(ZLE_CMD_REFRESH)`.
2909/// C reaches that branch from a signal handler and relies on
2910/// `queue_signals()` (`Src/signals.h:90`) to defer a SIGWINCH that lands
2911/// during a refresh; zshrs's ZLE video state sits behind non-reentrant
2912/// `std::sync::Mutex` handles, where the same re-entry hangs instead of
2913/// recursing. This flag drops the nested repaint; the outer one already
2914/// re-reads the geometry.
2915static ADJUSTWINSIZE_IN_ZLE: std::sync::atomic::AtomicBool =
2916 std::sync::atomic::AtomicBool::new(false);
2917
2918/// Port of `check_fd_table()` from `Src/utils.c:1969` — C decl `check_fd_table(int fd)`.
2919/// ```c
2920/// if (fd <= max_zsh_fd) return;
2921/// if (fd >= fdtable_size) {
2922/// int old_size = fdtable_size;
2923/// while (fd >= fdtable_size)
2924/// fdtable = zrealloc(fdtable, (fdtable_size *= 2) * sizeof(*fdtable));
2925/// memset(fdtable + old_size, 0, (fdtable_size - old_size) * sizeof(*fdtable));
2926/// }
2927/// max_zsh_fd = fd;
2928/// ```
2929/// C semantics: GROW the `fdtable` array so it can index `fd`, then
2930/// update `max_zsh_fd`. Returns void.
2931///
2932/// The fdtable global is `fdtable_lock()`; the body grows it with
2933/// `Vec::resize` and updates `MAX_ZSH_FD`. It does not validate the fd
2934/// (C does not either); the bool result is Rust-only and is false for a
2935/// negative `fd` beyond the current maximum.
2936pub fn check_fd_table(fd: i32) -> bool {
2937 // c:1969
2938 // c:1971-1972 — `if (fd <= max_zsh_fd) return;`
2939 let cur_max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:1971
2940 if fd <= cur_max {
2941 // c:1971
2942 return true; // c:1972 (return; in C)
2943 }
2944 if fd < 0 {
2945 // defensive — fdtable index must be ≥0
2946 return false;
2947 }
2948 // c:1974-1981 — `if (fd >= fdtable_size) { while (fd >= fdtable_size)
2949 // fdtable_size *= 2; zrealloc; memset(new_slots, 0); }`.
2950 // Rust Vec::resize handles the realloc + zero-fill in one call (the
2951 // expansion bumps capacity geometrically too).
2952 {
2953 let mut g = fdtable_lock().lock().unwrap();
2954 if (fd as usize) >= g.len() {
2955 g.resize((fd as usize) + 1, FDT_UNUSED); // c:1975-1979 grow
2956 }
2957 }
2958 // c:1982 — `max_zsh_fd = fd;`.
2959 MAX_ZSH_FD.store(fd, Ordering::Relaxed); // c:1982
2960 true
2961}
2962
2963/// Port of `movefd()` from `Src/utils.c:1990` — C decl `movefd(int fd)`.
2964/// ```c
2965/// if (fd != -1 && fd < 10) {
2966/// int fe = fcntl(fd, F_DUPFD, 10);
2967/// zclose(fd); // unconditional close, even when fe == -1
2968/// fd = fe;
2969/// }
2970/// if (fd != -1) {
2971/// check_fd_table(fd);
2972/// fdtable[fd] = FDT_INTERNAL;
2973/// }
2974/// return fd;
2975/// ```
2976/// Two divergences fixed this iteration:
2977/// 1. Old Rust closed the source fd ONLY on success; C closes
2978/// unconditionally per the c:1999-2004 comment "probably better
2979/// to avoid a leak."
2980/// 2. Old Rust added `FD_CLOEXEC` after the dup; C does NOT —
2981/// CLOEXEC is added later by `addmodulefd`/`addlockfd` callers
2982/// that need it. movefd itself does not.
2983pub fn movefd(fd: i32) -> i32 {
2984 #[cfg(unix)]
2985 {
2986 let mut fd = fd;
2987 if fd != -1 && fd < 10 {
2988 // c:1992
2989 let fe = unsafe { libc::fcntl(fd, libc::F_DUPFD, 10) }; // c:1994
2990 unsafe { libc::close(fd) }; // c:2004 zclose(fd) — unconditional
2991 fd = fe; // c:2005
2992 }
2993 // c:2007-2010 — `if (fd != -1) { check_fd_table(fd); fdtable[fd]
2994 // = FDT_INTERNAL; }`. The fdtable global IS now modeled (port
2995 // of utils.c:63 — `Vec<u8>` behind `fdtable_lock`). Mark the
2996 // new fd as FDT_INTERNAL so the rest of the shell knows the
2997 // shell is using it (a later `addmodulefd` / `addlockfd`
2998 // upgrade may reclassify; raw external code shouldn't touch
2999 // it). The previous port skipped this so internal-fd tracking
3000 // (which `closeallelse` and forkexec rely on) silently
3001 // never saw zshrs-internal fds.
3002 if fd != -1 {
3003 // c:2007
3004 check_fd_table(fd); // c:2008
3005 fdtable_set(fd, FDT_INTERNAL); // c:2009
3006 }
3007 fd
3008 }
3009 #[cfg(not(unix))]
3010 {
3011 fd
3012 }
3013}
3014
3015/// Port of `redup()` from `Src/utils.c:2021` — C decl `redup(int x, int y)`.
3016///
3017/// C signature: `int redup(int x, int y)`.
3018/// "Move fd x to y. If x == -1, fd y is closed. Returns y for
3019/// success, -1 for failure." (c:2014-2016 docstring.)
3020///
3021/// Body mirrors c:2023-2068: when `x == -1`, close `y` (return `y`);
3022/// when `x == y`, no-op (return `y`); otherwise `dup2(x, y)` +
3023/// `close(x)`.
3024///
3025/// C body fdtable updates (c:2053-2063), which are load-bearing:
3026/// * `fdtable[y] = fdtable[x]` — the new fd inherits the old fd's
3027/// ownership category (FDT_INTERNAL / FDT_MODULE / etc.).
3028/// * If the inherited type is `FDT_FLOCK` / `FDT_FLOCK_EXEC`, promote
3029/// to `FDT_INTERNAL` (the dup'd fd doesn't carry the flock).
3030/// * If `fdtable[x] == FDT_FLOCK`, decrement `fdtable_flocks` (the
3031/// original lock-holding fd is about to be closed).
3032///
3033/// Without these updates, redup'd fds would carry stale `FDT_UNUSED`
3034/// ownership and `closeallelse(FDT_EXTERNAL)` etc. could not classify them.
3035pub fn redup(x: i32, y: i32) -> i32 {
3036 // c:2021
3037 let mut ret = y; // c:2023
3038 #[cfg(unix)]
3039 {
3040 if x < 0 {
3041 // c:2047
3042 zclose(y); // c:2048
3043 } else if x != y {
3044 // c:2049
3045 // c:2053-2057 — successful dup2: copy fdtable + FLOCK promote.
3046 if unsafe { libc::dup2(x, y) } == -1 {
3047 // c:2050
3048 ret = -1; // c:2051
3049 } else {
3050 check_fd_table(y); // c:2053
3051 let kind_x = fdtable_get(x); // c:2054
3052 let kind_y = if kind_x == FDT_FLOCK || kind_x == FDT_FLOCK_EXEC {
3053 FDT_INTERNAL // c:2055-2056
3054 } else {
3055 kind_x // c:2054
3056 };
3057 fdtable_set(y, kind_y);
3058 }
3059 // c:2062-2063 — `if (fdtable[x] == FDT_FLOCK) fdtable_flocks--;`
3060 // The original lock-holding fd is about to be closed, so the
3061 // flock-count tracker must drop. Even on dup2 failure C still
3062 // closes x, so this runs in both arms.
3063 if fdtable_get(x) == FDT_FLOCK {
3064 // c:2062
3065 FDTABLE_FLOCKS.fetch_sub(1, Ordering::SeqCst); // c:2063
3066 }
3067 zclose(x); // c:2064
3068 }
3069 }
3070 #[cfg(not(unix))]
3071 {
3072 let _ = (x, y);
3073 }
3074 ret // c:2067
3075}
3076
3077/// Port of `addmodulefd()` from `Src/utils.c:2091` — C decl `addmodulefd(int fd, int fdt)`.
3078/// ```c
3079/// if (fd >= 0) {
3080/// check_fd_table(fd);
3081/// fdtable[fd] = fdt;
3082/// }
3083/// ```
3084/// Two divergences fixed this iteration:
3085/// 1. C accepts an `fdt` parameter (FDT_MODULE / FDT_INTERNAL /
3086/// FDT_EXTERNAL — see callers in Src/Modules/{random,socket,tcp,
3087/// db_gdbm}.c). Rust port hardcoded `FDT_MODULE` — silently
3088/// ignored caller intent. Now takes `fdt` per C.
3089/// 2. C does NOT set `FD_CLOEXEC`. Rust port was adding it
3090/// unconditionally — divergent. CLOEXEC is the caller's
3091/// responsibility based on the fdt semantics.
3092pub fn addmodulefd(fd: i32, fdt: i32) {
3093 // c:2091
3094 // c:2093 — `if (fd >= 0)`.
3095 if fd >= 0 {
3096 // c:2094 — `check_fd_table(fd)` — grow fdtable to cover fd.
3097 check_fd_table(fd); // c:2094
3098 // c:2095 — `fdtable[fd] = fdt`. Routes through the canonical
3099 // helper which mirrors C's direct `fdtable[fd] = fdt` write.
3100 fdtable_set(fd, fdt); // c:2095
3101 }
3102}
3103
3104/// Port of `addlockfd()` from `Src/utils.c:2112` — C decl `addlockfd(int fd, int cloexec)`.
3105/// ```c
3106/// if (cloexec) {
3107/// if (fdtable[fd] != FDT_FLOCK)
3108/// fdtable_flocks++;
3109/// fdtable[fd] = FDT_FLOCK;
3110/// } else {
3111/// fdtable[fd] = FDT_FLOCK_EXEC;
3112/// }
3113/// ```
3114/// Critical divergence fixed: C updates `fdtable[fd]` to FDT_FLOCK
3115/// (cloexec=true) or FDT_FLOCK_EXEC (cloexec=false). Previous Rust
3116/// port did the OPPOSITE — called `fcntl(F_SETFD, FD_CLOEXEC)` when
3117/// `cloexec` was true. That's wrong on two counts:
3118/// 1. C's "cloexec" parameter selects the FDT category, not a
3119/// libc fcntl flag.
3120/// 2. FDT_FLOCK means "lock survives across exec via fd inheritance"
3121/// — the OPPOSITE of close-on-exec. The Rust port was adding
3122/// CLOEXEC for the very case where the fd should be inheritable.
3123pub fn addlockfd(fd: i32, cloexec: bool) {
3124 // c:2112
3125 if cloexec {
3126 // c:2114
3127 // c:2115-2117 — track flock count, set FDT_FLOCK.
3128 if fdtable_get(fd) != FDT_FLOCK {
3129 FDTABLE_FLOCKS.fetch_add(1, Ordering::SeqCst);
3130 }
3131 fdtable_set(fd, FDT_FLOCK);
3132 } else {
3133 // c:2118
3134 // c:2119 — FDT_FLOCK_EXEC means flock inherited by exec.
3135 fdtable_set(fd, FDT_FLOCK_EXEC);
3136 }
3137}
3138
3139// FDTABLE_FLOCKS canonical declaration lives at utils.rs:5219
3140// (the exec.c global with same name). Reuse, don't redeclare.
3141
3142/// Port of `zclose()` from `Src/utils.c:2127` — C decl `int zclose(int fd)`.
3143///
3144/// Close the given fd, and clear it from fdtable. // c:2123
3145pub fn zclose(fd: i32) -> i32 {
3146 // c:2127
3147 if fd >= 0 {
3148 // c:2129
3149 // c:2130-2133 — comment carry: "Careful: we allow closing of
3150 // arbitrary fd's, beyond max_zsh_fd. In that case we don't
3151 // try anything clever."
3152 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed); // c:2134
3153 if fd <= max_fd {
3154 // c:2134
3155 // !!! ZSHRS THREADING ADAPTATION — deviates from C here !!!
3156 // C closes blindly; it is single-threaded, so a shell-recorded
3157 // fd number can never be concurrently recycled by someone else.
3158 // zshrs runs 18 worker threads whose Rust-owned fds (ReadDir
3159 // dirfds, SQLite, File) recycle freed numbers immediately — a
3160 // stale zclose then closes a FOREIGN fd, and std panics in
3161 // ReadDir::drop ("unexpected error during closedir: EBADF",
3162 // observed from the compinit bg scan). An in-range fd the
3163 // fdtable does NOT own is by definition not ours to close.
3164 if fdtable_get(fd) == FDT_UNUSED {
3165 tracing::debug!(
3166 fd,
3167 "zclose: skipping unowned in-range fd (thread-safety guard)"
3168 );
3169 // The guard exists for an fd a foreign thread has OPEN; an fd
3170 // nobody has open is what C's blind `close(fd)` (c:2145)
3171 // reports as -1/EBADF, and `{varid}<&-` on an already-closed
3172 // descriptor surfaces that as "failed to close file
3173 // descriptor N" (c:Src/exec.c:3928-3930). fcntl leaves errno
3174 // at EBADF for the caller's `%e`.
3175 #[cfg(unix)]
3176 if unsafe { libc::fcntl(fd, libc::F_GETFD) } < 0 {
3177 return -1;
3178 }
3179 return 0;
3180 }
3181 if fdtable_get(fd) == FDT_FLOCK {
3182 // c:2135
3183 FDTABLE_FLOCKS.fetch_sub(1, Ordering::Relaxed);
3184 // c:2136
3185 }
3186 fdtable_set(fd, FDT_UNUSED); // c:2137
3187 // c:2138-2139 — shrink max_zsh_fd past trailing UNUSED slots.
3188 let mut m = MAX_ZSH_FD.load(Ordering::Relaxed);
3189 while m > 0 && fdtable_get(m) == FDT_UNUSED {
3190 m -= 1;
3191 }
3192 MAX_ZSH_FD.store(m, Ordering::Relaxed);
3193 // c:2140-2143 — coproc fd tracking.
3194 if fd == coprocin.load(Ordering::Relaxed) {
3195 coprocin.store(-1, Ordering::Relaxed);
3196 }
3197 if fd == coprocout.load(Ordering::Relaxed) {
3198 coprocout.store(-1, Ordering::Relaxed);
3199 }
3200 }
3201 #[cfg(unix)]
3202 unsafe {
3203 return libc::close(fd);
3204 } // c:2145
3205 #[cfg(not(unix))]
3206 return 0;
3207 }
3208 -1 // c:2147
3209}
3210
3211/// Port of `zcloselockfd()` from `Src/utils.c:2156` — C decl `int zcloselockfd(int fd)`.
3212/// ```c
3213/// if (fd > max_zsh_fd) return -1;
3214/// if (fdtable[fd] != FDT_FLOCK && fdtable[fd] != FDT_FLOCK_EXEC)
3215/// return -1;
3216/// zclose(fd);
3217/// return 0;
3218/// ```
3219/// "Close an fd returning 0 if used for locking; return -1 if it
3220/// isn't." Caller (`bin_zsystem_flock -u`) uses the -1 return to
3221/// distinguish "fd not in the flock table" from "successfully
3222/// released a lock."
3223///
3224/// Previously the Rust port skipped the FDT_FLOCK / FDT_FLOCK_EXEC
3225/// check entirely and always returned 0 — meaning `zsystem flock -u
3226/// <unlocked-fd>` reported success on any fd. Now that the
3227/// `addlockfd` port (this iteration) populates the fdtable with the
3228/// canonical FDT_FLOCK / FDT_FLOCK_EXEC slots, the check can fire
3229/// faithfully.
3230pub fn zcloselockfd(fd: i32) -> i32 {
3231 // c:2156
3232 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
3233 // c:2158 — `if (fd > max_zsh_fd) return -1;`.
3234 if fd > max_fd {
3235 return -1;
3236 }
3237 // c:2160-2161 — `if (fdtable[fd] != FDT_FLOCK && != FDT_FLOCK_EXEC)
3238 // return -1;`.
3239 let slot = fdtable_get(fd);
3240 if slot != FDT_FLOCK && slot != FDT_FLOCK_EXEC {
3241 return -1;
3242 }
3243 zclose(fd); // c:2162
3244 0 // c:2163
3245}
3246
3247/// Port of `gettempname()` from `Src/utils.c:2178` — C decl `char *gettempname(const char *prefix, int use_heap)`.
3248///
3249/// Returns a unique tempfile name templated like C `mktemp(3)` —
3250/// `{prefix}.XXXXXX`. Falls back to `getsparam("TMPPREFIX")` and
3251/// then `DEFAULT_TMPPREFIX` when `prefix` is None. Does NOT create
3252/// the file (matches C — only `gettempfile` creates).
3253pub fn gettempname(prefix: Option<&str>, _use_heap: bool) -> Option<String> {
3254 // c:2178
3255 let suffix = if prefix.is_some() {
3256 ".XXXXXX"
3257 } else {
3258 "XXXXXX"
3259 }; // c:2178
3260 queue_signals(); // c:2182
3261 let prefix_owned: String = match prefix {
3262 // c:2183
3263 Some(p) => p.to_string(),
3264 // c:2184 — `getsparam("TMPPREFIX")`. Read from paramtab (not OS
3265 // env); fall back to compile-time default when unset.
3266 None => getsparam("TMPPREFIX")
3267 .unwrap_or_else(|| crate::ported::config_h::DEFAULT_TMPPREFIX.to_string()),
3268 };
3269 let template = format!("{}{}", prefix_owned, suffix); // c:2186-2188
3270 // C uses mktemp(3) which mutates the X's into a unique name // c:2192/2219
3271 // without creating the file. Rust has no mktemp; emulate with
3272 // pid+timestamp. Caller is responsible for O_EXCL open.
3273 let pid = std::process::id();
3274 let nanos = std::time::SystemTime::now()
3275 .duration_since(UNIX_EPOCH)
3276 .map(|d| d.as_nanos())
3277 .unwrap_or(0);
3278 // c:2192 — mkstemp(3) replaces the six X's with EXACTLY six
3279 // characters from [A-Za-z0-9], so every zsh temp name has a fixed
3280 // width (`/tmp/zshDYKzyg`). The `{pid:x}{nanos:x}` form here was 8-12
3281 // characters wide, which shows anywhere the name is displayed: the
3282 // `fc -` parity cell compares the editor's status line, where zsh
3283 // shows /private/tmp/zshDYKzyg and zshrs showed
3284 // /private/tmp/zshdb0ff10c48. Caller retries on EEXIST (c:2255-2269),
3285 // so a collision is handled rather than fatal.
3286 let mut mix: u128 = ((pid as u128) << 47) ^ nanos;
3287 let mut unique = String::with_capacity(6);
3288 for _ in 0..6 {
3289 let idx = (mix % 62) as usize;
3290 unique.push(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[idx] as char);
3291 mix /= 62;
3292 }
3293 let name = template.replace("XXXXXX", &unique);
3294 unqueue_signals(); // c:2221
3295 Some(name)
3296}
3297
3298/// Check if metafied - port from zsh/Src/utils.c has_token()
3299// Check if a string contains a token // c:2282
3300/// Port of `has_token()` from `Src/utils.c:2282` — C decl `int has_token(const char *s)`.
3301///
3302/// Port of `has_token(const char *s)` from `Src/utils.c:2282` — used by
3303/// `ecstrcode` (parse.rs:989) to flip the token-marker bit on the
3304/// encoded string offset. Token markers live in 0x83..=0x9f (Pound,
3305/// Stringg, Hat, Star, ..., Bnull, Nularg). Earlier impl checked
3306/// only 0x83 (Meta) which missed Dash/Equals/Inbrack/Inbrace/etc.,
3307/// so any string containing those (e.g. `dart-lang/dart` →
3308/// `dart\u{e19b}lang/dart`) got encoded with the no-token bit set,
3309/// breaking byte parity with C's wordcode-emitter output.
3310/// ```c
3311/// while (*s)
3312/// if (itok(*s++)) return 1;
3313/// return 0;
3314/// ```
3315/// Routes through the canonical `itok()` typtab-driven predicate.
3316/// Previous Rust port used hardcoded `0x83..=0x9f` which was:
3317/// - Wrong on the low end: includes `0x83` (Meta, NOT a token).
3318/// - Wrong on the high end: missing `0xa0..=0xa1` (part of the
3319/// Snull..Nularg range that `itok` legitimately covers).
3320/// The full canonical ITOK range per `Src/zsh.h:152-159` is
3321/// `0x84..=0xa1` (Pound..Nularg), with possible gaps in the middle
3322/// (Snull at 0x9d follows Bang at 0x9c).
3323pub fn has_token(s: &str) -> bool {
3324 // c:2282
3325 // C scans BYTES (c:2284-2285), which is safe THERE only because zsh
3326 // metafies: a raw byte >= 0x80 is stored escaped (Meta, then byte ^ 32),
3327 // so a bare 0x84..=0xA1 in a metafied string is always a real token.
3328 // zshrs does not metafy — it keeps UTF-8 `str` and stores tokens as
3329 // PUA CHARS (crate::token_char). Scanning bytes here therefore mistakes a UTF-8
3330 // CONTINUATION byte for a token: `—` U+2014 encodes as E2 80 94 and
3331 // `→` U+2192 as E2 86 92 — 0x94, 0x86 and 0x92 all answer `itok`, so
3332 // every em-dash, smart quote and arrow reported "has tokens".
3333 //
3334 // `lex.rs:6165` already carries this fix (it was made there because the
3335 // false positive never clears `execcmd_exec`'s
3336 // `while has_token(&args[0]) { zglob(..) }` sweep, c:3315-3318, and the
3337 // shell spins at 100% CPU); the copy here — the one at C's own home for
3338 // this function, and the one `cond.rs:856`/`cond.rs:893`/`text.rs:1272`
3339 // import — never inherited it.
3340 s.chars().any(|c| {
3341 // c:2285 itok(*s++)
3342 crate::token_char::itok_char(c)
3343 })
3344}
3345
3346// Delete a character in a string // c:2294
3347/// Port of `chuck()` from `Src/utils.c:2294`.
3348///
3349/// Remove character from string (from utils.c chuck)
3350pub fn chuck(s: &mut String, pos: usize) {
3351 // c:2294
3352 if pos < s.len() {
3353 s.remove(pos);
3354 }
3355}
3356
3357/// Port of `tulower()` from `Src/utils.c:2302` — C decl `tulower(int c)`.
3358///
3359/// Convert character to lowercase
3360/// To-lowercase that respects locale.
3361pub fn tulower(c: char) -> char {
3362 // c:2302
3363 c.to_lowercase().next().unwrap_or(c)
3364}
3365
3366/// Port of `tuupper()` from `Src/utils.c:2310` — C decl `tuupper(int c)`.
3367///
3368/// Convert character to uppercase
3369/// To-uppercase that respects locale.
3370pub fn tuupper(c: char) -> char {
3371 // c:2310
3372 c.to_uppercase().next().unwrap_or(c)
3373}
3374
3375/// Port of `ztrncpy()` from `Src/utils.c:2320` — C decl `void ztrncpy(char *s, char *t, int len)`.
3376///
3377/// C body (c:2320-2326):
3378/// ```c
3379/// while (len--)
3380/// *s++ = *t++;
3381/// *s = '\0';
3382/// ```
3383///
3384/// Copy `len` bytes from `t` into `s`, then NUL-terminate. C does no
3385/// bounds check; the caller must ensure `s` has `len + 1` bytes of
3386/// capacity. Rust port preserves the (s, t, len) parameter shape:
3387/// `s: &mut String` (output), `t: &str` (source), `len: usize` (copy
3388/// count). The C NUL terminator at c:2325 is implicit in Rust's
3389/// length-prefixed String. The Rust port clamps `len` to `t.len()` to
3390/// avoid the out-of-bounds read C's `*s++ = *t++` would perform when
3391/// `len > strlen(t)` (UB in C; explicitly bounded in Rust).
3392pub fn ztrncpy(s: &mut String, t: &str, len: usize) {
3393 // c:2320
3394 s.clear(); // C overwrites from start of *s
3395 let take = len.min(t.len()); // c:2322 while (len--)
3396 s.push_str(&t[..take]); // c:2322 *s++ = *t++
3397 // c:2325 — `*s = '\0';` (implicit in Rust String length)
3398}
3399
3400/// Port of `strucpy()` from `Src/utils.c:2331` — C decl `void strucpy(char **s, char *t)`.
3401/// ```c
3402/// char *u = *s;
3403/// while ((*u++ = *t++));
3404/// *s = u - 1; // leave *s pointing at the NUL terminator
3405/// ```
3406/// Body: `strcpy(*s, t)` + advance `*s` to point at the new
3407/// NUL terminator. The "u" in the name is a C convention for the
3408/// local pointer-walker, NOT "upper-case" — the function does NOT
3409/// change case.
3410///
3411/// Rust API: param name `s` matches C exactly per Rule E (despite Rust
3412/// convention of `dest` for output buffers). The C pointer-advance
3413/// `*s = u - 1` translates to "the new end of s is at s.len()", which
3414/// is implicit in Rust's owning-String model.
3415pub fn strucpy(s: &mut String, t: &str) {
3416 // c:2331
3417 s.push_str(t); // c:2335 `*u++ = *t++` loop
3418 // c:2336 — `*s = u - 1;` — pointer-advance is implicit in Rust
3419 // (`s.len()` is the new end-of-string position).
3420}
3421
3422/// Port of `struncpy()` from `Src/utils.c:2341` — C decl `void struncpy(char **s, char *t, int n)`.
3423/// ```c
3424/// char *u = *s;
3425/// while (n-- && (*u = *t++)) u++;
3426/// *s = u;
3427/// if (n > 0) *u = '\0';
3428/// ```
3429/// Body: copy up to `n` bytes from `t` to `*s`, NUL-terminate.
3430/// Note c:2348 — "just one null-byte will do, unlike strncpy(3)";
3431/// doesn't pad with NULs.
3432///
3433/// Param names match C exactly per Rule E.
3434pub fn struncpy(s: &mut String, t: &str, n: usize) {
3435 // c:2341
3436 // c:2345 — `while (n-- && (*u = *t++)) u++;` — copy up to n
3437 // bytes, stop at NUL (which in Rust &str is the end-of-string).
3438 let take = n.min(t.len());
3439 s.push_str(&t[..take]);
3440}
3441
3442/// Port of `arrlen()` from `Src/utils.c:2357` — C decl `arrlen(char **s)`.
3443///
3444/// Array length - port from arrlen()
3445pub fn arrlen<T>(s: &[T]) -> usize {
3446 // c:2357
3447 s.len()
3448}
3449
3450// Return TRUE iff arrlen(s) >= lower_bound, but more efficiently. // c:2382
3451/// Port of `arrlen_ge()` from `Src/utils.c:2369`.
3452///
3453/// Check if array length >= n (from utils.c arrlen_ge)
3454pub fn arrlen_ge<T>(arr: &[T], n: usize) -> bool {
3455 // c:2369
3456 arr.len() >= n
3457}
3458
3459// Return TRUE iff arrlen(s) > lower_bound, but more efficiently. // c:2400
3460/// Port of `arrlen_gt()` from `Src/utils.c:2382`.
3461///
3462/// Check if array length > n (from utils.c arrlen_gt)
3463pub fn arrlen_gt<T>(arr: &[T], n: usize) -> bool {
3464 // c:2382
3465 arr.len() > n
3466}
3467
3468// Return TRUE iff arrlen(s) <= upper_bound, but more efficiently. // c:2409
3469/// Port of `arrlen_le()` from `Src/utils.c:2391`.
3470///
3471/// Check if array length <= n (from utils.c arrlen_le)
3472pub fn arrlen_le<T>(arr: &[T], n: usize) -> bool {
3473 // c:2391
3474 arr.len() <= n
3475}
3476
3477// Return TRUE iff arrlen(s) < upper_bound, but more efficiently. // c:2400
3478/// Port of `arrlen_lt()` from `Src/utils.c:2400`.
3479///
3480/// Check if array length < n (from utils.c arrlen_lt)
3481pub fn arrlen_lt<T>(arr: &[T], n: usize) -> bool {
3482 // c:2400
3483 arr.len() < n
3484}
3485
3486/// Port of `skipparens()` from `Src/utils.c:2409`.
3487///
3488/// Direct port of `int skipparens(char inpar, char outpar, char **s)`
3489/// from `Src/utils.c:2409`. Walks `*s` past a balanced `inpar`/`outpar`
3490/// pair, advancing the input slice in place. Returns 0 on full
3491/// balance, `-1` when `**s != inpar` (no opening paren), or positive
3492/// when the scan ran off the end of `*s` with `level > 0`
3493/// (unbalanced).
3494///
3495/// Rust signature: `s: &mut &str` mirrors C's `char **s` out-arg —
3496/// the caller's slice handle is rewritten to point past the closing
3497/// paren (or to the empty tail on unbalanced input).
3498/// WARNING: param names don't match C — Rust=(open, close, s) vs C=(inpar, outpar, s)
3499pub fn skipparens(open: char, close: char, s: &mut &str) -> i32 {
3500 // c:2409
3501 let mut chars = s.char_indices();
3502 // c:2413-2414 — `if (**s != inpar) return -1;`
3503 match chars.next() {
3504 Some((_, c)) if c == open => {}
3505 _ => return -1,
3506 }
3507 // c:2416 — `for (level = 1; *++*s && level;)`
3508 let mut level: i32 = 1;
3509 let mut consumed = open.len_utf8();
3510 for (i, c) in chars {
3511 consumed = i + c.len_utf8();
3512 if c == open {
3513 level += 1; // c:2417-2418
3514 } else if c == close {
3515 level -= 1; // c:2419-2420
3516 }
3517 if level == 0 {
3518 break;
3519 }
3520 }
3521 *s = &s[consumed..];
3522 // c:2422 — `return level;`. 0 on balanced match, positive on
3523 // ran-off-end (unbalanced input).
3524 level
3525}
3526
3527/// Port of `zstrtol()` from `Src/utils.c:2427` — C decl `zstrtol(const char *s, char **t, int base)`.
3528///
3529/// zlong with base autodetection. Returns the parsed value AND a
3530/// `&str` slice pointing to the first unconsumed character —
3531/// matching C's `char **t` out-arg.
3532///
3533/// C signature: `zlong zstrtol(const char *s, char **t, int base)`.
3534/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
3535/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Otherwise `base`
3536/// must be in [2, 36]; values outside that range emit `zerr` and
3537/// return 0.
3538///
3539/// On overflow, mirrors C's "number truncated after N digits"
3540/// behaviour: emits a `zwarn` and returns the truncated value
3541/// (last in-range computation).
3542/// WARNING: param names don't match C — Rust=(s, base) vs C=(s, t, base)
3543pub fn zstrtol(s: &str, base: i32) -> (i64, &str) {
3544 // c:2427
3545 zstrtol_underscore(s, base, false) // c:2427
3546}
3547
3548/// Port of `zstrtol_underscore()` from `Src/utils.c:2438` — C decl `zstrtol_underscore(const char *s, char **t, int base, int underscore)`.
3549///
3550/// C signature: `zlong zstrtol_underscore(const char *s, char **t,
3551/// int base, int underscore)`.
3552///
3553/// Convert string to zlong with optional `_` digit-separator support.
3554/// `underscore != 0` allows underscores to appear inside the digit
3555/// run (skipped during accumulation). Returns the parsed value AND
3556/// the unconsumed-tail slice (matching C's `*t = (char *)s` writeback
3557/// at line 2516).
3558///
3559/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
3560/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Bases outside [2, 36]
3561/// emit `zerr` and return `(0, original_s)`. Overflow truncates and
3562/// emits a `zwarn` per c:2510-2512.
3563pub fn zstrtol_underscore(s: &str, base: i32, underscore: bool) -> (i64, &str) {
3564 // c:2438
3565 let bytes = s.as_bytes();
3566 let mut i = 0usize;
3567 // c:2444-2445 — skip leading `inblank` whitespace.
3568 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
3569 i += 1;
3570 }
3571 // c:2447-2450 — handle sign.
3572 let neg = if i < bytes.len() && (bytes[i] == b'-') {
3573 // c:2447 IS_DASH
3574 i += 1;
3575 true
3576 } else if i < bytes.len() && bytes[i] == b'+' {
3577 // c:2449
3578 i += 1;
3579 false
3580 } else {
3581 false
3582 };
3583
3584 // c:2452-2461 — base autodetect when base == 0.
3585 let mut base = base;
3586 if base == 0 {
3587 // c:2452
3588 if i >= bytes.len() || bytes[i] != b'0' {
3589 // c:2453
3590 base = 10; // c:2454
3591 } else {
3592 i += 1; // c:2455 ++s
3593 if i < bytes.len() && (bytes[i] == b'x' || bytes[i] == b'X') {
3594 // c:2455
3595 base = 16;
3596 i += 1;
3597 } else if i < bytes.len() && (bytes[i] == b'b' || bytes[i] == b'B') {
3598 // c:2457
3599 base = 2;
3600 i += 1;
3601 } else {
3602 // c:2459
3603 base = 8; // c:2460
3604 }
3605 }
3606 }
3607 let inp_idx = i; // c:2462 inp = s
3608 if base < 2 || base > 36 {
3609 // c:2463
3610 zerr(&format!(
3611 "invalid base (must be 2 to 36 inclusive): {}",
3612 base
3613 )); // c:2464
3614 return (0, s); // c:2465 return 0
3615 }
3616
3617 let mut calc: u64 = 0; // c:2441
3618 let mut trunc_idx: Option<usize> = None; // c:2440 trunc = NULL
3619 if base <= 10 {
3620 // c:2466
3621 // c:2467-2479 — digit accumulator, low bases.
3622 let max_d = b'0' + base as u8;
3623 while i < bytes.len() {
3624 let c = bytes[i];
3625 if c >= b'0' && c < max_d {
3626 if trunc_idx.is_none() {
3627 let newcalc = calc
3628 .wrapping_mul(base as u64)
3629 .wrapping_add((c - b'0') as u64);
3630 if newcalc < calc {
3631 // c:2473
3632 trunc_idx = Some(i); // c:2474
3633 } else {
3634 calc = newcalc;
3635 }
3636 }
3637 i += 1;
3638 } else if underscore && c == b'_' {
3639 // c:2467 underscore && *s == '_'
3640 i += 1;
3641 } else {
3642 break;
3643 }
3644 }
3645 } else {
3646 // c:2480
3647 // c:2481-2495 — digit accumulator, high bases (>10).
3648 while i < bytes.len() {
3649 let c = bytes[i];
3650 let digit = if c.is_ascii_digit() {
3651 // c:2481 idigit
3652 Some((c - b'0') as u32)
3653 } else if c >= b'a' && c < b'a' + base as u8 - 10 {
3654 // c:2482
3655 Some(((c & 0x1f) + 9) as u32) // c:2486 (*s & 0x1f) + 9
3656 } else if c >= b'A' && c < b'A' + base as u8 - 10 {
3657 // c:2483
3658 Some(((c & 0x1f) + 9) as u32)
3659 } else if underscore && c == b'_' {
3660 // c:2484
3661 i += 1;
3662 continue;
3663 } else {
3664 None
3665 };
3666 match digit {
3667 Some(d) => {
3668 if trunc_idx.is_none() {
3669 let newcalc = calc.wrapping_mul(base as u64).wrapping_add(d as u64);
3670 if newcalc < calc {
3671 // c:2489
3672 trunc_idx = Some(i); // c:2490
3673 } else {
3674 calc = newcalc;
3675 }
3676 }
3677 i += 1;
3678 }
3679 None => break,
3680 }
3681 }
3682 }
3683
3684 // c:2504-2511 — special-case: signed-overflow into top bit.
3685 // The lowest negative number triggers the first test but is
3686 // representable correctly; check it explicitly.
3687 if trunc_idx.is_none() && (calc as i64) < 0 {
3688 // c:2504
3689 let top_bit_only = calc & !(1u64 << 63); // c:2506
3690 if !neg || top_bit_only != 0 {
3691 // c:2505
3692 trunc_idx = Some(i.saturating_sub(1)); // c:2508
3693 calc /= base as u64; // c:2509
3694 }
3695 }
3696
3697 if let Some(t) = trunc_idx {
3698 // c:2512
3699 let digits = t.saturating_sub(inp_idx);
3700 let inp_str = &s[inp_idx..];
3701 zwarn(&format!(
3702 "number truncated after {} digits: {}",
3703 digits, inp_str
3704 )); // c:2513
3705 }
3706
3707 let result = if neg {
3708 // c:2517
3709 -(calc as i64)
3710 } else {
3711 calc as i64
3712 };
3713 (result, &s[i..])
3714}
3715
3716/// Port of `zstrtoul_underscore()` from `Src/utils.c:2529`.
3717///
3718/// Parse unsigned integer with underscore support
3719/// Port from zsh/Src/utils.c zstrtoul_underscore() lines 2528-2575
3720/// Port of `zstrtoul_underscore(const char *s, zulong *retval)` from
3721/// `Src/utils.c:2529-2590`. C body:
3722/// ```c
3723/// if (*s == '+') s++;
3724/// if (*s != '0') base = 10;
3725/// else if (*++s == 'x' || *s == 'X') base = 16, s++;
3726/// else if (*s == 'b' || *s == 'B') base = 2, s++;
3727/// else base = isset(OCTALZEROES) ? 8 : 10;
3728/// ```
3729/// The leading-zero case is option-gated on OCTALZEROES — without
3730/// the option (default), `0777` parses as decimal 777, NOT octal 511.
3731/// Previously the Rust port always treated leading-zero as octal —
3732/// divergent for the default shell setup.
3733pub fn zstrtoul_underscore(s: &str) -> Option<u64> {
3734 // c:2529
3735 let s = s.trim();
3736 let s = s.strip_prefix('+').unwrap_or(s); // c:2533-2534
3737
3738 let (base, rest) = if s.starts_with("0x") || s.starts_with("0X") {
3739 // c:2538
3740 (16, &s[2..])
3741 } else if s.starts_with("0b") || s.starts_with("0B") {
3742 // c:2540
3743 (2, &s[2..])
3744 } else if s.starts_with('0') && s.len() > 1 && isset(OCTALZEROES)
3745 // c:2543
3746 {
3747 (8, &s[1..])
3748 } else {
3749 (10, s)
3750 };
3751
3752 let rest = rest.replace('_', "");
3753 u64::from_str_radix(&rest, base).ok()
3754}
3755
3756/// Port of `setblock_fd()` from `Src/utils.c:2579`.
3757///
3758/// Port of `int setblock_fd(int turnonblocking, int fd, long *modep)`
3759/// from `Src/utils.c:2579`. Toggles O_NONBLOCK on `fd`.
3760///
3761/// **Signature matches C exactly** (param order `turnonblocking, fd`):
3762/// - `turnonblocking=1` → clear O_NONBLOCK (enable blocking).
3763/// - `turnonblocking=0` → set O_NONBLOCK (disable blocking).
3764///
3765/// Returns:
3766/// - `(true, mode)` if the fd's state was changed.
3767/// - `(false, -1)` if the fd is a regular file (C skips regular
3768/// files entirely — only operates on pipes/sockets/ttys per
3769/// `c:2599 if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`).
3770/// - `(false, mode)` if state was already correct.
3771///
3772/// Rust returns `(state_changed, modep_value)` as a tuple to mirror
3773/// C's `int` return + `long *modep` out-param.
3774pub fn setblock_fd(turnonblocking: bool, fd: i32) -> (bool, libc::c_long) {
3775 // c:2579
3776 #[cfg(unix)]
3777 unsafe {
3778 // c:2598-2600 — `if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`.
3779 let mut st: libc::stat = std::mem::zeroed();
3780 if libc::fstat(fd, &mut st) != 0 {
3781 return (false, -1);
3782 }
3783 // c:2599 — skip regular files (no nonblock concept).
3784 let mode_bits = st.st_mode as u32;
3785 if (mode_bits & libc::S_IFMT as u32) == libc::S_IFREG as u32 {
3786 return (false, -1); // c:2614 *modep = -1; return 0
3787 }
3788 // c:2601 — `*modep = fcntl(fd, F_GETFL, 0);`
3789 let modep = libc::fcntl(fd, libc::F_GETFL, 0) as libc::c_long;
3790 if modep < 0 {
3791 return (false, -1); // c:2602 if (*modep != -1)
3792 }
3793 const NONBLOCK: libc::c_long = libc::O_NONBLOCK as libc::c_long;
3794 if !turnonblocking {
3795 // c:2603-2606 — want to KNOW if blocking was off; set it on.
3796 if (modep & NONBLOCK) != 0 {
3797 return (true, modep); // already nonblock — no-op, but "off"
3798 }
3799 if libc::fcntl(fd, libc::F_SETFL, modep | NONBLOCK) == 0 {
3800 return (true, modep); // c:2606
3801 }
3802 } else {
3803 // c:2607-2611 — want to clear NONBLOCK if currently set.
3804 if (modep & NONBLOCK) != 0 && libc::fcntl(fd, libc::F_SETFL, modep & !NONBLOCK) == 0 {
3805 return (true, modep); // c:2611 state changed
3806 }
3807 }
3808 (false, modep)
3809 }
3810 #[cfg(not(unix))]
3811 {
3812 let _ = (turnonblocking, fd);
3813 (false, -1)
3814 }
3815}
3816
3817/// Port of `setblock_stdin()` from `Src/utils.c:2622` — C decl `int setblock_stdin(void)`.
3818/// ```c
3819/// long mode;
3820/// return setblock_fd(1, 0, &mode);
3821/// ```
3822/// `turnonblocking=1, fd=0` — turn ON blocking on stdin (clear
3823/// O_NONBLOCK).
3824pub fn setblock_stdin() -> i32 {
3825 // c:2624 — `return setblock_fd(1, 0, &mode);`.
3826 let (changed, _mode) = setblock_fd(true, 0); // c:2624
3827 changed as i32
3828}
3829
3830/// Port of `read_poll()` from `Src/utils.c:2645`.
3831///
3832/// Port of `int read_poll(int fd, int *readchar, int polltty, zlong microseconds)`
3833/// from `Src/utils.c:2645-2738`.
3834///
3835/// Check whether input is pending on `fd` within `timeout_us`
3836/// microseconds. Faithful port of all three C paths:
3837/// 1. The `polltty` non-canonical-tty path (VMIN/VTIME termios
3838/// setup so a raw tty can be polled — c:2665-2696).
3839/// 2. The `HAVE_SELECT` wait (c:2697-2705). Rust uses `poll(2)`
3840/// as the idiom substitution for `select(2)`; `poll` return
3841/// semantics are mapped onto C's `ret` so the `ret < 0`
3842/// error-fallback still triggers.
3843/// 3. The final `setblock_fd` + non-blocking `read` fallback that
3844/// captures a byte when `select`/`poll` errored (c:2718-2729).
3845///
3846/// `readchar` is C's `int *readchar` out-param: when a byte is
3847/// actually consumed during the poll it is written through
3848/// `readchar` (so `read -t -k` on a raw tty gets the byte). `polltty`
3849/// is C's `int polltty` truthiness. Returns C's `(ret > 0)`.
3850///
3851/// WARNING: param order differs from C — Rust=(fd, readchar,
3852/// polltty, timeout_us) vs C=(fd, readchar, polltty, microseconds);
3853/// C's `microseconds` is `timeout_us` here.
3854pub fn read_poll(fd: i32, readchar: &mut i32, polltty: bool, timeout_us: i64) -> bool {
3855 // c:2645
3856 #[cfg(unix)]
3857 {
3858 let mut ret: i32 = -1; // c:2647
3859 let mut mode: libc::c_long = -1; // c:2648
3860 // C reassigns the `polltty` param; hold it as the VMIN-derived int.
3861 let mut polltty: i32 = polltty as i32; // c:2645
3862 let mut ti: Option<libc::termios> = None; // c:2659 `struct ttyinfo ti;`
3863
3864 // c:2662-2663 — `if (fd < 0 || (polltty && !isatty(fd))) polltty = 0;`
3865 if fd < 0 || (polltty != 0 && unsafe { libc::isatty(fd) } == 0) {
3866 polltty = 0; // no tty to poll
3867 }
3868
3869 // c:2665-2696 — HAS_TIO && !__CYGWIN__: non-canonical VMIN poll setup.
3870 if polltty != 0 && fd >= 0 {
3871 // c:2686 — `gettyinfo(&ti);` (operates on global SHTTY, as in C).
3872 if let Some(mut t) = gettyinfo() {
3873 // c:2687 — `if ((polltty = ti.tio.c_cc[VMIN]))`
3874 polltty = t.c_cc[libc::VMIN] as i32;
3875 if polltty != 0 {
3876 t.c_cc[libc::VMIN] = 0; // c:2688
3877 // c:2690 — termios timeout is 10ths of a second.
3878 t.c_cc[libc::VTIME] = (timeout_us / 100_000) as libc::cc_t; // c:2690
3879 settyinfo(&t); // c:2691
3880 }
3881 ti = Some(t);
3882 } else {
3883 // gettyinfo failed (SHTTY closed) — nothing to poll via VMIN.
3884 polltty = 0;
3885 }
3886 }
3887
3888 // c:2697-2705 — HAVE_SELECT wait, using poll(2) as the select(2) idiom.
3889 let timeout_ms = (timeout_us / 1000) as i32; // c:2698-2699 (expire_tv)
3890 if fd > -1 {
3891 // c:2701-2703
3892 let mut fds = [libc::pollfd {
3893 fd: fd as RawFd,
3894 events: libc::POLLIN,
3895 revents: 0,
3896 }];
3897 let raw = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
3898 ret = if raw > 0 {
3899 // Map poll revents onto select's "fd is readable" ret>0.
3900 if (fds[0].revents & libc::POLLIN) != 0 {
3901 1
3902 } else {
3903 0
3904 }
3905 } else {
3906 raw // 0 = timeout, -1 = error (triggers fallback below)
3907 };
3908 } else {
3909 // c:2705 — `select(0, NULL, NULL, NULL, &expire_tv)`: pure sleep.
3910 ret = unsafe { libc::poll(std::ptr::null_mut(), 0, timeout_ms) };
3911 }
3912
3913 // c:2718 — `if (fd >= 0 && ret < 0 && !errflag)`
3914 if fd >= 0 && ret < 0 && errflag.load(Ordering::Relaxed) == 0 {
3915 // c:2723 — final attempt: non-blocking read of a single char.
3916 // `(polltty || setblock_fd(0, fd, &mode))`
3917 let can_read = if polltty != 0 {
3918 true // polltty short-circuits setblock_fd; mode stays -1
3919 } else {
3920 let (changed, m) = setblock_fd(false, fd); // c:2723
3921 mode = m; // C writes *modep unconditionally
3922 changed
3923 };
3924 if can_read {
3925 let mut c: u8 = 0;
3926 let n = unsafe { libc::read(fd, &mut c as *mut u8 as *mut libc::c_void, 1) };
3927 if n > 0 {
3928 *readchar = c as i32; // c:2724
3929 ret = 1; // c:2725
3930 }
3931 }
3932 // c:2727-2728 — `if (mode != -1) fcntl(fd, F_SETFL, mode);`
3933 if mode != -1 {
3934 unsafe {
3935 libc::fcntl(fd, libc::F_SETFL, mode);
3936 }
3937 }
3938 }
3939
3940 // c:2730-2736 — HAS_TIO: restore canonical VMIN=1 / VTIME=0.
3941 if polltty != 0 {
3942 if let Some(mut t) = ti {
3943 t.c_cc[libc::VMIN] = 1; // c:2732
3944 t.c_cc[libc::VTIME] = 0; // c:2733
3945 settyinfo(&t); // c:2734
3946 }
3947 }
3948
3949 ret > 0 // c:2737
3950 }
3951 #[cfg(not(unix))]
3952 {
3953 let _ = (fd, readchar, polltty, timeout_us);
3954 false
3955 }
3956}
3957
3958/// Port of `timespec_diff_us()` from `Src/utils.c:2752` — C decl `timespec_diff_us(const struct timespec *t1, const struct timespec *t2)`.
3959///
3960/// Compute time difference in microseconds (from utils.c timespec_diff_us)
3961/// Rust idiom replacement: `Instant::duration_since().as_micros()`
3962/// replaces the C tv_sec/tv_nsec arithmetic + sign-flip dance.
3963pub fn timespec_diff_us(t1: &std::time::Instant, t2: &std::time::Instant) -> i64 {
3964 if *t2 > *t1 {
3965 t2.duration_since(*t1).as_micros() as i64
3966 } else {
3967 -(t1.duration_since(*t2).as_micros() as i64)
3968 }
3969}
3970
3971/// Port of `zmonotime()` from `Src/utils.c:2780` — C decl `int zmonotime(time_t *tloc)`.
3972///
3973/// "Like time(), but uses the monotonic clock." Reads CLOCK_MONOTONIC
3974/// and returns `tv_sec`; writes the same value through `tloc` if
3975/// non-NULL (Rust: `Some(&mut t)`).
3976pub fn zmonotime(tloc: Option<&mut i64>) -> i64 {
3977 // c:2780
3978 #[cfg(unix)]
3979 {
3980 // c:2782 — `struct timespec ts;`
3981 let mut ts = libc::timespec {
3982 // c:2782
3983 tv_sec: 0,
3984 tv_nsec: 0,
3985 };
3986 // c:2783 — `zgettime_monotonic_if_available(&ts);`
3987 unsafe {
3988 libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); // c:2783
3989 }
3990 // c:2784-2785 — `if (tloc) *tloc = ts.tv_sec;`
3991 if let Some(t) = tloc {
3992 // c:2784
3993 *t = ts.tv_sec as i64; // c:2785
3994 }
3995 ts.tv_sec as i64 // c:2786 return ts.tv_sec
3996 }
3997 #[cfg(not(unix))]
3998 {
3999 let _ = tloc;
4000 0
4001 }
4002}
4003
4004/// Port of `zsleep()` from `Src/utils.c:2797` — C decl `int zsleep(long us)`.
4005///
4006/// Check if string looks like a number
4007/// Check whether a string parses as a decimal integer.
4008/// Sleep for a given number of seconds (fractional)
4009/// Sleep for a fractional number of seconds.
4010///
4011/// "Sleep for the given number of microseconds." Wraps
4012/// `nanosleep(2)` with EINTR retry, returning 1 on completion, 0
4013/// on permanent error.
4014pub fn zsleep(us: i64) -> i32 {
4015 // c:2797
4016 let mut sleeptime = libc::timespec {
4017 // c:2797-2803
4018 tv_sec: (us / 1_000_000) as libc::time_t,
4019 tv_nsec: ((us % 1_000_000) * 1000) as libc::c_long,
4020 };
4021 #[cfg(unix)]
4022 {
4023 loop {
4024 // c:2804
4025 let mut rem = libc::timespec {
4026 tv_sec: 0,
4027 tv_nsec: 0,
4028 };
4029 let ret = unsafe { libc::nanosleep(&sleeptime, &mut rem) }; // c:2806
4030 if ret == 0 {
4031 // c:2808
4032 return 1;
4033 }
4034 let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
4035 if err != libc::EINTR {
4036 // c:2810
4037 return 0; // c:2811
4038 }
4039 sleeptime = rem; // c:2812
4040 }
4041 }
4042 #[cfg(not(unix))]
4043 {
4044 let _ = sleeptime;
4045 std::thread::sleep(std::time::Duration::from_micros(us.max(0) as u64));
4046 1
4047 }
4048}
4049
4050/// Port of `zsleep_random()` from `Src/utils.c:2833`.
4051///
4052/// Sleep random amount up to max microseconds (from utils.c zsleep_random)
4053/// Port of `int zsleep_random(long max_us, time_t end_time)`
4054/// from Src/utils.c:2833.
4055///
4056/// "Sleep for time (fairly) randomly up to max_us microseconds.
4057/// Don't let the time extend beyond end_time. end_time is compared
4058/// to the current *monotonic* clock time. Return 1 if that seemed
4059/// to work, else 0."
4060pub fn zsleep_random(max_us: i64, end_time: i64) -> i32 {
4061 // c:2833
4062 let now = zmonotime(None); // c:2833
4063 let r16 = unsafe { libc::rand() } & 0xFFFF; // c:2845
4064 let mut r: i64 = (max_us >> 16) * (r16 as i64); // c:2852
4065 while r != 0 && now + (r / 1_000_000) > end_time {
4066 // c:2858
4067 r >>= 1; // c:2859
4068 }
4069 if r != 0 {
4070 // c:2860
4071 zsleep(r) // c:2861
4072 } else {
4073 0 // c:2862
4074 }
4075}
4076
4077/// Port of `checkrmall()` from `Src/utils.c:2867` — C decl `int checkrmall(char *s)`.
4078///
4079/// C body (c:2867-2919): count files in directory `s` (capped at 100,
4080/// honoring GLOBDOTS), emit one of 4 prompts based on count, optionally
4081/// sleep 10s under RMSTARWAIT, then `getquery("ny", 1)` for confirm.
4082///
4083/// Previous Rust port was a FAKE — single generic prompt, no file
4084/// count, no shout/errflag checks, no RMSTARWAIT, wrong valid-chars
4085/// order ("yn" instead of "ny" so 'n' isn't the default-first-arm).
4086/// Lost 9 of the 10 C semantic branches.
4087pub fn checkrmall(s: &str) -> bool {
4088 // c:2867
4089 // c:2871-2872 — `if (!shout) return 1;` — no controlling tty for
4090 // prompt output, default to yes. Rust analogue: if stderr isn't
4091 // a tty, skip the prompt and approve.
4092 let shout_is_tty = unsafe { libc::isatty(2) != 0 }; // c:2871
4093 if !shout_is_tty {
4094 // c:2871
4095 return true; // c:2872
4096 }
4097 // c:2873-2878 — `if (*s != '/')` build absolute via pwd prefix.
4098 let s_owned: String; // c:2873
4099 let s_abs: &str = if !s.starts_with('/') {
4100 // c:2873
4101 let pwd = getsparam("PWD").unwrap_or_default(); // c:2874 pwd[1]
4102 s_owned = if pwd.len() > 1 {
4103 // c:2874 if (pwd[1])
4104 crate::ported::string::tricat(&pwd, "/", s) // c:2875 zhtricat
4105 } else {
4106 // c:2876
4107 crate::ported::string::dyncat("/", s) // c:2877
4108 };
4109 s_owned.as_str()
4110 } else {
4111 s
4112 };
4113 let max_count: i32 = 100; // c:2879
4114 let mut count: i32 = 0; // c:2870 count = 0
4115 // c:2880-2892 — opendir + readdir loop, count entries (skip
4116 // dotfiles when !GLOBDOTS), cap at max_count.
4117 let ignoredots = !isset(GLOBDOTS); // c:2881
4118 // c:2880 — `if (!(dir = opendir(unmeta(s)))) return;` then
4119 // `while ((fn = zreaddir(dir, 1))) { ... }`.
4120 if let Ok(mut dir) = fs::read_dir(s_abs) {
4121 while let Some(fname) = zreaddir(&mut dir, 1) {
4122 if ignoredots && fname.starts_with('.') {
4123 // c:2885
4124 continue; // c:2886
4125 }
4126 count += 1; // c:2887
4127 if count > max_count {
4128 // c:2888
4129 break; // c:2889
4130 }
4131 }
4132 // c:closedir(dir) auto on drop
4133 }
4134 // c:2893-2904 — four-arm prompt based on file count.
4135 let stderr_h = io::stderr();
4136 let mut stderr_w = stderr_h.lock();
4137 if count > max_count {
4138 // c:2893
4139 let _ = write!(
4140 stderr_w,
4141 "zsh: sure you want to delete more than {} files in ",
4142 max_count
4143 ); // c:2894
4144 } else if count == 1 {
4145 // c:2896
4146 let _ = write!(stderr_w, "zsh: sure you want to delete the only file in ");
4147 // c:2897
4148 } else if count > 0 {
4149 // c:2898
4150 let _ = write!(
4151 stderr_w,
4152 "zsh: sure you want to delete all {} files in ",
4153 count
4154 ); // c:2899
4155 } else {
4156 // c:2901
4157 // c:2902 — `We don't know how many files the glob will expand to`
4158 let _ = write!(stderr_w, "zsh: sure you want to delete all the files in ");
4159 // c:2903
4160 }
4161 // c:2905 — `nicezputs(s, shout)` — escape non-printables in path.
4162 let _ = nicezputs(s_abs, &mut stderr_w); // c:2905
4163 // c:2906-2912 — RMSTARWAIT: print "? (waiting ten seconds)",
4164 // flush, beep, sleep 10, then newline.
4165 if isset(RMSTARWAIT) {
4166 // c:2906
4167 let _ = write!(stderr_w, "? (waiting ten seconds)"); // c:2907
4168 let _ = stderr_w.flush(); // c:2908
4169 drop(stderr_w); // release lock around zbeep + sleep
4170 zbeep(); // c:2909
4171 std::thread::sleep(std::time::Duration::from_secs(10)); // c:2910
4172 let _ = writeln!(io::stderr()); // c:2911 fputc('\n', shout)
4173 } else {
4174 drop(stderr_w);
4175 }
4176 // c:2913-2914 — `if (errflag) return 0;`
4177 if errflag.load(Ordering::Relaxed) != 0 {
4178 // c:2913
4179 return false; // c:2914
4180 }
4181 // c:2915-2917 — emit "[yn]? ", flush, beep.
4182 let _ = io::stderr().write_all(b" [yn]? "); // c:2915 nicezputs of " [yn]? "
4183 let _ = io::stderr().flush(); // c:2916
4184 zbeep(); // c:2917
4185 // c:2918 — `return (getquery("ny", 1) == 'y');`. Default-first-
4186 // char is 'n' (no) — getquery's first valid char is the default
4187 // returned when the user just hits Enter.
4188 getquery(Some("ny"), 1) == b'y' as i32 // c:2918
4189}
4190
4191/// Port of `read_loop()` from `Src/utils.c:2923`.
4192///
4193/// Port of `ssize_t read_loop(int fd, char *buf, size_t len)` from
4194/// `Src/utils.c:2923`.
4195///
4196/// C body (c:2923-2945):
4197/// ```c
4198/// while (1) {
4199/// ssize_t ret = read(fd, buf, len);
4200/// if (ret == len) break;
4201/// if (ret <= 0) {
4202/// if (ret < 0) {
4203/// if (errno == EINTR) continue; // c:2933
4204/// if (fd != SHTTY) // c:2935
4205/// zwarn("read failed: %e", errno); // c:2936
4206/// }
4207/// return ret;
4208/// }
4209/// buf += ret; len -= ret;
4210/// }
4211/// ```
4212///
4213/// The previous Rust port omitted the `zwarn("read failed: %e", errno)`
4214/// emission at c:2935-2936. That's a real diagnostic divergence:
4215/// any non-SHTTY read failure in zsh emits a stderr message; the
4216/// Rust port silently propagated the io::Error to the caller, which
4217/// in most call sites was discarded (`let _ = read_loop(...)`).
4218/// Users debugging an interrupted file read (e.g. `< /broken/fd`)
4219/// would see no error message at all.
4220/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
4221pub fn read_loop(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
4222 // c:2923
4223 #[cfg(unix)]
4224 {
4225 let mut total = 0;
4226 while total < buf.len() {
4227 let n = unsafe {
4228 libc::read(
4229 // c:2928
4230 fd,
4231 buf[total..].as_mut_ptr() as *mut libc::c_void,
4232 buf.len() - total,
4233 )
4234 };
4235 if n <= 0 {
4236 // c:2931
4237 if n < 0 {
4238 let e = io::Error::last_os_error();
4239 if e.kind() == io::ErrorKind::Interrupted {
4240 // c:2933
4241 continue; // c:2934
4242 }
4243 // c:2935-2936 — `if (fd != SHTTY) zwarn("read failed: %e", errno);`
4244 let shtty = SHTTY.load(Ordering::Relaxed);
4245 if fd != shtty {
4246 // c:2935
4247 zwarn(
4248 // c:2936
4249 &format!("read failed: {}", e),
4250 );
4251 }
4252 return Err(e);
4253 }
4254 break;
4255 }
4256 total += n as usize;
4257 }
4258 Ok(total)
4259 }
4260 #[cfg(not(unix))]
4261 {
4262 let _ = (fd, buf);
4263 Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
4264 }
4265}
4266
4267/// Port of `write_loop()` from `Src/utils.c:2949`.
4268///
4269/// Port of `ssize_t write_loop(int fd, const char *buf, size_t len)` from
4270/// `Src/utils.c:2949`.
4271///
4272/// C body (c:2949-2970):
4273/// ```c
4274/// while (1) {
4275/// ssize_t ret = write(fd, buf, len);
4276/// if (ret == len) break;
4277/// if (ret < 0) {
4278/// if (errno == EINTR) continue;
4279/// if (fd != SHTTY) // c:2960
4280/// zwarn("write failed: %e", errno); // c:2961
4281/// return -1;
4282/// }
4283/// buf += ret; len -= ret;
4284/// }
4285/// ```
4286///
4287/// Same divergence as `read_loop`: previous Rust port omitted the
4288/// `zwarn("write failed: %e", errno)` emission for non-SHTTY fds.
4289/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
4290pub fn write_loop(fd: i32, buf: &[u8]) -> io::Result<usize> {
4291 // c:2949
4292 #[cfg(unix)]
4293 {
4294 let mut total = 0;
4295 while total < buf.len() {
4296 let n = unsafe {
4297 libc::write(
4298 // c:2954
4299 fd,
4300 buf[total..].as_ptr() as *const libc::c_void,
4301 buf.len() - total,
4302 )
4303 };
4304 if n <= 0 {
4305 if n < 0 {
4306 let e = io::Error::last_os_error();
4307 if e.kind() == io::ErrorKind::Interrupted {
4308 // c:2958
4309 continue; // c:2959
4310 }
4311 // c:2960-2961 — `if (fd != SHTTY) zwarn("write failed: %e", errno);`
4312 let shtty = SHTTY.load(Ordering::Relaxed);
4313 if fd != shtty {
4314 // c:2960
4315 zwarn(
4316 // c:2961
4317 &format!("write failed: {}", e),
4318 );
4319 }
4320 return Err(e);
4321 }
4322 break;
4323 }
4324 total += n as usize;
4325 }
4326 Ok(total)
4327 }
4328 #[cfg(not(unix))]
4329 {
4330 let _ = (fd, buf);
4331 Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
4332 }
4333}
4334
4335/// Port of `read1char()` from `Src/utils.c:2972` — C decl `static int read1char(int echo)`.
4336///
4337/// Read a single character (from utils.c read1char).
4338///
4339///
4340/// C body (c:2972-2988):
4341/// ```c
4342/// char c;
4343/// int q = queue_signal_level();
4344/// dont_queue_signals();
4345/// while (read(SHTTY, &c, 1) != 1) {
4346/// if (errno != EINTR || errflag || retflag || breaks || contflag) {
4347/// restore_queue_signals(q);
4348/// return -1;
4349/// }
4350/// }
4351/// restore_queue_signals(q);
4352/// if (echo) write_loop(SHTTY, &c, 1);
4353/// return (unsigned char) c;
4354/// ```
4355///
4356/// Returns the byte read (0..=255) on success, `-1` on error. Three
4357/// divergences in the previous Rust port:
4358/// 1. **Read from stdin** instead of SHTTY (the tty fd). zsh reads
4359/// single chars during `read -k`, `bindkey`, query prompts —
4360/// always against the controlling terminal, not the standard
4361/// input which may be a pipe.
4362/// 2. **No `echo` parameter.** When `echo=1` C writes the byte
4363/// back to SHTTY so the user sees what they typed.
4364/// 3. **No `errflag` / `retflag` / `breaks` / `contflag` early-exit.**
4365/// A SIGINT-driven errflag should abort the read; previous Rust
4366/// port had no break path.
4367/// WARNING: returns i32 not `Option<char>` to match C's signature
4368/// (`(unsigned char) c` for success, `-1` for error).
4369pub fn read1char(echo: i32) -> i32 {
4370 // c:2972
4371 #[cfg(unix)]
4372 {
4373 let shtty = SHTTY.load(Ordering::Relaxed);
4374 if shtty < 0 {
4375 return -1;
4376 }
4377 let mut c: u8 = 0;
4378 loop {
4379 let rc = unsafe { libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1) };
4380 if rc == 1 {
4381 // c:2978
4382 break;
4383 }
4384 // c:2979 — `if (errno != EINTR || errflag || retflag || breaks
4385 // || contflag) return -1;`
4386 let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
4387 if err != libc::EINTR {
4388 return -1;
4389 }
4390 // Check errflag interrupt flag (the most observable of the
4391 // four guards; retflag/breaks/contflag are control-flow
4392 // states that don't apply outside the exec engine).
4393 if errflag.load(Ordering::Relaxed) != 0 {
4394 return -1;
4395 }
4396 }
4397 // c:2985-2986 — `if (echo) write_loop(SHTTY, &c, 1);`
4398 if echo != 0 {
4399 // c:2985
4400 let _ = write_loop(shtty, &[c]); // c:2986
4401 }
4402 // c:2987 — `return (unsigned char) c;`
4403 c as i32
4404 }
4405 #[cfg(not(unix))]
4406 {
4407 let _ = echo;
4408 -1
4409 }
4410}
4411
4412/// Port of `noquery()` from `Src/utils.c:2992` — C decl `int noquery(int purge)`.
4413///
4414/// "If anything has been typed before the query, return without
4415/// asking. Optionally also purge the input queue." Returns the
4416/// number of bytes pending on `SHTTY` (via FIONREAD ioctl); when
4417/// `purge` is set, drains them before returning.
4418pub fn noquery(purge: bool) -> i32 {
4419 // c:2992
4420 let mut val: libc::c_int = 0; // c:2992
4421 #[cfg(unix)]
4422 {
4423 let shtty = SHTTY.load(Ordering::Relaxed); // c:2999
4424 if shtty == -1 {
4425 return 0;
4426 }
4427 unsafe {
4428 // c:2999
4429 libc::ioctl(shtty, libc::FIONREAD, &mut val as *mut libc::c_int);
4430 }
4431 if purge {
4432 // c:3000
4433 let mut c: u8 = 0;
4434 for _ in 0..val {
4435 // c:3001
4436 let _ = unsafe {
4437 // c:3002
4438 libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1)
4439 };
4440 }
4441 }
4442 }
4443 val // c:3009
4444}
4445
4446/// Port of `getquery()` from `Src/utils.c:3014`.
4447///
4448/// Port of `int getquery(char *valid_chars, int purge)` from
4449/// `Src/utils.c:3014`.
4450///
4451/// Read a single keystroke from the TTY in raw mode and return its
4452/// character code (as `int`, mirroring C). If `valid_chars` is `None`
4453/// (C `NULL`) any byte is accepted. If `valid_chars` is `Some(s)`, only
4454/// chars in `s` (plus `\n` which maps to `s[0]`) are accepted; other
4455/// input rings the bell and re-prompts. `Y`/`N` are pre-normalised to
4456/// `y`/`n` per c:3041-3045.
4457///
4458/// `purge != 0` triggers `noquery(purge)`'s queue-purge path — if input
4459/// is queued, returns `'n'` without reading.
4460pub fn getquery(valid_chars: Option<&str>, purge: i32) -> i32 {
4461 // c:3014
4462 // c:3016 — `int c, d, nl = 0;`
4463 let mut c: i32;
4464 let mut d: i32;
4465 let mut nl: i32 = 0;
4466 // c:3017 — `int isem = !strcmp(term, "emacs");`
4467 // `term` is the $TERM global declared in `Src/init.c`; its value is the
4468 // TERM parameter, read from paramtab (absent → empty string).
4469 let term: String = getsparam("TERM").unwrap_or_default();
4470 let isem: bool = term == "emacs";
4471 // c:3018 — `struct ttyinfo ti;`
4472 // Rust: termios is the canonical TTY-state type returned by gettyinfo.
4473 let mut ti: libc::termios; // c:3018
4474
4475 attachtty(mypgrp.load(Ordering::Relaxed)); // c:3020 attachtty(mypgrp)
4476
4477 // c:3022 — `gettyinfo(&ti);`
4478 ti = match gettyinfo() {
4479 Some(t) => t,
4480 None => return -1,
4481 };
4482 // c:3023-3030 — `ti.tio.c_lflag &= ~ECHO; if (!isem) {
4483 // ti.tio.c_lflag &= ~ICANON;
4484 // ti.tio.c_cc[VMIN] = 1;
4485 // ti.tio.c_cc[VTIME] = 0; }`
4486 #[cfg(unix)]
4487 {
4488 ti.c_lflag &= !(libc::ECHO); // c:3024
4489 if !isem {
4490 // c:3025
4491 ti.c_lflag &= !(libc::ICANON); // c:3026
4492 ti.c_cc[libc::VMIN] = 1; // c:3027
4493 ti.c_cc[libc::VTIME] = 0; // c:3028
4494 }
4495 }
4496 // c:3037 — `settyinfo(&ti);`
4497 settyinfo(&ti);
4498
4499 // c:3039 — `if (noquery(purge))`
4500 if noquery(purge != 0) != 0 {
4501 if !isem {
4502 // c:3040
4503 let saved = SHTTYINFO.lock().ok().and_then(|g| *g);
4504 if let Some(saved) = saved {
4505 // c:3041 settyinfo(&shttyinfo)
4506 settyinfo(&saved);
4507 }
4508 }
4509 let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"n\n"); // c:3042
4510 return b'n' as i32; // c:3043
4511 }
4512
4513 // c:3046-3061 — `while ((c = read1char(0)) >= 0) { ... }`
4514 c = -1;
4515 loop {
4516 let cc = read1char(0); // c:3046
4517 if cc < 0 {
4518 c = cc;
4519 break;
4520 }
4521 c = cc;
4522 if c == b'Y' as i32 {
4523 // c:3047
4524 c = b'y' as i32; // c:3048
4525 } else if c == b'N' as i32 {
4526 // c:3049
4527 c = b'n' as i32; // c:3050
4528 }
4529 if valid_chars.is_none() {
4530 // c:3051
4531 break; // c:3052
4532 }
4533 if c == b'\n' as i32 {
4534 // c:3053
4535 // c:3054 — `c = *valid_chars;`
4536 c = valid_chars.unwrap().bytes().next().unwrap_or(b'\n') as i32;
4537 nl = 1; // c:3055
4538 break; // c:3056
4539 }
4540 // c:3058 — `if (strchr(valid_chars, c))`
4541 if valid_chars.unwrap().bytes().any(|b| b as i32 == c) {
4542 nl = 1; // c:3059
4543 break; // c:3060
4544 }
4545 zbeep(); // c:3062
4546 }
4547 if c >= 0 {
4548 // c:3064
4549 // c:3065-3066 — `char buf = (char)c; write_loop(SHTTY, &buf, 1);`
4550 let buf = [c as u8]; // c:3065
4551 let _ = write_loop(SHTTY.load(Ordering::Relaxed), &buf); // c:3066
4552 }
4553 if nl != 0 {
4554 // c:3068
4555 let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n"); // c:3069
4556 }
4557
4558 if isem {
4559 // c:3071
4560 if c != b'\n' as i32 {
4561 // c:3072
4562 // c:3073 — `while ((d = read1char(1)) >= 0 && d != '\n');`
4563 loop {
4564 d = read1char(1);
4565 if d < 0 || d == b'\n' as i32 {
4566 break;
4567 }
4568 }
4569 }
4570 } else if c != b'\n' as i32 && valid_chars.is_none() {
4571 // c:3075
4572 // c:3077-3094 — MULTIBYTE_SUPPORT branch: drain trailing bytes
4573 // of an incomplete multibyte sequence.
4574 if isset(MULTIBYTE) && c >= 0 {
4575 // c:3077
4576 // c:3083-3093 — `for (;;) { ret = mbrlen(&cc, 1, &mbs);
4577 // if (ret != MB_INCOMPLETE) break;
4578 // c = read1char(1); if (c < 0) break;
4579 // cc = (char)c; }`
4580 // Rust: model MB_INCOMPLETE detection by checking whether
4581 // the byte buffer so far forms a complete UTF-8 prefix.
4582 // `cc` carries the current byte under examination.
4583 let mut cc: u8 = c as u8; // c:3082
4584 let mut accum: Vec<u8> = vec![cc];
4585 loop {
4586 // mbrlen returns MB_INCOMPLETE when the partial buffer
4587 // doesn't yet form a complete UTF-8 character. Rust
4588 // equivalent: from_utf8 errors with error_len() == None
4589 // when more bytes are needed.
4590 let incomplete = match std::str::from_utf8(&accum) {
4591 Ok(_) => false,
4592 Err(e) => e.error_len().is_none(),
4593 };
4594 if !incomplete {
4595 // c:3088
4596 break;
4597 }
4598 let nc = read1char(1); // c:3089
4599 if nc < 0 {
4600 // c:3090
4601 break; // c:3091
4602 }
4603 cc = nc as u8; // c:3092
4604 accum.push(cc);
4605 }
4606 let _ = cc;
4607 }
4608 // c:3097 — `write_loop(SHTTY, "\n", 1);`
4609 let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n");
4610 }
4611
4612 // c:3101 — `settyinfo(&shttyinfo);` — restore saved TTY state.
4613 let saved = SHTTYINFO.lock().ok().and_then(|g| *g);
4614 if let Some(saved) = saved {
4615 settyinfo(&saved);
4616 }
4617
4618 c // c:3102 return c
4619}
4620
4621// `spscan` (Src/utils.c:3109) — canonical port lives below the
4622// thread_local block in this file. The pre-existing 3-arg
4623// `(name, candidates[], threshold) → Option<String>` shape was a
4624// drift fake (C is `void spscan(HashNode hn, scanflags)`); deleted
4625// because it had zero call sites and conflicted with the faithful
4626// port spckword needs.
4627
4628// spellcheck a word // c:3123
4629// fix s ; if hist is nonzero, fix the history list too // c:3124
4630
4631// File-static state shared with the (inlined) spscan callback. C uses
4632// raw file-statics at utils.c:3045-3050 (`best`, `d`, `guess`, `ic`,
4633// `spckpat`, `spnamepat`); Rust port mirrors them as thread_locals
4634// (per-evaluator per PORT_PLAN.md bucket-1) so concurrent worker
4635// threads each have their own correction state.
4636thread_local! {
4637 /// Port of `static int d;` from `Src/utils.c:3045`. Best dist seen.
4638 static SPCK_D: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
4639 /// Port of `static char *best;` from `Src/utils.c:3046`. Best-match.
4640 static SPCK_BEST: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4641 /// Port of `static char *guess;` from `Src/utils.c:3047`. Word to fix.
4642 static SPCK_GUESS: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4643 /// Port of `static Patprog spckpat;` from `Src/utils.c:3049`.
4644 static SPCK_PAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4645 = const { std::cell::RefCell::new(None) };
4646 /// Port of `static Patprog spnamepat;` from `Src/utils.c:3050`.
4647 static SPCK_NAMEPAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4648 = const { std::cell::RefCell::new(None) };
4649}
4650
4651/// Port of `spscan()` from `Src/utils.c:3109`.
4652///
4653/// Port of `static void spscan(HashNode hn, UNUSED(int scanflags))` from
4654/// `Src/utils.c:3109`. Inlined per-call below because hashtable.rs's
4655/// scan helpers don't model C's `scanhashtable(table, ..., scanfn, ...)`
4656/// callback shape — Rust call sites iterate the table directly. C
4657/// body:
4658/// ```c
4659/// int nd = spdist(hn->nam, guess, (int) strlen(guess) / 4 + 1);
4660/// if (nd < d && (!spckpat || !pattry(spckpat, hn->nam))) {
4661/// best = hn->nam;
4662/// d = nd;
4663/// }
4664/// ```
4665fn spscan(name: &str) {
4666 // c:3074
4667 let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4668 if guess.is_empty() {
4669 return;
4670 }
4671 let thresh = guess.len() / 4 + 1; // c:3076 `(int) strlen(guess) / 4 + 1`
4672 let nd = spdist(name, &guess, thresh) as i32; // c:3076
4673 let d = SPCK_D.with(|c| c.get());
4674 if nd < d {
4675 // c:3077
4676 // c:3078 — `if (!spckpat || !pattry(spckpat, hn->nam))`.
4677 let allow = SPCK_PAT.with(|p| {
4678 p.borrow()
4679 .as_ref()
4680 .map(|prog| !crate::ported::pattern::pattry(prog, name))
4681 .unwrap_or(true)
4682 });
4683 if allow {
4684 SPCK_BEST.with(|b| *b.borrow_mut() = Some(name.to_string())); // c:3079
4685 SPCK_D.with(|c| c.set(nd)); // c:3080
4686 }
4687 }
4688}
4689
4690/// Port of `spckword()` from `Src/utils.c:3128`.
4691///
4692/// Port of `void spckword(char **s, int hist, int cmd, int ask)` from
4693/// `Src/utils.c:3128`.
4694///
4695/// Spell-check `*s`. If a near-match is found in the appropriate
4696/// hashtable (params for `$x`, command tables for command position,
4697/// directory entries otherwise) AND the user accepts the correction
4698/// (`ask=0` auto-accepts), replace `*s` in place with the corrected
4699/// form and (if `hist!=0`) rewrite the history entry too.
4700///
4701/// Faithful 1:1 line-by-line port, including the interactive prompt
4702/// (c:3273-3287) that runs `SPROMPT` through `promptexpand` and reads
4703/// the answer with `getquery("nyae")`.
4704///
4705/// Caller updates: previous Rust signature `(word, candidates[], threshold)
4706/// → Option<String>` is gone — `lex.rs` builds candidate lists itself,
4707/// but C's spckword scans the canonical hashtables directly. The new
4708/// signature matches C exactly: takes `&mut String` for in-place fix.
4709pub fn spckword(s: &mut String, hist: i32, cmd: i32, ask: i32) {
4710 use crate::ported::hashtable::{
4711 aliastab_lock, cmdnamtab_lock, fillcmdnamtable, pathchecked, reswdtab_lock, shfunctab_lock,
4712 };
4713 use crate::ported::params::{getsparam, paramtab};
4714 use crate::ported::pattern::{patcompile, PAT_HEAPDUP};
4715 use crate::ported::zsh_h::{
4716 isset, Dash, Equals, Stringg as StringTok, Tilde, AUTOCD, HASHLISTALL,
4717 };
4718 // c:3130-3133 — locals.
4719 let _t: Option<String>;
4720 let mut ic: char = '\0'; // c:3131
4721 let mut preflen: usize = 0; // c:3132
4722 // c:3133 — `autocd = cmd && isset(AUTOCD) && strcmp(*s, ".") && strcmp(*s, "..")`.
4723 let autocd = cmd != 0 && isset(AUTOCD) && s != "." && s != "..";
4724
4725 // c:3135-3136 — `if (!(*s)[0] || !(*s)[1]) return;`
4726 if s.len() < 2 {
4727 return;
4728 }
4729 // c:3137-3140 — HISTFLAG_NOEXEC or leading %/- skip.
4730 let bytes = s.as_bytes();
4731 let first = bytes[0] as char;
4732 let histdone = crate::ported::hist::histdone.load(std::sync::atomic::Ordering::Relaxed); // c:3137
4733 if (histdone & HISTFLAG_NOEXEC) != 0
4734 || (if cmd != 0 {
4735 first == '%' // c:3139 — leading % is a job
4736 } else {
4737 first == '-' || first == Dash // c:3139 — leading hyphen is an option
4738 })
4739 {
4740 return; // c:3140
4741 }
4742 // c:3141-3142 — `if (!strcmp(*s, "in")) return;`.
4743 if s == "in" {
4744 return; // c:3142
4745 }
4746 // c:3143-3155 — `cmd` branch: skip if it's already a known
4747 // function/builtin/cmdname/alias/reswd. Optional HASHLISTALL
4748 // re-fill of cmdnamtab on miss.
4749 if cmd != 0 {
4750 // c:3143
4751 let known = shfunctab_lock() // c:3144
4752 .read()
4753 .map(|t| t.get(s).is_some())
4754 .unwrap_or(false)
4755 || BUILTINS // c:3145
4756 .iter()
4757 .any(|b| b.node.nam == *s)
4758 || cmdnamtab_lock() // c:3146
4759 .read()
4760 .map(|t| t.get(s).is_some())
4761 .unwrap_or(false)
4762 || aliastab_lock() // c:3147
4763 .read()
4764 .map(|t| t.get(s).is_some())
4765 .unwrap_or(false)
4766 || reswdtab_lock() // c:3148
4767 .read()
4768 .map(|t| t.get(s).is_some())
4769 .unwrap_or(false);
4770 if known {
4771 return; // c:3149
4772 }
4773 // c:3150-3154 — HASHLISTALL: bulk-hash $PATH then retry.
4774 if isset(HASHLISTALL) {
4775 // c:3150
4776 let path: Vec<String> =
4777 getsparam("PATH") // c:3151
4778 .map(|p| p.split(':').map(String::from).collect())
4779 .unwrap_or_default();
4780 fillcmdnamtable(&path); // c:3151
4781 if cmdnamtab_lock() // c:3152
4782 .read()
4783 .map(|t| t.get(s).is_some())
4784 .unwrap_or(false)
4785 {
4786 return; // c:3153
4787 }
4788 }
4789 }
4790 // c:3156-3165 — Tilde/Equals/String prefix skip + itok/Dash
4791 // detok + early-return on any other tokenized char.
4792 let mut start = 0usize;
4793 let bytes = s.as_bytes(); // re-bind after potential string ops
4794 if !bytes.is_empty() {
4795 let c0 = bytes[0] as char;
4796 if c0 == Tilde || c0 == Equals || c0 == StringTok {
4797 // c:3157
4798 start = 1; // c:3158 t++
4799 }
4800 }
4801 // Scan from `start` for tokenized bytes.
4802 {
4803 let mut buf = s.clone().into_bytes();
4804 let mut i = start;
4805 let mut had_dash_only = true; // accumulator
4806 while i < buf.len() {
4807 let b = buf[i];
4808 if itok(b) {
4809 // c:3160
4810 if b as char == Dash {
4811 // c:3161
4812 buf[i] = b'-'; // c:3162
4813 } else {
4814 return; // c:3164
4815 }
4816 } else {
4817 had_dash_only = had_dash_only && false;
4818 }
4819 i += 1;
4820 }
4821 let _ = had_dash_only;
4822 *s = String::from_utf8_lossy(&buf).into_owned();
4823 }
4824 // c:3166 — `best = NULL;`
4825 SPCK_BEST.with(|b| *b.borrow_mut() = None);
4826 SPCK_D.with(|c| c.set(100)); // initialised at each table-scan branch in C; mirror up-front.
4827 // c:3167-3169 — `for (t = *s; *t; t++) if (*t == '/') break;`
4828 // `t` is the position of the first slash (or end of string).
4829 let t_pos = s.find('/').unwrap_or(s.len()); // c:3167
4830 // c:3170-3171 — `if (**s == Tilde && !*t) return;`
4831 let bytes = s.as_bytes();
4832 if !bytes.is_empty() && (bytes[0] as char) == Tilde && t_pos == bytes.len() {
4833 // c:3170
4834 return; // c:3171
4835 }
4836
4837 // c:3173-3178 — compile CORRECT_IGNORE pattern if set.
4838 if let Some(ci) = getsparam("CORRECT_IGNORE") {
4839 // c:3173
4840 let prog = patcompile(
4841 &{
4842 let mut __pat_tok = (&ci).to_string();
4843 crate::ported::glob::tokenize(&mut __pat_tok);
4844 __pat_tok
4845 },
4846 PAT_HEAPDUP,
4847 None,
4848 ); // c:3176
4849 SPCK_PAT.with(|p| *p.borrow_mut() = prog);
4850 } else {
4851 SPCK_PAT.with(|p| *p.borrow_mut() = None); // c:3178
4852 }
4853 // c:3180-3185 — compile CORRECT_IGNORE_FILE pattern if set.
4854 if let Some(ci) = getsparam("CORRECT_IGNORE_FILE") {
4855 // c:3180
4856 let prog = patcompile(
4857 &{
4858 let mut __pat_tok = (&ci).to_string();
4859 crate::ported::glob::tokenize(&mut __pat_tok);
4860 __pat_tok
4861 },
4862 PAT_HEAPDUP,
4863 None,
4864 ); // c:3183
4865 SPCK_NAMEPAT.with(|p| *p.borrow_mut() = prog);
4866 } else {
4867 SPCK_NAMEPAT.with(|p| *p.borrow_mut() = None); // c:3185
4868 }
4869
4870 let bytes = s.as_bytes();
4871 let first = if bytes.is_empty() {
4872 '\0'
4873 } else {
4874 bytes[0] as char
4875 };
4876
4877 // c:3187-3193 — `**s == String && !*t`: $-prefixed name → scan paramtab.
4878 if first == StringTok && t_pos == bytes.len() {
4879 // c:3187
4880 // c:3188 — `guess = *s + 1;` strip leading $.
4881 let guess = s[1..].to_string();
4882 // c:3189-3190 — `if (itype_end(guess, INAMESPC, 1) == guess) return;`
4883 if itype_end(&guess, crate::ported::ztype_h::INAMESPC, true) == 0 {
4884 // c:3189
4885 return; // c:3190
4886 }
4887 ic = StringTok; // c:3191
4888 SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4889 SPCK_D.with(|c| c.set(100)); // c:3192
4890 if let Ok(t) = paramtab().read() {
4891 // c:3193 scanhashtable(paramtab, ..., spscan, ...)
4892 for k in t.keys() {
4893 spscan(k);
4894 }
4895 }
4896 // c:3194-3202 — `**s == Equals`: =cmd → hashcmd; then scan aliases+cmdnam.
4897 } else if first == Equals {
4898 // c:3194
4899 if t_pos != bytes.len() {
4900 // c:3195
4901 return; // c:3196
4902 }
4903 // c:3197-3198 — `if (hashcmd(guess = *s + 1, pathchecked)) return;`
4904 let guess = s[1..].to_string();
4905 let path: Vec<String> = getsparam("PATH")
4906 .map(|p| p.split(':').map(String::from).collect())
4907 .unwrap_or_default();
4908 let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4909 if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4910 return; // c:3198
4911 }
4912 SPCK_D.with(|c| c.set(100)); // c:3199
4913 ic = Equals; // c:3200
4914 SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4915 if let Ok(t) = aliastab_lock().read() {
4916 // c:3201
4917 for (k, _) in t.iter() {
4918 spscan(k);
4919 }
4920 }
4921 if let Ok(t) = cmdnamtab_lock().read() {
4922 // c:3202
4923 for (k, _) in t.iter() {
4924 spscan(k);
4925 }
4926 }
4927 // c:3203-3248 — default branch: filename / dir spell-check.
4928 } else {
4929 // c:3203
4930 let mut guess = s.clone(); // c:3204
4931 // c:3205-3218 — Tilde / String inline-expand prefix handling.
4932 if !guess.is_empty()
4933 && ((guess.as_bytes()[0] as char) == Tilde
4934 || (guess.as_bytes()[0] as char) == StringTok)
4935 {
4936 // c:3205
4937 ic = guess.as_bytes()[0] as char; // c:3207
4938 if t_pos + 1 >= s.len() {
4939 // c:3208 — `if (!*++t) return;`
4940 return; // c:3209
4941 }
4942 // c:3210-3214 — `noerrs=2; singsub(&guess); noerrs = ne;`
4943 let saved_noerrs = *crate::ported::utils::noerrs_lock().lock().unwrap();
4944 *crate::ported::utils::noerrs_lock().lock().unwrap() = 2; // c:3212
4945 guess = crate::ported::subst::singsub(&guess); // c:3213
4946 *crate::ported::utils::noerrs_lock().lock().unwrap() = saved_noerrs;
4947 if guess.is_empty() {
4948 return; // c:3216 `if (!guess) return;`
4949 }
4950 // c:3217 — `preflen = strlen(guess) - strlen(t);` t = original
4951 // s[t_pos..] (the post-slash remainder).
4952 let t_len = s.len() - t_pos;
4953 preflen = guess.len().saturating_sub(t_len);
4954 }
4955 // c:3219-3220 — `if (access(unmeta(guess), F_OK) == 0) return;`
4956 let cstr = match std::ffi::CString::new(unmeta(&guess).as_str()) {
4957 Ok(c) => c,
4958 Err(_) => return,
4959 };
4960 if unsafe { libc::access(cstr.as_ptr(), libc::F_OK) } == 0 {
4961 // c:3219
4962 return; // c:3220
4963 }
4964 // c:3221 — `best = spname(guess);`
4965 // The Rust spname has a signature-drift adaptation taking
4966 // `(name, dir)`; pass the parent dir extracted from guess.
4967 let path_obj = std::path::Path::new(&guess);
4968 let parent = path_obj
4969 .parent()
4970 .and_then(|p| p.to_str())
4971 .filter(|s| !s.is_empty())
4972 .unwrap_or(".");
4973 let basename = path_obj.file_name().and_then(|n| n.to_str()).unwrap_or("");
4974 let best = spname(basename, parent);
4975 SPCK_BEST.with(|b| *b.borrow_mut() = best);
4976 SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess.clone()));
4977 // c:3222-3247 — command-position default branch: hashcmd +
4978 // scan tables + autocd cdpath scan.
4979 if t_pos == s.len() && cmd != 0 {
4980 // c:3222
4981 // c:3223-3224 — hashcmd shortcut.
4982 let path: Vec<String> = getsparam("PATH")
4983 .map(|p| p.split(':').map(String::from).collect())
4984 .unwrap_or_default();
4985 let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4986 if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4987 return; // c:3224
4988 }
4989 SPCK_D.with(|c| c.set(100)); // c:3225
4990 // c:3226-3230 — scan reswd, alias, shfunc, builtin, cmdnam.
4991 if let Ok(t) = reswdtab_lock().read() {
4992 // c:3226
4993 for (k, _) in t.iter() {
4994 spscan(k);
4995 }
4996 }
4997 if let Ok(t) = aliastab_lock().read() {
4998 // c:3227
4999 for (k, _) in t.iter() {
5000 spscan(k);
5001 }
5002 }
5003 if let Ok(t) = shfunctab_lock().read() {
5004 // c:3228
5005 for (k, _) in t.iter() {
5006 spscan(k);
5007 }
5008 }
5009 // c:3229 — builtintab scan: BUILTINS is a static array.
5010 for b in BUILTINS.iter() {
5011 spscan(&b.node.nam);
5012 }
5013 if let Ok(t) = cmdnamtab_lock().read() {
5014 // c:3230
5015 for (k, _) in t.iter() {
5016 spscan(k);
5017 }
5018 }
5019 // c:3231-3247 — autocd $cdpath scan: for each cdpath
5020 // entry, find the closest filename match via mindist.
5021 // Strict `<` (not `<=` as in spscan) so a cdpath dir
5022 // wins only when STRICTLY better than the existing best
5023 // — preferring earlier cdpath dirs on ties.
5024 if autocd {
5025 // c:3232 — `if (cd_able_vars(unmeta(guess))) return;`
5026 let unmeta_guess = crate::ported::utils::unmeta(&guess);
5027 if crate::ported::builtin::cd_able_vars(&unmeta_guess).is_some() {
5028 return; // c:3233
5029 }
5030 // c:3234 — `for (pp = cdpath; *pp; pp++)`. Read CDPATH.
5031 let cdpath: Vec<String> = crate::ported::params::paramtab()
5032 .read()
5033 .ok()
5034 .and_then(|t| t.get("cdpath").and_then(|pm| pm.u_arr.clone()))
5035 .unwrap_or_default();
5036 let mut cur_d = SPCK_D.with(|c| c.get());
5037 let mut cur_best = SPCK_BEST.with(|b| b.borrow().clone());
5038 for pp in cdpath.iter() {
5039 // c:3235-3245 — `mindist(*pp, *s, bestcd, 1)`.
5040 if let Some((bestcd, thisdist)) = mindist(pp, &s) {
5041 // c:3239 — STRICT `<` (not `<=`) per C comment
5042 // at c:3236-3239.
5043 if (thisdist as i32) < cur_d {
5044 cur_best = Some(bestcd);
5045 cur_d = thisdist as i32;
5046 }
5047 }
5048 }
5049 SPCK_BEST.with(|b| *b.borrow_mut() = cur_best);
5050 SPCK_D.with(|c| c.set(cur_d));
5051 }
5052 }
5053 }
5054 // c:3250-3251 — `if (errflag) return;`
5055 // A user interrupt sets ERRFLAG_INT, never ERRFLAG_ERROR (signals.c:457), and
5056 // the C line cited above tests the WHOLE errflag, so masking here let an
5057 // interrupted shell keep going where zsh stops.
5058 if errflag.load(std::sync::atomic::Ordering::Relaxed) != 0 {
5059 return; // c:3251
5060 }
5061 // c:3252 — `if (best && strlen(best) > 1 && strcmp(best, guess))`.
5062 let best = SPCK_BEST.with(|b| b.borrow().clone());
5063 let mut guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
5064 let Some(mut best) = best else {
5065 return;
5066 };
5067 if best.len() <= 1 || best == guess {
5068 return;
5069 }
5070 // c:3253-3272 — assemble the prefixed/de-tokenized replacement.
5071 if ic != '\0' {
5072 // c:3254
5073 if preflen > 0 {
5074 // c:3256
5075 // c:3258-3259 — `if (strncmp(guess, best, preflen)) return;`
5076 if !best.starts_with(&guess[..preflen.min(guess.len())]) {
5077 return; // c:3259
5078 }
5079 // c:3261-3263 — `u = ...s[0..t-*s] + best[preflen..]`.
5080 let t_off = s.len() - (s.len() - t_pos);
5081 let mut u = String::with_capacity(t_off + best.len() - preflen + 1);
5082 u.push_str(&s[..t_off]);
5083 u.push_str(&best[preflen..]);
5084 best = u;
5085 } else {
5086 // c:3264 — `u = "\0" + best;` (prepend NUL placeholder).
5087 best = format!("\0{}", best);
5088 }
5089 // c:3269-3271 — `best = u; guess = *s; *guess = *best = ztokens[ic - Pound];`
5090 let pound = crate::ported::zsh_h::Pound as u8;
5091 let zt = crate::ported::lex::ztokens.as_bytes();
5092 let token_char = if (ic as u8) >= pound {
5093 let idx = (ic as u8 - pound) as usize;
5094 if idx < zt.len() {
5095 zt[idx] as char
5096 } else {
5097 ic
5098 }
5099 } else {
5100 ic
5101 };
5102 // Set first char of both `*s` (the original) and `best`.
5103 if !s.is_empty() {
5104 let mut sb = s.clone().into_bytes();
5105 sb[0] = token_char as u8;
5106 *s = String::from_utf8_lossy(&sb).into_owned();
5107 }
5108 if !best.is_empty() {
5109 let mut bb = best.into_bytes();
5110 bb[0] = token_char as u8;
5111 best = String::from_utf8_lossy(&bb).into_owned();
5112 }
5113 guess = s.clone(); // c:3270 `guess = *s;`
5114 }
5115 // c:3273-3289 — interactive prompt (`ask`) or auto-accept.
5116 let x: char;
5117 if ask != 0 {
5118 // c:3273
5119 if noquery(false) != 0 {
5120 // c:3274
5121 x = 'n'; // c:3275
5122 } else if *crate::ported::init::shout.lock().unwrap() != 0 {
5123 // c:3276 `else if (shout)`
5124 // c:3278 — `pptbuf = promptexpand(sprompt, 0, NULL, best, guess);`
5125 // rs/Rs reach the expansion through PROMPT_RS_STRINGS (see there).
5126 crate::ported::prompt::PROMPT_RS_STRINGS
5127 .with(|c| c.set((Some(best.clone()), Some(guess.clone()))));
5128 let sprompt = getsparam("SPROMPT").unwrap_or_default();
5129 let (pptbuf, _, _) = crate::ported::prompt::promptexpand(&sprompt, 0, None);
5130 let mut shout_buf: Vec<u8> = Vec::new();
5131 zputs(&pptbuf, &mut shout_buf); // c:3279
5132 crate::shout::write(&shout_buf);
5133 crate::shout::flush(); // c:3281 fflush(shout)
5134 zbeep(); // c:3282
5135 let q = getquery(Some("nyae"), 0); // c:3283
5136 x = if q < 0 { 'n' } else { q as u8 as char };
5137 if cmd != 0 && x == 'n' {
5138 // c:3284
5139 pathchecked.store(0, std::sync::atomic::Ordering::Relaxed); // c:3285 `pathchecked = path;`
5140 }
5141 } else {
5142 x = 'n'; // c:3287
5143 }
5144 } else {
5145 x = 'y'; // c:3289
5146 }
5147 // c:3290-3300 — apply chosen action.
5148 if x == 'y' {
5149 // c:3290
5150 *s = best.clone(); // c:3291 `*s = dupstring(best);`
5151 if hist != 0 {
5152 // c:3292
5153 crate::ported::hist::hwrep(&best); // c:3293
5154 }
5155 } else if x == 'a' {
5156 // c:3294
5157 crate::ported::hist::histdone
5158 .fetch_or(HISTFLAG_NOEXEC, std::sync::atomic::Ordering::Relaxed); // c:3295
5159 } else if x == 'e' {
5160 // c:3296
5161 crate::ported::hist::histdone.fetch_or(
5162 HISTFLAG_NOEXEC | crate::ported::zsh_h::HISTFLAG_RECALL,
5163 std::sync::atomic::Ordering::Relaxed,
5164 ); // c:3297
5165 }
5166 // c:3299-3300 — `if (ic) **s = ic;` — restore prefix sigil.
5167 if ic != '\0' && !s.is_empty() {
5168 let mut sb = s.clone().into_bytes();
5169 sb[0] = ic as u8;
5170 *s = String::from_utf8_lossy(&sb).into_owned();
5171 }
5172}
5173
5174/// Port of `ztrftimebuf()` from `Src/utils.c:3312` — C decl `ztrftimebuf(int *bufsizeptr, int decr)`.
5175///
5176/// ```c
5177/// static int ztrftimebuf(int *bufsizeptr, int decr) {
5178/// if (*bufsizeptr <= decr) return 1;
5179/// *bufsizeptr -= decr;
5180/// return 0;
5181/// }
5182/// ```
5183///
5184/// "Helper for ztrftime: try to fit decr more bytes (plus a NUL)
5185/// in the buffer, and a new string length to decrement from that.
5186/// Returns 0 if the new length fits, 1 otherwise."
5187///
5188/// Previous Rust port had wrong semantics — returned `needed.max(256)`
5189/// (a buffer-sizing helper) instead of the C "decrement-and-check"
5190/// semantics. New port matches C: takes &mut bufsize + decr,
5191/// returns i32 (0 = fit, 1 = doesn't fit).
5192pub fn ztrftimebuf(bufsizeptr: &mut i32, decr: i32) -> i32 {
5193 if *bufsizeptr <= decr {
5194 return 1;
5195 }
5196 *bufsizeptr -= decr;
5197 0
5198}
5199
5200/// Format time struct (from utils.c ztrftime)
5201/// Port of `ztrftime(char *buf, int bufsize, char *fmt, struct tm *tm, long nsec)` from `Src/utils.c:3337`.
5202/// WARNING: param names don't match C — Rust=(fmt, time) vs C=(buf, bufsize, fmt, tm, nsec)
5203// Rust idiom replacement: `SystemTime::duration_since(UNIX_EPOCH)`
5204// + libc::localtime covers the C tm-struct populate; the C source's
5205// 192-line body builds the fmt-walk by hand because C has no
5206// strftime extension support — Rust delegates to libc::strftime via
5207// the strftime crate equivalent inline.
5208/// Port of `ztrftime()` from `Src/utils.c:3337`.
5209///
5210/// `ztrftime` — see implementation.
5211///
5212/// `use_gmt` controls whether time fields are interpreted as UTC (true,
5213/// libc::gmtime) or local time (false, libc::localtime). C's ztrftime
5214/// takes a `struct tm *` produced by the caller via gmtime/localtime
5215/// (e.g. Src/Modules/stat.c:201 picks between them based on STF_GMT);
5216/// since the Rust signature takes `SystemTime` we hoist that choice into
5217/// this bool param.
5218pub fn ztrftime(fmt: &str, time: std::time::SystemTime, use_gmt: bool) -> String {
5219 // C takes `struct tm *` + nsec directly, so negative time_t
5220 // (pre-1970) flows through localtime untouched. SystemTime
5221 // pre-epoch surfaces as duration_since Err — recover the signed
5222 // seconds rather than collapsing to 0 (the old unwrap_or_default
5223 // pinned every pre-epoch timestamp to the epoch).
5224 let (secs, nsec): (i64, u64) = match time.duration_since(UNIX_EPOCH) {
5225 Ok(d) => (d.as_secs() as i64, d.subsec_nanos() as u64),
5226 Err(e) => {
5227 let d = e.duration();
5228 let s = -(d.as_secs() as i64);
5229 let n = d.subsec_nanos() as u64;
5230 // sub-second part rolls the seconds down one: -1.25s
5231 // before epoch is secs=-2, nsec=750ms in tm terms.
5232 if n > 0 {
5233 (s - 1, 1_000_000_000 - n)
5234 } else {
5235 (s, 0)
5236 }
5237 }
5238 };
5239
5240 #[cfg(unix)]
5241 unsafe {
5242 let tm = if use_gmt {
5243 libc::gmtime(&secs)
5244 } else {
5245 libc::localtime(&secs)
5246 };
5247 if tm.is_null() {
5248 return String::new();
5249 }
5250 let tm_ref = &*tm;
5251
5252 // c:3398-3406 — pre-pass: walk fmt and substitute zsh-specific
5253 // extensions BEFORE delegating to libc::strftime. The C source
5254 // handles these inline; Rust pre-rewrites them into literal
5255 // numbers so libc::strftime sees only standard specifiers.
5256 // %K = 24-hr clock, no leading zero (0-23)
5257 // %L = 12-hr clock, no leading zero (1-12)
5258 // %f = day of month, no leading zero (1-31)
5259 // %.N = fractional seconds, N digits (0-9)
5260 let mut preprocessed = String::with_capacity(fmt.len());
5261 let bytes = fmt.as_bytes();
5262 let mut i = 0;
5263 while i < bytes.len() {
5264 if bytes[i] == b'%'
5265 && i + 1 < bytes.len()
5266 && bytes[i + 1..].iter().all(|c| b"OE^#_-0123456789".contains(c))
5267 {
5268 // c:3329-3337 `morefmt:` skips the modifier/width chars
5269 // (`while (*fmt && strchr("OE^#_-0123456789", *fmt))`); when
5270 // that runs off the end, `switch (*fmt++)` sees the NUL and
5271 // c:3368-3372 `case '\0': *buf++ = '%'; fmt--;` emits a bare
5272 // `%`, dropping the dangling modifiers (`a%E` -> `a%`).
5273 // `%%` so strftime(3) below renders that single `%`.
5274 preprocessed.push_str("%%");
5275 break;
5276 }
5277 if bytes[i] == b'%' && i + 1 < bytes.len() {
5278 // c:3374-3384 — parse optional `N.` prefix (digit count
5279 // for the `%.` fractional-seconds specifier).
5280 let mut j = i + 1;
5281 while j < bytes.len() && bytes[j].is_ascii_digit() {
5282 j += 1;
5283 }
5284 let digs: u32 = if j > i + 1 {
5285 std::str::from_utf8(&bytes[i + 1..j])
5286 .unwrap_or("3")
5287 .parse()
5288 .unwrap_or(3)
5289 } else {
5290 3
5291 };
5292 // c:3408 — `switch (*fmt++)` examines the specifier
5293 // char (after the digit prefix).
5294 if j < bytes.len() && bytes[j] == b'.' {
5295 // c:3409-3430 — `case '.':` fractional seconds.
5296 // long fnsec = nsec;
5297 // if (digs < 0 || digs > 9) digs = 9;
5298 // if (digs < 9) {
5299 // int trunc; long max = 100000000;
5300 // for (trunc = 8 - digs; trunc; trunc--)
5301 // { max /= 10; fnsec /= 10; }
5302 // max -= 1;
5303 // fnsec = (fnsec + 5) / 10;
5304 // if (fnsec > max) fnsec = max;
5305 // }
5306 // sprintf(buf, "%0*ld", digs, fnsec);
5307 //
5308 // The `(fnsec + 5) / 10` is a ROUND-half-up, capped
5309 // at all-nines so a carry can't overflow the field.
5310 // The previous port plain-truncated, so 999_999 ns
5311 // under `%3.` printed `000` where zsh prints `001`.
5312 let mut fnsec: u64 = nsec;
5313 // c:3412-3413 — digs default is 3 (c:3362); >9 clamps to 9.
5314 let digs = if digs > 9 { 9 } else { digs };
5315 if digs < 9 {
5316 // c:3418-3422
5317 let mut max: u64 = 100_000_000;
5318 for _ in 0..(8 - digs) {
5319 max /= 10;
5320 fnsec /= 10;
5321 }
5322 max -= 1; // c:3423
5323 fnsec = (fnsec + 5) / 10; // c:3424
5324 if fnsec > max {
5325 fnsec = max; // c:3426
5326 }
5327 }
5328 // c:3428 — `sprintf(buf, "%0*ld", digs, fnsec)`.
5329 preprocessed.push_str(&format!("{:0width$}", fnsec, width = digs as usize));
5330 i = j + 1;
5331 continue;
5332 }
5333 let next = bytes[i + 1];
5334 // c:3445-3460 — %K / %L / %f extensions.
5335 match next {
5336 b'K' => {
5337 preprocessed.push_str(&format!("{}", tm_ref.tm_hour));
5338 i += 2;
5339 continue;
5340 }
5341 b'L' => {
5342 let mut h12 = tm_ref.tm_hour % 12;
5343 if h12 == 0 {
5344 h12 = 12;
5345 }
5346 preprocessed.push_str(&format!("{}", h12));
5347 i += 2;
5348 continue;
5349 }
5350 b'f' => {
5351 preprocessed.push_str(&format!("{}", tm_ref.tm_mday));
5352 i += 2;
5353 continue;
5354 }
5355 b'N' => {
5356 // c:3491-3495 — `%N`: nanoseconds, always 9 digits
5357 // (`sprintf(buf, "%09ld", nsec)`).
5358 preprocessed.push_str(&format!("{:09}", nsec));
5359 i += 2;
5360 continue;
5361 }
5362 _ => {}
5363 }
5364 }
5365 preprocessed.push(bytes[i] as char);
5366 i += 1;
5367 }
5368
5369 // c:3354-3359 — `if (*fmt == Meta) { int chr = fmt[1] ^ 32;
5370 // *buf++ = chr; fmt += 2; }`: C's format string is METAFIED, so an
5371 // embedded NUL arrives as `Meta` + 0x20 and is copied straight to
5372 // the output, never reaching `strftime(3)`. The Rust port carries
5373 // the format as un-metafied bytes, so a NUL is a real interior NUL
5374 // — `CString::new` rejected it and `unwrap_or_default()` silently
5375 // produced an EMPTY format, dropping the whole result
5376 // (`strftime $'%Y\0%m\0%d' …` printed nothing). Split on NUL,
5377 // run strftime(3) per segment, and re-join with NUL so the byte
5378 // passes through exactly as C's Meta arm does.
5379 let mut result = String::new();
5380 for (seg_idx, seg) in preprocessed.split('\0').enumerate() {
5381 if seg_idx > 0 {
5382 result.push('\0');
5383 }
5384 if seg.is_empty() {
5385 continue;
5386 }
5387 let mut buf = vec![0u8; 256];
5388 let c_fmt = match CString::new(seg) {
5389 Ok(c) => c,
5390 Err(_) => continue,
5391 };
5392 let len = libc::strftime(
5393 buf.as_mut_ptr() as *mut libc::c_char,
5394 buf.len(),
5395 c_fmt.as_ptr(),
5396 tm,
5397 );
5398 if len > 0 {
5399 buf.truncate(len);
5400 result.push_str(&String::from_utf8_lossy(&buf));
5401 }
5402 }
5403 result
5404 }
5405
5406 #[cfg(not(unix))]
5407 {
5408 let _ = (fmt, secs, nsec);
5409 String::new()
5410 }
5411}
5412
5413/// Port of `zjoin()` from `Src/utils.c:3622` — C decl `zjoin(char **arr, int delim, int heap)`.
5414///
5415/// Join array with delimiter (from utils.c zjoin)
5416/// Joins `arr` with `delim` between elements. When `delim` is itself a meta
5417/// byte (`imeta(delim)` — NUL, `Meta`, `Marker`, or a token in the
5418/// Pound..Nularg range; see `inittyptab` c:4195-4201), C writes the delimiter
5419/// in *metafied* form (`Meta` byte then `delim ^ 32`, c:3634-3637) rather than
5420/// the raw byte (c:3639). The earlier port emitted the raw char always, which
5421/// was byte-wrong for meta delimiters — notably `zjoin(array, '\0', …)` (NUL is
5422/// imeta), where C emits `0x83 0x20`, not a raw `0x00`.
5423///
5424/// WARNING: param names don't match C — Rust=(arr, delim) vs C=(arr, delim, heap).
5425/// The `heap` arg is dropped (Rust `String` owns its storage). As with
5426/// [`metafy`], the `Meta` byte cannot live in valid UTF-8, so the final
5427/// `String` boundary uses the same `from_utf8`/lossy fallback — byte-exact for
5428/// every ASCII (non-meta) delimiter, lossy only on meta delimiters. Callers
5429/// needing byte-exact meta-delimited output should join at the byte level.
5430pub fn zjoin(arr: &[String], delim: char) -> String {
5431 // c:3622
5432 // c:3629-3630 — empty array → "".
5433 if arr.is_empty() {
5434 return String::new();
5435 }
5436 let db = delim as u32;
5437 // c:3634 — `imeta(delim)`. Only byte-valued delimiters can be meta.
5438 let delim_meta = db < 0x100 && imeta_byte(db as u8);
5439 let mut out: Vec<u8> = Vec::new();
5440 for (i, s) in arr.iter().enumerate() {
5441 if i != 0 {
5442 // The separator that C writes after every element then chops off
5443 // the trailing one (c:3641) is equivalent to interposing it.
5444 if delim_meta {
5445 out.push(Meta); // c:3635
5446 out.push((db as u8) ^ 32); // c:3636
5447 } else if db < 0x100 {
5448 out.push(db as u8); // c:3639 — raw single byte
5449 } else {
5450 // Non-byte char delimiter (no C analogue): emit its UTF-8.
5451 let mut buf = [0u8; 4];
5452 out.extend_from_slice(delim.encode_utf8(&mut buf).as_bytes());
5453 }
5454 }
5455 out.extend_from_slice(s.as_bytes()); // c:3633 — strucpy(&ptr, *s)
5456 }
5457 String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
5458}
5459
5460/// Port of `colonsplit()` from `Src/utils.c:3650`.
5461///
5462/// Port of `char **colonsplit(char *s, int uniq)` from
5463/// `Src/utils.c:3650`. Splits `s` on `:`; when `uniq` is set,
5464/// duplicate segments are dropped (linear scan against the
5465/// already-emitted prefix).
5466/// ```c
5467/// char **
5468/// colonsplit(char *s, int uniq)
5469/// {
5470/// int ct;
5471/// char *t, **ret, **ptr, **p;
5472/// for (t = s, ct = 0; *t; t++)
5473/// if (*t == ':') ct++;
5474/// ptr = ret = zalloc(sizeof(char *) * (ct + 2));
5475/// t = s;
5476/// do {
5477/// s = t;
5478/// for (; *t && *t != ':'; t++);
5479/// if (uniq)
5480/// for (p = ret; p < ptr; p++)
5481/// if (strlen(*p) == t - s && !strncmp(*p, s, t - s))
5482/// goto cont;
5483/// *ptr = zalloc((t - s) + 1);
5484/// ztrncpy(*ptr++, s, t - s);
5485/// cont: ;
5486/// } while (*t++);
5487/// *ptr = NULL;
5488/// return ret;
5489/// }
5490/// ```
5491pub fn colonsplit(s: &str, uniq: bool) -> Vec<String> {
5492 // c:3650
5493 // c:3655-3657 — count colons.
5494 let ct = s.matches(':').count(); // c:3655
5495 let mut ret: Vec<String> = Vec::with_capacity(ct + 2); // c:3658 zalloc((ct+2)*sizeof(char *))
5496
5497 // c:3661-3673 — do-while loop walking segments.
5498 let bytes = s.as_bytes();
5499 let mut t: usize = 0;
5500 loop {
5501 let seg_start = t; // c:3662 s = t
5502 // c:3664 — `for (; *t && *t != ':'; t++)`
5503 while t < bytes.len() && bytes[t] != b':' {
5504 t += 1;
5505 }
5506 let seg = &s[seg_start..t];
5507 // c:3665-3668 — uniq dedupe.
5508 if !uniq || !ret.iter().any(|p| p == seg) {
5509 // c:3665
5510 ret.push(seg.to_string()); // c:3670 zalloc + ztrncpy
5511 }
5512 // c:3673 — `while (*t++)` — break if at end, else step past colon.
5513 if t >= bytes.len() {
5514 break;
5515 } // c:3673
5516 t += 1; // c:3673 t++
5517 }
5518 // c:3674 — `*ptr = NULL;` — Rust Vec needs no sentinel.
5519 ret // c:3675
5520}
5521
5522/// Port of `skipwsep()` from `Src/utils.c:3680` — C decl `skipwsep(char **s)`.
5523///
5524/// Skip whitespace separators (`iwsep`-true bytes) at the start of
5525/// `s`. Returns `(remaining_str, count)` — `count` is the number of
5526/// bytes / chars skipped. C body:
5527///
5528/// ```c
5529/// while (*t && iwsep(*t == Meta ? t[1] ^ 32 : *t)) {
5530/// if (*t == Meta) t++;
5531/// t++;
5532/// i++;
5533/// }
5534/// ```
5535///
5536/// The C version honours Meta-encoded bytes (`Meta` followed by
5537/// `^32`-XOR'd byte). Rust port mirrors that byte-by-byte.
5538pub fn skipwsep(s: &str) -> (&str, usize) {
5539 let bytes = s.as_bytes();
5540 let mut i: usize = 0;
5541 let mut count: usize = 0;
5542 while i < bytes.len() {
5543 // Character-level Meta pair (see spacesplit's char_at).
5544 if bytes[i] == 0xc2 && i + 3 < bytes.len() && bytes[i + 1] == Meta {
5545 let Some(n) = s[i + 2..].chars().next() else { break };
5546 if !iwsep((n as u32 as u8) ^ 32) {
5547 break;
5548 }
5549 i += 2 + n.len_utf8();
5550 count += 1;
5551 continue;
5552 }
5553 let b = if bytes[i] == Meta && i + 1 < bytes.len() {
5554 bytes[i + 1] ^ 32
5555 } else {
5556 bytes[i]
5557 };
5558 if !iwsep(b) {
5559 break;
5560 }
5561 if bytes[i] == Meta {
5562 i += 1;
5563 }
5564 i += 1;
5565 count += 1;
5566 }
5567 (&s[i..], count)
5568}
5569
5570/// Port of `spacesplit()` from `Src/utils.c:3711`.
5571///
5572/// Port of `spacesplit(char *s, int allownull, int heap, int quote)` from
5573/// `Src/utils.c:3711`. Splits `s` on `$IFS` via the `ISEP`/`IWSEP` char
5574/// classes (TYPTAB, kept in sync with `$IFS` by `inittyptab`, which
5575/// re-runs on every IFS set — params.rs:8691, c:4795), NOT on hardcoded
5576/// whitespace. zsh's word-splitting rules: runs of IFS-WHITESPACE
5577/// collapse and the leading/trailing ones yield real `""` fields (which
5578/// callers elide); each IFS-NON-WHITESPACE char delimits a field, and
5579/// consecutive ones yield `nulstring` (`Nularg`, `Src/subst.c:36`
5580/// `{Nularg,'\0'}`) empty fields, which survive and are later stripped to
5581/// `""` by remnulargs.
5582///
5583/// WARNING: param names don't match C — Rust=(s, allownull, quote) vs
5584/// C=(s, allownull, heap, quote). The `heap` C-buffer param drops: Rust owns
5585/// its String storage. With `quote` (c:3722-3727, c:3741-3743) a backslash
5586/// before a separator keeps it inside its field and is stripped from the
5587/// private copy of `s` the split works on (`findsep`'s quote arm,
5588/// c:3795-3811). The previous port split only on hardcoded
5589/// `[' ','\t','\n']`, ignoring `$IFS` entirely (Bug #636).
5590pub fn spacesplit(s: &str, allownull: bool, quote: bool) -> Vec<String> {
5591 // c:3711
5592 if quote {
5593 // c:3722-3727 — "we will be stripping quoted separators by hacking
5594 // string, so make sure it's hackable": work on a private copy.
5595 let mut buf = s.to_string();
5596 let mut ret: Vec<String> = Vec::new();
5597 let mut si = 0usize;
5598 let mut t = si; // c:3729 — `t = s;`
5599 si += buf[si..].len() - skipwsep(&buf[si..]).0.len(); // c:3730 — `skipwsep(&s);`
5600 if si < buf.len() && itype_end(&buf[si..], ISEP as u32, true) != 0 {
5601 // c:3732-3733 — `*ptr++ = dup(allownull ? "" : nulstring);`
5602 ret.push(if allownull { String::new() } else { Nularg.to_string() });
5603 } else if !allownull && t != si {
5604 ret.push(String::new()); // c:3734-3735
5605 }
5606 while si < buf.len() {
5607 // c:3736
5608 let iend = itype_end(&buf[si..], ISEP as u32, true); // c:3737
5609 if iend != 0 {
5610 // c:3738-3740 — `s = iend; skipwsep(&s);`
5611 si += iend;
5612 si += buf[si..].len() - skipwsep(&buf[si..]).0.len();
5613 } else if buf.as_bytes()[si] == b'\\' {
5614 // c:3741-3743 — `else if (quote && *s == '\\') { s++; skipwsep(&s); }`
5615 si += 1;
5616 si += buf[si..].len() - skipwsep(&buf[si..]).0.len();
5617 }
5618 t = si; // c:3745 — `t = s;`
5619 findsep(&mut buf, &mut si, None, true); // c:3746 — `findsep(&s, NULL, quote);`
5620 if si > t || allownull {
5621 // c:3747-3750 — `ztrncpy(*ptr++, t, s - t);`
5622 ret.push(buf[t..si].to_string());
5623 } else {
5624 ret.push(Nularg.to_string()); // c:3752 — `dup(nulstring)`
5625 }
5626 t = si; // c:3753 — `t = s;`
5627 si += buf[si..].len() - skipwsep(&buf[si..]).0.len(); // c:3754
5628 }
5629 if !allownull && t != si {
5630 ret.push(String::new()); // c:3756-3757
5631 }
5632 return ret;
5633 }
5634 use crate::ported::ztype_h::zistype;
5635 let bytes = s.as_bytes();
5636 // Meta-aware decode of the logical char value + its byte length at a
5637 // byte position (mirrors skipwsep's `*x == Meta ? x[1] ^ 32 : *x`).
5638 //
5639 // zshrs metafies at the CHARACTER level: a raw byte B >= 0x80 is the
5640 // char U+0083 followed by the char `B ^ 32` (compile_zsh.rs
5641 // meta_encode_byte), i.e. the UTF-8 run `c2 83 <2 bytes>`. Decode that
5642 // run as the ONE byte C sees, or each of its UTF-8 bytes was classified
5643 // separately: `unsetopt multibyte; IFS=$'\xe9'` split `a\xe9b` into
5644 // four fields instead of two.
5645 let char_at = |i: usize| -> (u8, usize) {
5646 if bytes[i] == 0xc2 && i + 3 < bytes.len() && bytes[i + 1] == Meta {
5647 if let Some(n) = s[i + 2..].chars().next() {
5648 return ((n as u32 as u8) ^ 32, 2 + n.len_utf8());
5649 }
5650 }
5651 // c:Src/zsh.h Meta — `\u{83}` prefix; next byte XOR 0x20.
5652 if bytes[i] == Meta && i + 1 < bytes.len() {
5653 (bytes[i + 1] ^ 32, 2)
5654 } else {
5655 (bytes[i], 1)
5656 }
5657 };
5658 // skipwsep(&s) — advance past a run of IWSEP (IFS-whitespace). c:3730
5659 let skipwsep_at = |mut i: usize| -> usize {
5660 while i < bytes.len() {
5661 let (c, l) = char_at(i);
5662 if !iwsep(c) {
5663 break;
5664 }
5665 i += l;
5666 }
5667 i
5668 };
5669 // itype_end(s, ISEP, 1) — `once` advances past exactly ONE ISEP char
5670 // if present, else leaves the index unchanged (c:4462 `if (once) break`).
5671 // c:4412-4455 itype_end / c:3813-3814 findsep under MULTIBYTE —
5672 // `MB_METACHARLENCONV` + `WC_ZISTYPE(c, ISEP)`: a valid non-ASCII
5673 // character is ISEP when it is in `ifs_wide`. zshrs's Meta pair (an
5674 // invalid byte) stays on the byte path below, where it cannot match.
5675 let multibyte = isset(crate::ported::zsh_h::MULTIBYTE);
5676 let wide_sep_at = |i: usize| -> Option<usize> {
5677 if !multibyte || bytes[i] < 0xc0 || (bytes[i] == 0xc2 && bytes.get(i + 1) == Some(&Meta)) {
5678 return None;
5679 }
5680 let c = s[i..].chars().next()?;
5681 Some(if wcsitype(c, ISEP as u32) { c.len_utf8() } else { 0 })
5682 };
5683 let isep_one = |i: usize| -> usize {
5684 if i < bytes.len() {
5685 if let Some(l) = wide_sep_at(i) {
5686 return i + l;
5687 }
5688 let (c, l) = char_at(i);
5689 if zistype(c, ISEP as u32) {
5690 return i + l;
5691 }
5692 }
5693 i
5694 };
5695 // findsep(&s, NULL, 0) — advance to the next ISEP char. c:3784
5696 let findsep_at = |mut i: usize| -> usize {
5697 while i < bytes.len() {
5698 if let Some(l) = wide_sep_at(i) {
5699 if l > 0 {
5700 break;
5701 }
5702 i += s[i..].chars().next().map_or(1, |c| c.len_utf8());
5703 continue;
5704 }
5705 let (c, l) = char_at(i);
5706 if zistype(c, ISEP as u32) {
5707 break;
5708 }
5709 i += l;
5710 }
5711 i
5712 };
5713 let nulstring = || Nularg.to_string(); // c:subst.c:36 nulstring = {Nularg,0}
5714
5715 let mut ret: Vec<String> = Vec::new();
5716 let mut si = 0usize;
5717 let mut t = si; // c:3729 — `t = s;`
5718 si = skipwsep_at(si); // c:3730 — `skipwsep(&s);`
5719 // c:3732-3735 — leading-field handling.
5720 if si < bytes.len() && isep_one(si) != si {
5721 // c:3733 — at an IFS-non-whitespace sep after skipping whitespace.
5722 ret.push(if allownull {
5723 String::new()
5724 } else {
5725 nulstring()
5726 });
5727 } else if !allownull && t != si {
5728 // c:3734-3735 — leading IFS-whitespace was skipped → real "".
5729 ret.push(String::new());
5730 }
5731 // c:3736-3756 — main word loop.
5732 while si < bytes.len() {
5733 let iend = isep_one(si); // c:3737 itype_end(s, ISEP, 1)
5734 let consumed_sep = iend != si;
5735 if consumed_sep {
5736 // c:3738-3741 — consume the single non-ws sep + following WS.
5737 si = iend;
5738 si = skipwsep_at(si);
5739 }
5740 // !!! POSIX-FAITHFUL GATE (no C counterpart) !!!
5741 // zsh (and `emulate sh`) emits a trailing empty field when a
5742 // non-whitespace IFS separator is the last thing in the input:
5743 // `IFS=:; set -- $v` on `a:b:` → (a, b, "") = 3. dash/ksh/bash drop
5744 // that trailing empty → (a, b) = 2. A MIDDLE empty (`a::b`) keeps
5745 // `si < len` here and is unaffected. Scoped to `!allownull` (the
5746 // unquoted-split path `set -- $v` uses); the quoted `"${=v}"` /
5747 // `${(s::)}` allownull path keeps zsh semantics. `zshrs --sh/--ksh/
5748 // --dash` sets posix_faithful(); `--... --zsh` leaves it false.
5749 if consumed_sep && si >= bytes.len() && !allownull && crate::dash_mode::posix_faithful() {
5750 break;
5751 }
5752 t = si; // c:3746 — `t = s;`
5753 si = findsep_at(si); // c:3747 — `findsep(&s, NULL, quote);`
5754 if si > t || allownull {
5755 // c:3748-3751 — emit the word `t..si`.
5756 ret.push(s[t..si].to_string());
5757 } else {
5758 // c:3753 — empty field between non-ws seps → nulstring.
5759 ret.push(nulstring());
5760 }
5761 t = si; // c:3754 — `t = s;`
5762 si = skipwsep_at(si); // c:3755 — `skipwsep(&s);`
5763 }
5764 // c:3757 — trailing IFS-whitespace → real "".
5765 if !allownull && t != si {
5766 ret.push(String::new());
5767 }
5768 ret
5769}
5770
5771/// Port of `findsep()` from `Src/utils.c:3784` — C decl `findsep(char **s, char *sep, int quote)`.
5772///
5773/// c:3762 — "Find a separator. Return 0 if already at separator, 1 if
5774/// separator found later, else -1." `pos` is C's walking `*s`: on
5775/// return it points at the separator (or end-of-string) so the caller
5776/// reads the word as `s[entry_pos..pos]`.
5777///
5778/// `sep` (c:3772):
5779/// - `None` → split on normal `$IFS` separators (ISEP chars).
5780/// - `Some(b"")` → no real separator: advance past one character.
5781/// - `Some(seq)` → look for the (possibly multi-byte) literal `seq`.
5782///
5783/// `quote` (c:3776) — a `\` before a separator suppresses it, and `\\`
5784/// collapses to `\`. This only applies when `sep` is `None` and it
5785/// strips backslashes in place, so the buffer must be modifiable
5786/// (C demands "something modifiable"; Rust takes `&mut String`).
5787///
5788/// The standalone form has no in-tree caller yet — the hot split paths
5789/// (`splitstring`, `findword`, `wordcount`) inline this logic — but it
5790/// is kept as a faithful, callable name-parity anchor.
5791/// WARNING: param names match C semantics — `pos` is C's `*s`.
5792pub fn findsep(s: &mut String, pos: &mut usize, sep: Option<&[u8]>, quote: bool) -> i32 {
5793 // c:3784
5794 use crate::ported::zsh_h::{MB_METACHARINIT, MB_METACHARLEN, MB_METACHARLENCONV};
5795 use crate::ported::ztype_h::{isep, ISEP, WC_ZISTYPE};
5796
5797 MB_METACHARINIT(); // c:3792
5798 let is_isep = |c: Option<char>| c.map(|c| WC_ZISTYPE(c, ISEP as u32)).unwrap_or(false);
5799
5800 match sep {
5801 // c:3793-3824 — default separators (ISEP), with optional quoting.
5802 None => {
5803 let start = *pos;
5804 let mut t = *pos;
5805 while t < s.len() {
5806 let b = s.as_bytes()[t];
5807 if quote && b == b'\\' {
5808 // c:3795 — `if (quote && *t == '\\')`
5809 if t + 1 < s.len() && s.as_bytes()[t + 1] == b'\\' {
5810 // c:3796-3799 — `\\` → `\`: drop one backslash,
5811 // advance past the one we keep (ilen = 1).
5812 chuck(s, t);
5813 t += 1;
5814 continue;
5815 }
5816 // c:3801 — measure the char *after* the backslash.
5817 let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t + 1..]);
5818 if is_isep(c) {
5819 // c:3802-3804 — escaped separator: strip the
5820 // backslash, then advance over the now-bare char.
5821 chuck(s, t);
5822 t += ilen.max(1);
5823 continue;
5824 }
5825 // c:3806-3810 — backslash is a normal byte.
5826 if isep(b) {
5827 break; // (never taken: '\\' is not an ISEP)
5828 }
5829 t += 1; // ilen = 1
5830 } else {
5831 // c:3814-3818 — ordinary character.
5832 let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t..]);
5833 if is_isep(c) {
5834 break; // c:3817
5835 }
5836 t += ilen.max(1);
5837 }
5838 }
5839 let i = if t > start { 1 } else { 0 }; // c:3821 `i = (t > *s)`
5840 *pos = t; // c:3822
5841 i // c:3823
5842 }
5843 // c:3825-3834 — empty separator: advance past the first char.
5844 Some(seq) if seq.is_empty() => {
5845 if *pos < s.len() {
5846 *pos += MB_METACHARLEN(&s.as_bytes()[*pos..]); // c:3831
5847 1 // c:3832
5848 } else {
5849 -1 // c:3833
5850 }
5851 }
5852 // c:3835-3845 — explicit (possibly multi-byte) literal separator.
5853 Some(seq) => {
5854 let mut i = 0i32;
5855 while *pos < s.len() {
5856 // c:3840 — `for (t=sep, tt=*s; *t && *tt && *t==*tt; ...)`
5857 let bytes = s.as_bytes();
5858 let mut k = 0usize;
5859 while k < seq.len() && *pos + k < bytes.len() && seq[k] == bytes[*pos + k] {
5860 k += 1;
5861 }
5862 if k == seq.len() {
5863 return if i > 0 { 1 } else { 0 }; // c:3841 `return (i > 0)`
5864 }
5865 *pos += MB_METACHARLEN(&bytes[*pos..]); // c:3842
5866 i += 1;
5867 }
5868 -1 // c:3844
5869 }
5870 }
5871}
5872
5873/// Port of `findword()` from `Src/utils.c:3849` — C decl `findword(char **s, char *sep)`.
5874///
5875/// Find word at position (from utils.c findword)
5876pub fn findword<'a>(s: &'a str, sep: Option<&'a str>) -> Option<(&'a str, &'a str)> {
5877 let s = match sep {
5878 Some(_) => s,
5879 None => s.trim_start(),
5880 };
5881 if s.is_empty() {
5882 return None;
5883 }
5884 match sep {
5885 Some(sep) => {
5886 if let Some(pos) = s.find(sep) {
5887 Some((&s[..pos], &s[pos + sep.len()..]))
5888 } else {
5889 Some((s, ""))
5890 }
5891 }
5892 None => {
5893 let end = s.find(|c: char| c.is_ascii_whitespace()).unwrap_or(s.len());
5894 Some((&s[..end], &s[end..]))
5895 }
5896 }
5897}
5898
5899/// Port of `wordcount()` from `Src/utils.c:3879` — C decl `wordcount(char *s, char *sep, int mul)`.
5900///
5901/// Returns the number of words in `s` that would result from splitting
5902/// on `sep` (or `$IFS` when `sep` is None). `mul` controls how
5903/// consecutive empty fields are counted:
5904/// - `mul == 0`: don't count leading/trailing/consecutive empties.
5905/// - `mul > 0`: count consecutive empties (each `sep` boundary
5906/// produces one word, even if the surrounding text is empty).
5907/// - `mul < 0`: count empty trailing fields (final separator after
5908/// the last non-empty field counts as one extra empty word).
5909///
5910/// C body (paraphrased):
5911/// ```c
5912/// if (sep) {
5913/// r = 1;
5914/// sl = strlen(sep);
5915/// for (; (c = findsep(&s, sep, 0)) >= 0; s += sl)
5916/// if ((c || mul) && (sl || *(s + sl)))
5917/// r++;
5918/// } else {
5919/// /* IFS-based: walk skipwsep / itype_end(s, ISEP, 1) */
5920/// }
5921/// ```
5922///
5923/// This port walks the metafied byte stream directly to mirror C's
5924/// pointer arithmetic. The `sep`-based branch is exact; the IFS
5925/// branch uses [`iwsep`] for whitespace-separator detection (C's
5926/// `ISEP` char class collapsed to whitespace, which is the common
5927/// case for default `$IFS` = `" \t\n"`).
5928pub fn wordcount(s: &str, sep: Option<&str>, mul: i32) -> i32 {
5929 // c:3879
5930 let bytes = s.as_bytes();
5931 if let Some(sep) = sep {
5932 // C: r = 1; sl = strlen(sep); for (; findsep(&s,sep,0) >= 0; s+=sl)
5933 // if ((c || mul) && (sl || *(s+sl))) r++;
5934 let sep_bytes = sep.as_bytes();
5935 let sl = sep_bytes.len();
5936 let mut r: i32 = 1;
5937 let mut pos = 0;
5938 while pos <= bytes.len() {
5939 let rest = &bytes[pos..];
5940 let c_offset = match sep_bytes.is_empty() {
5941 true => Some(0usize),
5942 false => rest.windows(sl).position(|w| w == sep_bytes),
5943 };
5944 let Some(c) = c_offset else { break };
5945 // C `c` is the chars before the separator; `(sl || *(s+sl))`
5946 // means: if sl is zero (empty sep), only count when there's a
5947 // following char. Otherwise (sl > 0), the second clause is true
5948 // when sep is non-empty AND there are bytes after sep.
5949 let after_off = pos + c + sl;
5950 let following_nonempty = after_off < bytes.len();
5951 let cond_b = sl != 0 || following_nonempty;
5952 if (c != 0 || mul != 0) && cond_b {
5953 r += 1;
5954 }
5955 if sl == 0 {
5956 // Avoid infinite loop on empty sep — mirrors C's findsep
5957 // which advances by 1 byte when sep is empty.
5958 pos += 1;
5959 } else {
5960 pos += c + sl;
5961 }
5962 }
5963 r
5964 } else {
5965 // c:3888-3905 — the `sep == NULL` (IFS) branch, ported statement by
5966 // statement. The previous rewrite inverted C's leading test — it
5967 // counted a leading WORD where C counts a leading SEPARATOR — and
5968 // used `iwsep` (IFS-whitespace) for a test C makes on `ISEP` (any
5969 // IFS char), so an all-separator string came out short:
5970 // `s=' '; ${(W)#s}` gave 2 against zsh's 4, and `'a b c '` gave
5971 // 4 against 5.
5972 //
5973 // Meta-aware char reads mirror spacesplit's local closures
5974 // (c:Src/utils.c:3711): a Meta byte prefixes the escaped char.
5975 use crate::ported::ztype_h::zistype;
5976 let char_at = |i: usize| -> (u8, usize) {
5977 if bytes[i] == Meta && i + 1 < bytes.len() {
5978 (bytes[i + 1] ^ 32, 2)
5979 } else {
5980 (bytes[i], 1)
5981 }
5982 };
5983 // skipwsep(&s) — c:Src/utils.c:3730.
5984 let skipwsep_at = |mut i: usize| -> usize {
5985 while i < bytes.len() {
5986 let (c, l) = char_at(i);
5987 if !iwsep(c) {
5988 break;
5989 }
5990 i += l;
5991 }
5992 i
5993 };
5994 // itype_end(s, ISEP, 1) — consumes exactly ONE ISEP char
5995 // (c:Src/utils.c:4462 `if (once) break`).
5996 let isep_one = |i: usize| -> usize {
5997 if i < bytes.len() {
5998 let (c, l) = char_at(i);
5999 if zistype(c, ISEP as u32) {
6000 return i + l;
6001 }
6002 }
6003 i
6004 };
6005 // findsep(&s, NULL, 0) — advance to the next ISEP char (c:3784).
6006 let findsep_at = |mut i: usize| -> usize {
6007 while i < bytes.len() {
6008 let (c, l) = char_at(i);
6009 if zistype(c, ISEP as u32) {
6010 break;
6011 }
6012 i += l;
6013 }
6014 i
6015 };
6016 let mut s_pos = 0usize; // c:3888 `char *t = s;`
6017 let mut t = s_pos; // c:3888
6018 let mut r: i32 = 0; // c:3889
6019 if mul <= 0 {
6020 // c:3890
6021 s_pos = skipwsep_at(s_pos); // c:3891
6022 }
6023 // c:3892-3894 — `if ((*s && itype_end(s, ISEP, 1) != s) ||
6024 // (mul < 0 && t != s)) r++;`
6025 if (s_pos < bytes.len() && isep_one(s_pos) != s_pos) || (mul < 0 && t != s_pos) {
6026 r += 1; // c:3894
6027 }
6028 // c:3895-3903 — `for (; *s; r++) { … }`.
6029 while s_pos < bytes.len() {
6030 let ie = isep_one(s_pos); // c:3896
6031 if ie != s_pos {
6032 // c:3897
6033 s_pos = ie; // c:3898
6034 if mul <= 0 {
6035 // c:3899
6036 s_pos = skipwsep_at(s_pos);
6037 }
6038 }
6039 s_pos = findsep_at(s_pos); // c:3901 `(void)findsep(&s, NULL, 0);`
6040 t = s_pos; // c:3902
6041 if mul <= 0 {
6042 // c:3903
6043 s_pos = skipwsep_at(s_pos);
6044 }
6045 r += 1; // c:3895 (loop increment)
6046 }
6047 if mul < 0 && t != s_pos {
6048 // c:3904
6049 r += 1; // c:3905
6050 }
6051 r
6052 }
6053}
6054
6055/// Join array with separator - port from zsh/Src/utils.c sepjoin() lines 3926-3958
6056///
6057/// If sep is None, uses first char of IFS (defaults to space).
6058/// Join an array with separator.
6059/// Port of `sepjoin(char **s, char *sep, int heap)` from Src/utils.c:3928.
6060/// WARNING: param names don't match C — Rust=(arr, sep) vs C=(s, sep, heap)
6061// Rust idiom replacement: `slice::join` covers the C `zalloc`+`strcpy`
6062// loop with running length pre-compute; the `heap` arg drops since
6063// String owns its own allocation.
6064/// Port of `sepjoin()` from `Src/utils.c:3928`.
6065///
6066/// `sepjoin` — see implementation.
6067pub fn sepjoin(arr: &[String], sep: Option<&str>) -> String {
6068 // c:3928
6069 // c:3934-3935 — if (!*s) return heap ? dupstring("") : ztrdup("");
6070 if arr.is_empty() {
6071 return String::new();
6072 }
6073 // c:3936-3945 — `if (!sep)` default-sep arm:
6074 // if (ifs && *ifs != ' ') sep = dupstrpfx(ifs, MB_METACHARLEN(ifs));
6075 // else sep = " ";
6076 // IFS SET-but-EMPTY takes the first branch (`*ifs` is '\0' != ' ')
6077 // and dupstrpfx("", …) yields an EMPTY separator — `"$*"` with
6078 // IFS="" concatenates. Only IFS UNSET (ifs == NULL → getsparam
6079 // None) or starting with a space falls back to " ".
6080 let ifs_storage: String;
6081 let sep_str: &str = match sep {
6082 Some(s) => s, // c:3936
6083 None => {
6084 ifs_storage = match getsparam("IFS") {
6085 // c:3938-3940 — set, first char not space (empty → "").
6086 Some(ifs) if !ifs.starts_with(' ') => ifs
6087 .chars()
6088 .next()
6089 .map(|c| c.to_string())
6090 .unwrap_or_default(),
6091 // c:3941-3944 — unset (NULL) or leading space → " ".
6092 _ => " ".to_string(),
6093 };
6094 &ifs_storage
6095 }
6096 };
6097 // c:3947-3956 — pre-compute total length, alloc, copy elements
6098 // interleaving sep. Rust slice::join collapses all that
6099 // into the one canonical call.
6100 arr.join(sep_str)
6101}
6102
6103/// Port of `sepsplit()` from `Src/utils.c:3962` — C decl `sepsplit(char *s, char *sep, int allownull, int heap)`.
6104///
6105/// Split string by separator - port from zsh/Src/utils.c sepsplit() lines 3961-3992
6106///
6107/// If sep is None, performs IFS-style word splitting (spacesplit).
6108/// Otherwise splits on the given separator string.
6109/// allownull: if true, allows empty strings in result
6110/// Split a string on `IFS` separators.
6111/// WARNING: param names don't match C — Rust=(s, sep, allownull) vs C=(s, sep, allownull, heap)
6112pub fn sepsplit(s: &str, sep: Option<&str>, allownull: bool) -> Vec<String> {
6113 // c:3962
6114 // Handle Nularg at start (zsh internal marker) - line 3968
6115 let s = if s.starts_with('\x00') && s.len() > 1 {
6116 &s[1..]
6117 } else {
6118 s
6119 };
6120
6121 match sep {
6122 None => spacesplit(s, allownull, false),
6123 Some("") => {
6124 // Empty separator: split into characters
6125 if allownull {
6126 s.chars().map(|c| c.to_string()).collect()
6127 } else {
6128 s.chars()
6129 .map(|c| c.to_string())
6130 .filter(|c| !c.is_empty())
6131 .collect()
6132 }
6133 }
6134 Some(sep) => {
6135 let parts: Vec<String> = s.split(sep).map(|p| p.to_string()).collect();
6136 if allownull {
6137 parts
6138 } else {
6139 parts.into_iter().filter(|p| !p.is_empty()).collect()
6140 }
6141 }
6142 }
6143}
6144
6145/// Port of `getshfunc()` from `Src/utils.c:3998` — C decl `getshfunc(char *nam)`.
6146///
6147/// C body:
6148/// ```c
6149/// Shfunc getshfunc(char *nam) {
6150/// return (Shfunc) shfunctab->getnode(shfunctab, nam);
6151/// }
6152/// ```
6153///
6154/// Routes through the global `shfunctab` singleton in
6155/// hashtable.rs (hashtable::shfunctab_lock). Returns an owned
6156/// `shfunc` clone so callers can read `flags` (to detect
6157/// `PM_UNDEFINED` autoload stubs), `funcdef`, `filename`, and
6158/// `body` without holding the table lock. Owned clone vs C's
6159/// `*Shfunc` raw pointer trades one allocation for Rust
6160/// lifetime safety — `getshfunc` is a function-lookup site,
6161/// not per-statement, so the cost is irrelevant.
6162///
6163/// Returning `Option<String>` of just `body` (the old contract)
6164/// made every PM_UNDEFINED autoload stub invisible because
6165/// `body=None` collapsed via `and_then` to `None`, which callers
6166/// then read as "function doesn't exist."
6167pub fn getshfunc(nam: &str) -> Option<shfunc> {
6168 let tab = shfunctab_lock().read().expect("shfunctab poisoned");
6169 tab.get(nam).cloned()
6170}
6171
6172/// Port of `subst_string_by_func()` from `Src/utils.c:4017`.
6173///
6174/// Port of `char **subst_string_by_func(Shfunc func, char *arg1, char *orig)`
6175/// from Src/utils.c:4017.
6176///
6177/// Calls the named shell function with `[func, arg1?, orig]` as
6178/// positional args under `sfcontext = SFC_SUBST` and returns the
6179/// `$reply` array on success. Routes through `callhookfunc` (the
6180/// static-linked equivalent of `doshfunc`), then reads `$reply`
6181/// from the env-var fallback because the global `paramtab` is not
6182/// yet a singleton in the Rust port (params::getaparam takes a
6183/// `&ParamTable` arg).
6184pub fn subst_string_by_func(
6185 func_name: &str,
6186 arg1: Option<&str>,
6187 orig: &str,
6188) -> Option<Vec<String>> // c:4017
6189{
6190 let osc = SFCONTEXT.load(Ordering::Relaxed); // c:4019
6191 let osm = STOPMSG.load(Ordering::Relaxed);
6192 let old_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
6193 let mut args: Vec<String> = Vec::with_capacity(3); // c:4020-4026
6194 args.push(func_name.to_string()); // c:4023
6195 if let Some(a) = arg1 {
6196 // c:4024
6197 args.push(a.to_string()); // c:4025
6198 }
6199 args.push(orig.to_string()); // c:4026
6200 SFCONTEXT // c:4027
6201 .store(SFC_SUBST, Ordering::Relaxed);
6202 INCOMPFUNC.store(0, Ordering::Relaxed); // c:4028
6203
6204 // c:Src/exec.c:1429 — `oldlineno = lineno;` / c:1696 — `lineno = oldlineno;`
6205 // C runs the hook body through `execlist`, which saves the caller's
6206 // `lineno` on entry and restores it on exit, so a shell function
6207 // invoked from inside an expansion leaves the CALLER's line number
6208 // untouched. zshrs drives `$LINENO` from a per-pipe
6209 // BUILTIN_SET_LINENO op: the statement that triggered this expansion
6210 // already emitted its own SET_LINENO, the hook body's ops then
6211 // overwrite `$LINENO`, and nothing puts it back — so `PS4='%N:%i> '`
6212 // rendered the hook's last line for the triggering statement
6213 // (`+fn:14>` where zsh prints `+fn:7>`, D01prompt.ztst "Recursive
6214 // use of prompts"). Same save/restore shape as the `$(...)` site in
6215 // vm_helper.rs:4733/5090.
6216 let saved_lineno = getsparam("LINENO");
6217
6218 // c:4030 — `if (doshfunc(func, l, 1))`. Direct doshfunc call
6219 // mirrors C. body_runner routes through the host body-only entry
6220 // (matching the same shfunc that `callhookfunc` would resolve).
6221 let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
6222 .read()
6223 .ok()
6224 .and_then(|t| t.get(func_name).cloned());
6225 // !!! WARNING: RUST-ONLY CARRIER SCOPING — NO C COUNTERPART !!!
6226 // In C the `${~spec}` GLOB_SUBST switch is a paramsubst LOCAL
6227 // (c:Src/subst.c:1671, c:2596), so a hook run from inside that expansion
6228 // (`~[name]` -> zsh_directory_name) sees the user's option and cannot
6229 // touch the local. zshrs carries the switch through the global option
6230 // table plus subst::TILDE_GLOBSUBST_CARRIER, so the hook body's command
6231 // boundary consumed the carrier, and under LOCAL_OPTIONS doshfunc's
6232 // restore then put the forced ON back with nothing left to undo it:
6233 // GLOB_SUBST stayed on for the rest of the caller, and compsys `_expand`
6234 // filename-generated a word it meant to quote (Y01completion #10).
6235 // Hand the hook the user's value and re-apply the forced one afterwards.
6236 let tilde_user = crate::ported::subst::TILDE_GLOBSUBST_CARRIER.with(|c| c.take());
6237 let forced_globsubst = crate::ported::zsh_h::isset(crate::ported::zsh_h::GLOBSUBST);
6238 if let Some(user) = tilde_user {
6239 crate::ported::options::opt_state_set("globsubst", user);
6240 }
6241 let rc = if let Some(mut shf) = shf_clone {
6242 let name_for_body = func_name.to_string();
6243 let body_args = args.clone();
6244 let body_runner = move || -> i32 {
6245 crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
6246 };
6247 crate::ported::exec::doshfunc(&mut shf, args.clone(), true, body_runner)
6248 } else {
6249 callhookfunc(func_name, Some(&args), 0, std::ptr::null_mut())
6250 };
6251 if let Some(user) = tilde_user {
6252 crate::ported::options::opt_state_set("globsubst", forced_globsubst);
6253 crate::ported::subst::TILDE_GLOBSUBST_CARRIER.with(|c| c.set(Some(user)));
6254 }
6255 // c:4033 — `ret = getaparam("reply")` against paramtab. `reply`
6256 // is a shell-local PM_ARRAY entry, never exported to env.
6257 let ret: Option<Vec<String>> = if rc != 0 {
6258 None // c:4031
6259 } else {
6260 getaparam("reply") // c:4033
6261 };
6262
6263 // c:1696 — `lineno = oldlineno;` (see the save above). `LINENO`
6264 // carries PM_READONLY (`integer-readonly-special`), so the restore
6265 // writes `u_val` directly instead of going through setsparam — the
6266 // same bypass BUILTIN_SET_LINENO uses (fusevm_bridge.rs:7205).
6267 if let Some(ln) = saved_lineno {
6268 let n: crate::ported::zsh_h::zlong = ln.parse().unwrap_or(0);
6269 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
6270 if let Some(pm) = tab.get_mut("LINENO") {
6271 // c:Src/utils.c:121 `zlong lineno` — the value lives in the C
6272 // GLOBAL, reached through LINENO's GSU. A `typeset -h +g LINENO`
6273 // local shadow has no PM_SPECIAL and no GSU, so C's `lineno = N`
6274 // never touches it; skip the paramtab mirror for the same reason.
6275 if (pm.node.flags & crate::ported::zsh_h::PM_SPECIAL as i32) != 0 {
6276 pm.u_val = n;
6277 pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
6278 }
6279 }
6280 }
6281 set_lineno(n as i32);
6282 crate::ported::lex::set_lineno(n as u64);
6283 }
6284 SFCONTEXT.store(osc, Ordering::Relaxed); // c:4035
6285 STOPMSG.store(osm, Ordering::Relaxed); // c:4036
6286 INCOMPFUNC.store(old_incompfunc, Ordering::Relaxed); // c:4037
6287 ret // c:4038
6288}
6289
6290/// Port of `subst_string_by_hook()` from `Src/utils.c:4049`.
6291///
6292/// Port of `char **subst_string_by_hook(char *name, char *arg1, char *orig)`
6293/// from Src/utils.c:4049.
6294///
6295/// Looks up `name` as a shell function and calls
6296/// `subst_string_by_func` on it. If that returns no result, walks
6297/// `${name}_hook` as an array of function names, trying each in
6298/// order until one yields a `$reply`.
6299pub fn subst_string_by_hook(name: &str, arg1: Option<&str>, orig: &str) -> Option<Vec<String>> // c:4049
6300{
6301 let mut ret: Option<Vec<String>> = None;
6302 if getshfunc(name).is_some() {
6303 // c:4054
6304 ret = subst_string_by_func(name, arg1, orig); // c:4055
6305 }
6306 if ret.is_none() {
6307 // c:4058
6308 let arrnam = format!("{}_hook", name); // c:4061-4063
6309 // c:4065 — `arr = getaparam(arrnam)`. The previous Rust port
6310 // read `env::var(arrnam)` and NUL-split — wrong: the hook
6311 // array is a shell-local PM_ARRAY in paramtab, not env.
6312 if let Some(arr) = getaparam(&arrnam) {
6313 // c:4065
6314 for f in arr.iter() {
6315 // c:4068
6316 if f.is_empty() {
6317 continue;
6318 }
6319 if getshfunc(f).is_some() {
6320 // c:4069
6321 ret = subst_string_by_func(f, arg1, orig); // c:4070
6322 if ret.is_some() {
6323 // c:4071
6324 break; // c:4072
6325 }
6326 }
6327 }
6328 }
6329 }
6330 ret // c:4094
6331}
6332
6333/// Port of `mkarray()` from `Src/utils.c:4083` — C decl `mkarray(char *s)`.
6334///
6335/// Make single-element array (from utils.c mkarray)
6336pub fn mkarray(s: Option<&str>) -> Vec<String> {
6337 match s {
6338 Some(val) => vec![val.to_string()],
6339 None => Vec::new(),
6340 }
6341}
6342
6343/// Port of `hmkarray()` from `Src/utils.c:4094` — C decl `hmkarray(char *s)`.
6344///
6345/// Make single-element array on heap (from utils.c hmkarray)
6346pub fn hmkarray(s: &str) -> Vec<String> {
6347 if s.is_empty() {
6348 Vec::new()
6349 } else {
6350 vec![s.to_string()]
6351 }
6352}
6353
6354/// Port of `zbeep()` from `Src/utils.c:4105` — C decl `void zbeep(void)`.
6355///
6356/// Honours `$ZBEEP` (a key-string sequence) when set and the BEEP
6357/// option when unset; emits the BEL char (\007) to SHTTY by
6358/// default. The Rust port writes via `write_loop` to mirror C's
6359/// raw-write semantics.
6360pub fn zbeep() {
6361 // c:4105
6362 queue_signals(); // c:4105
6363 if let Ok(zbeep) = std::env::var("ZBEEP") {
6364 // c:4109
6365 let (decoded, _) = getkeystring(&zbeep); // c:4111
6366 #[cfg(unix)]
6367 {
6368 let shtty = SHTTY.load(Ordering::Relaxed);
6369 if shtty != -1 {
6370 let _ = write_loop(shtty, decoded.as_bytes()); // c:4112
6371 } else {
6372 eprint!("{}", decoded);
6373 }
6374 }
6375 #[cfg(not(unix))]
6376 eprint!("{}", decoded);
6377 } else if isset(BEEP) {
6378 // c:4113
6379 #[cfg(unix)]
6380 {
6381 let shtty = SHTTY.load(Ordering::Relaxed);
6382 if shtty != -1 {
6383 let _ = write_loop(shtty, b"\x07"); // c:4114
6384 } else {
6385 eprint!("\x07");
6386 }
6387 }
6388 #[cfg(not(unix))]
6389 eprint!("\x07");
6390 }
6391 unqueue_signals(); // c:4115
6392}
6393
6394/// Port of `freearray()` from `Src/utils.c:4120` — C decl `freearray(char **s)`.
6395///
6396/// Free array (no-op in Rust, provided for API compat)
6397pub fn freearray(s: Vec<String>) {
6398 // c:4124 — DPUTS(!s, "freearray() with zero argument")
6399 // Rust takes Vec<String> by value (never null), so the C !s
6400 // check maps to no condition that can fire in Rust. Document
6401 // the gap.
6402 let _ = &s; // c:4124 (no-op; Vec is never NULL in Rust)
6403 // Rust Drop handles this
6404}
6405
6406/// Port of `equalsplit()` from `Src/utils.c:4133` — C decl `equalsplit(char *s, char **t)`.
6407///
6408/// Split on '=' returning (name, value) (from utils.c equalsplit)
6409/// WARNING: param names don't match C — Rust=(s) vs C=(s, t)
6410pub fn equalsplit(s: &str) -> Option<(String, String)> {
6411 let eq = s.find('=')?;
6412 Some((s[..eq].to_string(), s[eq + 1..].to_string()))
6413}
6414
6415// initialize the ztypes table // c:4151
6416/// Port of `inittyptab()` from `Src/utils.c:4155`. Initialise the
6417/// `typtab[256]` lookup table that backs the `idigit`/`ialnum`/etc.
6418/// predicates in `ztype_h`.
6419///
6420/// C body (c:4155-4250) does:
6421/// 1. Zero the table.
6422/// 2. Mark 0..=31 and 128..=159 + 127 as ICNTRL.
6423/// 3. Mark '0'..='9' as IDIGIT|IALNUM|IWORD|IIDENT|IUSER.
6424/// 4. Mark 'a'..='z' and 'A'..='Z' as IALPHA|IALNUM|IIDENT|IUSER|IWORD.
6425/// 5. Mark '_' as IIDENT|IUSER.
6426/// 6. Mark '-', '.', Dash as IUSER.
6427/// 7. Mark ' '/'\t' as IBLANK|INBLANK; '\n' as INBLANK.
6428/// 8. Mark '\0', Meta, Marker as IMETA.
6429/// 9. Mark Pound..LAST_NORMAL_TOK as ITOK|IMETA.
6430/// 10. Mark Snull..Nularg as ITOK|IMETA|INULL.
6431/// 11. Walk $IFS adding ISEP and IWSEP for blanks.
6432///
6433/// This first-pass port covers steps 1-7. Steps 8-11 require Meta /
6434/// Marker / Pound / Snull / Nularg constants from zsh_h/zsh.h and
6435/// the `ifs` global; the remaining Meta/IFS marks are skipped until
6436/// those land. Idempotent — safe to call multiple times.
6437pub fn inittyptab() {
6438 // utils.c:4155
6439
6440 // c:4160-4164 — `if (!(typtab_flags & ZTF_INIT))` one-off init:
6441 //
6442 // typtab_flags = ZTF_INIT;
6443 // if (interact && isset(SHINSTDIN))
6444 // typtab_flags |= ZTF_INTERACT;
6445 //
6446 // The `ZTF_INTERACT` half was missing, and it is load-bearing: it is the
6447 // ONLY writer of that bit, c:4257 reads it to decide `ZTF_BANGCHAR`, and
6448 // c:4291 reads THAT to decide whether `bangchar` is `ISPECIAL`. Without
6449 // it the chain never starts, so `!` was never a special character in an
6450 // interactive shell and `${(q)v}` on `v='a!b'` published `a!b` where zsh
6451 // publishes `a\!b`.
6452 //
6453 // C latches this ONCE, on the first call, and never revisits it — later
6454 // calls keep whatever the first decided. Preserved exactly: the whole
6455 // block is inside the `ZTF_INIT` guard.
6456 {
6457 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
6458 if (flags & ZTF_INIT) == 0 {
6459 let mut init = ZTF_INIT; // c:4161
6460 if interact() && isset(crate::ported::zsh_h::SHINSTDIN) {
6461 init |= ZTF_INTERACT; // c:4163
6462 }
6463 TYPTAB_FLAGS.store(init, Ordering::Relaxed);
6464 }
6465 }
6466
6467 // Local scratch, flushed to the atomic table at fn end — C mutates
6468 // typtab[] in place unlocked; the atomic store-per-slot flush keeps
6469 // readers lock-free (see TYPTAB doc in ztype_h.rs).
6470 let mut t = [0u32; 256]; // c:4168 memset(typtab, 0, sizeof(typtab))
6471 // c:4169-4170 — control chars 0..32 and 128..160.
6472 for c in 0..32u32 {
6473 t[c as usize] = ICNTRL as u32;
6474 t[(c + 128) as usize] = ICNTRL as u32;
6475 }
6476 // c:4171 — `typtab[127] = ICNTRL;`
6477 t[127] = ICNTRL as u32;
6478 // c:4172-4173 — '0'..='9'.
6479 for c in (b'0' as usize)..=(b'9' as usize) {
6480 t[c] = (IDIGIT | IALNUM | IWORD | IIDENT | IUSER) as u32;
6481 }
6482 // c:4174-4175 — 'a'..='z' and matching 'A'..='Z'.
6483 for c in (b'a' as usize)..=(b'z' as usize) {
6484 let upper = c - (b'a' as usize) + (b'A' as usize);
6485 let bits = (IALPHA | IALNUM | IIDENT | IUSER | IWORD) as u32;
6486 t[c] = bits;
6487 t[upper] = bits;
6488 }
6489 // c:4190 — `typtab['_'] = IIDENT | IUSER;`
6490 t[b'_' as usize] = (IIDENT | IUSER) as u32;
6491 // c:4191 — `typtab['-'] = typtab['.'] = typtab[(unsigned char) Dash] = IUSER;`.
6492 t[b'-' as usize] = IUSER as u32;
6493 t[b'.' as usize] = IUSER as u32;
6494 // c:4191 — `Dash` token marker (0x9b per zsh.h:182, "Only in patterns").
6495 // Marking it IUSER lets pattern-side $-named-character paths
6496 // accept it as a user-name byte. Previously omitted in the port.
6497 t[crate::token_char::token_byte(crate::ported::zsh_h::Dash).unwrap() as usize] = IUSER as u32;
6498 // c:4192-4194 — blanks.
6499 t[b' ' as usize] |= (IBLANK | INBLANK) as u32;
6500 t[b'\t' as usize] |= (IBLANK | INBLANK) as u32;
6501 t[b'\n' as usize] |= INBLANK as u32;
6502
6503 // c:4195 — `typtab['\0'] |= IMETA;`. Previously omitted in the
6504 // Rust port — '\0' had only ICNTRL (set by the 0..32 loop at
6505 // c:4169) and was missing IMETA. C `imeta('\0')` is true (the
6506 // NUL byte must be Meta-encoded as `Meta + ('\0' ^ 32)`); the
6507 // Rust `imeta()` typtab predicate returned false, so any code
6508 // routing through `imeta(c)` (e.g. `input::shingetline`)
6509 // failed to Meta-encode NUL bytes from stdin, corrupting the
6510 // SHIN buffer when piped binary data hit a `\0`.
6511 t[0] |= IMETA as u32; // c:4195
6512 // c:4196-4197 — Meta + Marker marked IMETA.
6513 {
6514 t[Meta as usize] |= IMETA as u32;
6515 t[crate::token_char::token_byte(Marker).unwrap() as usize] |= IMETA as u32;
6516 }
6517
6518 // c:4198-4199 — `for (t0 = (int) (unsigned char) Pound;
6519 // t0 <= (int) (unsigned char) LAST_NORMAL_TOK; t0++)
6520 // typtab[t0] |= ITOK | IMETA;`
6521 // Marks all char-rewrite token markers (Pound, Stringg, Hat,
6522 // Star, ...). Without this, `itok(Stringg)` returns false and
6523 // `is_valid_assignment_target("$NAME")` wrongly accepts the
6524 // leading `$` as part of an identifier prefix → `$=cmd` lexes
6525 // as ENVSTRING instead of STRING.
6526 {
6527 let lo = crate::token_char::token_byte(Pound).unwrap() as usize;
6528 let hi = crate::token_char::token_byte(LAST_NORMAL_TOK).unwrap() as usize;
6529 for t0 in lo..=hi {
6530 t[t0] |= (ITOK | IMETA) as u32;
6531 }
6532 }
6533
6534 // c:4200-4201 — `for (t0 = (int) (unsigned char) Snull;
6535 // t0 <= (int) (unsigned char) Nularg; t0++)
6536 // typtab[t0] |= ITOK | IMETA | INULL;`
6537 {
6538 let lo = crate::token_char::token_byte(Snull).unwrap() as usize;
6539 let hi = crate::token_char::token_byte(Nularg).unwrap() as usize;
6540 for t0 in lo..=hi {
6541 t[t0] |= (ITOK | IMETA | INULL) as u32;
6542 }
6543 }
6544
6545 // c:4202-4231 — IFS walk. Sets ISEP on every IFS char and IWSEP
6546 // on the blank (inblank) subset. Reads the current `ifs` global
6547 // (defaulting to DEFAULT_IFS), demetafies `Meta+X` pairs, and
6548 // skips a doubled blank (`s[1]==c`) so the IWSEP bit doesn't
6549 // mark "blank repeated → no-skip" IFS chars. Mirrors C exactly.
6550 {
6551 // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; *s; s++)`.
6552 // The default-IFS fallback is keyed on the `ifs` POINTER being
6553 // NULL (i.e. IFS unset), NOT on it pointing at an empty string.
6554 // `IFS=''` leaves a non-NULL empty `ifs`, the loop body never
6555 // runs, and typtab ends up with ZERO ISEP/IWSEP bits — which is
6556 // exactly how an empty IFS disables all word splitting
6557 // (`IFS=''; v='a b c'; print -rl -- ${=v}` is one word). The
6558 // port used to coerce "" → DEFAULT_IFS at both the paramtab read
6559 // and the fallback, so an empty IFS still split on whitespace.
6560 // c:4207 / c:4216 read the global `ifs`, which `ifssetfn` keeps (the
6561 // paramtab node's getfn, `ifsgetfn`, returns the same global). `None`
6562 // is C's `ifs == NULL` after an unset (c:Src/params.c:4748): the node
6563 // is not yet PM_UNSET when `stdunsetfn` rebuilds the table, so it must
6564 // not be consulted.
6565 let pt_ifs: Option<String> = crate::ported::params::ifs_lock()
6566 .lock()
6567 .map(|g| g.clone())
6568 .unwrap_or_default();
6569 // A SET IFS (possibly "") is walked verbatim; c:4216
6570 // `ifs ? ifs : CURRENT_DEFAULT_IFS`. The default is taken in its
6571 // parameter-value (unmetafied) spelling, the one `ifs_lock` starts with
6572 // and c:4210's reset below stores; `DEFAULT_IFS` is C's metafied
6573 // spelling (c:Src/zsh.h:149), which the MULTIBYTE check rejects.
6574 let src: String = pt_ifs.unwrap_or_else(|| " \t\n\0".to_string());
6575 // c:4205-4213 — under MULTIBYTE, `set_widearray(ifs)`; an IFS that
6576 // does not convert (`!ifs_wide.chars`) warns and is reset to the
6577 // default, which is what the ISEP walk below then reads.
6578 let multibyte = isset(crate::ported::zsh_h::MULTIBYTE);
6579 let src = if multibyte && !src.is_empty() && set_widearray(&src).is_empty() {
6580 zwarn("IFS has an invalid character; resetting IFS to default"); // c:4208
6581 // c:4210 `ztrdup(CURRENT_DEFAULT_IFS)` — as the parameter value,
6582 // i.e. unmetafied (`DEFAULT_IFS` is the metafied spelling, c:149).
6583 let dflt = " \t\n\0".to_string();
6584 *crate::ported::params::ifs_lock().lock().expect("ifs poisoned") = Some(dflt.clone());
6585 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
6586 if let Some(pm) = tab.get_mut("IFS") {
6587 pm.u_str = Some(dflt.clone());
6588 }
6589 }
6590 dflt
6591 } else {
6592 src
6593 };
6594 // c:4207/4212 — `set_widearray(ifs, &ifs_wide)`.
6595 if multibyte {
6596 if let Ok(mut w) = IFS_WIDE.write() {
6597 *w = set_widearray(&src);
6598 }
6599 }
6600 let bytes = src.as_bytes();
6601 let mut i = 0;
6602 while i < bytes.len() {
6603 // c:4217 — `int c = (unsigned char) (*s == Meta ? *++s ^ 32 : *s)`.
6604 // zshrs's character-level Meta pair (U+0083 + char `B ^ 32`, the
6605 // UTF-8 run `c2 83 xx xx`) is ONE logical byte, as spacesplit
6606 // decodes it.
6607 let c = if bytes[i] == 0xc2 && i + 3 < bytes.len() && bytes[i + 1] == Meta {
6608 let n = src[i + 2..].chars().next().map_or(0, |n| n as u32 as u8);
6609 i += 1 + src[i + 2..].chars().next().map_or(1, |n| n.len_utf8());
6610 n ^ 32
6611 } else if bytes[i] == Meta && i + 1 < bytes.len() {
6612 i += 1;
6613 bytes[i] ^ 32
6614 } else {
6615 bytes[i]
6616 };
6617 // c:4218-4223 — MULTIBYTE non-ASCII skip. Bytes >= 0x80
6618 // (after demetafy) are not classified by typtab — they
6619 // reach wcsitype via WC_ZISTYPE instead.
6620 if multibyte && c >= 0x80 {
6621 i += 1;
6622 continue;
6623 }
6624 let cu = c as usize;
6625 // c:4224-4229 — `if (inblank(c))` — for default-IFS
6626 // chars space/tab/newline, mark IWSEP unless the next
6627 // byte repeats the same char.
6628 let is_inblank = (t[cu] & (INBLANK as u32)) != 0;
6629 if is_inblank {
6630 if i + 1 < bytes.len() && bytes[i + 1] == c {
6631 i += 1; // c:4226 — skip the dup
6632 } else {
6633 t[cu] |= IWSEP as u32; // c:4228
6634 }
6635 }
6636 // c:4230 — `typtab[c] |= ISEP;`
6637 t[cu] |= ISEP as u32;
6638 i += 1;
6639 }
6640 }
6641
6642 // c:4232-4252 — wordchars walk. ORs IWORD onto every byte in
6643 // `$WORDCHARS` (or DEFAULT_WORDCHARS when unset). Used by every
6644 // word-class lookup in pattern matching, `${var:#word}`, etc.
6645 // Drops to ASCII-only under MULTIBYTE_SUPPORT (the non-ASCII path
6646 // routes through wordchars_wide).
6647 {
6648 // Same fallback as IFS above: paramtab first, then wordchars_lock
6649 // so wordcharssetfn updates that bypass paramtab still reach
6650 // the typtab rebuild.
6651 // c:4237 — `for (s = wordchars ? wordchars : DEFAULT_WORDCHARS;
6652 // *s; s++)`. C tests the POINTER, so `WORDCHARS=''` is a
6653 // legitimately EMPTY word-character set and only an UNSET
6654 // `wordchars` falls back to the default. Treating "" as "unset"
6655 // pinned `/` as a word character forever, so
6656 // `WORDCHARS="" [[ / = [[:WORD:]] ]]` wrongly matched.
6657 let wc: Option<String> = crate::ported::params::paramtab()
6658 .read()
6659 .ok()
6660 .and_then(|t| {
6661 t.get("WORDCHARS")
6662 .map(|pm| crate::ported::params::wordcharsgetfn(pm))
6663 })
6664 .or_else(|| {
6665 crate::ported::params::wordchars_lock()
6666 .lock()
6667 .ok()
6668 .map(|g| g.clone())
6669 });
6670 let src: String = wc.unwrap_or_else(|| DEFAULT_WORDCHARS.to_string());
6671 let bytes = src.as_bytes();
6672 let mut i = 0;
6673 while i < bytes.len() {
6674 // c:4238 — Meta+X demetafy.
6675 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
6676 i += 1;
6677 bytes[i] ^ 32
6678 } else {
6679 bytes[i]
6680 };
6681 // c:4239-4249 — MULTIBYTE non-ASCII skip.
6682 if c < 0x80 {
6683 t[c as usize] |= IWORD as u32; // c:4251
6684 }
6685 i += 1;
6686 }
6687 }
6688
6689 // c:4253-4254 — SPECCHARS walk. ORs ISPECIAL onto every member
6690 // of the hardcoded SPECCHARS string. Drives glob-special and
6691 // quote-special detection.
6692 {
6693 for &b in SPECCHARS.as_bytes() {
6694 t[b as usize] |= ISPECIAL as u32; // c:4254
6695 }
6696 }
6697
6698 // c:4255-4256 — comma special only when ZTF_SP_COMMA was set
6699 // via `makecommaspecial(1)`. KSH_GLOB / extended-glob path.
6700 {
6701 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
6702 if (flags & ZTF_SP_COMMA) != 0 {
6703 // c:4255
6704 t[b',' as usize] |= ISPECIAL as u32; // c:4256
6705 }
6706 }
6707
6708 // c:4257-4261 — bangchar special when BANGHIST + interact +
6709 // bangchar != 0. Sets ZTF_BANGCHAR flag bit then marks the
6710 // bangchar byte ISPECIAL.
6711 {
6712 let bangchar2 = bangchar.load(Ordering::SeqCst) as usize;
6713 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
6714 let interact_flag = (flags & ZTF_INTERACT) != 0;
6715 let banghist = isset(BANGHIST);
6716 if banghist && bangchar2 != 0 && bangchar2 < 256 && interact_flag {
6717 // c:4257
6718 TYPTAB_FLAGS.fetch_or(ZTF_BANGCHAR, Ordering::Relaxed); // c:4258
6719 t[bangchar2] |= ISPECIAL as u32; // c:4259
6720 } else {
6721 TYPTAB_FLAGS.fetch_and(!ZTF_BANGCHAR, Ordering::Relaxed); // c:4261
6722 }
6723 }
6724
6725 // c:4262-4263 — PATCHARS walk. ORs IPATTERN onto every member.
6726 // Used by pattern compilation to detect glob metachars.
6727 {
6728 for &b in PATCHARS.as_bytes() {
6729 t[b as usize] |= IPATTERN as u32; // c:4263
6730 }
6731 }
6732
6733 // Flush the scratch to the lock-free table (see header note).
6734 for (i, v) in t.iter().enumerate() {
6735 TYPTAB[i].store(*v, Ordering::Relaxed);
6736 }
6737}
6738
6739/// Port of `makecommaspecial()` from `Src/utils.c:4270` — C decl `void makecommaspecial(int yesno)`.
6740///
6741/// Toggles `ZTF_SP_COMMA` and the `ISPECIAL` bit on `,` in the
6742/// global typtab — used by glob/extended-glob to flag `,`
6743/// (KSH_GLOB) as a metacharacter.
6744pub fn makecommaspecial(yesno: bool) {
6745 // c:4270
6746 if yesno {
6747 // c:4272
6748 TYPTAB_FLAGS.fetch_or(ZTF_SP_COMMA, Ordering::Relaxed); // c:4273
6749 TYPTAB[b',' as usize].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4274
6750 } else {
6751 TYPTAB_FLAGS.fetch_and(!ZTF_SP_COMMA, Ordering::Relaxed); // c:4276
6752 TYPTAB[b',' as usize].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4277
6753 }
6754}
6755
6756/// Port of `makebangspecial()` from `Src/utils.c:4283` — C decl `void makebangspecial(int yesno)`.
6757///
6758/// Toggles `ISPECIAL` on the current `bangchar`. When `yesno==0`
6759/// always clears; when nonzero, sets only if `ZTF_BANGCHAR` was
6760/// stored by `inittyptab` (i.e. BANGHIST is on).
6761pub fn makebangspecial(yesno: bool) {
6762 // c:4283
6763 let bc = bangchar.load(Ordering::SeqCst) as usize;
6764 if bc == 0 || bc >= 256 {
6765 return;
6766 }
6767 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
6768 if !yesno {
6769 // c:4289
6770 TYPTAB[bc].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4290
6771 } else if (flags & ZTF_BANGCHAR) != 0 {
6772 // c:4291
6773 TYPTAB[bc].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4292
6774 }
6775}
6776
6777/// Port of `wcsiblank()` from `Src/utils.c:4302` — C decl `wcsiblank(wint_t wc)`.
6778///
6779/// ```c
6780/// mod_export int wcsiblank(wint_t wc) {
6781/// if (iswspace(wc) && wc != L'\n')
6782/// return 1;
6783/// return 0;
6784/// }
6785/// ```
6786///
6787/// "wide-character version of the iblank() macro" — true for any
6788/// whitespace EXCEPT newline. The previous Rust port included
6789/// newline (since `c.is_whitespace()` returns true for '\n') —
6790/// wrong for callers that use this to find token boundaries.
6791pub fn wcsiblank(wc: char) -> bool {
6792 wc.is_whitespace() && wc != '\n'
6793}
6794
6795/// Port of `wcsitype()` from `Src/utils.c:4321` — C decl `int wcsitype(wchar_t c, int itype)`.
6796///
6797/// "zistype macro extended to support wide characters. Works for
6798/// IIDENT, IWORD, IALNUM, ISEP."
6799///
6800/// The Rust port checks whether `c` falls in the typtab class
6801/// represented by `itype`. ASCII chars consult the global TYPTAB
6802/// (the same one C's `zistype()` macro indexes); non-ASCII chars
6803/// route through Unicode predicates that mirror the C `iswalnum`
6804/// fallback at line 4346.
6805pub fn wcsitype(c: char, itype: u32) -> bool {
6806 // c:4321
6807 if !isset(MULTIBYTE) {
6808 // c:4327
6809 if (c as u32) < 256 {
6810 return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
6811 }
6812 return false;
6813 }
6814 if (c as u32) < 128 {
6815 // c:4343
6816 return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
6817 }
6818 let cls = itype as u16;
6819 if cls == IIDENT {
6820 // c:4347
6821 if isset(POSIXIDENTIFIERS) {
6822 return false; // c:4348
6823 }
6824 return c.is_alphanumeric(); // c:4350
6825 }
6826 if cls == IWORD {
6827 // c:4352
6828 if c.is_alphanumeric() {
6829 return true;
6830 } // c:4353
6831 // C: IS_COMBINING(c) — no Rust crate-free combining-mark
6832 // predicate. zero-width chars (combining, zero-width-joiner,
6833 // etc.) are treated as word per c:4362.
6834 if unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0 {
6835 // c:4362
6836 return true;
6837 }
6838 // c:4364 — `wmemchr(wordchars_wide.chars, c, …)`. Reads from
6839 // the canonical `wordchars` global (writable by
6840 // `wordcharssetfn` at `Src/params.c:5143`). Previously routed
6841 // through `std::env::var("WORDCHARS")` which is the libc
6842 // process environment — never reflects runtime `WORDCHARS=:`
6843 // assignments inside the shell.
6844 let w = crate::ported::params::paramtab()
6845 .read()
6846 .ok()
6847 .and_then(|t| {
6848 t.get("WORDCHARS")
6849 .map(|pm| crate::ported::params::wordcharsgetfn(pm))
6850 })
6851 .unwrap_or_default();
6852 return w.chars().any(|x| x == c);
6853 }
6854 if cls == ISEP {
6855 // c:4366
6856 // c:4367 — `return !!wmemchr(ifs_wide.chars, c, ifs_wide.len);`
6857 return IFS_WIDE.read().map(|w| w.contains(&c)).unwrap_or(false);
6858 }
6859 let _ = IALNUM;
6860 c.is_alphanumeric() // c:4370
6861}
6862
6863/// Port of `itype_end()` from `Src/utils.c:4395` — C decl `char *itype_end(const char *ptr, int itype, int once)`.
6864///
6865/// Check if a character type at end of string (from utils.c itype_end)
6866///
6867/// Walks `ptr` byte-by-byte advancing past every char whose
6868/// type-bits include `itype`. Returns the byte offset where the
6869/// walk stops (C returns the advanced pointer; the byte offset is
6870/// the Rust equivalent — `ptr + return_value` reproduces the C
6871/// pointer).
6872///
6873/// C body branches (ported line-for-line):
6874///
6875/// 1. **INAMESPC special-case** (c:4399-4413). If itype == INAMESPC
6876/// and we're not POSIXIDENTIFIERS-strict outside ksh emulation,
6877/// recurse on `ptr+(*ptr=='.')` with IIDENT to allow dotted
6878/// ksh93 namespace names. The recursion result determines
6879/// whether to advance past the dot or fall through.
6880///
6881/// 2. **MULTIBYTE branch** (c:4416-4470, gated `isset(MULTIBYTE)
6882/// && (itype != IIDENT || !isset(POSIXIDENTIFIERS))`). Walks
6883/// multibyte chars via mb_metacharlenconv (multibyte len +
6884/// wchar conversion). Per-codepoint test:
6885/// - itok byte (raw command line): test the byte via zistype
6886/// - Meta-prefixed pair: extract real byte via XOR, test via
6887/// zistype
6888/// - ASCII (< 0x80): test via zistype
6889/// - Non-ASCII codepoint: route through wcsitype (which
6890/// already handles IWORD/ISEP via WORDCHARS/IFS paramtab
6891/// lookup matching c:4364/c:4367)
6892///
6893/// 3. **Non-multibyte branch** (c:4474-4480). Simple Meta-byte +
6894/// zistype byte loop.
6895///
6896/// `once = true` stops after the first matching char (used for
6897/// "is this a valid first char" checks per c:1576 etc).
6898pub fn itype_end(s: &str, mut itype: u32, once: bool) -> usize {
6899 use crate::ported::zsh_h::{isset, Meta, EMULATE_KSH, EMULATION, MULTIBYTE, POSIXIDENTIFIERS};
6900 use crate::ported::ztype_h::{itok, zistype, IIDENT, INAMESPC};
6901
6902 // c:4399-4413 — INAMESPC handling with ksh93 namespace dot.
6903 let mut start = 0usize;
6904 if itype == INAMESPC {
6905 itype = IIDENT as u32;
6906 if !isset(POSIXIDENTIFIERS) || EMULATION(EMULATE_KSH) {
6907 // c:4403 — `t = itype_end(ptr + (*ptr == '.'), itype, 0);`
6908 let first_is_dot = s.as_bytes().first().copied() == Some(b'.');
6909 let recurse_start = if first_is_dot { 1 } else { 0 };
6910 let t = recurse_start + itype_end(&s[recurse_start..], itype, false);
6911 // c:4404-4410 — `if (t > ptr + (*ptr == '.')) {
6912 // if (*t == '.') ptr = t + 1; /* Fall through */
6913 // else if (!once) return t;
6914 // }`
6915 if t > recurse_start {
6916 let t_byte = s.as_bytes().get(t).copied();
6917 if t_byte == Some(b'.') {
6918 start = t + 1; // c:4407
6919 } else if !once {
6920 return t; // c:4409
6921 }
6922 }
6923 }
6924 }
6925
6926 let bytes = s.as_bytes();
6927 let mb = isset(MULTIBYTE) && (itype != IIDENT as u32 || !isset(POSIXIDENTIFIERS));
6928
6929 if mb {
6930 // c:4416-4470 — multibyte walk. mb_charinit() in C is a
6931 // no-op stateless reset; not needed in Rust since str::chars
6932 // is already stateless UTF-8.
6933 let mut i = start;
6934 while i < bytes.len() {
6935 let b = bytes[i];
6936 // c:4422-4425 — itok: raw token byte, test via zistype,
6937 // advance 1.
6938 if itok(b) {
6939 if !zistype(b, itype) {
6940 break;
6941 }
6942 i += 1;
6943 } else if b == Meta {
6944 // c:4438-4440 (WEOF arm) + non-MB Meta fallback:
6945 // Meta-prefixed pair = single unescaped char via XOR.
6946 let nxt = bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20;
6947 if (nxt as u32) > 127 || !zistype(nxt, itype) {
6948 break;
6949 }
6950 i += 2;
6951 } else if b < 0x80 {
6952 // c:4441-4443 (len == 1 && isascii) — ASCII: direct
6953 // zistype on the byte.
6954 if !zistype(b, itype) {
6955 break;
6956 }
6957 i += 1;
6958 } else {
6959 // c:4446-4467 — non-ASCII codepoint. Decode the UTF-8
6960 // sequence starting at `i` and route through wcsitype
6961 // (which already handles IWORD via WORDCHARS paramtab
6962 // c:4364, ISEP via IFS paramtab c:4367, default via
6963 // iswalnum/is_alphanumeric c:4370).
6964 let tail = match std::str::from_utf8(&bytes[i..]) {
6965 Ok(t) => t,
6966 Err(_) => break, // invalid UTF-8 — stop walk
6967 };
6968 let ch = match tail.chars().next() {
6969 Some(c) => c,
6970 None => break,
6971 };
6972 if !wcsitype(ch, itype) {
6973 break;
6974 }
6975 i += ch.len_utf8();
6976 }
6977 if once {
6978 break;
6979 }
6980 }
6981 i
6982 } else {
6983 // c:4474-4480 — non-multibyte arm. Simple byte loop with
6984 // Meta-prefix collapse.
6985 let mut i = start;
6986 while i < bytes.len() {
6987 let b = bytes[i];
6988 let chr = if b == Meta {
6989 bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20
6990 } else {
6991 b
6992 };
6993 if !zistype(chr, itype) {
6994 break;
6995 }
6996 i += if b == Meta { 2 } else { 1 };
6997 if once {
6998 break;
6999 }
7000 }
7001 i
7002 }
7003}
7004
7005/// Port of `arrdup()` from `Src/utils.c:4493` — C decl `arrdup(char **s)`.
7006///
7007/// Duplicate array (from utils.c arrdup)
7008pub fn arrdup(s: &[String]) -> Vec<String> {
7009 s.to_vec()
7010}
7011
7012/// Port of `arrdup_max()` from `Src/utils.c:4508` — C decl `arrdup_max(char **s, unsigned max)`.
7013///
7014/// Duplicate array with max elements (from utils.c arrdup_max)
7015pub fn arrdup_max(s: &[String], max: usize) -> Vec<String> {
7016 s.iter().take(max).cloned().collect()
7017}
7018
7019/// Port of `zarrdup()` from `Src/utils.c:4532` — C decl `zarrdup(char **s)`.
7020///
7021/// Duplicate array with zsh allocation (from utils.c zarrdup)
7022pub fn zarrdup(s: &[String]) -> Vec<String> {
7023 s.to_vec()
7024}
7025
7026/// Port of `wcs_zarrdup()` from `Src/utils.c:4547` — C decl `wcs_zarrdup(wchar_t **s)`.
7027///
7028/// Duplicate array of wide strings (from utils.c wcs_zarrdup) - same as zarrdup in Rust
7029pub fn wcs_zarrdup(s: &[String]) -> Vec<String> {
7030 s.to_vec()
7031}
7032
7033/// Port of `spname()` from `Src/utils.c:4562` — C decl `spname(char *oldname)`.
7034///
7035/// Spelling correction: find closest match (from utils.c spname)
7036/// WARNING: param names don't match C — Rust=(name, dir) vs C=(oldname)
7037pub fn spname(name: &str, dir: &str) -> Option<String> {
7038 let entries = match fs::read_dir(dir) {
7039 Ok(e) => e,
7040 Err(_) => return None,
7041 };
7042
7043 let mut best = None;
7044 let mut best_dist = 4; // threshold
7045
7046 for entry in entries.flatten() {
7047 if let Some(entry_name) = entry.file_name().to_str() {
7048 let dist = spdist(name, entry_name, best_dist);
7049 if dist < best_dist {
7050 best_dist = dist;
7051 best = Some(entry_name.to_string());
7052 }
7053 }
7054 }
7055 best
7056}
7057
7058/// Port of `mindist()` from `Src/utils.c:4624` — C decl `mindist(char *dir, char *mindistguess, char *mindistbest, int wantdir)`.
7059///
7060/// Spelling correction with full path (from utils.c mindist)
7061/// WARNING: param names don't match C — Rust=(dir, name) vs C=(dir, mindistguess, mindistbest, wantdir)
7062pub fn mindist(dir: &str, name: &str) -> Option<(String, usize)> {
7063 let entries = match fs::read_dir(dir) {
7064 Ok(e) => e,
7065 Err(_) => return None,
7066 };
7067
7068 let mut best = None;
7069 let mut best_dist = 4;
7070
7071 for entry in entries.flatten() {
7072 if let Some(entry_name) = entry.file_name().to_str() {
7073 let dist = spdist(name, entry_name, best_dist);
7074 if dist < best_dist {
7075 best_dist = dist;
7076 best = Some(entry_name.to_string());
7077 }
7078 }
7079 }
7080 best.map(|name| (name, best_dist))
7081}
7082
7083// spellcheck a word // c:3123
7084// fix s ; if hist is nonzero, fix the history list too // c:3124
7085/// Port of `spdist()` from `Src/utils.c:4675`.
7086///
7087/// Compute edit distance between two strings (for spelling correction)
7088/// Direct port of `int spdist(char *s, char *t, int thresh)` from
7089/// `Src/utils.c:4675-4750`. Drives the CORRECT-option typo prompt:
7090/// returns 0 for identical, 1 for case-only mistakes, 2 for one
7091/// transposition / missing letter / QWERTY-adjacent mistype, 200 for
7092/// anything farther.
7093///
7094/// **This is NOT a Levenshtein DP — that was the previous Rust impl
7095/// dressed as a port.** The C source uses a keyboard-adjacency model
7096/// (qwertykeymap / dvorakkeymap arrays + tulower equality) which is
7097/// the actual behaviour `setopt CORRECT` depends on. Restored
7098/// faithfully.
7099pub fn spdist(s: &str, t: &str, thresh: usize) -> usize {
7100 // c:4679-4690 — qwerty keymap (rows of 14 chars each: numeric row,
7101 // QWERTY top row, home row, bottom row, then shift versions).
7102 // Embedded `\n` / `\t` mark off-grid cells.
7103 const QWERTYKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
7104\t1234567890-=\t\
7105\tqwertyuiop[]\t\
7106\tasdfghjkl;'\n\t\
7107\tzxcvbnm,./\t\t\t\
7108\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
7109\t!@#$%^&*()_+\t\
7110\tQWERTYUIOP{}\t\
7111\tASDFGHJKL:\"\n\t\
7112\tZXCVBNM<>?\n\n\t\
7113\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
7114 // c:4691-4702 — dvorak keymap, same shape.
7115 const DVORAKKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
7116\t1234567890[]\t\
7117\t',.pyfgcrl/=\t\
7118\taoeuidhtns-\n\t\
7119\t;qjkxbmwvz\t\t\t\
7120\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
7121\t!@#$%^&*(){}\t\
7122\t\"<>PYFGCRL?+\t\
7123\tAOEUIDHTNS_\n\t\
7124\t:QJKXBMWVZ\n\n\t\
7125\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
7126
7127 // c:4703-4707 — `keymap = isset(DVORAK) ? dvorakkeymap : qwertykeymap;`
7128 let keymap = if isset(DVORAK) {
7129 DVORAKKEYMAP.as_bytes()
7130 } else {
7131 QWERTYKEYMAP.as_bytes()
7132 };
7133
7134 let s_b = s.as_bytes();
7135 let t_b = t.as_bytes();
7136
7137 // c:4709-4710 — `if (!strcmp(s, t)) return 0;`
7138 if s == t {
7139 return 0;
7140 }
7141
7142 // c:4712-4714 — `for (p, q; *p && tulower(*p) == tulower(*q); p++, q++);
7143 // if (!*p && !*q) return 1;` — case-only mismatch.
7144 let mut p = 0usize;
7145 let mut q = 0usize;
7146 while p < s_b.len() && q < t_b.len() && tulower(s_b[p] as char) == tulower(t_b[q] as char) {
7147 p += 1;
7148 q += 1;
7149 }
7150 if p == s_b.len() && q == t_b.len() {
7151 return 1;
7152 }
7153
7154 // c:4715-4716 — `if (!thresh) return 200;`
7155 if thresh == 0 {
7156 return 200;
7157 }
7158
7159 // c:4717-4727 — first walk: detect transposition / missing letter at
7160 // first divergence.
7161 p = 0;
7162 q = 0;
7163 while p < s_b.len() && q < t_b.len() {
7164 if s_b[p] == t_b[q] {
7165 // c:4718-4719 — match: skip (don't count `aa` as transposed).
7166 p += 1;
7167 q += 1;
7168 continue;
7169 }
7170 // c:4720-4721 — `if (p[1] == q[0] && q[1] == p[0]) return
7171 // spdist(p+2, q+2, thresh-1) + 1;` transposition.
7172 if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p + 1] == t_b[q] && t_b[q + 1] == s_b[p] {
7173 // SAFETY: bytes are ASCII for the keymap test below; from_utf8
7174 // is fine on a non-ASCII tail here because spdist takes &str.
7175 let s_tail = std::str::from_utf8(&s_b[p + 2..]).unwrap_or("");
7176 let t_tail = std::str::from_utf8(&t_b[q + 2..]).unwrap_or("");
7177 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
7178 }
7179 // c:4722-4723 — `if (p[1] == q[0]) return spdist(p+1, q, thresh-1) + 2;`
7180 if p + 1 < s_b.len() && s_b[p + 1] == t_b[q] {
7181 let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
7182 let t_tail = std::str::from_utf8(&t_b[q..]).unwrap_or("");
7183 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
7184 }
7185 // c:4724-4725 — `if (p[0] == q[1]) return spdist(p, q+1, thresh-1) + 2;`
7186 if q + 1 < t_b.len() && s_b[p] == t_b[q + 1] {
7187 let s_tail = std::str::from_utf8(&s_b[p..]).unwrap_or("");
7188 let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
7189 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
7190 }
7191 // c:4726-4727 — `if (*p != *q) break;`
7192 break;
7193 }
7194 // c:4728-4729 — `if ((!*p && strlen(q) == 1) || (!*q && strlen(p) == 1))
7195 // return 2;` — single trailing-char insertion.
7196 if (p == s_b.len() && (t_b.len() - q) == 1) || (q == t_b.len() && (s_b.len() - p) == 1) {
7197 return 2;
7198 }
7199 // c:4730-4748 — second walk: keyboard-adjacency mistype detection.
7200 p = 0;
7201 q = 0;
7202 while p < s_b.len() && q < t_b.len() {
7203 if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p] != t_b[q] && s_b[p + 1] == t_b[q + 1] {
7204 // c:4737-4738 — `if (!(z = strchr(keymap, p[0])) || *z == '\n' ||
7205 // *z == '\t') return spdist(p+1, q+1,
7206 // thresh-1) + 1;`
7207 let pos = keymap.iter().position(|&b| b == s_b[p]);
7208 let z_ok = match pos {
7209 Some(i) => keymap[i] != b'\n' && keymap[i] != b'\t',
7210 None => false,
7211 };
7212 if !z_ok {
7213 let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
7214 let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
7215 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
7216 }
7217 // c:4739 — `t0 = z - keymap;`
7218 let t0 = pos.unwrap() as isize;
7219 // c:4740-4744 — eight adjacency offsets (-15,-14,-13,-1,+1,
7220 // +13,+14,+15) → keyboard neighbours.
7221 let offsets: [isize; 8] = [-15, -14, -13, -1, 1, 13, 14, 15];
7222 let adjacent = offsets.iter().any(|&off| {
7223 let idx = t0 + off;
7224 if idx >= 0 && (idx as usize) < keymap.len() {
7225 keymap[idx as usize] == t_b[q]
7226 } else {
7227 false
7228 }
7229 });
7230 if adjacent {
7231 // c:4745 — `return spdist(p+1, q+1, thresh-1) + 2;`
7232 let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
7233 let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
7234 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
7235 }
7236 // c:4746 — `return 200;`
7237 return 200;
7238 } else if p < s_b.len() && q < t_b.len() && s_b[p] != t_b[q] {
7239 // c:4747-4748 — `else if (*p != *q) break;`
7240 break;
7241 }
7242 p += 1;
7243 q += 1;
7244 }
7245 // c:4749 — `return 200;`
7246 200
7247}
7248
7249/// Set terminal to cbreak mode (from utils.c setcbreak)
7250#[cfg(unix)]
7251/// Port of `setcbreak` from `Src/utils.c:4756`.
7252pub fn setcbreak() -> bool {
7253 // c:4760 — `ti = shttyinfo;`
7254 let saved = SHTTYINFO.lock().ok().and_then(|g| *g);
7255 if let Some(mut ti) = saved {
7256 ti.c_lflag &= !libc::ICANON; // c:4762
7257 ti.c_cc[libc::VMIN] = 1; // c:4763
7258 ti.c_cc[libc::VTIME] = 0; // c:4764
7259 settyinfo(&ti) // c:4768
7260 } else {
7261 false
7262 }
7263}
7264
7265#[cfg(not(unix))]
7266/// Port of `setcbreak` from `Src/utils.c:4756`.
7267pub fn setcbreak() -> bool {
7268 false
7269}
7270
7271/// Port of `attachtty()` from `Src/utils.c:4775` — C decl `void attachtty(pid_t pgrp)`.
7272/// Hands the controlling terminal to `pgrp`. Gated by `jobbing &&
7273/// interact`; falls back to `mypgrp` and disables MONITOR on permanent
7274/// failure (matching the C source's recursion + `opts[MONITOR]=0` path).
7275#[cfg(unix)]
7276pub fn attachtty(pgrp: i32) {
7277 // c:4775
7278
7279 if !(jobbing() && interact()) {
7280 return; // c:4779
7281 }
7282 let shtty = SHTTY.load(Ordering::Relaxed); // c:4781
7283 if shtty == -1 {
7284 return;
7285 }
7286 let ep = ATTACHTTY_EP.load(Ordering::Relaxed);
7287 let rc = unsafe { libc::tcsetpgrp(shtty, pgrp) }; // c:4781
7288 if rc == -1 && ep == 0 {
7289 // c:4781
7290 let mypgrp_val = *crate::ported::jobs::MYPGRP // c:4792
7291 .get_or_init(|| Mutex::new(0))
7292 .lock()
7293 .unwrap();
7294 if pgrp != mypgrp_val && unsafe { libc::kill(-pgrp, 0) } == -1 {
7295 attachtty(mypgrp_val); // c:4793
7296 } else {
7297 let errno_val = io::Error::last_os_error().raw_os_error().unwrap_or(0);
7298 if errno_val != libc::ENOTTY {
7299 // c:4795
7300 zwarn(&format!(
7301 "can't set tty pgrp: {}", // c:4797
7302 io::Error::from_raw_os_error(errno_val)
7303 ));
7304 let _ = io::stderr().flush(); // c:4798
7305 }
7306 opt_state_set("monitor", false); // c:4815 opts[MONITOR]=0
7307 ATTACHTTY_EP.store(1, Ordering::Relaxed); // c:4815
7308 }
7309 } else if rc != -1 {
7310 // c:4815
7311 *crate::ported::jobs::LAST_ATTACHED_PGRP // c:4815
7312 .get_or_init(|| Mutex::new(0))
7313 .lock()
7314 .unwrap() = pgrp;
7315 }
7316}
7317
7318/// Port of `gettygrp()` from `Src/utils.c:4815` — C decl `pid_t gettygrp(void)`.
7319#[cfg(unix)]
7320pub fn gettygrp() -> i32 {
7321 // c:4815
7322 let shtty = SHTTY.load(Ordering::Relaxed);
7323 if shtty == -1 {
7324 // c:4819
7325 return -1; // c:4820
7326 }
7327 unsafe { libc::tcgetpgrp(shtty) } // c:4823
7328}
7329
7330/// Port of `metafy()` from `Src/utils.c:4856`.
7331///
7332/// Convert raw bytes (possibly containing NUL / 0x83-0x9b) to
7333/// zsh's metafied form: each `imeta(b)` byte becomes `Meta` (0x83)
7334/// followed by `b ^ 32`.
7335///
7336/// NOTE ON THE RETURN TYPE: C hands back a `char *`, and a metafied byte
7337/// string is routinely NOT valid UTF-8 — metafication escapes every byte
7338/// in `{0x00} ∪ [0x83, 0xa2]` (c:4195-4201), a range that sits inside the
7339/// UTF-8 lead/continuation ranges, so `日` (`e6 97 a5`) metafies to
7340/// `e6 83 b7 a5`. This `String`-returning form therefore falls back to
7341/// `from_utf8_lossy` on exactly those inputs. A caller that needs the
7342/// bytes C would see (`matchcmp` at `Src/Zle/compcore.c:3194`, which
7343/// collates the metafied form) must run the c:4880 loop over `u8`
7344/// directly rather than through here.
7345///
7346/// Port of `metafy(char *buf, int len, int heap)` from Src/utils.c:4856. The C source takes a
7347/// `heap` mode controlling whether the result is `zalloc`'d /
7348/// `zhalloc`'d / written into a static buffer / appended to the
7349/// existing buffer; in Rust we always return an owned `String`
7350/// since allocation strategy is uniform. The byte-level transform
7351/// is identical: walk the input, count metafy hits, allocate
7352/// `len + meta` bytes, expand each `Meta+X` pair in reverse.
7353/// Rust idiom replacement: forward byte-walk + Vec::push covers the
7354/// C two-pass (count + alloc + reverse-expand) approach; Vec grows
7355/// on demand so the pre-count is unnecessary in Rust.
7356/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len, heap)
7357pub fn metafy(buf: &str) -> String {
7358 // c:4856
7359 let bytes = buf.as_bytes();
7360 let mut out = Vec::with_capacity(bytes.len());
7361 for &b in bytes {
7362 // C: `#define imeta(c) ((c) >= Meta)` from Src/zsh.h —
7363 // every byte >= 0x83 needs escaping. The previous
7364 // narrow-range check `(0x83..=0x9b)` was a bug: bytes
7365 // 0x9c..=0xff (e.g. UTF-8 continuation bytes, high-Latin
7366 // characters) escaped C's imeta() but not the Rust
7367 // version, which then fed un-escaped bytes downstream
7368 // and corrupted Meta-aware loops.
7369 if imeta_byte(b) {
7370 out.push(Meta);
7371 out.push(b ^ 32);
7372 } else {
7373 out.push(b);
7374 }
7375 }
7376 // metafied bytes are in [0..=0x7f]∪{0x83}∪[expanded ^ 32 range];
7377 // String::from_utf8 may fail on the high bytes — fall back to
7378 // lossy. The lossy fallback inserts U+FFFD for invalid
7379 // sequences, which is fine for String-level callers (lexer /
7380 // pattern compile / display) but breaks byte-round-trip with
7381 // `unmetafy`.
7382 //
7383 // Callers that need the bytes back unchanged go through
7384 // `mb_metacharlenconv`, which hands back each unit in a recoverable form
7385 // instead of falling back to lossy.
7386 String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
7387}
7388
7389/// Port of `ztrdup_metafy()` from `Src/utils.c:4929` — C decl `ztrdup_metafy(const char *s)`.
7390///
7391/// ```c
7392/// mod_export char *
7393/// ztrdup_metafy(const char *s)
7394/// {
7395/// if (!s) return NULL;
7396/// return metafy((char *)s, -1, META_DUP);
7397/// }
7398/// ```
7399pub fn ztrdup_metafy(s: &str) -> String {
7400 metafy(s)
7401}
7402
7403/// Port of `unmetafy()` from `Src/utils.c:4954` — C decl `unmetafy(char *s, int *len)`.
7404///
7405/// Take a metafied byte buffer in `s` and convert it in place to
7406/// its literal representation. C signature:
7407///
7408/// ```c
7409/// char *unmetafy(char *s, int *len);
7410/// ```
7411///
7412/// The Rust port mutates `s` in place and returns the resulting
7413/// length (mirroring C's `*len` out-parameter). C control flow:
7414///
7415/// ```c
7416/// for (p = s; *p && *p != Meta; p++); // skip prefix with no Meta
7417/// for (t = p; (*t = *p++);) // walk the rest
7418/// if (*t++ == Meta && *p)
7419/// t[-1] = *p++ ^ 32; // un-escape: XOR with 32
7420/// ```
7421///
7422/// Same algorithm here, byte-indexed against the Vec rather than
7423/// pointer-walked.
7424/// WARNING: param names don't match C — Rust=(s) vs C=(s, len)
7425pub fn unmetafy(s: &mut Vec<u8>) -> usize {
7426 // c:4954
7427 // c:4958 `for (p = s; *p && *p != Meta; p++);` — when the string holds no
7428 // Meta byte at all, C's second loop copies each byte onto itself and the
7429 // call is just that scan. Ask the question with `<[u8]>::contains`, which
7430 // resolves to libcore's precompiled `memchr`, and skip both loops: the
7431 // index-by-index walk below is monomorphised into this crate and compiled
7432 // UNOPTIMISED in the debug build, where it measured ~24% of a `pr <TAB>`
7433 // (one `unmetafy` per history entry while paging the history in).
7434 // Returning `s.len()` is exactly what the two loops below produce on this
7435 // input — `p` runs to the end, the copy loop never executes, and
7436 // `truncate(t)` is a no-op.
7437 if !s.contains(&Meta) {
7438 return s.len();
7439 }
7440 // First loop: find the first `Meta` byte. Everything before it
7441 // stays as-is, so we don't need to copy.
7442 let mut p: usize = 0;
7443 while p < s.len() && s[p] != Meta {
7444 p += 1;
7445 }
7446 // Second loop: walk from `p` onward, copying each byte into the
7447 // `t` slot (which trails `p` by one position per Meta-escape
7448 // we collapse).
7449 let mut t: usize = p;
7450 while p < s.len() {
7451 let b = s[p];
7452 s[t] = b;
7453 p += 1;
7454 if b == Meta && p < s.len() {
7455 // C: t[-1] = *p++ ^ 32; — overwrite the just-written
7456 // Meta with the un-escaped byte.
7457 s[t] = s[p] ^ 32;
7458 p += 1;
7459 }
7460 t += 1;
7461 }
7462 s.truncate(t);
7463 t
7464}
7465
7466/// Port of `metalen()` from `Src/utils.c:4972` — C decl `int metalen(const char *s, int len)`.
7467/// ```c
7468/// int mlen = len;
7469/// while (len--) {
7470/// if (*s++ == Meta) { mlen++; s++; }
7471/// }
7472/// return mlen;
7473/// ```
7474/// Doc: "Return the character length of a metafied substring, given
7475/// the unmetafied substring length." So **input `len` is the
7476/// UNMETAFIED char count, output is the METAFIED byte count.**
7477///
7478/// Previously the Rust port had INVERTED semantics: looped while
7479/// `i < len` byte-walk, returned char count. That's the reverse
7480/// operation. Pin the correct C contract.
7481pub fn metalen(s: &str, len: usize) -> usize {
7482 // c:4972
7483 let bytes = s.as_bytes();
7484 let mut mlen = len; // c:4974
7485 let mut remaining = len;
7486 let mut i = 0;
7487 while remaining > 0 && i < bytes.len() {
7488 // c:4976 `while (len--)`
7489 if bytes[i] == Meta {
7490 // c:4977
7491 mlen += 1; // c:4978
7492 i += 2; // c:4979 s++ (already advanced past Meta)
7493 } else {
7494 i += 1;
7495 }
7496 remaining -= 1;
7497 }
7498 mlen
7499}
7500
7501/// Port of `unmeta(const char *file_name)` from `Src/utils.c:4994`.
7502///
7503/// Convert a zsh internal (metafied) string to a system-call-safe
7504/// form (e.g. for passing to `open(2)`).
7505///
7506/// C body shape (c:4994-5010):
7507/// ```c
7508/// meta = 0;
7509/// for (t = file_name; *t; t++)
7510/// if (*t == Meta) { meta = 1; break; }
7511/// if (!meta) return (char *) file_name; // no-copy fast path
7512/// for (t = file_name, p = fn; *t; p++)
7513/// if ((*p = *t++) == Meta && *t)
7514/// *p = *t++ ^ 32;
7515/// ```
7516// Rust idiom replacement: byte-scan fast path + `unmetafy` covers
7517// the C in-place decode + alloc-on-copy dance; String owns its own
7518// allocation so no `heap` flag needed.
7519/// Port of `unmeta()` from `Src/utils.c:4994`.
7520///
7521/// `unmeta` — see implementation.
7522pub fn unmeta(s: &str) -> String {
7523 // c:4994
7524 // c:4995-4996 — Meta scan; no-copy fast path. A `&str` carries Meta as
7525 // the CHAR U+0083 (the char-level encoding `unmetafy_str` decodes), never
7526 // as a lone 0x83 byte: in valid UTF-8 that byte is only ever a
7527 // continuation byte (`ホ` is E3 83 9B). Scanning bytes decoded those
7528 // continuations as Meta pairs and turned every such character into
7529 // U+FFFD before a write or syscall (`zpty -w z ホ`, zsh/files `rm ホ`).
7530 if !s.contains(Meta as char) {
7531 return s.to_string();
7532 }
7533 // c:4999-5001 — `if ((*p = *t++) == Meta && *t) *p = *t++ ^ 32;`
7534 String::from_utf8_lossy(&unmetafy_str(s)).into_owned()
7535}
7536
7537/// Port of `unmeta_one()` from `Src/utils.c:5058` — C decl `convchar_t unmeta_one(const char *in, int *sz)`.
7538/// Non-MULTIBYTE branch (c:5077-5083):
7539/// ```c
7540/// if (in[0] == Meta) { *sz = 2; wc = (unsigned char)(in[1] ^ 32); }
7541/// else { *sz = 1; wc = (unsigned char) in[0]; }
7542/// ```
7543/// Returns `(decoded_char, bytes_consumed)`. The c:5070 NULL/empty
7544/// guard returns `(0, 0)`. Previously used hardcoded `0x83` for the
7545/// Meta byte — now routes through the canonical `Meta` constant
7546/// (defined as `'\u{83}'` at zsh.h:144).
7547/// WARNING: param names don't match C — Rust=(s) vs C=(in, sz)
7548pub fn unmeta_one(s: &str) -> (char, usize) {
7549 // c:5058
7550 let bytes = s.as_bytes();
7551 // c:5070 — `if (!in || !*in) return 0;`
7552 if bytes.is_empty() {
7553 return ('\0', 0);
7554 }
7555 // c:5077 — `if (in[0] == Meta)`.
7556 if bytes[0] == Meta && bytes.len() > 1 {
7557 // c:5078-5079 — `*sz = 2; wc = (unsigned char)(in[1] ^ 32);`.
7558 ((bytes[1] ^ 32) as char, 2)
7559 } else {
7560 // c:5081-5082 — `*sz = 1; wc = (unsigned char) in[0];`.
7561 (bytes[0] as char, 1)
7562 }
7563}
7564
7565/// Port of `ztrcmp()` from `Src/utils.c:5106` — C decl `ztrcmp(char const *s1, char const *s2)`.
7566///
7567/// Byte-walking compare with lazy Meta resolution. C body skips
7568/// matching bytes wholesale, then on the first differing byte
7569/// un-meta-fies just that byte and compares. Faster than
7570/// `unmeta(s1).cmp(unmeta(s2))` because:
7571/// 1. No allocation up front,
7572/// 2. Only the first differing position pays the un-meta cost.
7573///
7574/// ```c
7575/// while(*s1 && *s1 == *s2) { s1++; s2++; }
7576/// if (!(c1 = *s1)) c1 = -1;
7577/// else if (c1 == Meta) c1 = *++s1 ^ 32;
7578/// if (!(c2 = *s2)) c2 = -1;
7579/// else if (c2 == Meta) c2 = *++s2 ^ 32;
7580/// return c1 - c2;
7581/// ```
7582pub fn ztrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
7583 // c:5106
7584 let b1 = s1.as_bytes();
7585 let b2 = s2.as_bytes();
7586 let mut i1 = 0;
7587 let mut i2 = 0;
7588 // Skip the matching prefix.
7589 while i1 < b1.len() && i2 < b2.len() && b1[i1] == b2[i2] {
7590 i1 += 1;
7591 i2 += 1;
7592 }
7593 // Resolve c1: -1 for end-of-string, else the next byte
7594 // (un-meta-fied if it's a Meta marker).
7595 let c1: i32 = if i1 >= b1.len() {
7596 -1
7597 } else if b1[i1] == Meta && i1 + 1 < b1.len() {
7598 (b1[i1 + 1] ^ 32) as i32
7599 } else {
7600 b1[i1] as i32
7601 };
7602 let c2: i32 = if i2 >= b2.len() {
7603 -1
7604 } else if b2[i2] == Meta && i2 + 1 < b2.len() {
7605 (b2[i2 + 1] ^ 32) as i32
7606 } else {
7607 b2[i2] as i32
7608 };
7609 c1.cmp(&c2)
7610}
7611
7612// pastebuf() DELETED — was a misplaced fn. The real `pastebuf()`
7613// lives in `Src/Zle/zle_misc.c:558` (a ZLE clipboard helper that
7614// pastes a Cutbuffer into the line-edit buffer). It has nothing to
7615// do with metafication, despite this file's prior body which
7616// reimplemented half of `metafy`. The real metafy lives below at
7617// `pub fn metafy()` (port of utils.c:4856).
7618
7619/// Port of `ztrlen()` from `Src/utils.c:5136`.
7620///
7621/// Unmetafied string length (from utils.c ztrlen lines 5135-5152)
7622pub fn ztrlen(s: &str) -> usize {
7623 // c:5136
7624 let mut len = 0;
7625 let chars: Vec<char> = s.chars().collect();
7626 let mut i = 0;
7627 while i < chars.len() {
7628 len += 1;
7629 if chars[i] as u32 == Meta as u32 && i + 1 < chars.len() {
7630 i += 2;
7631 } else {
7632 i += 1;
7633 }
7634 }
7635 len
7636}
7637
7638/// Port of `ztrlenend()` from `Src/utils.c:5162` — C decl `ztrlenend(char const *s, char const *eptr)`.
7639///
7640/// ```c
7641/// for (l = 0; s < eptr; l++) {
7642/// if (*s++ == Meta) s++; // skip past Meta-escaped pair
7643/// }
7644/// return l;
7645/// ```
7646///
7647/// Count the unmetafied character length from `s` up to `end`
7648/// bytes. Each Meta-escaped pair counts as 1 character.
7649/// Previous Rust port called `chars().count()` which counts UTF-8
7650/// codepoints, not byte-walked Meta-pairs — wrong semantics.
7651pub fn ztrlenend(s: &str, eptr: usize) -> usize {
7652 let bytes = s.as_bytes();
7653 let cap = eptr.min(bytes.len());
7654 let mut l = 0;
7655 let mut i = 0;
7656 while i < cap {
7657 if bytes[i] == Meta {
7658 // Meta sentinel + escaped byte = 1 visible char.
7659 i += 2;
7660 } else {
7661 i += 1;
7662 }
7663 l += 1;
7664 }
7665 l
7666}
7667
7668/// Port of `ztrsub()` from `Src/utils.c:5187` — C decl `int ztrsub(char const *t, char const *s)`.
7669/// ```c
7670/// int l = t - s;
7671/// while (s != t) {
7672/// if (*s++ == Meta) { s++; l--; }
7673/// }
7674/// return l;
7675/// ```
7676/// "Subtract two pointers in a metafied string." `s` is the start
7677/// pointer, `t` is the end pointer; both point into the SAME buffer.
7678/// Returns the count of unmetafied chars in `[s, t)` — same as
7679/// `ztrlen` of the substring but expressed via pointer arithmetic.
7680///
7681/// Rust API: takes the full buffer and a byte-offset pair
7682/// `[start..end)` since Rust can't replicate raw-pointer subtraction
7683/// across distinct `&str` arguments without UB risk. The semantics
7684/// stay faithful: count of unmetafied chars between two offsets.
7685/// Previous Rust port took two unrelated `&str` and computed
7686/// `ztrlen(&t[..t.len()-s.len()])` — a fundamentally different
7687/// operation that worked only when `s` was the suffix of `t`.
7688/// WARNING: param names don't match C — Rust=(buf, start, end) vs C=(t, s)
7689pub fn ztrsub(buf: &str, start: usize, end: usize) -> usize {
7690 // c:5187
7691 let bytes = buf.as_bytes();
7692 let end = end.min(bytes.len());
7693 let start = start.min(end);
7694 let mut l = (end - start) as isize; // c:5189
7695 let mut i = start;
7696 while i < end {
7697 if bytes[i] == Meta {
7698 // c:5192
7699 i += 2; // c:5198
7700 l -= 1; // c:5199
7701 } else {
7702 i += 1;
7703 }
7704 }
7705 l.max(0) as usize
7706}
7707
7708/// Port of `zreaddir()` from `Src/utils.c:5217`.
7709///
7710/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
7711/// `Src/utils.c:5217`.
7712///
7713/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
7714/// `Src/utils.c:5217-5240`. Pulls the next entry from a DIR* the
7715/// caller owns; returns the entry's bare name (metafied String) or
7716/// `None` at EOF. The C source's `ignoredots` flag (c:5232) skips
7717/// `.` and `..` when set; callers (`spnamepat` at c:4648) that
7718/// want dot entries pass 0.
7719///
7720/// Caller pattern matches C exactly:
7721/// let mut dir = fs::read_dir(path)?;
7722/// while let Some(name) = zreaddir(&mut dir, 1) { ... }
7723/// // dir auto-closed on drop (= closedir)
7724pub fn zreaddir(dir: &mut fs::ReadDir, ignoredots: i32) -> Option<String> {
7725 // c:5217
7726 for entry in dir.by_ref() {
7727 // c:5221 readdir loop
7728 let Ok(e) = entry else { continue };
7729 let Ok(name) = e.file_name().into_string() else {
7730 continue;
7731 };
7732 // c:5232 — `if (ignoredots && de->d_name[0] == '.' &&
7733 // (!de->d_name[1] || (de->d_name[1] == '.' && !de->d_name[2])))`.
7734 if ignoredots != 0 && (name == "." || name == "..") {
7735 continue; // c:5234
7736 }
7737 return Some(name); // c:5238 return de->d_name
7738 }
7739 None // c:5240 — fell off the end
7740}
7741
7742/// Port of `zputs()` from `Src/utils.c:5265` — C decl `int zputs(char const *s, FILE *stream)`.
7743///
7744/// ```c
7745/// while (*s) {
7746/// if (*s == Meta) c = *++s ^ 32;
7747/// else if (itok(*s)) { s++; continue; }
7748/// else c = *s;
7749/// s++;
7750/// if (fputc(c, stream) < 0) return EOF;
7751/// }
7752/// return 0;
7753/// ```
7754///
7755/// Writes `s` to `stream` with Meta+X pair decoding and ITOK-char
7756/// skipping. Returns `0` on success, `-1` on write error (mirroring
7757/// C's `EOF` sentinel for the int return).
7758///
7759/// The walk is over CHARS, not bytes. C can scan bytes at c:5267
7760/// because zsh metafies: a raw byte >= 0x80 is stored escaped
7761/// (`Meta`, then `byte ^ 32`), so a bare `0x84..=0xa1` in a metafied
7762/// string is always a real token. zshrs keeps UTF-8 `str` and stores
7763/// tokens as PUA CHARS (crate::token_char) — the representation
7764/// `lex::untokenize` and `has_token` (utils.rs:3089) both test with
7765/// `c as u32`. Scanning bytes here mistook a UTF-8 CONTINUATION byte
7766/// for a token and DROPPED it: `functions f` printed `print "a<E2><80>b"`
7767/// for a body containing `—` (U+2014 = E2 80 94, 0x94 = `Inang`), and
7768/// the same for `→` (E2 86 92) and `“` (E2 80 9C). This is the
7769/// `has_token` defect (fixed in 1e549c1dec) at a second site.
7770pub fn zputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
7771 // c:5265
7772 let mut it = s.chars(); // c:5265 *s walk
7773 while let Some(ch) = it.next() {
7774 // c:5267 while (*s)
7775 if ch as u32 == Meta as u32 {
7776 // c:5269 if (*s == Meta)
7777 // c:5270 — `c = *++s ^ 32;`. zshrs metafies at the CHARACTER
7778 // level (U+0083 then `char::from(byte ^ 32)`, see
7779 // mb_niceformat's note at utils.rs:7574), so the escaped
7780 // byte is recovered as a single raw byte exactly as C's
7781 // `fputc` writes it.
7782 let Some(n) = it.next() else {
7783 continue; // c:5267 — lone trailing Meta ends the walk
7784 };
7785 let c = (n as u32 as u8) ^ 32; // c:5270
7786 // c:5276 s++ — `it.next()` already advanced past both chars.
7787 if stream.write_all(&[c]).is_err() {
7788 // c:5277 fputc(c, stream)
7789 return -1; // c:5278 return EOF
7790 }
7791 } else if crate::token_char::itok_char(ch) {
7792 // c:5271 else if (itok(*s))
7793 // c:5272 — `s++; continue;` (skip token char)
7794 continue;
7795 } else {
7796 // c:5274 else c = *s — a plain char goes out in its own
7797 // UTF-8 encoding; C's byte-at-a-time `fputc` emits the same
7798 // bytes because its `s` is already the encoded form.
7799 let mut buf = [0u8; 4];
7800 // c:5276 s++
7801 if stream.write_all(ch.encode_utf8(&mut buf).as_bytes()).is_err() {
7802 // c:5277 fputc(c, stream)
7803 return -1; // c:5278 return EOF
7804 }
7805 }
7806 }
7807 0 // c:5280 return 0
7808}
7809
7810/// Port of `nicedup()` from `Src/utils.c:5530`.
7811///
7812/// Port of `nicedup(char const *s, int heap)` from `Src/utils.c:5289`
7813/// (single-byte build) and `Src/utils.c:5530` (multibyte build).
7814///
7815/// C bodies:
7816/// ```c
7817/// /* Src/utils.c:5289 — !MULTIBYTE_SUPPORT */
7818/// char *retstr;
7819/// (void)sb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
7820/// return retstr;
7821///
7822/// /* Src/utils.c:5530 — MULTIBYTE_SUPPORT */
7823/// char *retstr;
7824/// (void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
7825/// return retstr;
7826/// ```
7827///
7828/// zshrs targets MULTIBYTE_SUPPORT (matches every modern zsh
7829/// build) — route through `mb_niceformat`. Previous Rust port
7830/// went through `sb_niceformat`, which is the !MULTIBYTE path —
7831/// strips wide-char handling and corrupts non-ASCII via byte-mask
7832/// `nicechar(c & 0xff)`.
7833///
7834/// C signature faithful: `(s, heap) -> String`. `heap` selects between
7835/// arena (`NICEFLAG_HEAP`) and persistent (default `ztrdup`) allocation
7836/// in C; Rust has a single allocator so both paths produce the same
7837/// owned `String`, but the `heap` parameter is preserved per Rule S1.
7838pub fn nicedup(s: &str, heap: i32) -> String {
7839 // c:5530
7840 // c:5532 — `char *retstr;`
7841 let retstr: Option<String>;
7842 // c:5534 — `(void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);`
7843 let mut slot: Option<String> = None;
7844 let _ = mb_niceformat(
7845 s,
7846 None,
7847 Some(&mut slot),
7848 if heap != 0 { NICEFLAG_HEAP } else { 0 },
7849 );
7850 retstr = slot;
7851 // c:5536 — `return retstr;`
7852 retstr.unwrap_or_default()
7853}
7854
7855/// Port of `nicedupstring()` from `Src/utils.c:5301` — C decl `nicedupstring(char const *s)`.
7856///
7857/// Nice-format and duplicate string.
7858/// C body: `return nicedup(s, 1);` — heap-arena allocation form.
7859pub fn nicedupstring(s: &str) -> String {
7860 // c:5301
7861 nicedup(s, 1) // c:5303
7862}
7863
7864/// Port of `nicezputs()` from `Src/utils.c:5313` — C decl `nicezputs(char const *s, FILE *stream)`.
7865///
7866/// Nicely format a string
7867///
7868/// Under `MULTIBYTE_SUPPORT` (the daily-driver path), C defines
7869/// `nicezputs(str, outs)` as a macro at zsh.h:3274:
7870/// `(void)mb_niceformat((str), (outs), NULL, 0)`.
7871/// Without MULTIBYTE (c:5313): `sb_niceformat(s, stream, NULL, 0)`.
7872///
7873/// The previous Rust impl was `s.chars().map(nicechar).collect()` —
7874/// wrong on every front:
7875/// 1. No unmetafy step (Meta-byte pairs surfaced as `\M-…` mangled).
7876/// 2. `nicechar` is byte-based (`c & 0xff`), corrupting non-ASCII
7877/// multibyte codepoints into spurious `\M-X` escapes.
7878/// 3. No `itok` ZSH-token-byte handling.
7879///
7880/// Route through `mb_niceformat` to match the C macro under
7881/// MULTIBYTE — which itself unmetafies and uses `wcs_nicechar` for
7882/// proper wide-char handling.
7883pub fn nicezputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
7884 // c:5313 (non-MULTIBYTE) / zsh.h:3274 (MULTIBYTE macro:
7885 // `(void)mb_niceformat((str), (outs), NULL, 0)`).
7886 let _ = mb_niceformat(s, Some(stream), None, 0);
7887 0 // c:5316 return 0
7888}
7889
7890/// Port of `niceztrlen()` from `Src/utils.c:5324` — C decl `niceztrlen(char const *s)`.
7891///
7892/// Returns the length (in bytes) of the visible representation of
7893/// the metafied string `s`. C body walks each char via `nicechar`
7894/// and accumulates `strlen(nicechar(c))`; the Rust port mirrors
7895/// this via [`sb_niceformat`] which is the same render-then-measure
7896/// path.
7897pub fn niceztrlen(s: &str) -> usize {
7898 // c:5343-5348 — under MULTIBYTE_SUPPORT (which zshrs targets) `niceztrlen`
7899 // IS `mb_niceformat`, so the unit it walks is whatever the current locale's
7900 // `mbrtowc` returns: a whole character under a UTF-8 locale (U+2010 `‐`,
7901 // 3 bytes, counts 1), a single byte under a C/POSIX one (the same `‐`
7902 // counts 1+5+5 = 11, because its two continuation bytes are non-printable
7903 // and take `\M-^@`-style five-column representations). `mb_niceformat`
7904 // carries that locale branch; see its body.
7905 //
7906 // The port previously called `sb_niceformat`, the single-byte (`#else`,
7907 // c:5320-5341) version, which counts every byte through `nicechar` and so
7908 // charges 4+5+5 = 14 for that `‐` in EITHER locale — over-measuring in a
7909 // UTF-8 locale and still not matching zsh in a C one.
7910 //
7911 // A string of printable ASCII (0x20-0x7e) is the identity under every
7912 // step of `mb_niceformat` with `flags == 0`: `untokenize` and `unmetafy`
7913 // only touch bytes >= 0x83, `mbrtowc` yields one character per byte in
7914 // either locale, and `wcs_nicechar_sel` gives each a width of 1
7915 // (c:644-704). Its width is its length. `calclist` measures every match
7916 // with this (c:1600/1615), twice per `$compstate[list_lines]` read, and
7917 // the general walk allocated three copies of each string to arrive at
7918 // that number: about 4.5k of 14k samples on `arch <TAB>`'s 47058 matches.
7919 if s.bytes().all(|b| (0x20..0x7f).contains(&b)) {
7920 return s.len();
7921 }
7922 mb_niceformat(s, None, None, 0)
7923}
7924
7925/// Port of `mb_niceformat()` from `Src/utils.c:5366`.
7926///
7927/// Multibyte-aware nice-format of a string.
7928/// Port of `mb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from Src/utils.c:5366. Walks the
7929/// (un-metafied) string char-by-char; for each control byte or
7930/// invalid sequence emits a `^X`/`\\xNN` representation, otherwise
7931/// passes the char through. The C source threads an
7932/// `mbstate_t` through `mbrtowc()` and falls back to single-byte
7933/// `\M-` notation on `MB_INVALID`.
7934///
7935/// `mbrtowc` is locale-driven, so the port is too: under a UTF-8
7936/// codeset it decodes one UTF-8 scalar per iteration (Rust's own
7937/// UTF-8 validation standing in for the `mbstate_t` machine, with
7938/// the invalid-byte arm reached through `from_utf8`'s error), and
7939/// under a single-byte codeset (`MB_CUR_MAX == 1`, the C/POSIX
7940/// locale) it takes one byte per iteration whose value IS the wide
7941/// character. The width this function returns differs between the
7942/// two for any non-ASCII input — see the `mb_single_byte` block.
7943/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream, outstrp, flags)
7944/// `mb_niceformat` — see implementation.
7945pub fn mb_niceformat(
7946 s: &str,
7947 mut stream: Option<&mut dyn std::io::Write>,
7948 outstrp: Option<&mut Option<String>>,
7949 flags: i32,
7950) -> usize {
7951 // c:5366
7952 let mut l: usize = 0; // c:5368 size_t l = 0
7953 let mut newl: usize; // c:5368 size_t newl
7954 // c:5369 — `int umlen, outalloc, outleft, eol = 0;`. outalloc/outleft
7955 // model C's buffer-growth math; Rust String auto-grows so the realloc
7956 // loop at c:5430-5440 collapses to push_str. umlen and eol carry.
7957 let mut umlen: usize; // c:5369
7958 let mut eol: bool = false; // c:5369 int eol = 0
7959 // c:5370 — `wchar_t c;` (carried inside loop in Rust)
7960 // c:5371 — `char *ums, *ptr, *fmt, *outstr, *outptr;`
7961 let mut ums: Vec<u8>; // c:5371 char *ums
7962 let mut ptr: usize; // c:5371 char *ptr
7963 let mut fmt: String; // c:5371 char *fmt
7964 let mut outstr: Option<String>; // c:5371 char *outstr
7965 // c:5372 — `mbstate_t mbs;` (Rust UTF-8 has no shift state; tracked
7966 // by the `valid_up_to` / `error_len` logic below).
7967
7968 // c:5374-5380 — `if (outstrp) outptr = outstr = zalloc(5*strlen(s));`
7969 if outstrp.is_some() {
7970 // c:5374
7971 outstr = Some(String::with_capacity(5 * s.len())); // c:5376
7972 } else {
7973 // c:5377
7974 outstr = None; // c:5379 outstr = NULL
7975 }
7976
7977 // c:5382 — `ums = ztrdup(s);`
7978 // c:5383-5387 — comment-only block carried verbatim:
7979 /*
7980 * is this necessary at this point? niceztrlen does this
7981 * but it's used in lots of places. however, one day this may
7982 * be, too.
7983 */ // c:5383-5387
7984 // c:5388 — `untokenize(ums);` — Rust port uses the char-based
7985 // `lex::untokenize` (only fires on codepoints in the 0x84..=0xa1
7986 // ITOK range, never on UTF-8 continuation bytes); the byte-level
7987 // shape of C's untokenize is unsafe on raw UTF-8 input because a
7988 // continuation byte like 0x97 (part of `字` = 0xE5 0xAD 0x97) sits
7989 // inside the ITOK range and would be misclassified as a token.
7990 // c:5389 — `ptr = unmetafy(ums, ¨en);` — `unmeta` is the safe
7991 // UTF-8 wrapper that runs unmetafy only when Meta bytes are
7992 // present and otherwise no-ops.
7993 //
7994 // `unmetafy_str` (not `unmeta`) is what produces the raw byte buffer C
7995 // expects here. zshrs metafies at the CHARACTER level — an undecodable byte
7996 // is stored as U+0083 followed by `char::from(byte ^ 32)` — and U+0083
7997 // itself encodes as the two UTF-8 bytes C2 83. `unmeta` scans for a 0x83
7998 // BYTE, so it finds the trailing byte of that pair and mangles the string:
7999 // `$'\M-a'` came back out as `\M-^C\M-A` instead of `\M-a`.
8000 //
8001 // The variant matters: C's `untokenize` (c:Src/exec.c:2077) MAPS every
8002 // token char back to its source character through `ztokens`
8003 // (c:Src/lex.c:38) — Dnull -> `"`, Snull -> `'`, String -> `$`. Its
8004 // `lex::untokenize` namesake here is the SUBSTITUTION-stream variant,
8005 // which DELETES the quote nulls instead, so `%s` rendered a still-
8006 // tokenized word with its quoting silently erased: zsh prints
8007 // `condition expected: "$glob"` where zshrs printed the invisible
8008 // Dnull/Qstring bytes around a bare `glob`. The two `sb_*` twins below
8009 // already inline the ztokens mapping (c:5871 / c:5943); use the ported
8010 // form of the same walk here.
8011 let detok = crate::ported::lex::untokenize_ztokens(s);
8012 ums = unmetafy_str(&detok);
8013 umlen = ums.len(); // c:5389 *umlen
8014 ptr = 0; // c:5389 ptr starts at 0 in ums
8015
8016 // c:5393 — `mbrtowc` is LOCALE-driven: with `MB_CUR_MAX == 1` (the
8017 // C/POSIX locale) it consumes exactly one byte per call and hands
8018 // back that byte's value as the wide character, so a UTF-8 sequence
8019 // sitting in the string is measured as its separate bytes — a
8020 // printable one (`\xe2` = U+00E2) costs its own column, a
8021 // non-printable one (`\x80`, `\x90`) costs the five columns of the
8022 // `\M-^@` representation `wcs_nicechar_sel` returns for it. zsh
8023 // therefore charges 1+5+5 = 11 columns for `‐` (U+2010) under
8024 // `LC_ALL=C`, not the 1 column it charges under a UTF-8 locale, and
8025 // every `ZMB_nicewidth` consumer (described-list truncation in
8026 // computil.c's `cd_get`, `cd_calc`'s `premaxw`, `calclist`'s column
8027 // widths) budgets against that number.
8028 //
8029 // The port decoded UTF-8 unconditionally, so under the C locale it
8030 // measured 1 where zsh measured 11 and let descriptions run past
8031 // zsh's cut column. Branch on the active locale's CODESET, the same
8032 // way the `${#…}` length arm does (subst.rs, Bug #378), and take one
8033 // byte at a time when it is not UTF-8. Inlined rather than factored
8034 // out because src/ported/ forbids Rust-original helper fns.
8035 //
8036 // Platform note: an 8-bit locale's `mbrtowc` accepts all 256 byte
8037 // values; the ASCII C locale rejects bytes >= 0x80 (glibc, and zshrs on
8038 // every platform via `crate::c_locale` — macOS libc would accept them),
8039 // so such a byte takes C's `MB_INVALID` arm and costs `nicechar_sel`'s
8040 // `\M-b` (4) rather than 1, and the numbers above become 4+5+5 = 14.
8041 //
8042 // Scope: the branch applies to the WIDTH-ONLY shape — C names it
8043 // `ZMB_nicewidth(s)` = `mb_niceformat(s, NULL, NULL, 0)`
8044 // (Src/Zle/zle.h:57), reached through `niceztrlen` — AND to the STREAM
8045 // shape, which is how `printmatch` (compresult.c:2260) writes a match's
8046 // display string to the terminal. The stream shape was excluded because C
8047 // emits the RAW byte for a printable high byte (`wcrtomb` in a
8048 // single-byte locale writes one byte, and `zputs` un-metafies
8049 // `wcs_nicechar_sel`'s buffer on the way out) while a Rust
8050 // `String`/`write_all` of that scalar writes its two UTF-8 bytes; the
8051 // emission below now converts back to bytes, so the objection is gone.
8052 // Until then `gifdiff -` printed `don’t` where zsh prints
8053 // `don<e2>\M-^@\M-^Yt`, and the same for every description carrying a
8054 // curly apostrophe, an en dash or a typographic minus.
8055 //
8056 // The `outstrp` shape (`nicedup`, `quotedzputs`) takes the branch too. C's
8057 // result there is METAFIED: `wcs_nicechar_sel` copies `wcrtomb`'s byte into
8058 // its buffer through `imeta` (c:690-697), and the printers write it with
8059 // `zputs`, which un-metafies. The append below stores such a byte the way
8060 // zshrs metafies any lone byte (U+0083, byte ^ 32), so the String carries it
8061 // and `zputs` emits the ONE byte. Excluding this shape left
8062 // `LC_ALL=C which ヌ` printing the UTF-8 name raw where zsh prints
8063 // `$'\M-c\M-\C-C\M-\C-L'` (E02xtrace.ztst; 0xe3 is MB_INVALID in
8064 // the ASCII C locale).
8065 let mb_single_byte = unsafe {
8066 // c:5583 — `mbrtowc(&wc, &inchar, 1, mbsp)` is LOCALE-driven: with
8067 // MB_CUR_MAX == 1 it consumes one byte and returns that byte as
8068 // the wide character, so `\303\255` is TWO characters under
8069 // LC_ALL=C, not `í`. Rust's UTF-8 decoder has no such notion, so
8070 // the locale has to be asked directly. Inlined rather than
8071 // factored out: src/ported/ is a port and build.rs rejects any fn
8072 // with no C counterpart (its stated remedy #1 is to inline).
8073 //
8074 // Rust never calls `setlocale` on its own, so run it once from the
8075 // environment before asking `nl_langinfo` — otherwise the startup
8076 // default ("C") would shadow the process's LC_CTYPE.
8077 let _ = *MB_LOCALE_READY;
8078 let cs_ptr = libc::nl_langinfo(libc::CODESET);
8079 if cs_ptr.is_null() {
8080 false
8081 } else {
8082 let cs = std::ffi::CStr::from_ptr(cs_ptr).to_string_lossy();
8083 !(cs.eq_ignore_ascii_case("UTF-8") || cs.eq_ignore_ascii_case("utf8"))
8084 }
8085 };
8086
8087 // c:5391 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
8088 while umlen > 0 {
8089 // c:5392
8090 // c:5393 — `cnt = eol ? MB_INVALID : mbrtowc(&c, ptr, umlen, &mbs);`
8091 // Rust equivalent: try to decode one UTF-8 scalar from ums[ptr..],
8092 // honoring `eol` (force the invalid arm when set).
8093 let cnt: usize;
8094 let decoded_c: Option<char>;
8095 if eol {
8096 // c:5396 — MB_INVALID arm via `eol = 1`.
8097 decoded_c = None;
8098 cnt = 1;
8099 } else if mb_single_byte {
8100 // c:5393 — single-byte locale: `mbrtowc` returns 1 and sets
8101 // `c` to the byte value itself, except that the ASCII C locale
8102 // rejects a byte above 0x7f (`crate::c_locale`), which then takes
8103 // the c:5397 MB_INVALID arm and prints as `\M-c`.
8104 let mut wc: libc::wchar_t = 0;
8105 let mut mbs: MbStateBuf = MBSTATE_ZERO;
8106 let r = unsafe {
8107 mbrtowc(
8108 &mut wc,
8109 ums[ptr..].as_ptr() as *const libc::c_char,
8110 1,
8111 &mut mbs as *mut MbStateBuf as *mut libc::c_void,
8112 )
8113 };
8114 decoded_c = if r == MB_INVALID { None } else { Some(char::from(ums[ptr])) };
8115 cnt = 1;
8116 } else {
8117 // c:5393 — `mbrtowc` decodes at most ONE character and looks at no
8118 // more than the bytes that character occupies; it never validates
8119 // the rest of the buffer. Handing the whole tail to `from_utf8`
8120 // validated every remaining byte on every iteration, making the
8121 // walk quadratic in the string length. A UTF-8 scalar is at most
8122 // four bytes, so a four-byte window sees exactly what `mbrtowc`
8123 // sees: a sequence that runs past it cannot be valid UTF-8, and a
8124 // sequence truncated by end-of-input is still reported incomplete
8125 // because the window is then shorter than four bytes.
8126 let remaining = &ums[ptr..ptr + umlen.min(4)];
8127 match std::str::from_utf8(remaining) {
8128 Ok(s_slice) => {
8129 // Valid UTF-8 throughout remaining; consume one char.
8130 if let Some(ch) = s_slice.chars().next() {
8131 decoded_c = Some(ch);
8132 cnt = ch.len_utf8();
8133 } else {
8134 // Empty? Shouldn't happen given umlen > 0, but
8135 // mirror MB_INVALID.
8136 decoded_c = None;
8137 cnt = 1;
8138 }
8139 }
8140 Err(e) => {
8141 let valid_up_to = e.valid_up_to();
8142 if valid_up_to > 0 {
8143 // Decode the first valid char then continue.
8144 let valid_slice =
8145 unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
8146 let ch = valid_slice.chars().next().unwrap();
8147 decoded_c = Some(ch);
8148 cnt = ch.len_utf8();
8149 } else if e.error_len().is_none() {
8150 // Incomplete sequence at end of input — MB_INCOMPLETE.
8151 // c:5394 — `case MB_INCOMPLETE: eol = 1; FALL THROUGH`
8152 eol = true;
8153 decoded_c = None; // MB_INVALID
8154 cnt = 1;
8155 } else {
8156 // Definite invalid byte — MB_INVALID.
8157 decoded_c = None;
8158 cnt = 1;
8159 }
8160 }
8161 }
8162 }
8163
8164 // c:5396-5403 / c:5404-5424 switch dispatch on cnt/decoded_c.
8165 let cnt_used: usize;
8166 match decoded_c {
8167 None => {
8168 // c:5397 — `case MB_INVALID:` (or fall-through from
8169 // MB_INCOMPLETE at c:5394).
8170 // c:5400 — `fmt = nicechar_sel(*ptr, flags & NICEFLAG_QUOTE);`
8171 fmt = nicechar_sel(ums[ptr] as char, (flags & NICEFLAG_QUOTE) != 0);
8172 newl = fmt.len(); // c:5401 newl = strlen(fmt)
8173 cnt_used = 1; // c:5402 cnt = 1
8174 // c:5403 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless)
8175 }
8176 Some(c) if c == '\0' => {
8177 // c:5406 — `case 0:` — '\0' decodes to 0; consume 1 byte
8178 // and fall through to default.
8179 cnt_used = 1; // c:5409
8180 // c:5411-5421 default arm (FALL THROUGH from case 0)
8181 if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
8182 // c:5413
8183 fmt = "\\'".to_string(); // c:5414
8184 newl = 2; // c:5415
8185 } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
8186 // c:5417
8187 fmt = "\\\\".to_string(); // c:5418
8188 newl = 2; // c:5419
8189 } else {
8190 // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
8191 // flags & NICEFLAG_QUOTE);`
8192 let mut width: usize = 0;
8193 fmt =
8194 wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
8195 newl = width;
8196 }
8197 }
8198 Some(c) => {
8199 // c:5411 — `default:` arm (cnt > 0, valid char).
8200 cnt_used = cnt;
8201 if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
8202 // c:5413
8203 fmt = "\\'".to_string(); // c:5414
8204 newl = 2; // c:5415
8205 } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
8206 // c:5417
8207 fmt = "\\\\".to_string(); // c:5418
8208 newl = 2; // c:5419
8209 } else {
8210 // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
8211 // flags & NICEFLAG_QUOTE);`
8212 let mut width: usize = 0;
8213 fmt =
8214 wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
8215 newl = width;
8216 }
8217 }
8218 }
8219
8220 umlen -= cnt_used; // c:5427 umlen -= cnt
8221 ptr += cnt_used; // c:5428 ptr += cnt
8222 l += newl; // c:5429 l += newl
8223
8224 if let Some(ref mut w) = stream {
8225 // c:5431 if (stream)
8226 // c:5432 — `zputs(fmt, stream);`. In a single-byte locale
8227 // `wcs_nicechar_sel`'s printable arm is C's `wcrtomb`, which
8228 // writes exactly ONE byte; this port holds that byte as a `char`
8229 // in U+0080..U+00FF whose UTF-8 form is two bytes, so it has to be
8230 // narrowed back on the way out. Escapes (`\M-^@`) are ASCII and
8231 // pass through either way.
8232 if mb_single_byte {
8233 let mut out: Vec<u8> = Vec::with_capacity(fmt.len());
8234 let mut b4 = [0u8; 4];
8235 for ch in fmt.chars() {
8236 if (ch as u32) < 0x100 {
8237 out.push(ch as u8);
8238 } else {
8239 out.extend_from_slice(ch.encode_utf8(&mut b4).as_bytes());
8240 }
8241 }
8242 let _ = w.write_all(&out);
8243 } else {
8244 let _ = w.write_all(fmt.as_bytes());
8245 }
8246 }
8247 if let Some(ref mut buf) = outstr {
8248 // c:5433 if (outstr)
8249 // c:5434-5446 — append fmt to outstr, growing on demand. Rust
8250 // String auto-grows; the realloc loop collapses to push_str.
8251 if mb_single_byte {
8252 // c:690-697 — the single byte `wcrtomb` produced is stored
8253 // metafied; in zshrs's char-level form that is U+0083 followed
8254 // by `byte ^ 32` for every byte >= 0x80 (see the note above).
8255 for ch in fmt.chars() {
8256 let cu = ch as u32;
8257 if (0x80..0x100).contains(&cu) {
8258 buf.push(char::from(Meta));
8259 buf.push(char::from((cu as u8) ^ 32));
8260 } else {
8261 buf.push(ch);
8262 }
8263 }
8264 } else {
8265 buf.push_str(&fmt); // c:5446 memcpy(outptr, fmt, outlen)
8266 }
8267 }
8268 let _ = fmt;
8269 }
8270
8271 // c:5451 — `free(ums);` (Rust drop at scope exit)
8272 drop(ums);
8273 if let Some(slot) = outstrp {
8274 // c:5452 if (outstrp)
8275 // c:5453 — `*outptr = '\0';` (no-op for Rust String)
8276 // c:5455-5460 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping. Rust has
8277 // a single allocator so all three paths produce identical owned
8278 // String contents; transfer ownership into caller's slot.
8279 *slot = outstr.take();
8280 }
8281
8282 l // c:5462 return l
8283}
8284
8285/// Port of `is_mb_niceformat()` from `Src/utils.c:5474` — C decl `is_mb_niceformat(const char *s)`.
8286///
8287/// Predicate: would any character in `s` need representation by
8288/// `mb_niceformat` / `nicedup`? C body:
8289/// ```c
8290/// ums = ztrdup(s);
8291/// untokenize(ums);
8292/// ptr = unmetafy(ums, ¨en);
8293/// while (umlen > 0) {
8294/// cnt = mbrtowc(&c, ptr, umlen, &mbs);
8295/// switch (cnt) {
8296/// case MB_INCOMPLETE: case MB_INVALID:
8297/// if (is_nicechar(*ptr)) { ret = 1; break; }
8298/// cnt = 1;
8299/// memset(&mbs, 0, sizeof mbs);
8300/// break;
8301/// case 0: cnt = 1; /* FALLTHROUGH */
8302/// default:
8303/// if (is_wcs_nicechar(c)) ret = 1;
8304/// break;
8305/// }
8306/// if (ret) break;
8307/// umlen -= cnt; ptr += cnt;
8308/// }
8309/// ```
8310///
8311/// Rust port: unmetafy in place via [`unmetafy`], then walk the
8312/// resulting bytes. For valid UTF-8 sequences, check
8313/// `is_wcs_nicechar(scalar)`; for invalid bytes, check
8314/// `is_nicechar(byte)`. Either path bailing positive returns true.
8315pub fn is_mb_niceformat(s: &str) -> i32 {
8316 // c:5474
8317 // c:5476 — `int umlen, ret = 0;`
8318 let umlen: usize; // c:5476
8319 let mut ret: i32 = 0; // c:5476
8320 // c:5477 — `char *ums, *ptr;` (eptr modelled by bytes.len())
8321 let mut ums: Vec<u8>; // c:5477
8322 let mut ptr: usize; // c:5477
8323
8324 // c:5481-5483 — `ums = ztrdup(s); untokenize(ums); ptr =
8325 // unmetafy(ums, ¨en);`. Use BYTE-level `unmetafy_str`,
8326 // not char-level `unmeta`: `unmeta` decodes a metafied eight-bit byte
8327 // (Meta 0x83 + 0xC1 for `$'\M-a'`) into the valid Rust char U+00E1
8328 // (`á`), which the UTF-8 scan below reads as a PRINTABLE Latin-1 letter
8329 // → is_mb_niceformat=0 → `(q+)`/quotedzputs left the raw byte unquoted
8330 // (`\341`) where zsh emits `$'\M-a'`. `unmetafy_str` preserves the raw
8331 // 0xE1 byte (C's `unmetafy`), so the MB_INVALID arm's is_nicechar(0xE1)
8332 // fires (high-bit → nice) exactly as C does.
8333 // c:5481 `untokenize` is exec.c:2077 — the ztokens-MAPPING walk, not the
8334 // substitution-stream variant that deletes quote nulls. Must agree with
8335 // `mb_niceformat` above or the "does this need $'…'" answer is computed
8336 // over a different string than the one that gets formatted.
8337 let detok = crate::ported::lex::untokenize_ztokens(s);
8338 ums = unmetafy_str(&detok);
8339 umlen = ums.len(); // c:5483 *umlen
8340 ptr = 0; // c:5483 ptr starts at 0
8341
8342 // c:5488 — `mbrtowc` is LOCALE-driven, exactly as in `mb_niceformat`
8343 // above: under a single-byte codeset (`LC_ALL=C`) every byte is one wide
8344 // character whose value is the byte. Decoding UTF-8 regardless answered
8345 // "no reformat needed" for `ヌ` (E3 83 8C) under LC_ALL=C, so `which`
8346 // and `${(q+)…}` printed it bare where zsh sees the non-printable 0x83
8347 // and quotes the whole name. Same inlined CODESET probe as there.
8348 let mb_single_byte = unsafe {
8349 let _ = *MB_LOCALE_READY;
8350 let cs_ptr = libc::nl_langinfo(libc::CODESET);
8351 if cs_ptr.is_null() {
8352 false
8353 } else {
8354 let cs = std::ffi::CStr::from_ptr(cs_ptr).to_string_lossy();
8355 !(cs.eq_ignore_ascii_case("UTF-8") || cs.eq_ignore_ascii_case("utf8"))
8356 }
8357 };
8358 if mb_single_byte {
8359 // c:5486-5516 — one byte per `mbrtowc`; `case 0` (NUL) falls through
8360 // to the default arm, so every converted byte reaches
8361 // `is_wcs_nicechar`. A byte the ASCII C locale rejects
8362 // (`crate::c_locale`) takes the c:5494 MB_INVALID arm: `is_nicechar`.
8363 while ret == 0 && ptr < umlen {
8364 let mut wc: libc::wchar_t = 0;
8365 let mut mbs: MbStateBuf = MBSTATE_ZERO;
8366 let r = unsafe {
8367 mbrtowc(
8368 &mut wc,
8369 ums[ptr..].as_ptr() as *const libc::c_char,
8370 1,
8371 &mut mbs as *mut MbStateBuf as *mut libc::c_void,
8372 )
8373 };
8374 let nice = if r == MB_INVALID {
8375 is_nicechar(char::from(ums[ptr])) // c:5495
8376 } else {
8377 is_wcs_nicechar(char::from(ums[ptr])) // c:5508
8378 };
8379 if nice {
8380 ret = 1; // c:5509
8381 }
8382 ptr += 1; // c:5515
8383 }
8384 }
8385 // c:5485 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
8386 while ret == 0 && ptr < ums.len() {
8387 // c:5486 while (umlen > 0)
8388 let remaining = &ums[ptr..];
8389 match std::str::from_utf8(remaining) {
8390 Ok(s_slice) => {
8391 // c:5503-5511 — `default: if (is_wcs_nicechar(c)) ret = 1;`
8392 for ch in s_slice.chars() {
8393 if is_wcs_nicechar(ch) {
8394 // c:5508
8395 ret = 1; // c:5509
8396 break;
8397 }
8398 }
8399 break;
8400 }
8401 Err(e) => {
8402 let valid_up_to = e.valid_up_to();
8403 if valid_up_to > 0 {
8404 let valid = unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
8405 for ch in valid.chars() {
8406 if is_wcs_nicechar(ch) {
8407 // c:5508
8408 ret = 1; // c:5509
8409 break;
8410 }
8411 }
8412 if ret != 0 {
8413 break;
8414 }
8415 ptr += valid_up_to;
8416 continue;
8417 }
8418 // c:5493-5498 — `case MB_INVALID: if (is_nicechar(*ptr))
8419 // ret = 1; break;`
8420 if is_nicechar(remaining[0] as char) {
8421 // c:5494
8422 ret = 1; // c:5495
8423 break;
8424 }
8425 ptr += 1;
8426 }
8427 }
8428 }
8429
8430 drop(ums); // c:5519 free(ums)
8431 ret // c:5523 return ret
8432}
8433
8434/// Multibyte metachar length with conversion (from utils.c mb_metacharlenconv_r)
8435/// Port of `mb_metacharlenconv_r(const char *s, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5548`.
8436/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, wcp, mbsp)
8437// Rust idiom replacement: Rust strings are UTF-8 with valid scalar
8438// values, so `chars().next()` covers the mbrtowc state machine; no
8439// mbstate_t threading required.
8440/// Port of `mb_metacharlenconv_r()` from `Src/utils.c:5548`.
8441///
8442/// `mb_metacharlenconv_r` — see implementation.
8443pub fn mb_metacharlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
8444 if let Some(c) = s[pos..].chars().next() {
8445 (c.len_utf8(), Some(c))
8446 } else {
8447 (0, None)
8448 }
8449}
8450
8451/// Port of `mb_metacharlenconv()` from `Src/utils.c:5611`.
8452///
8453/// Multibyte-aware metafied-string char advance.
8454/// Port of `mb_metacharlenconv(const char *s, wint_t *wcp)` from Src/utils.c:5611. Returns
8455/// `(bytes_consumed, scalar_char)` for the next character of `s`.
8456///
8457/// `s` is the DEMETAFIED byte stream, not the port's `String` form. C
8458/// takes a metafied `char *` and un-metafies inline as it walks (c:5616
8459/// `*s == Meta ? s[1] ^ 32 : *s`, c:5562-5563 in the `_r` guts); the port
8460/// hoists that one step to the caller, which reaches this function via
8461/// `unmetafy_str`. The hoist is forced, not stylistic: with the MULTIBYTE
8462/// option off a unit is a single BYTE, so a caller must be able to cut
8463/// mid-character — and a Rust `String` cannot be sliced there. Working on
8464/// the byte stream is also what makes a `Meta`+byte pair and a literal
8465/// high byte the same input, exactly as they are to C. A demetafied
8466/// stream contains no `Meta` bytes to re-interpret, so a literal 0x83 in
8467/// a value (`$'\x83'`) stays one byte here instead of being mistaken for
8468/// an escape prefix.
8469///
8470/// c:5613 is the guard that makes byte mode work: with MULTIBYTE unset —
8471/// or for any 7-bit byte — the answer is one byte, no decoding. Without
8472/// it every caller silently kept counting characters in a mode where zsh
8473/// counts bytes.
8474///
8475/// The third return value has no counterpart in C's signature: it is the
8476/// unit itself, back in the port's `String` form. C needs no such thing —
8477/// its caller just slices the `char *` it already holds at the returned
8478/// length. A Rust caller cannot, because in byte mode the unit can be half
8479/// a character, so the re-encode has to happen where the boundary is known.
8480/// Returning it here is what keeps that one rule in one place: every
8481/// subscript, slice, split and quote walk re-encodes identically, and
8482/// concatenating any run of units reproduces exactly the bytes C would have
8483/// copied.
8484/// WARNING: param names don't match C — Rust=(s) vs C=(s, wcp)
8485pub fn mb_metacharlenconv(s: &[u8]) -> (usize, Option<char>, String) {
8486 if s.is_empty() {
8487 // c:5594-5595 — nothing left to decode.
8488 return (0, None, String::new());
8489 }
8490 // The unit in the port's `String` form: well-formed text verbatim,
8491 // anything else escaped as `Meta` + `byte ^ 32` — the representation
8492 // `unmetafy_str` decodes. C's own metafication (c:4869 `metafy`) escapes
8493 // the `imeta` set for the same reason; Rust's `String` additionally
8494 // cannot hold a byte that is not part of a valid sequence, so the escape
8495 // covers those too.
8496 let encode = |chunk: &[u8]| -> String {
8497 match std::str::from_utf8(chunk) {
8498 Ok(v) => v.to_string(),
8499 Err(_) => {
8500 let mut v = String::with_capacity(2 * chunk.len());
8501 for &b in chunk {
8502 if b < 0x80 {
8503 v.push(b as char);
8504 } else {
8505 v.push(char::from(Meta));
8506 v.push(char::from(b ^ 32));
8507 }
8508 }
8509 v
8510 }
8511 }
8512 };
8513 // c:5613 — `if (!isset(MULTIBYTE) || (unsigned char) *s <= 0x7f)`:
8514 // treat as a single byte. The option is read from the live slot with
8515 // its declared default (on, c:Src/options.c:197) rather than through
8516 // `isset()`, which maps a never-written slot to false and would invert
8517 // the default in any context that skips init's `emulate()`.
8518 // `mb_single_byte` above is the SAME single-byte outcome reached the
8519 // other way: the option is on, but the locale's `mbrtowc` is a single-byte
8520 // codec, so c:5583 `mbrtowc(&wc, &inchar, 1, mbsp)` returns 1 with `wc` =
8521 // the byte. C needs no such test because it calls the real `mbrtowc`; the
8522 // Rust UTF-8 decoder below is locale-blind, so it has to ask.
8523 // Rust never calls `setlocale` on its own, so run it once from the
8524 // environment; otherwise `mbrtowc` below would still be the startup
8525 // "C" codec and shadow the process's LC_CTYPE.
8526 let _ = *MB_LOCALE_READY;
8527 // c:5613 — `if (!isset(MULTIBYTE) || (unsigned char) *s <= 0x7f)`:
8528 // treat as a single byte. The option is read from the live slot with
8529 // its declared default (on, c:Src/options.c:197) rather than through
8530 // `isset()`, which maps a never-written slot to false and would invert
8531 // the default in any context that skips init's `emulate()`.
8532 //
8533 // There is deliberately NO codeset test here, because C has none: it
8534 // calls `mbrtowc` and lets the locale decide. A `CODESET != "UTF-8"`
8535 // short-circuit used to send EVERY non-UTF-8 locale down the
8536 // single-byte path, but `zh_CN.GB2312` and `ja_JP.eucJP` are multibyte
8537 // (`MB_CUR_MAX` 2 and 3), so `${s[1]}` yielded one byte where zsh
8538 // yields the whole two-byte character. Single-byte locales still take
8539 // the single-byte path — `mbrtowc` itself consumes exactly one byte
8540 // under `LC_ALL=C` and returns that byte as the wide character.
8541 if !crate::ported::options::opt_state_get("multibyte").unwrap_or(true) || s[0] <= 0x7f {
8542 // c:5616 — the byte itself is the scalar.
8543 return (1, Some(s[0] as char), encode(&s[..1]));
8544 }
8545 // c:5635 → mb_metacharlenconv_r: feed bytes to mbrtowc until one
8546 // character completes (c:5577-5585).
8547 let mut mbs: MbStateBuf = MBSTATE_ZERO;
8548 let mut n: usize = 0;
8549 while n < s.len() {
8550 let inchar = s[n];
8551 n += 1;
8552 let mut wc: libc::wchar_t = 0;
8553 let ret = unsafe {
8554 mbrtowc(
8555 &mut wc,
8556 &inchar as *const u8 as *const libc::c_char,
8557 1,
8558 &mut mbs as *mut MbStateBuf as *mut libc::c_void,
8559 )
8560 };
8561 if ret == MB_INVALID {
8562 // c:5579-5580 — break out and fall through to the single-byte
8563 // answer below.
8564 break;
8565 }
8566 if ret == MB_INCOMPLETE {
8567 // c:5581-5582 — keep feeding bytes.
8568 continue;
8569 }
8570 // c:5583-5585 — one complete character of `n` bytes.
8571 return (n, char::from_u32(wc as u32), encode(&s[..n]));
8572 }
8573 // c:5592-5593 — no valid multibyte sequence: treat as a single byte.
8574 (1, None, encode(&s[..1]))
8575}
8576
8577/// Port of `mb_metastrlenend()` from `Src/utils.c:5655` — C decl `mb_metastrlenend(char *ptr, int width, char *eptr)`.
8578///
8579/// Multibyte metastring length to end (from utils.c mb_metastrlenend)
8580///
8581/// Counts characters (`width == false`) or display columns
8582/// (`width == true`) in a metafied string. C walks the metafied
8583/// byte stream, un-metafies inline (`if (*ptr == Meta) inchar =
8584/// *++ptr ^ 32`, c:5672), and feeds one byte at a time to
8585/// `mbrtowc` to assemble each multibyte character (c:5691); a
8586/// complete character counts 1 (or its `WCWIDTH`), an invalid byte
8587/// counts as a single character (c:5712-5714).
8588///
8589/// Rust-port note: zshrs stores both metafied byte-pairs (`Meta` +
8590/// `byte ^ 32`, produced by `getkeystring` for `$'\xNN'` escapes)
8591/// AND real multibyte `char`s (the literal `$'é'` path) in one
8592/// `String`, whereas C only ever holds metafied bytes. So the port
8593/// first reduces the slice to its raw byte stream via `unmetafy_str`
8594/// (metafied pair → its byte; real `char` → its UTF-8 bytes — the
8595/// same bytes C's inline demetafy would yield), then runs C's
8596/// `mbrtowc` character-assembly loop over those bytes. Rust's UTF-8
8597/// validation is the `mbrtowc` analogue: the shortest valid prefix
8598/// is one character; a byte that begins no valid sequence counts as
8599/// one (c:5712).
8600pub fn mb_metastrlenend(ptr: &str, width: i32, eptr: usize) -> usize {
8601 // c:5672 — un-metafy the (optionally end-bounded) slice to raw bytes.
8602 let bytes = unmetafy_str(&ptr[..eptr.min(ptr.len())]);
8603 // c:5662-5663 — `if (!isset(MULTIBYTE) || MB_CUR_MAX == 1) return
8604 // eptr ? (int)(eptr - ptr) : ztrlen(ptr);`. With the MULTIBYTE
8605 // option unset every length op counts BYTES, and `ztrlen` counts a
8606 // `Meta`+byte pair as the ONE byte it encodes — which is exactly
8607 // the length of the demetafied stream computed above.
8608 //
8609 // This guard belongs HERE, not at the call sites. It used to live
8610 // only in the `${#v}` length arm (subst.rs), so `${#v}` counted
8611 // bytes under `unsetopt multibyte` while every other caller of this
8612 // function — dopadding's (l)/(r)/(m) widths above all — kept
8613 // counting characters.
8614 //
8615 // The option is read from the live slot with the DECLARED DEFAULT
8616 // (on, c:Src/options.c:197) rather than through `isset()`, which
8617 // maps a never-written slot to false and would invert the
8618 // default-on semantics in any context that skips init's
8619 // `emulate()` — unit tests most of all.
8620 // The decode is `mbrtowc`, not Rust's UTF-8 validator, because C's is
8621 // LOCALE-driven (c:5689). `zh_CN.GB2312` and `ja_JP.eucJP` are multibyte
8622 // codesets that are not UTF-8: zsh counts `\xc4\xe3\xba\xc3\xca\xc0` as
8623 // 3 characters there, and a UTF-8-only scan counted 6. `setlocale` has
8624 // to have run first or `mbrtowc` would still be the startup "C" codec —
8625 // and the `MB_CUR_MAX` test below reads the same locale, so the
8626 // initialisation has to happen BEFORE the guard, not after it.
8627 let _ = *MB_LOCALE_READY;
8628 // c:5662-5663 — the guard is `!isset(MULTIBYTE) || MB_CUR_MAX == 1`.
8629 // The `MB_CUR_MAX == 1` half was missing, and it is the half that
8630 // makes a SINGLE-BYTE locale report BYTES for a WIDTH request too:
8631 // C returns `ztrlen` here and never looks at `width`, so under
8632 // `LC_ALL=C` every byte counts 1 whatever glyph it belongs to.
8633 // Without it the port ran the `mbrtowc` loop, which under that
8634 // locale decodes each byte to its own wide character and then
8635 // charges C1 bytes (0x80-0x9f) zero columns via the width table:
8636 // `${(m)#日本語}` was 6 against zsh's 9 (the three C1 bytes 0x97,
8637 // 0x9c, 0x9e dropped), `${(m)#aé日}` 5 against 6, and the same
8638 // count reached `dopadding`, so `${(ml:12::x:)日本語}` padded with
8639 // six `x` where zsh pads with three. The character COUNT was
8640 // already right because that path adds 1 per decoded byte.
8641 // `MB_CUR_MAX` is a C macro over a per-platform accessor, so it is
8642 // read inline here exactly as C writes it — there is no C function
8643 // to port it as, and src/ported/ takes no Rust-original helpers.
8644 if !crate::ported::options::opt_state_get("multibyte").unwrap_or(true)
8645 || (unsafe { mb_cur_max_raw() } as usize) == 1
8646 {
8647 return bytes.len();
8648 }
8649 let mut num: usize = 0; // c:5658 size_t ret / counter
8650 let mut num_in_char: usize = 0; // c:5660
8651 let mut complete = true; // c:5660
8652 let mut i: usize = 0;
8653 let mut laststart: usize = 0; // c:5657
8654 // c:5670 — memset(&mb_shiftstate, 0, sizeof(mb_shiftstate)).
8655 let mut mbs: MbStateBuf = MBSTATE_ZERO;
8656 while i < bytes.len() {
8657 // c:5672-5676 — the stream is already demetafied above, so C's
8658 // inline `*ptr == Meta ? *++ptr ^ 32` step has nothing left to undo.
8659 let inchar = bytes[i];
8660 i += 1;
8661
8662 // c:5678-5687 — 7-bit US-ASCII subset: one character, skip mbrtowc.
8663 if complete && inchar <= 0x7f {
8664 num += 1;
8665 laststart = i;
8666 num_in_char = 0;
8667 continue;
8668 }
8669
8670 // c:5689 — `ret = mbrtowc(&wc, &inchar, 1, &mb_shiftstate);`.
8671 let mut wc: libc::wchar_t = 0;
8672 let ret = unsafe {
8673 mbrtowc(
8674 &mut wc,
8675 &inchar as *const u8 as *const libc::c_char,
8676 1,
8677 &mut mbs as *mut MbStateBuf as *mut libc::c_void,
8678 )
8679 };
8680
8681 if ret == MB_INCOMPLETE {
8682 // c:5691-5708 — trailing octets of a character still being
8683 // assembled; counted once at c:5736 if it never completes.
8684 num_in_char += 1;
8685 complete = false;
8686 } else {
8687 if ret == MB_INVALID {
8688 // c:5710-5714 — reset the shift state and treat the byte
8689 // that started the run as ONE character. This arm runs
8690 // BEFORE the width test, so an invalid byte counts 1 even
8691 // when a WIDTH was asked for: `${(m)#}` over six invalid
8692 // bytes is 6 in zsh, and taking `WCWIDTH` of an
8693 // undecodable byte (0, via `unwrap_or`) made it 0 here.
8694 mbs = MBSTATE_ZERO;
8695 i = laststart + 1;
8696 num += 1;
8697 } else if width != 0 {
8698 // c:5715-5726 — WCWIDTH(wc); C turns "not printable" (-1)
8699 // into 0 and adds nothing. WCWIDTH is u9_wcwidth in a
8700 // --enable-unicode9 build (Src/zsh.h:3302).
8701 let wcw = char::from_u32(wc as u32).map_or(-1, crate::ported::compat::u9_wcwidth);
8702 if wcw > 0 {
8703 let wcw = wcw as usize;
8704 // c:5722-5725 — `width == 1` adds the glyph's COLUMNS,
8705 // any larger value adds ONE per printable character.
8706 // C takes `width` as an `int` straight from the `(m)`
8707 // flag's `multi_width` counter (c:2376), which counts
8708 // the flag's repetitions: `(m)` is 1, `(mm)` is 2. A
8709 // `bool` here collapsed the two, so `${(mm)#日本語}`
8710 // answered 6 (the `(m)` column total) against zsh's 3.
8711 if width == 1 {
8712 num += wcw;
8713 } else {
8714 num += 1;
8715 }
8716 }
8717 } else {
8718 // c:5727-5728 — one character.
8719 num += 1;
8720 }
8721 laststart = i;
8722 num_in_char = 0;
8723 complete = true;
8724 }
8725 }
8726 // c:5735-5736 — "If incomplete, treat remainder as trailing single
8727 // character".
8728 num + usize::from(num_in_char != 0)
8729}
8730
8731/// Port of `mb_charlenconv_r()` from `Src/utils.c:5747` — C decl `mb_charlenconv_r(const char *s, int slen, wint_t *wcp, mbstate_t *mbsp)`.
8732///
8733/// Multibyte char length with conversion (from utils.c mb_charlenconv_r)
8734/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp, mbsp)
8735pub fn mb_charlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
8736 mb_metacharlenconv_r(s, pos)
8737}
8738
8739/// Port of `mb_charlenconv()` from `Src/utils.c:5793` — C decl `mb_charlenconv(const char *s, int slen, wint_t *wcp)`.
8740///
8741/// Multibyte char length (from utils.c mb_charlenconv)
8742/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp)
8743pub fn mb_charlenconv(s: &str, pos: usize) -> usize {
8744 s[pos..].chars().next().map(|c| c.len_utf8()).unwrap_or(0)
8745}
8746
8747/// Port of `metacharlenconv()` from `Src/utils.c:5811` — C decl `int metacharlenconv(const char *x, int *c)`.
8748/// ```c
8749/// if (*x == Meta) {
8750/// if (c) *c = x[1] ^ 32;
8751/// return 2;
8752/// }
8753/// if (c) *c = (char)*x;
8754/// return 1;
8755/// ```
8756/// Single-byte metafied char advance — Meta+X is 2 bytes (decode
8757/// via XOR 32), plain byte is 1 byte. Previously used hardcoded
8758/// `0x83`; now routes through the canonical `Meta` const for
8759/// maintainability parity.
8760/// WARNING: param names don't match C — Rust=(s) vs C=(x, c)
8761pub fn metacharlenconv(s: &str) -> (usize, Option<char>) {
8762 // c:5811
8763 let bytes = s.as_bytes();
8764 if bytes.is_empty() {
8765 return (0, None);
8766 }
8767 if bytes[0] == Meta && bytes.len() >= 2 {
8768 // c:5818
8769 let raw = bytes[1] as u32 ^ 32; // c:5820
8770 return (2, char::from_u32(raw)); // c:5821
8771 }
8772 (1, Some(bytes[0] as char)) // c:5823-5825
8773}
8774
8775/// Port of `charlenconv()` from `Src/utils.c:5832`.
8776///
8777/// Plain (non-metafy) char advance.
8778/// Port of `charlenconv(const char *x, int len, int *c)` from Src/utils.c:5832 — the
8779/// non-MULTIBYTE_SUPPORT branch. Single-byte read with
8780/// `len`-bound check; matches the C source's `if (!len)` early
8781/// exit.
8782/// WARNING: param names don't match C — Rust=(s, len) vs C=(x, len, c)
8783pub fn charlenconv(s: &str, len: usize) -> (usize, Option<char>) {
8784 if len == 0 {
8785 return (0, None);
8786 }
8787 let bytes = s.as_bytes();
8788 if bytes.is_empty() {
8789 return (0, None);
8790 }
8791 (1, Some(bytes[0] as char))
8792}
8793
8794/// Port of `sb_niceformat()` from `Src/utils.c:5851` — C decl `size_t sb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)`.
8795/// ```c
8796/// ums = ztrdup(s); untokenize(ums); ptr = unmetafy(ums, ¨en);
8797/// while (ptr < eptr) {
8798/// int c = (unsigned char) *ptr;
8799/// if (c == '\'' && (flags & NICEFLAG_QUOTE)) fmt = "\\'";
8800/// else if (c == '\\' && (flags & NICEFLAG_QUOTE)) fmt = "\\\\";
8801/// else fmt = nicechar_sel(c, ...);
8802/// }
8803/// ```
8804/// Single-byte nice format. **Unmetafies the input first** (c:5872),
8805/// then calls `nicechar_sel` for EVERY byte (not just controls).
8806///
8807/// Param mapping (Rule S1, faithful to C):
8808/// - `s: &str` ← C `const char *s`
8809/// - `stream: Option<&mut dyn Write>` ← C `FILE *stream` (`None` ≡ `NULL`)
8810/// - `outstrp: Option<&mut Option<String>>` ← C `char **outstrp`
8811/// (outer `None` ≡ `NULL`; inner `Option<String>` is the storage slot
8812/// the caller passes as `&local`, mirroring `char *retstr;
8813/// sb_niceformat(s, NULL, &retstr, ...);`).
8814/// - returns `usize` ← C `size_t l`.
8815pub fn sb_niceformat(
8816 s: &str,
8817 mut stream: Option<&mut dyn std::io::Write>,
8818 outstrp: Option<&mut Option<String>>,
8819 flags: i32,
8820) -> usize {
8821 // c:5851
8822 let mut l: usize = 0; // c:5853 size_t l = 0
8823 let mut newl: usize; // c:5853 size_t newl
8824 // c:5854 — `int umlen, outalloc, outleft;`. outalloc/outleft model
8825 // C's buffer-growth math; Rust String auto-grows so the realloc
8826 // loop at c:5897-5906 collapses to push_str. umlen carries through.
8827 let umlen: usize; // c:5854
8828 // c:5855 — `char *ums, *ptr, *eptr, *fmt, *outstr, *outptr;`
8829 let mut ums: Vec<u8>; // c:5855 char *ums
8830 let mut ptr: usize; // c:5855 char *ptr
8831 let eptr: usize; // c:5855 char *eptr
8832 let mut fmt: String; // c:5855 char *fmt
8833 let mut outstr: Option<String>; // c:5855 char *outstr
8834
8835 // c:5857-5863 — `if (outstrp) outptr = outstr = zalloc(2*strlen(s));`
8836 if outstrp.is_some() {
8837 // c:5857
8838 outstr = Some(String::with_capacity(2 * s.len())); // c:5859
8839 } else {
8840 // c:5860
8841 outstr = None; // c:5862 outstr = NULL
8842 }
8843
8844 // c:5865 — `ums = ztrdup(s);`
8845 ums = s.as_bytes().to_vec();
8846 // c:5866-5870 — comment-only block carried verbatim:
8847 /*
8848 * is this necessary at this point? niceztrlen does this
8849 * but it's used in lots of places. however, one day this may
8850 * be, too.
8851 */ // c:5866-5870
8852 // c:5871 — `untokenize(ums);`, over the bytes (see untokenize_bytes).
8853 ums = crate::token_char::untokenize_bytes(s.as_bytes());
8854 // c:5872 — `ptr = unmetafy(ums, ¨en);`
8855 umlen = unmetafy(&mut ums);
8856 ums.truncate(umlen);
8857 eptr = umlen; // c:5873 eptr = ptr + umlen
8858 ptr = 0; // c:5872 ptr starts at 0 in ums
8859
8860 while ptr < eptr {
8861 // c:5875
8862 let c: i32 = ums[ptr] as i32; // c:5876 int c = (unsigned char) *ptr
8863 if c == b'\'' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
8864 // c:5877
8865 fmt = "\\'".to_string(); // c:5878
8866 newl = 2; // c:5879
8867 } else if c == b'\\' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
8868 // c:5881
8869 fmt = "\\\\".to_string(); // c:5882
8870 newl = 2; // c:5883
8871 } else {
8872 // c:5885
8873 fmt = nicechar_sel(c as u8 as char, (flags & NICEFLAG_QUOTE) != 0); // c:5886
8874 newl = 1; // c:5887
8875 }
8876
8877 ptr += 1; // c:5890 ++ptr
8878 l += newl; // c:5891 l += newl
8879
8880 if let Some(ref mut w) = stream {
8881 // c:5893 if (stream)
8882 // c:5894 — `zputs(fmt, stream);`
8883 let _ = w.write_all(fmt.as_bytes());
8884 }
8885 if let Some(ref mut buf) = outstr {
8886 // c:5895 if (outstr)
8887 // c:5896-5912 — append fmt to outstr, growing on demand. Rust
8888 // String auto-grows; the realloc loop at c:5897-5906 collapses
8889 // to push_str (memcpy + outptr/outleft bookkeeping unneeded).
8890 buf.push_str(&fmt); // c:5907 memcpy(outptr, fmt, outlen)
8891 }
8892 // `fmt` is consumed at next iter assignment (C reuses pointer).
8893 let _ = fmt;
8894 }
8895
8896 // c:5915 — `free(ums);` (Rust drop at scope exit)
8897 drop(ums);
8898 if let Some(slot) = outstrp {
8899 // c:5916 if (outstrp)
8900 // c:5917 — `*outptr = '\0';` (no-op for String — push_str leaves
8901 // no embedded NUL, and Rust String is length-prefixed).
8902 // c:5919-5925 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping: in C this
8903 // selects between ztrdup (perm) / dupstring (heap arena) / direct
8904 // ownership-transfer (NODUP). Rust has a single allocator so all
8905 // three paths produce identical owned String contents; transfer
8906 // ownership of `outstr` into the caller's slot.
8907 *slot = outstr.take();
8908 }
8909
8910 l // c:5928 return l
8911}
8912
8913/// Port of `is_sb_niceformat()` from `Src/utils.c:5937` — C decl `int is_sb_niceformat(const char *s)`.
8914///
8915/// Predicate: would `sb_niceformat` change the input? Walks each byte
8916/// after unmetafy, returns `1` if any byte is "nice" per `is_nicechar`,
8917/// else `0` (C `int` return faithful to Rule S1).
8918pub fn is_sb_niceformat(s: &str) -> i32 {
8919 // c:5937
8920 // c:5939 — `int umlen, ret = 0;`
8921 let umlen: usize; // c:5939
8922 let mut ret: i32 = 0; // c:5939
8923 // c:5940 — `char *ums, *ptr, *eptr;`
8924 let mut ums: Vec<u8>; // c:5940 char *ums
8925 let mut ptr: usize; // c:5940 char *ptr
8926 let eptr: usize; // c:5940 char *eptr
8927
8928 // c:5942-5943 — `ums = ztrdup(s); untokenize(ums);`
8929 ums = crate::token_char::untokenize_bytes(s.as_bytes());
8930 umlen = unmetafy(&mut ums); // c:5944 ptr = unmetafy(ums, ¨en)
8931 ums.truncate(umlen);
8932 eptr = umlen; // c:5945 eptr = ptr + umlen
8933 ptr = 0; // c:5944 ptr starts at 0 in ums
8934
8935 while ptr < eptr {
8936 // c:5947
8937 if is_nicechar(ums[ptr] as char) {
8938 // c:5948 is_nicechar(*ptr)
8939 ret = 1; // c:5949
8940 break; // c:5950
8941 }
8942 ptr += 1; // c:5952 ++ptr
8943 }
8944
8945 drop(ums); // c:5955 free(ums)
8946 ret // c:5957 return ret
8947}
8948
8949/// Port of `zexpandtabs()` from `Src/utils.c:5975`.
8950///
8951/// Tab expansion — direct port of `zexpandtabs(const char *s, int len, int width, int startpos, FILE *fout, int all)` in zsh/Src/utils.c:5975.
8952/// Writes `s` into `out` with TAB characters expanded to spaces against
8953/// a tabstop of `width`. `startpos` carries the cumulative emitted
8954/// column from previous calls (used by `print -X` which preserves
8955/// alignment across args). When `all_tabs` is false, only leading TABs
8956/// (those at the start of a line) are expanded; embedded TABs are
8957/// emitted verbatim and `startpos` is advanced by one tabstop. When
8958/// `all_tabs` is true, every TAB expands. Returns the new `startpos`.
8959pub(crate) fn zexpandtabs(
8960 s: &str,
8961 width: i32,
8962 startpos: i32,
8963 all_tabs: bool,
8964 out: &mut String,
8965) -> i32 {
8966 let mut startpos = startpos;
8967 let mut at_start = true;
8968 for c in s.chars() {
8969 if c == '\t' {
8970 if all_tabs || at_start {
8971 if width <= 0 || startpos % width == 0 {
8972 out.push(' ');
8973 startpos += 1;
8974 }
8975 if width > 0 {
8976 while startpos % width != 0 {
8977 out.push(' ');
8978 startpos += 1;
8979 }
8980 }
8981 } else {
8982 let rem = startpos % width;
8983 startpos += width - rem;
8984 out.push('\t');
8985 }
8986 continue;
8987 } else if c == '\n' || c == '\r' {
8988 out.push(c);
8989 startpos = 0;
8990 at_start = true;
8991 continue;
8992 }
8993 at_start = false;
8994 out.push(c);
8995 startpos += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as i32;
8996 }
8997 startpos
8998}
8999
9000/// Port of `hasspecial()` from `Src/utils.c:6072` — C decl `int hasspecial(char const *s)`.
9001/// ```c
9002/// while (*s) {
9003/// if (ispecial(*s == Meta ? *++s ^ 32 : *s)) return 1;
9004/// s++;
9005/// }
9006/// return 0;
9007/// ```
9008/// Predicate: does `s` contain any byte that needs shell-quoting?
9009/// Routes through the canonical typtab-driven `ztype_h::ispecial`
9010/// which respects the `ZTF_SP_COMMA` (set by `makecommaspecial`)
9011/// and `ZTF_BANGCHAR` (BANGHIST + interact) flags. Previously used
9012/// a hardcoded char list — diverged from C for `,` (KSH_GLOB
9013/// extended-glob) and for the dynamic bangchar (`!` by default but
9014/// user-rewritable via `$HISTCHARS`).
9015pub fn hasspecial(s: &str) -> bool {
9016 // c:6072
9017 let bytes = s.as_bytes();
9018 let mut i = 0;
9019 while i < bytes.len() {
9020 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
9021 // c:6075 — `*s == Meta ? *++s ^ 32 : *s`.
9022 let v = bytes[i + 1] ^ 32;
9023 i += 2;
9024 v
9025 } else {
9026 let v = bytes[i];
9027 i += 1;
9028 v
9029 };
9030 if crate::ported::ztype_h::ispecial(c) {
9031 // c:6075
9032 return true;
9033 }
9034 }
9035 false
9036}
9037
9038/// Port of `addunprintable()` from `Src/utils.c:6083`.
9039///
9040/// Port of `static char *addunprintable(char *v, const char *u, const char *uend)`
9041/// from `Src/utils.c:6083-6124`. Renders unprintable bytes using
9042/// **shell-compatible C-string escapes**:
9043/// - `\0` (NUL, with `\000` form if next byte is octal-digit)
9044/// - `\a` (0x07 BEL), `\b` (BS), `\f` (FF), `\n` (LF),
9045/// `\r` (CR), `\t` (TAB), `\v` (VT)
9046/// - `\nnn` 3-digit octal fallback for any other control byte
9047///
9048/// Previously the Rust port emitted ZLE-style caret notation (`^X`,
9049/// `\u{:04x}`) — completely different convention. C semantics target
9050/// `printf %q` / `$'...'` reuse where the output must round-trip
9051/// through `printf %b` to reconstruct the original byte. Caret
9052/// notation is what ZLE *displays* but not what `addunprintable`
9053/// emits.
9054/// WARNING: param names don't match C — Rust=(c) vs C=(v, u, uend)
9055pub fn addunprintable(c: char) -> String {
9056 // c:6082
9057 let b = c as u32 & 0xff;
9058 match b as u8 {
9059 // c:6097-6103 — `\0`. C peeks next byte and uses `\000` form
9060 // when followed by an octal digit. Rust port works on a single
9061 // char so emits just `\0`; callers needing the disambiguation
9062 // form can pass the lookahead byte separately.
9063 0x00 => "\\0".to_string(),
9064 0x07 => "\\a".to_string(), // c:6106
9065 0x08 => "\\b".to_string(), // c:6107
9066 0x0c => "\\f".to_string(), // c:6108
9067 0x0a => "\\n".to_string(), // c:6109
9068 0x0d => "\\r".to_string(), // c:6110
9069 0x09 => "\\t".to_string(), // c:6111
9070 0x0b => "\\v".to_string(), // c:6112
9071 // c:6114-6119 — `\nnn` 3-digit octal default.
9072 _ => format!("\\{:o}{:o}{:o}", (b >> 6) & 7, (b >> 3) & 7, b & 7),
9073 }
9074}
9075
9076/// One element of a metafied string, as C's `MB_METACHARLENCONV` loop sees it:
9077/// either a decoded character, or a raw byte that is not one.
9078///
9079/// A byte that cannot appear in a Rust `String` (an invalid-UTF-8 byte such as
9080/// the 0xE1 produced by `$'\M-a'`) is stored metafied — `Meta` (0x83) followed
9081/// by `byte ^ 32` — so a plain `.chars()` walk sees two innocuous characters
9082/// where the shell means one raw byte, and passes them straight through. C hits
9083/// exactly this case as `WEOF` out of `MB_METACHARLENCONV` (c:6210) and routes
9084/// it to `addunprintable()`.
9085#[derive(Clone, Copy, PartialEq)]
9086enum MetaChar {
9087 Ch(char),
9088 Raw(u8),
9089}
9090
9091/// Port of `quotestring()` from `Src/utils.c:6141`.
9092///
9093/// Quote a string according to the specified type
9094/// Port from zsh/Src/utils.c quotestring() (lines 6141-6452)
9095/// Quote a string per the requested bslashquote style.
9096/// Port of `quotestring(const char *s, int instring)` from Src/utils.c — used by `print
9097/// -%q`, `${(q)var}`, completion-output escaping, history
9098/// re-emission.
9099/// WARNING: param names don't match C — Rust=(s, quote_type) vs C=(s, instring)
9100pub fn quotestring(s: &str, quote_type: i32) -> String {
9101 // c:6207-6210 — C walks the string with MB_METACHARINIT/MB_METACHARLENCONV,
9102 // which yields one decoded character at a time, or WEOF for a byte that does
9103 // not decode. These three closures are that walk, kept inline exactly as C
9104 // keeps it inline in each arm below.
9105 //
9106 // `meta_chars` is the decoder: a byte that cannot live in a Rust `String`
9107 // (an invalid-UTF-8 byte such as the 0xE1 from `$'\M-a'`) is stored metafied
9108 // — Meta (0x83) then `byte ^ 32` — so a naive `.chars()` walk sees two
9109 // innocuous characters where the shell means one raw byte and passes them
9110 // straight through. That is C's WEOF case, and it must reach addunprintable.
9111 //
9112 // The walk is `mb_metacharlenconv` (c:Src/utils.c:5611) over the DEMETAFIED
9113 // byte stream, which is the input C's macro actually sees. The port used to
9114 // decode each `Meta`+byte pair to its own `Raw` unit and stop there, so a
9115 // pair of pairs that spells ONE character never got assembled: `${(q)v}` for
9116 // `v=$'\xce\xb1'` emitted `$'\316'$'\261'` where zsh emits the two bytes of
9117 // `α` verbatim (`ce b1`), because C's mbrtowc joins them into a printable
9118 // wide character at c:6422 and copies the unit whole (c:6431-6433).
9119 // c:Src/utils.c:5613 — `if (!isset(MULTIBYTE) || …)`. C reads the option
9120 // through `isset(X)` = `opts[X]`, a single array load. The port went via
9121 // `opt_state_get("multibyte")`, which allocates a lowercased copy of the
9122 // name in `optlookup` and then does two String-keyed HashMap lookups
9123 // under an RwLock — on EVERY quotestring call. `quotestring` runs once
9124 // per completion match (`multiquote` → `quotestring`, compcore.c:1073),
9125 // so a 46765-match `compadd -k functions` paid that name lookup 46765
9126 // times. Same precedent as options.rs:768-770, which replaced
9127 // `isset(optlookup("shoptionletters"))` with the optno constant for
9128 // exactly this reason.
9129 let mb = isset(crate::ported::zsh_h::MULTIBYTE);
9130 // c:5583 — `mbrtowc` is locale-driven; in a single-byte locale it consumes
9131 // ONE byte and hands back that byte's value. Rust's UTF-8 decoder is not,
9132 // so read the locale once per call (an `nl_langinfo` load, no allocation)
9133 // instead of per byte. This runs once per completion match
9134 // (`multiquote` -> `quotestring`, c:Src/Zle/compcore.c:1073), which is why
9135 // it is hoisted out of the walk below.
9136 let mb_sb = mb
9137 && unsafe {
9138 // c:5583 — `mbrtowc(&wc, &inchar, 1, mbsp)` is LOCALE-driven: with
9139 // MB_CUR_MAX == 1 it consumes one byte and returns that byte as
9140 // the wide character, so `\303\255` is TWO characters under
9141 // LC_ALL=C, not `í`. Rust's UTF-8 decoder has no such notion, so
9142 // the locale has to be asked directly. Inlined rather than
9143 // factored out: src/ported/ is a port and build.rs rejects any fn
9144 // with no C counterpart (its stated remedy #1 is to inline).
9145 //
9146 // Rust never calls `setlocale` on its own, so run it once from the
9147 // environment before asking `nl_langinfo` — otherwise the startup
9148 // default ("C") would shadow the process's LC_CTYPE.
9149 let _ = *MB_LOCALE_READY;
9150 let cs_ptr = libc::nl_langinfo(libc::CODESET);
9151 if cs_ptr.is_null() {
9152 false
9153 } else {
9154 let cs = std::ffi::CStr::from_ptr(cs_ptr).to_string_lossy();
9155 !(cs.eq_ignore_ascii_case("UTF-8") || cs.eq_ignore_ascii_case("utf8"))
9156 }
9157 };
9158 let meta_chars = |s: &str| -> Vec<MetaChar> {
9159 // c:5672 — the byte stream is what mbrtowc consumes; a `Meta`+byte pair
9160 // and a literal high byte are the same input to the step below.
9161 let bytes = unmetafy_str(s);
9162 let mut out = Vec::with_capacity(bytes.len());
9163 let mut i = 0usize;
9164 while i < bytes.len() {
9165 let b = bytes[i];
9166 // c:5613 — `if (!isset(MULTIBYTE) || (unsigned char) *s <= 0x7f)`:
9167 // one byte, and `*wcp` is that byte widened. With the option off
9168 // that is EVERY byte, which is why `unsetopt multibyte;
9169 // ${(q)$'\xe6\x97\xa5'}` comes out as `e6` `$'\227'` `a5` in zsh —
9170 // 0xe6 and 0xa5 are printable Latin-1, 0x97 is a C1 control.
9171 // `mb_sb` reaches the same place through the LOCALE rather than
9172 // the option: c:5635 hands the bytes to `mb_metacharlenconv_r`,
9173 // whose `mbrtowc` (c:5583) in a single-byte locale returns 1 with
9174 // `wc` = the byte. `Raw(b)` is the right unit for it — c:6431-6433
9175 // copies the ONE source byte through, which `push_metachar` does
9176 // and `MetaChar::Ch(char::from(b))` would not (that emits the two
9177 // UTF-8 bytes of U+00`b`). The printability test below then decides
9178 // raw vs `$'\NNN'`, exactly as `WC_ISPRINT(cc)` does.
9179 if !mb || mb_sb || b <= 0x7f {
9180 out.push(if b <= 0x7f {
9181 MetaChar::Ch(b as char)
9182 } else {
9183 MetaChar::Raw(b)
9184 });
9185 i += 1;
9186 continue;
9187 }
9188 // c:5635 → mb_metacharlenconv_r: shortest valid multibyte prefix.
9189 let hi = (i + 4).min(bytes.len());
9190 let mut n = 0usize;
9191 for end in (i + 1)..=hi {
9192 if let Some(c) = std::str::from_utf8(&bytes[i..end])
9193 .ok()
9194 .and_then(|v| v.chars().next())
9195 {
9196 out.push(MetaChar::Ch(c));
9197 n = end - i;
9198 break;
9199 }
9200 }
9201 if n == 0 {
9202 // c:5592-5593 — no valid sequence: WEOF, one byte.
9203 out.push(MetaChar::Raw(b));
9204 n = 1;
9205 }
9206 i += n;
9207 }
9208 out
9209 };
9210 // c:6212 / c:6424 — `cc != WEOF && WC_ISPRINT(cc)`.
9211 let mc_printable = |mc: MetaChar| -> bool {
9212 match mc {
9213 MetaChar::Ch(c) => crate::ported::compat::u9_iswprint(c),
9214 // With MULTIBYTE on an undecodable byte IS the WEOF case, so the
9215 // `cc != WEOF` conjunct short-circuits. With it off there is no
9216 // WEOF at all (c:5615-5617 always sets `*wcp` to the byte), so the
9217 // test is `WC_ISPRINT` of that Latin-1 scalar. In a single-byte
9218 // locale c:5583 `mbrtowc` decides: the ASCII C locale rejects a
9219 // byte above 0x7f (`crate::c_locale`) — WEOF, not printable.
9220 MetaChar::Raw(b) => {
9221 let converts = || {
9222 let mut wc: libc::wchar_t = 0;
9223 let mut mbs: MbStateBuf = MBSTATE_ZERO;
9224 let r = unsafe {
9225 mbrtowc(
9226 &mut wc,
9227 &b as *const u8 as *const libc::c_char,
9228 1,
9229 &mut mbs as *mut MbStateBuf as *mut libc::c_void,
9230 )
9231 };
9232 r != MB_INVALID
9233 };
9234 (!mb || (mb_sb && converts()))
9235 && crate::ported::compat::u9_iswprint(char::from(b))
9236 }
9237 }
9238 };
9239 // c:6431-6433 — `while (u < uend) { if (*u == Meta) *v++ = *u++; *v++ = *u++; }`
9240 // copies a printable unit through STILL METAFIED. A raw byte therefore has
9241 // to go back out as `Meta` + `byte ^ 32`; pushing `b as char` would emit the
9242 // UTF-8 encoding of U+00`b` instead of the one byte the shell holds.
9243 let push_metachar = |out: &mut String, mc: MetaChar| match mc {
9244 MetaChar::Ch(c) => out.push(c),
9245 MetaChar::Raw(b) => {
9246 out.push(char::from(Meta));
9247 out.push(char::from(b ^ 32));
9248 }
9249 };
9250 // c:6082-6124 addunprintable — emit a non-printable element BYTE by byte, so
9251 // a non-printable multibyte character becomes one escape per UTF-8 byte.
9252 // `\0` widens to `\000` when an octal digit follows so the escape cannot
9253 // swallow it (c:6097-6103). There is deliberately no `\e` case: C has none,
9254 // so ESC comes out as the octal `\033`.
9255 let push_unprintable = |out: &mut String, mc: MetaChar, next: Option<MetaChar>| {
9256 let mut buf = [0u8; 4];
9257 let bytes: &[u8] = match mc {
9258 MetaChar::Ch(c) => c.encode_utf8(&mut buf).as_bytes(),
9259 MetaChar::Raw(b) => {
9260 buf[0] = b;
9261 &buf[0..1]
9262 }
9263 };
9264 for i in 0..bytes.len() {
9265 let b = bytes[i];
9266 if b == 0 {
9267 out.push_str("\\0");
9268 let follows = if i + 1 < bytes.len() {
9269 Some(bytes[i + 1])
9270 } else {
9271 next.map(|n| match n {
9272 MetaChar::Ch(c) => {
9273 let mut nb = [0u8; 4];
9274 c.encode_utf8(&mut nb).as_bytes()[0]
9275 }
9276 MetaChar::Raw(rb) => rb,
9277 })
9278 };
9279 if matches!(follows, Some(b'0'..=b'7')) {
9280 out.push_str("00");
9281 }
9282 } else {
9283 out.push_str(&addunprintable(b as char));
9284 }
9285 }
9286 };
9287
9288 // c:6141
9289 if s.is_empty() {
9290 return if quote_type == QT_NONE {
9291 String::new()
9292 } else if quote_type == QT_BACKSLASH {
9293 // c:6194 `if (!*s && shownull)` — shownull is 0 for plain
9294 // QT_BACKSLASH, so an empty string produces NO quotes.
9295 String::new()
9296 } else if quote_type == QT_BACKSLASH_SHOWNULL {
9297 // c:6165 sets shownull=1, so empty → '' (c:6194 adds the pair).
9298 "''".to_string()
9299 } else if quote_type == QT_SINGLE_OPTIONAL {
9300 // c:6187 sets `quotesub = shownull = 1`, so empty → '' (c:6194).
9301 "''".to_string()
9302 } else if quote_type == QT_SINGLE || quote_type == QT_DOUBLE || quote_type == QT_DOLLARS {
9303 // c:6194 — `if (!*s && shownull)`. `shownull` is set ONLY by
9304 // QT_BACKSLASH_SHOWNULL (c:6165) and QT_SINGLE_OPTIONAL (c:6187);
9305 // it is 0 for QT_SINGLE / QT_DOUBLE / QT_DOLLARS, which take the
9306 // `default:` arm at c:6190. Those styles return the quoted BODY
9307 // only — c:6131-6134: "Most quote styles other than backslash
9308 // assume the quotes are to be added outside quotestring()" — so an
9309 // empty input yields an empty body, and the CALLER supplies the
9310 // `''` / `""` / `$''` pair (subst.c:4083-4090, text.c:1086-1092).
9311 String::new()
9312 } else {
9313 String::new()
9314 };
9315 }
9316
9317 if quote_type == QT_NONE {
9318 s.to_string()
9319 } else if quote_type == QT_BACKSLASH_PATTERN {
9320 // Only bslashquote pattern characters (lines 6242-6247)
9321 let mut result = String::with_capacity(s.len() * 2);
9322 for c in s.chars() {
9323 if matches!(
9324 c,
9325 '*' | '?' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '#' | '^' | '~'
9326 ) {
9327 result.push('\\');
9328 }
9329 result.push(c);
9330 }
9331 result
9332 } else if quote_type == QT_BACKSLASH
9333 || quote_type == QT_BACKSLASH_SHOWNULL
9334 || quote_type == QT_SINGLE
9335 || quote_type == QT_SINGLE_OPTIONAL
9336 || quote_type == QT_DOUBLE
9337 {
9338 // c:Src/utils.c:6248-6443 — the `else` arm of quotestring's three-way
9339 // split, shared by EVERY style except QT_DOLLARS (c:6203) and
9340 // QT_BACKSLASH_PATTERN (c:6242). C runs ONE loop here and branches on
9341 // `instring` inside it. This port used to hold four separate loops —
9342 // one per style — and the three non-BACKSLASH ones had each decayed
9343 // into a plain per-character walk missing the c:6262-6299 verbatim-copy
9344 // preludes, the c:6272-6280 `$'…'`-in-double-quotes arm, the
9345 // c:6392-6416 token pass-through and the c:6396-6412 Inparmath group.
9346 // They are one loop again, exactly as C has it, so a fix to a prelude
9347 // cannot land in one style and miss the other three.
9348 //
9349 // Per-style behaviour lives entirely in the c:6307-6313 subcondition
9350 // and the two branches at c:6314 / c:6364:
9351 // QT_BACKSLASH every ispecial char takes a `\` (c:6307); `\n`
9352 // upgrades to `$'\n'` (c:6366-6371) and a
9353 // non-printable char to `$'…'` (c:6435-6442).
9354 // QT_SINGLE only `'` (c:6313), rewritten `'\''`, or `''`
9355 // under RCQUOTES (c:6372-6379).
9356 // QT_DOUBLE only `$`, `` ` ``, `"`, `\` (c:6311-6312), plus
9357 // the history character under BANGHIST
9358 // (c:6309-6310).
9359 // QT_SINGLE_OPTIONAL every ispecial char (c:6308), but it opens
9360 // `'…'` spans only where they are needed
9361 // (c:6314-6363).
9362 //
9363 // c:6166 — QT_BACKSLASH_SHOWNULL sets `shownull` and FALLS THROUGH to
9364 // QT_BACKSLASH, so from here the two are one style; `shownull` only
9365 // changes the empty-input case, handled at c:6194 above.
9366 let instring = if quote_type == QT_BACKSLASH_SHOWNULL {
9367 QT_BACKSLASH
9368 } else {
9369 quote_type
9370 };
9371 let mut result = String::with_capacity(s.len() * 2);
9372 // c:6304-6305 lookbehind `u[-1]`. The verbatim arms below advance `u`
9373 // several bytes at a time, so they carry it forward themselves.
9374 let mut prev: char = '\0'; // would-be u[-1]
9375 // c:6149-6156 — `quotesub` drives QT_SINGLE_OPTIONAL and nothing else:
9376 // 0 = mechanism off, 1 = pending (no `'` emitted yet; one belongs at
9377 // `quotestart`), 2 = a `'…'` span is open and gets closed at c:6445.
9378 let mut quotesub: u8 = if instring == QT_SINGLE_OPTIONAL { 1 } else { 0 }; // c:6187
9379 let mut quotestart: usize = 0; // c:6197 — byte index into `result`
9380 // c:6392 — `if (itok(*u) || instring != QT_BACKSLASH)`: a parser TOKEN
9381 // byte "needs to be passed straight through" — never backslashed and
9382 // never printability-tested. That half of the test was missing from
9383 // this arm, and `meta_chars` cannot express it: it demetafies, and a
9384 // token is NOT a metafied pair (this port stores C's token bytes as
9385 // PUA chars, crate::token_char), so `unmetafy_str` re-encodes one as its
9386 // UTF-8 bytes. `Inbrace` therefore fell through to the c:6435
9387 // not-printable arm and `quotename(Inbrace)` produced `$'\302\217'`
9388 // where C produces the raw byte that `untokenize` then maps back to
9389 // `{` — which is what the c:1931-2218 brace tail in zle_tricky.c
9390 // stores in `brbeg` (`ls /usr/{b<TAB>`).
9391 // Split the input at token chars, hand only the runs BETWEEN them to
9392 // `meta_chars`, and remember which units were tokens. A `Meta` byte is
9393 // deliberately NOT a split point (C gives it IMETA but not ITOK,
9394 // utils.c:4196-4201), and its partner byte is skipped so a metafied
9395 // pair whose second byte lands in the token range stays intact.
9396 let (mcs, tokmask): (Vec<MetaChar>, Vec<bool>) = {
9397 let mut v: Vec<MetaChar> = Vec::with_capacity(s.len());
9398 let mut m: Vec<bool> = Vec::with_capacity(s.len());
9399 let mut run = String::new();
9400 let mut prev_meta = false;
9401 for c in s.chars() {
9402 let cu = c as u32;
9403 if !prev_meta && crate::token_char::itok_char(c) {
9404 if !run.is_empty() {
9405 let seg = meta_chars(&run);
9406 m.resize(m.len() + seg.len(), false);
9407 v.extend(seg);
9408 run.clear();
9409 }
9410 v.push(MetaChar::Raw(cu as u8));
9411 m.push(true);
9412 } else {
9413 prev_meta = !prev_meta && c == char::from(Meta);
9414 run.push(c);
9415 }
9416 }
9417 if !run.is_empty() {
9418 let seg = meta_chars(&run);
9419 m.resize(m.len() + seg.len(), false);
9420 v.extend(seg);
9421 }
9422 (v, m)
9423 };
9424 // c:6262-6299 — before ANY quoting decision C copies three
9425 // constructs through verbatim. Each `*u` C tests there is one BYTE,
9426 // which is one element of `mcs` here. C can compare bytes directly
9427 // because a metafied string never holds a bare token byte as data;
9428 // `mcs` is DEMETAFIED, so a data byte in the token range does reach
9429 // this loop as `Raw` — every test is therefore gated on `tokmask`,
9430 // which marks the elements that really were tokens in `s`.
9431 let cval = |k: usize| -> u32 {
9432 match mcs[k] {
9433 // A token element holds its C byte; hand back its token char.
9434 MetaChar::Raw(b) if tokmask[k] => crate::token_char::token_char_from_byte(b)
9435 .map_or(b as u32, |t| t as u32),
9436 MetaChar::Raw(b) => b as u32,
9437 MetaChar::Ch(c) => c as u32,
9438 }
9439 };
9440 let is_tok = |k: usize, t: char| -> bool { tokmask[k] && cval(k) == t as u32 };
9441 // The mirror of `is_tok`: element `k` is the literal character `ch` as
9442 // DATA. Needed because c:6272 compares against `'$'` and `'\''`, which
9443 // are ordinary bytes, not tokens.
9444 let is_ch = |k: usize, ch: char| -> bool { !tokmask[k] && cval(k) == ch as u32 };
9445 // c:6431-6433 / c:6414 — copy one element out unchanged.
9446 let verbatim = |out: &mut String, k: usize| {
9447 if tokmask[k] {
9448 out.push(char::from_u32(cval(k)).unwrap_or('\0'));
9449 } else {
9450 push_metachar(out, mcs[k]);
9451 }
9452 };
9453 let bangchar_v = crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
9454 let mut i = 0usize;
9455 while i < mcs.len() {
9456 // c:6261 — set by the c:6382-6389 arm, consumed at c:6394 or
9457 // c:6428. It is deliberately DROPPED by the c:6435 not-printable
9458 // arm, which emits `$'…'` instead of a backslash.
9459 let mut dobackslash = false;
9460 // c:6272-6280 has NO `continue`: it copies the sigil, advances,
9461 // and falls through to the c:6392 pass-through with the `'` as the
9462 // current element — so it must skip the c:6301 gate, not the tail.
9463 let mut skip_special = false;
9464 // c:6281-6299 — `$(…)`, `$[…]`, `${…}` (and the `Qstring` forms
9465 // they take inside double quotes) are already syntactically
9466 // quoted, so the whole group goes through untouched. Without this
9467 // the `(` of `echo "${(` reached the c:6301 ispecial arm and came
9468 // out `${\(`, so `$PREFIX` was `${\(` where zsh publishes `${(`
9469 // and `_brace_parameter` never saw a parameter-flag word.
9470 // (Computed up front only because Rust cannot take a block as an
9471 // `else if let` scrutinee; it is side-effect free, and the chain
9472 // below still consults it in C's order.)
9473 let opener = if (is_tok(i, Stringg) || is_tok(i, Qstring)) && i + 1 < mcs.len() {
9474 if is_tok(i + 1, Inpar) {
9475 Some((Inpar, Outpar))
9476 } else if is_tok(i + 1, Inbrack) {
9477 Some((Inbrack, Outbrack))
9478 } else if is_tok(i + 1, Inbrace) {
9479 Some((Inbrace, Outbrace))
9480 } else {
9481 None
9482 }
9483 } else {
9484 None
9485 };
9486 // c:6262-6271 — a backtick group is copied whole, and C emits the
9487 // closing tick even when the input ran out before one appeared.
9488 if is_tok(i, Tick) || is_tok(i, Qtick) {
9489 let tick = if is_tok(i, Tick) { Tick } else { Qtick };
9490 result.push(tick);
9491 i += 1;
9492 while i < mcs.len() && !is_tok(i, tick) {
9493 verbatim(&mut result, i);
9494 prev = char::from_u32(cval(i)).unwrap_or('\0');
9495 i += 1;
9496 }
9497 result.push(tick); // c:6268
9498 prev = tick;
9499 if i < mcs.len() {
9500 i += 1; // c:6269-6270
9501 }
9502 continue;
9503 } else if instring == QT_DOUBLE
9504 && i + 1 < mcs.len()
9505 && (is_tok(i, Qstring) || is_ch(i, '$'))
9506 && is_ch(i + 1, '\'')
9507 {
9508 // c:6272-6280 — `$'…'` needs no quoting inside a double-quoted
9509 // string, so the sigil goes through bare. C copies only the
9510 // `$`/`Qstring` here; the `'` is handled by the c:6392
9511 // pass-through on the next statement, which is why there is no
9512 // `continue` and why the c:6301 gate has to be skipped.
9513 verbatim(&mut result, i); // c:6280
9514 prev = char::from_u32(cval(i)).unwrap_or('\0');
9515 i += 1;
9516 skip_special = true;
9517 } else if let Some((open, close)) = opener {
9518 // c:6283-6286 — `beg` is the `String`/`Qstring` byte, and it is
9519 // what raises the nesting level, not the bracket itself.
9520 let beg = if is_tok(i, Stringg) { Stringg } else { Qstring };
9521 let mut level = 0i32;
9522 result.push(beg); // c:6288
9523 result.push(open); // c:6289
9524 i += 2;
9525 // c:6290-6296
9526 prev = open;
9527 while i < mcs.len() && (!is_tok(i, close) || level != 0) {
9528 if is_tok(i, beg) {
9529 level += 1;
9530 } else if is_tok(i, close) {
9531 level -= 1;
9532 }
9533 verbatim(&mut result, i);
9534 prev = char::from_u32(cval(i)).unwrap_or('\0');
9535 i += 1;
9536 }
9537 // c:6297-6298 — the closer, only when the input actually had one.
9538 if i < mcs.len() {
9539 verbatim(&mut result, i);
9540 prev = char::from_u32(cval(i)).unwrap_or('\0');
9541 i += 1;
9542 }
9543 continue;
9544 }
9545 let mc = mcs[i];
9546 let c = match mc {
9547 MetaChar::Ch(c) => c,
9548 // A raw byte is never `\n` and never ispecial — it is the
9549 // not-printable case below. Stand in with its byte value so the
9550 // `=`/`~` lookbehind still advances correctly.
9551 MetaChar::Raw(b) => b as char,
9552 };
9553 if !skip_special {
9554 // c:6301 — `ispecial(*u)`. A token is excluded because C's
9555 // typtab carries ISPECIAL only for SPECCHARS (zsh.h:228), the
9556 // comma (c:4256) and the history character (c:4259) — all
9557 // ASCII — so `ispecial(Inbrace)` is false in C too.
9558 let is_sp = matches!(mc, MetaChar::Ch(_)) && !tokmask[i] && ispecial(c);
9559 // c:6302-6306 gate for `=`/`~` — mid-word they are left bare
9560 // unless the char is at position 0 (c:6303), MAGICEQUALSUBST is
9561 // on and the previous byte is `=` or `:` (c:6304-6305), or it
9562 // is `~` under EXTENDEDGLOB (c:6306).
9563 let eq_tilde_gate = if c == '=' || c == '~' {
9564 i == 0 // c:6303 u == s
9565 || (isset(MAGICEQUALSUBST) && (prev == '=' || prev == ':')) // c:6304-6305
9566 || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
9567 } else {
9568 true // c:6302 *u != '=' && *u != '~'
9569 };
9570 // c:6307-6313 — the per-style subcondition.
9571 let style_gate = instring == QT_BACKSLASH // c:6307
9572 || instring == QT_SINGLE_OPTIONAL // c:6308
9573 || (isset(BANGHIST)
9574 && bangchar_v != 0
9575 && c as u32 == bangchar_v as u32
9576 && instring != QT_SINGLE) // c:6309-6310
9577 || (instring == QT_DOUBLE && matches!(c, '$' | '`' | '"' | '\\')) // c:6311-6312
9578 || (instring == QT_SINGLE && c == '\''); // c:6313
9579 if is_sp && eq_tilde_gate && style_gate {
9580 if instring == QT_SINGLE_OPTIONAL {
9581 // c:6314-6363 — minimum quoting. `quotestart` is where
9582 // an opening `'` would be back-filled, so a run of
9583 // ordinary characters ends up INSIDE the span rather
9584 // than each special char getting its own `'…'`.
9585 if quotesub == 1 {
9586 if c == '\'' {
9587 // c:6319-6323 — a bare `'` is cheaper as `\'`
9588 // than as an opened-and-closed span, so no
9589 // span starts here and quotesub stays 1.
9590 result.push('\\');
9591 } else {
9592 // c:6328-6337 — time to quote: insert the
9593 // opening `'` at `quotestart`, shifting
9594 // everything after it right (C's addq loop).
9595 result.insert(quotestart, '\''); // c:6335
9596 quotesub = 2; // c:6337
9597 }
9598 verbatim(&mut result, i); // c:6339
9599 prev = c;
9600 i += 1;
9601 quotestart = result.len(); // c:6343
9602 } else if c == '\'' {
9603 if unset(RCQUOTES) {
9604 // c:6346-6348 — close the span, then `\'`.
9605 result.push('\''); // c:6346
9606 result.push('\\'); // c:6347
9607 result.push('\''); // c:6348
9608 quotesub = 1; // c:6350
9609 quotestart = result.len(); // c:6351
9610 } else {
9611 // c:6353-6355 — inside a span, `''` IS a
9612 // literal `'`, so the span never breaks.
9613 result.push('\''); // c:6354
9614 result.push('\''); // c:6355
9615 }
9616 prev = c;
9617 i += 1; // c:6358
9618 } else {
9619 // c:6360-6361 — already quoting, just add.
9620 verbatim(&mut result, i);
9621 prev = c;
9622 i += 1;
9623 }
9624 continue; // c:6363
9625 } else if c == '\n' || (instring == QT_SINGLE && c == '\'') {
9626 // c:6364-6381
9627 if c == '\n' {
9628 // c:6366-6371 — a newline cannot be backslashed,
9629 // so it is upgraded to a `$'\n'` island.
9630 result.push_str("$'\\n'");
9631 } else if unset(RCQUOTES) {
9632 // c:6372-6377 — `'` → `'\''`.
9633 result.push('\''); // c:6373
9634 if c == '\'' {
9635 result.push('\\'); // c:6374-6375
9636 }
9637 result.push(c); // c:6376
9638 result.push('\''); // c:6377
9639 } else {
9640 // c:6378-6379 — RCQUOTES: `''` is a literal `'`.
9641 result.push('\'');
9642 result.push('\'');
9643 }
9644 prev = c;
9645 i += 1; // c:6380
9646 continue; // c:6381
9647 } else {
9648 // c:6382-6388 — a backslash is owed, but it is not
9649 // emitted yet: if the character turns out not to be
9650 // printable the c:6435 arm upgrades it to `$'…'` and
9651 // drops the backslash entirely.
9652 dobackslash = true;
9653 }
9654 }
9655 }
9656 // c:6392 — `if (itok(*u) || instring != QT_BACKSLASH)`. Everything
9657 // that is not a plain QT_BACKSLASH character is passed straight
9658 // through here; only QT_BACKSLASH reaches the printability test.
9659 if tokmask[i] || instring != QT_BACKSLASH {
9660 if dobackslash {
9661 result.push('\\'); // c:6394-6395
9662 }
9663 if is_tok(i, Inparmath) {
9664 // c:6396-6412 — `$((…))` is already syntactically quoted,
9665 // so the whole math group is copied out with no further
9666 // quoting. C counts nesting on Inparmath/Outparmath and
9667 // stops at the matching close or at end of string.
9668 let mut inmath = 1i32; // c:6401
9669 verbatim(&mut result, i); // c:6402
9670 prev = char::from_u32(cval(i)).unwrap_or('\0');
9671 i += 1;
9672 loop {
9673 // c:6404-6407 — C reads `uc = *u` and breaks AFTER
9674 // copying it; a NUL there is the end of the string,
9675 // which is `i == mcs.len()` here.
9676 if i >= mcs.len() {
9677 break;
9678 }
9679 let was_out = is_tok(i, Outparmath);
9680 let was_in = is_tok(i, Inparmath);
9681 verbatim(&mut result, i); // c:6405
9682 prev = char::from_u32(cval(i)).unwrap_or('\0');
9683 i += 1;
9684 if was_out {
9685 // c:6408-6409
9686 inmath -= 1;
9687 if inmath == 0 {
9688 break;
9689 }
9690 } else if was_in {
9691 inmath += 1; // c:6410-6411
9692 }
9693 }
9694 } else {
9695 verbatim(&mut result, i); // c:6413-6414
9696 prev = c;
9697 i += 1;
9698 }
9699 continue; // c:6415
9700 }
9701 // c:6418-6427 — QT_BACKSLASH only: is the element printable in the
9702 // current character set? (`uend = u + MB_METACHARLENCONV(u, &cc)`
9703 // then `cc != WEOF && WC_ISPRINT(cc)`; `meta_chars` already did the
9704 // decode, so the test is `mc_printable`.)
9705 if mc_printable(mc) {
9706 // c:6428-6433 — owed backslash, then the unit copied through
9707 // still metafied.
9708 if dobackslash {
9709 result.push('\\'); // c:6428-6429
9710 }
9711 push_metachar(&mut result, mc); // c:6430-6433
9712 } else {
9713 // c:6435-6442 — not printable: one `$'…'` island per element,
9714 // built by addunprintable. The owed backslash is dropped.
9715 result.push_str("$'"); // c:6437-6438
9716 push_unprintable(&mut result, mc, mcs.get(i + 1).copied()); // c:6439
9717 result.push('\''); // c:6440
9718 }
9719 prev = c;
9720 i += 1;
9721 }
9722 // c:6445-6446 — QT_SINGLE_OPTIONAL left a span open.
9723 if quotesub == 2 {
9724 result.push('\'');
9725 }
9726 result
9727 } else if quote_type == QT_DOLLARS {
9728 // c:6203-6241 — `$'…'` quoting. Each element is either printable, in
9729 // which case it is emitted verbatim (backslashing `\`, `'`, and — under
9730 // BANGHIST — the history character), or it is NOT, in which case C hands
9731 // its raw bytes to addunprintable(): a named escape (`\t`, `\n`, `\a`, …)
9732 // or a 3-digit octal one.
9733 //
9734 // Two things this must NOT do, both of which the previous port did:
9735 // * pass an undecodable byte through raw. `$'\M-a'` is the byte 0xE1,
9736 // which is not valid UTF-8 and is stored metafied; walking `.chars()`
9737 // re-emitted it verbatim instead of `\341`, so the output could not
9738 // be re-parsed.
9739 // * emit `\e` for ESC. C's addunprintable has no `\e` case — ESC comes
9740 // out as the octal `\033`.
9741 //
9742 // c:6131-6134 — the `$'` … `'` wrapper is added by the CALLER
9743 // (subst.c:4085-4090 writes the quote pair and then `val[0] = '$'`),
9744 // so this arm returns the BODY only.
9745 let mut result = String::with_capacity(s.len() + 4);
9746 // c:6204-6209 — "The only way to get Nularg here is when it is
9747 // placeholding for the empty string": `if (inull(*u)) u++;`. Without
9748 // the skip, `${(qqqq):-""}` quoted the Nularg itself (`$'¡'` under LANG=C).
9749 let s = match s.chars().next() {
9750 Some(c) if crate::token_char::token_byte(c).is_some_and(crate::ported::ztype_h::inull) => {
9751 &s[c.len_utf8()..]
9752 }
9753 _ => s,
9754 };
9755 let mcs = meta_chars(s);
9756 let bang = crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
9757 for i in 0..mcs.len() {
9758 let mc = mcs[i];
9759 if mc_printable(mc) {
9760 // c:6214-6224. The escape test looks at the DECODED character
9761 // (`cc`), so a raw byte — which only reaches here with MULTIBYTE
9762 // off — can never be `\`, `'` or the history bang, all ASCII.
9763 if let MetaChar::Ch(c) = mc {
9764 if c == '\\'
9765 || c == '\''
9766 || (isset(BANGHIST) && bang != 0 && c as u32 == bang as u32)
9767 {
9768 result.push('\\');
9769 }
9770 }
9771 // c:6224-6225 — `while (u < uend) *v++ = *u++;`
9772 push_metachar(&mut result, mc);
9773 } else {
9774 // c:6226-6229
9775 push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
9776 }
9777 }
9778 result
9779 } else if quote_type == QT_BACKTICK {
9780 // c:6199 — C's DPUTS rejects QT_BACKTICK as a `quotestring` mode; the
9781 // only caller that asks for it wants the backticks escaped.
9782 s.replace('`', "\\`")
9783 } else {
9784 // Unknown quote_type — treat as no-op to match C's `default:` arm.
9785 s.to_string()
9786 }
9787}
9788
9789// ===========================================================
9790// xtrace helpers moved from src/ported/vm_helper.
9791// printprompt4 is a direct port of utils.c:1718-1735; quotedzputs
9792// is its argument-formatter companion (zsh formats `set -x` lines
9793// via the same utils.c path).
9794// ===========================================================
9795
9796/// Port of `quotedzputs()` from `Src/utils.c:6464` — C decl `quotedzputs(char const *s, FILE *stream)`.
9797///
9798/// Quote a string for re-readable output (`set -x`, `typeset -p`,
9799/// `set` listing, etc.). zsh's algorithm under MULTIBYTE_SUPPORT
9800/// (c:6464-6543):
9801/// 1. empty input → `''` (c:6470-6475)
9802/// 2. needs nice-format (controls / non-printables) → emit
9803/// `$'<mb_niceformat output>'` (c:6478-6492)
9804/// 3. no SPECCHARS member → return string unchanged (c:6511-6517)
9805/// 4. otherwise wrap in `'…'` (Bourne) or RCQUOTES form,
9806/// with embedded `'` rewritten as `'\''` / `''` (c:6533-6587)
9807///
9808/// Previous Rust port omitted step 2 (the `$'…'` branch).
9809/// Result: strings containing control bytes (`\n`, `\t`, escape
9810/// sequences) were single-quoted instead of `$'…'`-quoted,
9811/// breaking round-trip through `typeset -p` / `set` / `set -x`
9812/// because POSIX single-quotes are *strong* — embedded `\n` would
9813/// be re-fed as a literal newline rather than the C-escape.
9814///
9815/// The C signature is `char *quotedzputs(char const *s, FILE *stream)`
9816/// — when `stream` is non-NULL it writes there and returns NULL,
9817/// otherwise it returns the quoted string. Rust's variant covers
9818/// only the `stream==NULL` form (the `set -x` callers all want the
9819/// string back, not direct stdout writing). The `stream==Some` form
9820/// is fputs/fputc-direct in C; Rust callers that need writer-based
9821/// output should compose `print!`/`write!` with this fn's return.
9822/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream)
9823pub(crate) fn quotedzputs(s: &str) -> String {
9824 // c:6464
9825 // c:6469-6475 — `if (!*s)` empty string emits `''` literal.
9826 if s.is_empty() {
9827 return "''".to_string(); // c:6472
9828 }
9829 // c:6477-6508 — `is_mb_niceformat(s)` / `is_sb_niceformat(s)` arm:
9830 // if the string contains nice-formatted chars (controls,
9831 // non-printables), wrap in `$'…'` using sb/mb_niceformat with
9832 // NICEFLAG_QUOTE so embedded `'`/`\` get backslash-escaped.
9833 if is_mb_niceformat(s) != 0 {
9834 // c:6478-6492 (MULTIBYTE_SUPPORT branch): use mb_niceformat
9835 // with NICEFLAG_QUOTE so multi-byte chars round-trip through
9836 // wcs_nicechar (raw UTF-8 for printable wides, `\u`/`\U` for
9837 // large codepoints) AND `'`/`\\` get backslash-escaped.
9838 // Under !MULTIBYTE_SUPPORT (c:6494-6508) C would use
9839 // sb_niceformat instead. Static-link path: MULTIBYTE is
9840 // always available in Rust.
9841 // c:6485-6492 — `mb_niceformat(s, NULL, &substr,
9842 // NICEFLAG_QUOTE|NICEFLAG_NODUP);`
9843 let mut substr: Option<String> = None;
9844 let _ = mb_niceformat(s, None, Some(&mut substr), NICEFLAG_QUOTE | NICEFLAG_NODUP);
9845 return format!("$'{}'", substr.unwrap_or_default()); // c:6488
9846 }
9847 // c:6511-6518 — `if (!hasspecial(s)) return dupstring(s);`.
9848 if !hasspecial(s) {
9849 // c:6511
9850 return s.to_string(); // c:6516
9851 }
9852 // c:6520-6529 — outstr buffer alloc (zhalloc). Rust uses growable
9853 // String; the C `l = strlen(s) + 2 + (per-' overhead)` size hint
9854 // is just allocation tuning and doesn't affect output.
9855 let mut out = String::with_capacity(s.len() + 2);
9856 let bytes = s.as_bytes();
9857 let csh_junkie = isset(CSHJUNKIEQUOTES); // c:6554 / c:6612
9858
9859 if isset(RCQUOTES) {
9860 // c:6533 — RCQUOTES: wrap entire string in `'…'`; each
9861 // embedded `'` becomes `''` (the rc-style doubled quote).
9862 out.push('\''); // c:6539
9863 // c:6540-6547 — C walks METAFIED bytes; the Rust String is
9864 // UTF-8, so the faithful transposition walks CHARS (a byte
9865 // walk Latin-1-casts every multibyte char: em-dash E2 80 94
9866 // became "â" + a token byte that downstream passes ate).
9867 // Token chars (Dash U+e19B, Meta U+0083) are codepoints here.
9868 let chars_v: Vec<char> = s.chars().collect();
9869 let mut i = 0;
9870 while i < chars_v.len() {
9871 // c:6565-6569 — a decoded byte with imeta(c) goes back out as
9872 // Meta + c^32, i.e. the pair it came in as.
9873 let meta_pair = chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len();
9874 let c = if chars_v[i] == Dash {
9875 // c:6541 — `if (*s == Dash) c = '-';`
9876 i += 1;
9877 '-'
9878 } else if meta_pair {
9879 // c:6543 — `else if (*s == Meta) c = *++s ^ 32;`
9880 let n = chars_v[i + 1] as u32;
9881 i += 2;
9882 char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
9883 } else {
9884 // c:6546 — `else c = *s;`
9885 let dec = chars_v[i];
9886 i += 1;
9887 dec
9888 };
9889 if c == '\'' {
9890 // c:6548-6553 — `if (c == '\'') *ptr++ = '\'';` (the
9891 // rc-quote doubling).
9892 out.push('\''); // c:6553
9893 } else if c == '\n' && csh_junkie {
9894 // c:6554-6560 — `if (c == '\n' && isset(CSHJUNKIEQUOTES))
9895 // *ptr++ = '\\';`
9896 out.push('\\'); // c:6559
9897 }
9898 // c:6561-6570 — emit c (metafy on imeta).
9899 if meta_pair {
9900 out.push(char::from(Meta)); // c:6566
9901 out.push(chars_v[i - 1]); // c:6567 — c ^ 32
9902 } else {
9903 out.push(c); // c:6569
9904 }
9905 }
9906 out.push('\''); // c:6576
9907 } else {
9908 // c:6578-6637 — Bourne-style quoting, "avoiding empty quoted
9909 // strings". Tracks `inquote` so that `it's` becomes
9910 // `'it'\''s'` (no empty `''` runs).
9911 let mut inquote = false; // c:6466 (initialised at top of C fn)
9912 // c:6579-6586 — char-wise decode, same transposition as the
9913 // RCQUOTES arm above (C's byte walk is over metafied bytes).
9914 let chars_v: Vec<char> = s.chars().collect();
9915 let mut i = 0;
9916 while i < chars_v.len() {
9917 // c:6623-6627 — imeta(c) re-emits the Meta pair, as above.
9918 let meta_pair = chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len();
9919 let c = if chars_v[i] == Dash {
9920 i += 1;
9921 '-' // c:6581
9922 } else if meta_pair {
9923 let n = chars_v[i + 1] as u32; // c:6583
9924 i += 2;
9925 char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
9926 } else {
9927 let dec = chars_v[i]; // c:6585
9928 i += 1;
9929 dec
9930 };
9931 if c == '\'' {
9932 // c:6587-6602 — `'` closes any open inquote then emits
9933 // `\'` outside the quotes.
9934 if inquote {
9935 out.push('\''); // c:6593
9936 inquote = false; // c:6594
9937 }
9938 out.push('\\'); // c:6600
9939 out.push('\''); // c:6601
9940 } else {
9941 // c:6603-6629 — other chars open a quote run if not
9942 // already open, optionally backslash-escape `\n` under
9943 // CSHJUNKIEQUOTES, then emit the byte.
9944 if !inquote {
9945 out.push('\''); // c:6609
9946 inquote = true; // c:6610
9947 }
9948 if c == '\n' && csh_junkie {
9949 out.push('\\'); // c:6617
9950 }
9951 if meta_pair {
9952 out.push(char::from(Meta)); // c:6624
9953 out.push(chars_v[i - 1]); // c:6625 — c ^ 32
9954 } else {
9955 out.push(c); // c:6627
9956 }
9957 }
9958 }
9959 if inquote {
9960 out.push('\''); // c:6636
9961 }
9962 }
9963 // c:6639-6640 — `if (!stream) *ptr++ = '\0';` — Rust String already
9964 // NUL-terminated implicitly; no-op.
9965 out // c:6642
9966}
9967
9968/// Port of `dquotedztrdup()` from `Src/utils.c:6649` — C decl `char *dquotedztrdup(char const *s)`.
9969/// Two arms (selected by `isset(CSHJUNKIEQUOTES)` at c:6655):
9970///
9971/// **CSHJUNKIEQUOTES path** (c:6656-6686): the csh-junk-quote style
9972/// where only the non-special sections are wrapped in `"..."` and
9973/// special chars (`"`, `$`, `` ` ``) appear OUTSIDE the quotes with
9974/// backslash escape. `\n` inside the quotes gets an extra `\` so it
9975/// round-trips through history.
9976///
9977/// **Default path** (c:6687-6719): wraps the whole string in `"..."`.
9978/// `\` is doubled to `\\`. `"`, `$`, `` ` `` get backslash-escaped.
9979/// A trailing `\` gets an extra `\` appended (the `pending` quirk).
9980///
9981/// Previously the Rust port only implemented the default arm; the
9982/// CSHJUNKIEQUOTES path is now ported faithfully.
9983pub fn dquotedztrdup(s: &str) -> String {
9984 // c:6648
9985 let mut out = String::with_capacity(s.len() * 4 + 2);
9986 let bytes = s.as_bytes();
9987 // c:6655 — `if (isset(CSHJUNKIEQUOTES))`.
9988 if isset(CSHJUNKIEQUOTES) {
9989 let mut inquote = false;
9990 let mut i = 0;
9991 while i < bytes.len() {
9992 // c:6661-6662 — Meta byte decode.
9993 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
9994 i += 2;
9995 (bytes[i - 1] ^ 32) as char
9996 } else {
9997 i += 1;
9998 bytes[i - 1] as char
9999 };
10000 match c {
10001 // c:6664-6673 — `"` / `$` / `` ` `` — close quote
10002 // (if open), then `\<c>`.
10003 '"' | '$' | '`' => {
10004 if inquote {
10005 out.push('"');
10006 inquote = false;
10007 }
10008 out.push('\\');
10009 out.push(c);
10010 }
10011 // c:6674-6682 — default arm: open quote if needed,
10012 // backslash-escape newline, emit char.
10013 _ => {
10014 if !inquote {
10015 out.push('"');
10016 inquote = true;
10017 }
10018 if c == '\n' {
10019 out.push('\\');
10020 }
10021 out.push(c);
10022 }
10023 }
10024 }
10025 // c:6685-6686 — close trailing quote.
10026 if inquote {
10027 out.push('"');
10028 }
10029 } else {
10030 // c:6687-6718 — default (non-CSH) arm.
10031 out.push('"');
10032 let mut pending = false;
10033 let mut i = 0;
10034 while i < bytes.len() {
10035 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
10036 i += 2;
10037 (bytes[i - 1] ^ 32) as char
10038 } else {
10039 i += 1;
10040 bytes[i - 1] as char
10041 };
10042 match c {
10043 '\\' => {
10044 if pending {
10045 out.push('\\');
10046 }
10047 out.push('\\');
10048 pending = true;
10049 }
10050 '"' | '$' | '`' => {
10051 if pending {
10052 out.push('\\');
10053 }
10054 out.push('\\');
10055 out.push(c);
10056 pending = false;
10057 }
10058 other => {
10059 out.push(other);
10060 pending = false;
10061 }
10062 }
10063 }
10064 if pending {
10065 out.push('\\');
10066 }
10067 out.push('"');
10068 }
10069 // c:6720 — `ret = metafy(buf, p - buf, META_DUP);` re-metafy result.
10070 metafy(&out)
10071}
10072
10073/// Port of `dquotedzputs()` from `Src/utils.c:6729`.
10074///
10075/// Port of `dquotedzputs(char const *s, FILE *stream)` from
10076/// Src/utils.c:6729. C body (4 lines):
10077/// `char *d = dquotedztrdup(s);
10078/// int ret = zputs(d, stream);
10079/// zsfree(d);
10080/// return ret;`
10081/// Rust returns the quoted string directly (callers compose via
10082/// `format!` rather than streaming through a FILE*), so the zputs
10083/// call drops.
10084pub fn dquotedzputs(s: &str) -> String {
10085 // c:6729
10086 dquotedztrdup(s) // c:6731
10087}
10088
10089/// Port of `ucs4toutf8()` from `Src/utils.c:6743` — C decl `ucs4toutf8(char *dest, unsigned int wval)`.
10090///
10091/// Convert UCS-4 to UTF-8 (from utils.c ucs4toutf8)
10092///
10093/// C accepts 0..=0x7FFFFFFF (legacy UCS-4 / 6-byte UTF-8) — wider
10094/// than Unicode's 0..=0x10FFFF — to match `wctomb(3)` on Linux
10095/// with UTF-8 locale. The Rust `char::from_u32` path would reject
10096/// codepoints above 0x10FFFF (and surrogates), so this port mirrors
10097/// the C bit-pattern dispatch directly.
10098///
10099/// C body (c:6743-6779):
10100/// ```c
10101/// if (wval < 0x80) len = 1; // ASCII
10102/// else if (wval < 0x800) len = 2;
10103/// else if (wval < 0x10000) len = 3;
10104/// else if (wval < 0x200000) len = 4;
10105/// else if (wval < 0x4000000) len = 5;
10106/// else if (wval < 0x80000000) len = 6;
10107/// else { zerr("character not in range"); return -1; }
10108///
10109/// switch (len) { /* falls through except to the last case */
10110/// case 6: dest[5] = (wval & 0x3f) | 0x80; wval >>= 6;
10111/// case 5: dest[4] = (wval & 0x3f) | 0x80; wval >>= 6;
10112/// case 4: dest[3] = (wval & 0x3f) | 0x80; wval >>= 6;
10113/// case 3: dest[2] = (wval & 0x3f) | 0x80; wval >>= 6;
10114/// case 2: dest[1] = (wval & 0x3f) | 0x80; wval >>= 6;
10115/// *dest = wval | ((0xfc << (6 - len)) & 0xfc);
10116/// break;
10117/// case 1: *dest = wval;
10118/// }
10119/// return len;
10120/// ```
10121///
10122/// Returns the encoded bytes (1..=6 long) on success; out of range it
10123/// raises C's `zerr("character not in range")` (c:6763) and returns None.
10124/// WARNING: param names don't match C — Rust=(wval) vs C=(dest, wval)
10125pub fn ucs4toutf8(wval: u32) -> Option<Vec<u8>> {
10126 // c:6743
10127 let len: usize = if wval < 0x80 {
10128 1
10129 }
10130 // c:6750
10131 else if wval < 0x800 {
10132 2
10133 }
10134 // c:6752
10135 else if wval < 0x10000 {
10136 3
10137 }
10138 // c:6754
10139 else if wval < 0x200000 {
10140 4
10141 }
10142 // c:6756
10143 else if wval < 0x4000000 {
10144 5
10145 }
10146 // c:6758
10147 else if wval < 0x80000000 {
10148 6
10149 }
10150 // c:6760
10151 else {
10152 // c:6762
10153 zerr("character not in range"); // c:6763
10154 return None; // c:6764
10155 };
10156 let mut buf = [0u8; 6];
10157 let mut w = wval;
10158 // c:6767-6776 — fall-through switch building trailing bytes first.
10159 match len {
10160 1 => {
10161 buf[0] = w as u8;
10162 } // c:6775
10163 n => {
10164 // Trailing (len-1) bytes: each is `(w & 0x3f) | 0x80`,
10165 // shifting w right 6 between them (c:6768-6772).
10166 for i in (1..n).rev() {
10167 buf[i] = ((w & 0x3f) as u8) | 0x80;
10168 w >>= 6;
10169 }
10170 // Leading byte: `w | ((0xfc << (6 - len)) & 0xfc)` (c:6773).
10171 buf[0] = (w as u8) | (((0xfcu32 << (6 - n)) & 0xfc) as u8);
10172 }
10173 }
10174 // c:6777 — the bytes go to `dest` as built. A surrogate's 3-byte
10175 // form is not UTF-8 a `String` can hold, so they stay bytes here.
10176 Some(buf[..len].to_vec())
10177}
10178
10179/// Port of `ucs4tomb()` from `Src/utils.c:6788` — C decl `ucs4tomb(unsigned int wval, char *buf)`.
10180///
10181/// Encode a UCS-4 codepoint into the buffer `buf` using the current
10182/// locale's multibyte encoding. Returns the number of bytes written,
10183/// or -1 on conversion failure (after `zerr("character not in range")`).
10184///
10185/// C picks one of two bodies at compile time on `__STDC_ISO_10646__`
10186/// (c:6790): `wctomb(3)` where it is defined, else `nl_langinfo(CODESET)`
10187/// → `ucs4toutf8` for "UTF-8" and `iconv(3)` from "UCS-4BE" for any other
10188/// codeset. glibc defines the macro; macOS libc does NOT (checked with
10189/// `cc` on Darwin 25), so the zsh macOS builds run the iconv body, which
10190/// rejects every value above 0x7f in the C locale ("US-ASCII"). macOS
10191/// `wctomb` instead accepts 0x80-0xff as raw bytes there, so taking the
10192/// wctomb body everywhere let `LC_ALL=C; : ${(#X):-0x80}` succeed.
10193pub fn ucs4tomb(wval: u32, buf: &mut [u8]) -> i32 {
10194 // c:6790-6795 — `#if … && defined(__STDC_ISO_10646__)`.
10195 #[cfg(any(target_os = "linux", target_os = "android"))]
10196 {
10197 // wchar_t is i32 on the Linux targets; declare the POSIX prototype
10198 // directly (libc crate does not expose wctomb on all platforms).
10199 extern "C" {
10200 fn wctomb(s: *mut libc::c_char, wc: libc::wchar_t) -> libc::c_int;
10201 }
10202 let mut local = [0 as libc::c_char; 16];
10203 let count = unsafe { wctomb(local.as_mut_ptr(), wval as libc::wchar_t) }; // c:6791
10204 if count < 0 || count as usize > buf.len() {
10205 zerr("character not in range"); // c:6793
10206 return -1;
10207 }
10208 for i in 0..count as usize {
10209 buf[i] = local[i] as u8;
10210 }
10211 count // c:6794
10212 }
10213 // c:6795-6863 — the `!__STDC_ISO_10646__` body.
10214 #[cfg(not(any(target_os = "linux", target_os = "android")))]
10215 {
10216 #[link(name = "iconv")]
10217 extern "C" {
10218 fn iconv_open(tocode: *const libc::c_char, fromcode: *const libc::c_char) -> *mut libc::c_void;
10219 fn iconv(
10220 cd: *mut libc::c_void,
10221 inbuf: *mut *mut libc::c_char,
10222 inbytesleft: *mut libc::size_t,
10223 outbuf: *mut *mut libc::c_char,
10224 outbytesleft: *mut libc::size_t,
10225 ) -> libc::size_t;
10226 fn iconv_close(cd: *mut libc::c_void) -> libc::c_int;
10227 }
10228 // c:6797 — `nl_langinfo(CODESET)`.
10229 let codeset: String = unsafe {
10230 let p = libc::nl_langinfo(libc::CODESET);
10231 if p.is_null() {
10232 String::new()
10233 } else {
10234 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
10235 }
10236 };
10237 if codeset == "UTF-8" {
10238 // c:6798 — `return ucs4toutf8(buf, wval);`
10239 let Some(enc) = ucs4toutf8(wval) else {
10240 return -1;
10241 };
10242 let b = enc.as_slice();
10243 if b.len() > buf.len() {
10244 zerr("character not in range");
10245 return -1;
10246 }
10247 buf[..b.len()].copy_from_slice(b);
10248 return b.len() as i32;
10249 }
10250 // c:6823-6826 — ICONV_FROM_LIBICONV: an empty codeset is US-ASCII.
10251 let codeset = if codeset.is_empty() { "US-ASCII".to_string() } else { codeset };
10252 let to = std::ffi::CString::new(codeset.clone()).unwrap_or_default();
10253 let from = std::ffi::CString::new("UCS-4BE").unwrap_or_default();
10254 let mut cd = unsafe { iconv_open(to.as_ptr(), from.as_ptr()) }; // c:6827
10255 if cd as isize == -1 && codeset == "646" {
10256 // c:6829-6832 — Solaris' "646" spelling of US-ASCII.
10257 let ascii = std::ffi::CString::new("US-ASCII").unwrap_or_default();
10258 cd = unsafe { iconv_open(ascii.as_ptr(), from.as_ptr()) };
10259 }
10260 if cd as isize == -1 {
10261 zerr("cannot do charset conversion (iconv failed)"); // c:6835
10262 return -1;
10263 }
10264 // c:6839-6843 — store value in big endian form.
10265 let mut inbuf: [libc::c_char; 4] = [0; 4];
10266 let mut w = wval;
10267 for i in (0..4).rev() {
10268 inbuf[i] = (w & 0xff) as u8 as libc::c_char;
10269 w >>= 8;
10270 }
10271 let mut local = [0 as libc::c_char; 6];
10272 let mut inptr = inbuf.as_mut_ptr();
10273 let mut outptr = local.as_mut_ptr();
10274 let mut inbytes: libc::size_t = 4;
10275 let mut outbytes: libc::size_t = 6;
10276 let count = unsafe { iconv(cd, &mut inptr, &mut inbytes, &mut outptr, &mut outbytes) }; // c:6844
10277 unsafe { iconv_close(cd) }; // c:6845
10278 if count != 0 {
10279 // c:6846-6851 — -1 is an error; a positive count is the number of
10280 // non-reversible conversions, also "out-of-range".
10281 zerr("character not in range");
10282 return -1;
10283 }
10284 let n = 6 - outbytes as usize; // c:6853 `buf - bsave`
10285 if n > buf.len() {
10286 zerr("character not in range");
10287 return -1;
10288 }
10289 for i in 0..n {
10290 buf[i] = local[i] as u8;
10291 }
10292 n as i32
10293 }
10294}
10295
10296/// Port of `getkeystring()` from `Src/utils.c:6915` — C decl `getkeystring(char *s, int *len, int how, int *misc)`.
10297///
10298/// Parse getkeystring escape sequences (from utils.c getkeystring)
10299/// Handles \n \t \r \e \a \b \f \v \\ \' \" \xNN \uNNNN \UNNNNNNNN \0NNN
10300/// WARNING: param names don't match C — Rust=(s) vs C=(s, len, how, misc)
10301pub fn getkeystring(s: &str) -> (String, usize) {
10302 // c:6915
10303 let mut result = String::new();
10304 let mut chars = s.chars().peekable();
10305 let mut consumed = 0;
10306
10307 while let Some(c) = chars.next() {
10308 consumed += c.len_utf8();
10309 if c != '\\' {
10310 result.push(c);
10311 continue;
10312 }
10313 match chars.next() {
10314 Some('n') => {
10315 result.push('\n');
10316 consumed += 1;
10317 }
10318 Some('t') => {
10319 result.push('\t');
10320 consumed += 1;
10321 }
10322 Some('r') => {
10323 result.push('\r');
10324 consumed += 1;
10325 }
10326 Some('e') | Some('E') => {
10327 result.push('\x1b');
10328 consumed += 1;
10329 }
10330 Some('a') => {
10331 result.push('\x07');
10332 consumed += 1;
10333 }
10334 Some('b') => {
10335 result.push('\x08');
10336 consumed += 1;
10337 }
10338 Some('f') => {
10339 result.push('\x0c');
10340 consumed += 1;
10341 }
10342 Some('v') => {
10343 result.push('\x0b');
10344 consumed += 1;
10345 }
10346 Some('\\') => {
10347 result.push('\\');
10348 consumed += 1;
10349 }
10350 Some('\'') => {
10351 result.push('\'');
10352 consumed += 1;
10353 }
10354 Some('"') => {
10355 result.push('"');
10356 consumed += 1;
10357 }
10358 Some('x') => {
10359 consumed += 1;
10360 let mut hex = String::new();
10361 for _ in 0..2 {
10362 if let Some(&c) = chars.peek() {
10363 if c.is_ascii_hexdigit() {
10364 hex.push(chars.next().unwrap());
10365 consumed += 1;
10366 } else {
10367 break;
10368 }
10369 }
10370 }
10371 // c:Src/utils.c:7169-7170 — `\x` ALWAYS runs `*t++ =
10372 // zstrtol(s+1, &s, 16)`, even with no hex digit. zstrtol
10373 // over a non-hex byte parses zero digits and returns 0, so
10374 // a NUL byte is emitted (the offending char is processed
10375 // next). `$'\xg'` → NUL + 'g'; `$'\x'` → NUL. Previously
10376 // `from_str_radix("")` errored and nothing was pushed.
10377 let val: u8 = if hex.is_empty() {
10378 0 // c:7170 zstrtol on empty reads as 0
10379 } else {
10380 u8::from_str_radix(&hex, 16).unwrap_or(0)
10381 };
10382 // c:Src/utils.c — `\xNN` is one raw BYTE; metafied
10383 // per c:7289-7294 (vm_helper::meta_encode_byte) so
10384 // the String stays valid UTF-8. Bug #127.
10385 {
10386 // c:Src/utils.c metafy byte-encode step:
10387 // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
10388 let b_ = val;
10389 if b_ < 0x80 {
10390 result.push(b_ as char);
10391 } else {
10392 result.push('\u{83}');
10393 result.push(char::from(b_ ^ 32));
10394 }
10395 }
10396 }
10397 Some('u') => {
10398 consumed += 1;
10399 let mut hex = String::new();
10400 for _ in 0..4 {
10401 if let Some(&c) = chars.peek() {
10402 if c.is_ascii_hexdigit() {
10403 hex.push(chars.next().unwrap());
10404 consumed += 1;
10405 } else {
10406 break;
10407 }
10408 }
10409 }
10410 if let Ok(val) = u32::from_str_radix(&hex, 16) {
10411 if let Some(c) = char::from_u32(val) {
10412 result.push(c);
10413 }
10414 }
10415 }
10416 Some('U') => {
10417 consumed += 1;
10418 let mut hex = String::new();
10419 for _ in 0..8 {
10420 if let Some(&c) = chars.peek() {
10421 if c.is_ascii_hexdigit() {
10422 hex.push(chars.next().unwrap());
10423 consumed += 1;
10424 } else {
10425 break;
10426 }
10427 }
10428 }
10429 if let Ok(val) = u32::from_str_radix(&hex, 16) {
10430 if let Some(c) = char::from_u32(val) {
10431 result.push(c);
10432 }
10433 }
10434 }
10435 Some(c @ '0'..='7') => {
10436 consumed += 1;
10437 let mut oct = String::new();
10438 oct.push(c);
10439 for _ in 0..2 {
10440 if let Some(&c) = chars.peek() {
10441 if ('0'..='7').contains(&c) {
10442 oct.push(chars.next().unwrap());
10443 consumed += 1;
10444 } else {
10445 break;
10446 }
10447 }
10448 }
10449 if let Ok(val) = u8::from_str_radix(&oct, 8) {
10450 // c:Src/utils.c — octal escape is one raw BYTE;
10451 // metafied per c:7289-7294. Bug #127.
10452 {
10453 // c:Src/utils.c metafy byte-encode step:
10454 // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
10455 let b_ = val;
10456 if b_ < 0x80 {
10457 result.push(b_ as char);
10458 } else {
10459 result.push('\u{83}');
10460 result.push(char::from(b_ ^ 32));
10461 }
10462 }
10463 }
10464 }
10465 Some('c') => {
10466 consumed += 1;
10467 // \cX = control character
10468 if let Some(c) = chars.next() {
10469 consumed += 1;
10470 result.push((c as u8 & 0x1f) as char);
10471 }
10472 }
10473 Some(mod_letter @ ('C' | 'M')) => {
10474 // c:Src/utils.c:7029-7052 + c:7265-7275 — `\C` /
10475 // `\M` set control / meta flags; optional `-`
10476 // separator; then read the next char (possibly
10477 // through additional `\C`/`\M` chains) and apply
10478 // the mask: control → `& 0x9f` (or `\C-?` → 0x7f),
10479 // meta → `| 0x80`. Bug #113 in docs/BUGS.md: the
10480 // previous Rust port matched these in the default
10481 // arm and emitted literal `\C` / `\M`. This is the
10482 // second getkeystring (utils.rs); the in-tree
10483 // `getkeystring_dollar_quote` (lex.rs) also needs
10484 // the fix and gets it in the same commit.
10485 consumed += 1;
10486 let mut control = mod_letter == 'C';
10487 let mut meta = mod_letter == 'M';
10488 // Consume optional `-` separator + chained `\C`/`\M`.
10489 loop {
10490 if chars.peek() == Some(&'-') {
10491 chars.next();
10492 consumed += 1;
10493 continue;
10494 }
10495 // chained `\C` / `\M`?
10496 let mut iter_clone = chars.clone();
10497 if iter_clone.next() == Some('\\') {
10498 if let Some(nx) = iter_clone.next() {
10499 if nx == 'C' || nx == 'M' {
10500 chars.next(); // consume '\'
10501 chars.next(); // consume C/M
10502 consumed += 2;
10503 if nx == 'C' {
10504 control = true;
10505 } else {
10506 meta = true;
10507 }
10508 continue;
10509 }
10510 }
10511 }
10512 break;
10513 }
10514 // Read one base character (allowing nested simple
10515 // escapes for common cases).
10516 let base: Option<char> = if chars.peek() == Some(&'\\') {
10517 chars.next();
10518 consumed += 1;
10519 match chars.next() {
10520 Some('n') => {
10521 consumed += 1;
10522 Some('\n')
10523 }
10524 Some('t') => {
10525 consumed += 1;
10526 Some('\t')
10527 }
10528 Some('r') => {
10529 consumed += 1;
10530 Some('\r')
10531 }
10532 Some('a') => {
10533 consumed += 1;
10534 Some('\x07')
10535 }
10536 Some('b') => {
10537 consumed += 1;
10538 Some('\x08')
10539 }
10540 Some('e') | Some('E') => {
10541 consumed += 1;
10542 Some('\x1b')
10543 }
10544 Some('f') => {
10545 consumed += 1;
10546 Some('\x0c')
10547 }
10548 Some('v') => {
10549 consumed += 1;
10550 Some('\x0b')
10551 }
10552 Some('\\') => {
10553 consumed += 1;
10554 Some('\\')
10555 }
10556 Some('\'') => {
10557 consumed += 1;
10558 Some('\'')
10559 }
10560 Some('"') => {
10561 consumed += 1;
10562 Some('"')
10563 }
10564 Some(other) => {
10565 consumed += 1;
10566 Some(other)
10567 }
10568 None => None,
10569 }
10570 } else {
10571 chars.next().inspect(|c| {
10572 consumed += c.len_utf8();
10573 })
10574 };
10575 if let Some(ch) = base {
10576 let mut byte = ch as u32;
10577 if control {
10578 if byte == '?' as u32 {
10579 byte = 0x7f;
10580 } else {
10581 byte &= 0x9f;
10582 }
10583 }
10584 if meta {
10585 byte |= 0x80;
10586 }
10587 // c:Src/utils.c:7265-7275 — masked result is one
10588 // raw BYTE (`$'\M-i'` = 0xe9), metafied per
10589 // c:7289-7294. Multibyte base chars (> 0xff)
10590 // keep the codepoint form. Bug #127.
10591 if byte <= 0xff {
10592 {
10593 // c:Src/utils.c metafy byte-encode step:
10594 // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
10595 let b_ = byte as u8;
10596 if b_ < 0x80 {
10597 result.push(b_ as char);
10598 } else {
10599 result.push('\u{83}');
10600 result.push(char::from(b_ ^ 32));
10601 }
10602 }
10603 } else if let Some(c) = char::from_u32(byte) {
10604 result.push(c);
10605 }
10606 }
10607 }
10608 Some(c)
10609 if isset(BANGHIST)
10610 && crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst)
10611 != 0
10612 && c as u32
10613 == crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst)
10614 as u32 =>
10615 {
10616 // The reverse of quotestring's BANGHIST escape (c:Src/utils.c:6230
10617 // — `\!` is emitted for the history char): unquoting `$'a\!b'`
10618 // must strip the backslash so `${(Q)${(qqqq)v}}` round-trips.
10619 // In zsh this happens because `(Q)` re-lexes the string and
10620 // history expansion consumes the `\<bangchar>`; here the decoder
10621 // consumes it directly.
10622 consumed += 1;
10623 result.push(c);
10624 }
10625 Some(c) => {
10626 consumed += 1;
10627 result.push('\\');
10628 result.push(c);
10629 }
10630 None => {
10631 result.push('\\');
10632 }
10633 }
10634 }
10635 (result, consumed)
10636}
10637
10638/// Check if s is a prefix of t (from utils.c strpfx)
10639// Return non-zero if s is a prefix of t. // c:7345
10640/// Port of `strpfx()` from `Src/utils.c:7334`.
10641///
10642/// `strpfx` — see implementation.
10643pub fn strpfx(s: &str, t: &str) -> bool {
10644 t.starts_with(s)
10645}
10646
10647/// Check if s is a suffix of t (from utils.c strsfx)
10648// Return non-zero if s is a suffix of t. // c:7345
10649/// Port of `strsfx()` from `Src/utils.c:7345`.
10650///
10651/// `strsfx` — see implementation.
10652pub fn strsfx(s: &str, t: &str) -> bool {
10653 t.ends_with(s)
10654}
10655
10656/// Port of `upchdir()` from `Src/utils.c:7356` — C decl `upchdir(int n)`.
10657///
10658/// Go up n directories (from utils.c upchdir)
10659pub fn upchdir(n: usize) -> io::Result<()> {
10660 let mut path = String::new();
10661 for i in 0..n {
10662 if i > 0 {
10663 path.push('/');
10664 }
10665 path.push_str("..");
10666 }
10667 std::env::set_current_dir(&path)?;
10668 Ok(())
10669}
10670
10671/// Port of `struct dirsav` from `Src/zsh.h:1159`.
10672///
10673/// ```c
10674/// struct dirsav {
10675/// int dirfd, level;
10676/// char *dirname;
10677/// dev_t dev;
10678/// ino_t ino;
10679/// };
10680/// ```
10681///
10682/// The previous Rust port omitted `dev` and `ino` which the
10683/// `restoredir` integrity check (utils.c:7592) reads. Adding them
10684/// so callers can verify the saved-and-restored cwd matches the
10685/// captured device + inode.
10686// `struct dirsav` lives in `dirsav` per Rule C
10687// (its C definition is `Src/zsh.h:1159`, not utils.c). The previous
10688// Rust port had a `pub struct DirSav` PascalCase duplicate of the
10689// canonical lowercase struct; deleted in favour of routing through
10690// `zsh_h::dirsav` directly.
10691
10692/// Port of `init_dirsav()` from `Src/utils.c:7381` — C decl `init_dirsav(Dirsav d)`. Initialize a
10693/// `dirsav` struct to its empty/default state. C body memset's the
10694/// fields to 0 (dirfd to -1).
10695///
10696/// C signature: `void init_dirsav(Dirsav d)` where
10697/// `Dirsav = struct dirsav *`. Rust port returns the initialised
10698/// struct since callers always pair-with a fresh allocation.
10699/// WARNING: param names don't match C — Rust=() vs C=(path)
10700/// C body (3 lines):
10701/// `d->ino = d->dev = 0; d->dirname = NULL; d->dirfd = d->level = -1;`
10702///
10703/// `dirname` stays `None` exactly as C leaves it: the field is filled
10704/// later, by `lchdir`/`zgetdir`, only on the paths that need a name to
10705/// get back. An earlier Rust port prefilled it with `current_dir()`,
10706/// which put a `getcwd` (plus an `open` on macOS) in front of every
10707/// `scanner()` call in `glob.rs:444` — a syscall pair C never issues.
10708/// The two consumers both cope with `None`: `restoredir` (c:7565) falls
10709/// through to `fchdir(dirfd)` or `upchdir(level)`, and `lchdir` (c:7442-7448)
10710/// fills `d->level` for the soft path before any restore can happen.
10711pub fn init_dirsav() -> dirsav {
10712 // c:7381
10713 dirsav {
10714 ino: 0,
10715 dev: 0, // c:7383 `d->ino = d->dev = 0;`
10716 dirname: None, // c:7384 `d->dirname = NULL;`
10717 dirfd: -1,
10718 level: -1, // c:7385 `d->dirfd = d->level = -1;`
10719 }
10720}
10721
10722/// Port of `lchdir()` from `Src/utils.c:7400` — C decl `lchdir(char const *path, struct dirsav *d, int hard)`.
10723///
10724/// c:7388 — "Change directory, without following symlinks." With `hard`
10725/// set, descends `path` one component at a time: `lstat`s each component
10726/// (so a symlink is never followed), `chdir`s into it, then re-`lstat`s
10727/// `.` and compares dev/ino against the component it just stat'd. A
10728/// symlink swapped under us between the `lstat` and the `chdir` is caught
10729/// by the mismatch and the saved directory is restored via `restoredir`.
10730/// `d` (when non-NULL) is filled so the caller can later restore the cwd.
10731///
10732/// Returns 0 on success, -1 on a normal descent failure, -2 if the cwd
10733/// could not even be restored afterwards.
10734///
10735/// zshrs targets macOS/Linux, so the live C arms are HAVE_LSTAT +
10736/// HAVE_FCHDIR; the no-lstat / no-fchdir fallbacks (which never compile
10737/// on our platforms) are not ported. C restores `errno = err` before each
10738/// non-zero return so the caller can read the break-reason; the same
10739/// restore is done here with the per-platform errno setter.
10740/// WARNING: param names match C — (path, d, hard).
10741#[cfg(unix)]
10742pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
10743 // c:7400
10744 use std::ffi::CString;
10745
10746 let mut ds = init_dirsav(); // c:7405 — local fallback `struct dirsav ds`
10747 let used_local = d.is_none(); // tracks C's `d == &ds`
10748 // c:7415-7418 — `if (!d) { init_dirsav(&ds); d = &ds; }`
10749 let d: &mut dirsav = d.unwrap_or(&mut ds);
10750
10751 let bytes = path.as_bytes();
10752 let mut level: i32;
10753
10754 // c:7419-7424 (HAVE_LSTAT arm) —
10755 // `if ((*path == '/' || !hard) && (d != &ds || hard))`
10756 if (bytes.first() == Some(&b'/') || hard == 0) && (!used_local || hard != 0) {
10757 level = -1; // c:7425
10758 } else {
10759 level = 0; // c:7431
10760 // c:7432-7435 — record dev/ino of `.` if not already captured.
10761 if d.dev == 0 && d.ino == 0 {
10762 if let Ok(meta) = fs::metadata(".") {
10763 d.dev = meta.dev(); // c:7433
10764 d.ino = meta.ino(); // c:7434
10765 }
10766 }
10767 }
10768
10769 // c:7439-7451 — soft (`!hard`) path: count components, set d->level,
10770 // and delegate to zchdir.
10771 if hard == 0 {
10772 if !used_local {
10773 // c:7443-7447 — count '/'-separated components into `level`.
10774 let mut p = 0usize;
10775 while p < bytes.len() {
10776 while p < bytes.len() && bytes[p] != b'/' {
10777 p += 1;
10778 }
10779 while p < bytes.len() && bytes[p] == b'/' {
10780 p += 1;
10781 }
10782 level += 1;
10783 }
10784 d.level = level; // c:7448
10785 }
10786 return zchdir(path); // c:7450
10787 }
10788
10789 // c:7453+ — hard path (HAVE_LSTAT + HAVE_FCHDIR).
10790 let mut close_dir = false; // c:7412
10791 // c:7455-7460 — save the starting dir via an fd if we don't have one.
10792 if d.dirfd < 0 {
10793 close_dir = true; // c:7456
10794 let dot = CString::new(".").unwrap();
10795 d.dirfd = unsafe { libc::open(dot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:7457
10796 if d.dirfd < 0
10797 && zgetdir(Some(&mut *d)).is_some()
10798 && d.dirname
10799 .as_deref()
10800 .map(|s| !s.starts_with('/'))
10801 .unwrap_or(false)
10802 {
10803 // c:7458-7459 — cwd is relative; fall back to opening "..".
10804 let dotdot = CString::new("..").unwrap();
10805 d.dirfd = unsafe { libc::open(dotdot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
10806 }
10807 }
10808
10809 // c:7462-7464 — absolute path: start the descent from root.
10810 if bytes.first() == Some(&b'/') {
10811 let root = CString::new("/").unwrap();
10812 if unsafe { libc::chdir(root.as_ptr()) } < 0 {
10813 zwarn(&format!(
10814 "failed to chdir(/): {}",
10815 io::Error::last_os_error()
10816 )); // c:7464
10817 }
10818 }
10819
10820 let dot = CString::new(".").unwrap();
10821 let mut pos = 0usize; // index into `path`, replaces C `path`/`pptr`
10822 let mut err: i32 = 0; // c:7408
10823 loop {
10824 // c:7466-7467 — skip leading slashes.
10825 while pos < bytes.len() && bytes[pos] == b'/' {
10826 pos += 1;
10827 }
10828 // c:7468-7480 — end of path reached: success.
10829 if pos >= bytes.len() {
10830 if !used_local {
10831 d.level = level; // c:7472
10832 }
10833 // c:7474-7477 — close the saved-dir fd if we opened it.
10834 if d.dirfd >= 0 && close_dir {
10835 unsafe { libc::close(d.dirfd) };
10836 d.dirfd = -1;
10837 }
10838 return 0; // c:7479
10839 }
10840 // c:7481 — find the end of this path component.
10841 let start = pos;
10842 let mut end = start + 1;
10843 while end < bytes.len() && bytes[end] != b'/' {
10844 end += 1;
10845 }
10846 // c:7482-7485 — component too long.
10847 if end - start > libc::PATH_MAX as usize {
10848 err = libc::ENAMETOOLONG;
10849 break;
10850 }
10851 // c:7486-7488 — copy the component into `buf`.
10852 let comp = &bytes[start..end];
10853 pos = end;
10854 let cbuf = match CString::new(comp) {
10855 Ok(c) => c,
10856 Err(_) => {
10857 err = libc::ENOENT; // embedded NUL — lstat would fail anyway.
10858 break;
10859 }
10860 };
10861 // c:7489-7492 — lstat the component (does NOT follow a symlink).
10862 let mut st1: libc::stat = unsafe { std::mem::zeroed() };
10863 if unsafe { libc::lstat(cbuf.as_ptr(), &mut st1) } != 0 {
10864 err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
10865 break;
10866 }
10867 // c:7493-7496 — must be a real directory, not a symlink/other.
10868 if st1.st_mode as u32 & libc::S_IFMT as u32 != libc::S_IFDIR as u32 {
10869 err = libc::ENOTDIR;
10870 break;
10871 }
10872 // c:7497-7500 — chdir into it.
10873 if unsafe { libc::chdir(cbuf.as_ptr()) } != 0 {
10874 err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
10875 break;
10876 }
10877 if level >= 0 {
10878 level += 1; // c:7501-7502
10879 }
10880 // c:7503-7510 — re-lstat `.` and verify the dir we landed in is
10881 // the same dev/ino we lstat'd, catching a symlink swap race.
10882 let mut st2: libc::stat = unsafe { std::mem::zeroed() };
10883 if unsafe { libc::lstat(dot.as_ptr(), &mut st2) } != 0 {
10884 err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
10885 break;
10886 }
10887 if st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino {
10888 err = libc::ENOTDIR; // c:7508
10889 break;
10890 }
10891 }
10892
10893 // c:7512-7548 — descent failed; restore the saved directory.
10894 if restoredir(&mut *d) != 0 {
10895 let restoreerr = io::Error::last_os_error(); // c:7513
10896 // c:7519-7532 — restore failed too; force cwd to $HOME then "/".
10897 let mut reached = false;
10898 for i in 0..2 {
10899 // c:7521-7527 — destination: $HOME on pass 0, "/" on pass 1.
10900 let cdest = if i != 0 {
10901 "/".to_string()
10902 } else {
10903 match getsparam("HOME") {
10904 Some(h) => h, // c:7526
10905 None => continue, // c:7525 `if (!home) continue;`
10906 }
10907 };
10908 setsparam("PWD", &cdest); // c:7528-7529 `zsfree(pwd); pwd = ztrdup(cdest);`
10909 if let Ok(cd) = CString::new(cdest.as_bytes()) {
10910 if unsafe { libc::chdir(cd.as_ptr()) } == 0 {
10911 // c:7530
10912 reached = true;
10913 break;
10914 }
10915 }
10916 }
10917 if !reached {
10918 // c:7533-7534 — couldn't even reach "/".
10919 zerr(&format!(
10920 "lost current directory, failed to cd to /: {}",
10921 io::Error::last_os_error()
10922 ));
10923 } else {
10924 // c:7535-7537
10925 let pwd = getsparam("PWD").unwrap_or_default();
10926 zerr(&format!(
10927 "lost current directory: {}: changed to `{}'",
10928 restoreerr, pwd
10929 ));
10930 }
10931 // c:7540-7545 — close the saved-dir fd if we opened it.
10932 if d.dirfd >= 0 && close_dir {
10933 unsafe { libc::close(d.dirfd) };
10934 d.dirfd = -1;
10935 }
10936 #[cfg(target_os = "macos")]
10937 unsafe {
10938 *libc::__error() = err; // c:7546
10939 }
10940 #[cfg(target_os = "linux")]
10941 unsafe {
10942 *libc::__errno_location() = err; // c:7546
10943 }
10944 return -2; // c:7547
10945 }
10946 // c:7549-7558 — descent failed but the saved directory was restored.
10947 if d.dirfd >= 0 && close_dir {
10948 unsafe { libc::close(d.dirfd) };
10949 d.dirfd = -1;
10950 }
10951 #[cfg(target_os = "macos")]
10952 unsafe {
10953 *libc::__error() = err; // c:7557
10954 }
10955 #[cfg(target_os = "linux")]
10956 unsafe {
10957 *libc::__errno_location() = err;
10958 }
10959 -1 // c:7558
10960}
10961
10962/// Port of `lchdir()` from `Src/utils.c:7400`.
10963///
10964/// Non-unix build: lchdir's symlink-safe descent is built on POSIX
10965/// `lstat`/`fchdir`/`chdir`, which have no Windows equivalent here.
10966#[cfg(not(unix))]
10967pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
10968 let _ = (path, d, hard);
10969 -1
10970}
10971
10972/// Port of `restoredir()` from `Src/utils.c:7565` — C decl `restoredir(struct dirsav *d)`.
10973///
10974/// ```c
10975/// int restoredir(struct dirsav *d) {
10976/// if (d->dirname && *d->dirname == '/')
10977/// return chdir(d->dirname);
10978/// if (d->dirfd >= 0) {
10979/// if (!fchdir(d->dirfd)) {
10980/// if (!d->dirname) return 0;
10981/// else if (chdir(d->dirname)) {
10982/// close(d->dirfd); d->dirfd = -1; err = -2;
10983/// }
10984/// } else {
10985/// close(d->dirfd); d->dirfd = err = -1;
10986/// }
10987/// } else if (d->level > 0)
10988/// err = upchdir(d->level);
10989/// else if (d->level < 0) err = -1;
10990/// // dev/ino integrity check ...
10991/// }
10992/// ```
10993///
10994/// Restore the cwd captured in `d`. Absolute `dirname` short-
10995/// circuits to `chdir`. Otherwise tries `fchdir(dirfd)` (when
10996/// supported) then falls through to `upchdir(level)` for the
10997/// nested-fn-exit case. Returns 0 on success, non-zero on failure
10998/// (matching C's int return).
10999///
11000/// Signature change: previous Rust port took `saved: &str` and
11001/// returned `bool` — different shape from C, missed the dirfd /
11002/// level / dev / ino fields entirely.
11003pub fn restoredir(d: &mut dirsav) -> i32 {
11004 // C: if (d->dirname && *d->dirname == '/') return chdir(d->dirname);
11005 if let Some(name) = d.dirname.as_ref() {
11006 if name.starts_with('/') {
11007 return match std::env::set_current_dir(name) {
11008 Ok(_) => 0,
11009 Err(_) => -1,
11010 };
11011 }
11012 }
11013 let mut err: i32 = 0;
11014 // C: HAVE_FCHDIR path — try fchdir(dirfd) first.
11015 #[cfg(unix)]
11016 if d.dirfd >= 0 {
11017 let rc = unsafe { libc::fchdir(d.dirfd) };
11018 if rc == 0 {
11019 if d.dirname.is_none() {
11020 return 0;
11021 }
11022 let name = d.dirname.as_ref().unwrap();
11023 if std::env::set_current_dir(name).is_err() {
11024 unsafe { libc::close(d.dirfd) };
11025 d.dirfd = -1;
11026 err = -2;
11027 }
11028 } else {
11029 unsafe { libc::close(d.dirfd) };
11030 d.dirfd = -1;
11031 err = -1;
11032 }
11033 } else if d.level > 0 {
11034 // C: err = upchdir(d->level);
11035 // The result is the caller's break-reason (glob.c:696 reports
11036 // "current directory lost during glob" on it), so it must not be
11037 // dropped — this is the branch every `init_dirsav`-then-`lchdir`
11038 // soft restore lands in now that `dirname` starts out NULL.
11039 if upchdir(d.level as usize).is_err() {
11040 err = -1;
11041 }
11042 } else if d.level < 0 {
11043 err = -1;
11044 }
11045 // C: dev/ino integrity check after the chdir/fchdir — unconditional
11046 // in C (c:7591-7595), it can only escalate an existing failure to -2.
11047 if d.dev != 0 || d.ino != 0 {
11048 if let Ok(meta) = fs::metadata(".") {
11049 if meta.ino() != d.ino || meta.dev() != d.dev {
11050 err = -2; // c:7594
11051 }
11052 } else {
11053 err = -2; // c:7592 `stat(".", &sbuf) < 0`
11054 }
11055 }
11056 err
11057}
11058
11059/// Port of `privasserted()` from `Src/utils.c:7608`.
11060///
11061/// "Check whether the shell is running with privileges in effect.
11062/// This is the case if EITHER the euid is zero, OR (if the system
11063/// supports POSIX.1e (POSIX.6) capability sets) the process'
11064/// Effective or Inheritable capability sets are non-empty."
11065///
11066/// ```c
11067/// if (!geteuid()) return 1;
11068/// #ifdef HAVE_CAP_GET_PROC
11069/// cap_t caps = cap_get_proc();
11070/// if (caps) {
11071/// cap_flag_value_t val;
11072/// for (cap_value_t cap = 0;
11073/// !cap_get_flag(caps, cap, CAP_EFFECTIVE, &val); cap++)
11074/// if (val && cap != CAP_WAKE_ALARM) {
11075/// cap_free(caps);
11076/// return 1;
11077/// }
11078/// }
11079/// cap_free(caps);
11080/// #endif
11081/// return 0;
11082/// ```
11083///
11084/// The previous Rust port checked `getuid() != geteuid()` which is
11085/// the SUID-binary detection, not the "running with privileges"
11086/// check. The capability-set inspection requires libcap (gated
11087/// behind the `libcap` feature in `crate::ported::modules::cap`);
11088/// without it, only the euid==0 path is exercised — same as the
11089/// C `#else` arm when HAVE_CAP_GET_PROC isn't defined.
11090pub fn privasserted() -> bool {
11091 #[cfg(unix)]
11092 {
11093 if unsafe { libc::geteuid() } == 0 {
11094 return true;
11095 }
11096 }
11097 // POSIX.1e capabilities check (HAVE_CAP_GET_PROC) — only
11098 // active on Linux when zshrs is built with `--features
11099 // libcap`. The cap module's `cap_get_proc` returns Ok(text)
11100 // when the process has any capability set; we treat any
11101 // non-default-empty result as "privileges asserted".
11102 #[cfg(all(target_os = "linux", feature = "libcap"))]
11103 {
11104 // Pending: walk the capability set with cap_get_flag and
11105 // skip CAP_WAKE_ALARM as the C source does. The cap.rs
11106 // port doesn't yet expose the flag-iteration FFI; until
11107 // it does, conservative-true on any non-empty cap text.
11108 if let Ok(text) = crate::ported::modules::cap::cap_get_proc() {
11109 // Empty / default-empty cap text = no privileges.
11110 if !text.is_empty() && text != "=" {
11111 return true;
11112 }
11113 }
11114 }
11115 false
11116}
11117
11118/// Port of `mode_to_octal()` from `Src/utils.c:7634` — C decl `mode_to_octal(mode_t mode)`.
11119///
11120/// Convert a `mode_t` into the equivalent canonical octal value
11121/// by testing each `S_I*` flag explicitly and OR-ing the matching
11122/// octal bit. This is NOT just `mode & 07777` — on systems where
11123/// the libc `S_IRUSR`/etc. constants don't match the canonical
11124/// values (e.g. when zsh runs against a non-POSIX libc),
11125/// `mode & 07777` returns the libc representation. The C version
11126/// translates to canonical bits explicitly so callers get a stable
11127/// portable result.
11128///
11129/// ```c
11130/// int mode_to_octal(mode_t mode)
11131/// {
11132/// int m = 0;
11133/// if (mode & S_ISUID) m |= 04000;
11134/// ... (12 bit-by-bit mappings)
11135/// return m;
11136/// }
11137/// ```
11138pub fn mode_to_octal(mode: u32) -> i32 {
11139 // c:7634
11140 #[cfg(not(unix))]
11141 {
11142 // No POSIX permission bits on non-Unix; fall back to canonical
11143 // octal-bit layout via the same mask values C uses on POSIX.
11144 let mut o: i32 = 0;
11145 if mode & 0o4000 != 0 {
11146 o |= 0o4000;
11147 } // c:7638-7639
11148 if mode & 0o2000 != 0 {
11149 o |= 0o2000;
11150 } // c:7640-7641
11151 if mode & 0o1000 != 0 {
11152 o |= 0o1000;
11153 } // c:7642-7643
11154 if mode & 0o0400 != 0 {
11155 o |= 0o0400;
11156 } // c:7644-7645
11157 if mode & 0o0200 != 0 {
11158 o |= 0o0200;
11159 } // c:7646-7647
11160 if mode & 0o0100 != 0 {
11161 o |= 0o0100;
11162 } // c:7648-7649
11163 if mode & 0o0040 != 0 {
11164 o |= 0o0040;
11165 } // c:7650-7651
11166 if mode & 0o0020 != 0 {
11167 o |= 0o0020;
11168 } // c:7652-7653
11169 if mode & 0o0010 != 0 {
11170 o |= 0o0010;
11171 } // c:7654-7655
11172 if mode & 0o0004 != 0 {
11173 o |= 0o0004;
11174 } // c:7656-7657
11175 if mode & 0o0002 != 0 {
11176 o |= 0o0002;
11177 } // c:7658-7659
11178 if mode & 0o0001 != 0 {
11179 o |= 0o0001;
11180 } // c:7660-7661
11181 return o; // c:7662
11182 }
11183 #[cfg(unix)]
11184 {
11185 // c:7636 — int m = 0;
11186 let mut m: i32 = 0;
11187 // c:7638-7661 — 12 bit-by-bit mappings from libc S_I* → canonical octal.
11188 if mode & S_ISUID as u32 != 0 {
11189 m |= 0o4000;
11190 } // c:7638-7639
11191 if mode & S_ISGID as u32 != 0 {
11192 m |= 0o2000;
11193 } // c:7640-7641
11194 if mode & S_ISVTX as u32 != 0 {
11195 m |= 0o1000;
11196 } // c:7642-7643
11197 if mode & S_IRUSR as u32 != 0 {
11198 m |= 0o0400;
11199 } // c:7644-7645
11200 if mode & S_IWUSR as u32 != 0 {
11201 m |= 0o0200;
11202 } // c:7646-7647
11203 if mode & S_IXUSR as u32 != 0 {
11204 m |= 0o0100;
11205 } // c:7648-7649
11206 if mode & S_IRGRP as u32 != 0 {
11207 m |= 0o0040;
11208 } // c:7650-7651
11209 if mode & S_IWGRP as u32 != 0 {
11210 m |= 0o0020;
11211 } // c:7652-7653
11212 if mode & S_IXGRP as u32 != 0 {
11213 m |= 0o0010;
11214 } // c:7654-7655
11215 if mode & S_IROTH as u32 != 0 {
11216 m |= 0o0004;
11217 } // c:7656-7657
11218 if mode & S_IWOTH as u32 != 0 {
11219 m |= 0o0002;
11220 } // c:7658-7659
11221 if mode & S_IXOTH as u32 != 0 {
11222 m |= 0o0001;
11223 } // c:7660-7661
11224 m // c:7662
11225 }
11226}
11227
11228/// Port of `mailstat()` from `Src/utils.c:7685` — C decl `mailstat(char *path, struct stat *st)`.
11229///
11230/// C signature: `int mailstat(char *path, struct stat *st)`.
11231/// Writes maildir aggregate stats into `*st` (or the native stat for
11232/// non-directory paths) and returns the underlying `stat(2)` return
11233/// (0 on success, -1 on error — matches C's `i = stat(path, st);
11234/// return i;`).
11235///
11236/// When `path` is a maildir directory (containing `cur/`, `tmp/`,
11237/// `new/` subdirs), walks `new/` + `cur/` and aggregates into `*st`:
11238/// nlink=1, S_IFDIR→S_IFREG, size=Σ message bytes,
11239/// blocks=Σ messages, atime=newest, mtime=newest. When the path is
11240/// a plain file, leaves the native `stat(2)` result in `*st`.
11241pub fn mailstat(path: &str, st: &mut libc::stat) -> i32 {
11242 // c:7685
11243 let c_path = match CString::new(path) {
11244 Ok(c) => c,
11245 Err(_) => return -1,
11246 };
11247 // C: if ((i = stat(path, st)) != 0 || !S_ISDIR(st->st_mode)) return i;
11248 let i = unsafe { libc::stat(c_path.as_ptr(), st as *mut _) }; // c:7693
11249 if i != 0 || (st.st_mode & libc::S_IFMT) != libc::S_IFDIR {
11250 // c:7693
11251 return i; // c:7693
11252 }
11253 // C 7700-7706: nlink=1, S_IFDIR → S_IFREG, zero size/blocks.
11254 st.st_nlink = 1; // c:7701
11255 st.st_mode &= !libc::S_IFDIR; // c:7702
11256 st.st_mode |= libc::S_IFREG; // c:7703
11257 st.st_size = 0; // c:7704
11258 st.st_blocks = 0; // c:7705
11259 // C 7707-7712: stat(path/cur). If absent or not a dir, return 0
11260 // with the partial out (just the IFREG-coerced root).
11261 let cur_path = match CString::new(format!("{}/cur", path)) {
11262 Ok(c) => c,
11263 Err(_) => return 0,
11264 };
11265 let mut sub: libc::stat = unsafe { std::mem::zeroed() };
11266 if unsafe { libc::stat(cur_path.as_ptr(), &mut sub) } != 0 // c:7708
11267 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
11268 // c:7708
11269 {
11270 return 0; // c:7710
11271 }
11272 st.st_atime = sub.st_atime; // c:7712
11273 // C 7715-7722: stat(path/tmp).
11274 let tmp_path = match CString::new(format!("{}/tmp", path)) {
11275 Ok(c) => c,
11276 Err(_) => return 0,
11277 };
11278 if unsafe { libc::stat(tmp_path.as_ptr(), &mut sub) } != 0 // c:7716
11279 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
11280 // c:7716
11281 {
11282 return 0; // c:7718
11283 }
11284 st.st_mtime = sub.st_mtime; // c:7720
11285 // C 7724-7730: stat(path/new). C overwrites mtime with new/'s mtime.
11286 let new_path = match CString::new(format!("{}/new", path)) {
11287 Ok(c) => c,
11288 Err(_) => return 0,
11289 };
11290 if unsafe { libc::stat(new_path.as_ptr(), &mut sub) } != 0 // c:7724
11291 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
11292 // c:7724
11293 {
11294 return 0; // c:7726
11295 }
11296 st.st_mtime = sub.st_mtime; // c:7728
11297 // C 7749-7778: walk new/ and cur/, sum size + blocks, track newest
11298 // atime / mtime.
11299 let mut atime: libc::time_t = 0; // c:7748
11300 let mut mtime: libc::time_t = 0; // c:7748
11301 for sub_name in ["new", "cur"] {
11302 let dir = format!("{}/{}", path, sub_name);
11303 let entries = match fs::read_dir(&dir) {
11304 Ok(e) => e,
11305 Err(_) => return 0,
11306 };
11307 for entry in entries.flatten() {
11308 let name = entry.file_name();
11309 let name_bytes = name.as_encoded_bytes();
11310 // C: if (fn->d_name[0] == '.') continue;
11311 if name_bytes.first() == Some(&b'.') {
11312 // c:7758
11313 continue;
11314 }
11315 let entry_path = match CString::new(entry.path().to_string_lossy().as_bytes()) {
11316 Ok(c) => c,
11317 Err(_) => continue,
11318 };
11319 let mut entry_st: libc::stat = unsafe { std::mem::zeroed() };
11320 if unsafe { libc::stat(entry_path.as_ptr(), &mut entry_st) } != 0 {
11321 // c:7762
11322 continue;
11323 }
11324 st.st_size += entry_st.st_size; // c:7766
11325 st.st_blocks += 1; // c:7767
11326 // C: if (atime != mtime && atime > atime_max) atime_max = atime;
11327 if entry_st.st_atime != entry_st.st_mtime && entry_st.st_atime > atime {
11328 // c:7769
11329 atime = entry_st.st_atime;
11330 }
11331 if entry_st.st_mtime > mtime {
11332 // c:7771
11333 mtime = entry_st.st_mtime;
11334 }
11335 }
11336 }
11337 // C 7783-7784: if (atime) st_ret.st_atime = atime;
11338 if atime != 0 {
11339 // c:7783
11340 st.st_atime = atime;
11341 }
11342 if mtime != 0 {
11343 // c:7785
11344 st.st_mtime = mtime;
11345 }
11346 0 // c:7787
11347}
11348
11349/// Script name for error messages
11350pub static mut SCRIPT_NAME: Option<String> = None;
11351/// Script filename
11352pub static mut SCRIPT_FILENAME: Option<String> = None;
11353
11354/// Print a fatal error to stderr.
11355/// Port of `zerr(VA_ALIST1(const char *fmt))` from Src/utils.c:173. C source sets `errflag`
11356/// after emitting `<prefix>: <msg>\n` so the running script aborts
11357/// at the next safe point. The Rust port currently just prints —
11358// =====================================================================
11359// Module-static state for the zerr/zwarn/zerrnam/zwarnnam family —
11360// port of the file-statics in Src/init.c + Src/exec.c that
11361// `zwarning()` (utils.c:142) reads to build the error prefix.
11362// =====================================================================
11363
11364// name of script being sourced // c:33
11365/// Port of `char *scriptname` from `Src/init.c`. Set when `source`
11366/// is reading a script; cleared on return. Used by `zwarning()`
11367/// (utils.c:147) as the diagnostic prefix.
11368///
11369/// Storage is [`crate::thread_shell_state::ThreadMutex`], not a plain
11370/// `Mutex`: C's `scriptname` says where the ONE thread of execution is,
11371/// and `doshfunc` overwrites it with the callee's name on every function
11372/// entry (c:Src/exec.c:5903). An `async_precmd` hook running on a pool
11373/// worker therefore stamped its own name over the shell thread's, and a
11374/// diagnostic the user caused at the prompt came out prefixed with the
11375/// hook's name. Per-thread is C's model — background work there is a
11376/// forked child with its own copy.
11377static SCRIPTNAME: crate::thread_shell_state::ThreadMutex<Option<String>> =
11378 crate::thread_shell_state::ThreadMutex::new(None, || None);
11379
11380/// Port of `char *argzero` from `Src/init.c`. The shell's argv[0].
11381/// Used by `zwarning()` (utils.c:147) as the fallback diagnostic
11382/// prefix when scriptname is unset.
11383static ARGZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
11384
11385/// Port of `char *scriptfilename` from `Src/init.c` (listed in
11386/// `zsh.export:356`). The FILE that the current code was PARSED
11387/// from. Distinct from `scriptname` (which doshfunc overrides on
11388/// function entry at c:5903). `scriptfilename` stays at the outer
11389/// file across function calls; PS4's `%x` reads it at
11390/// `Src/prompt.c:937`.
11391///
11392/// Set at init.c:479 (`-c` mode → `scriptname = scriptfilename
11393/// = "zsh"`), init.c:1367 (`source` enters the named file),
11394/// init.c:1558+1592+1667 (save/install/restore around `.` /
11395/// `source` bin_dot dispatch).
11396///
11397/// Per-thread for the same reason as [`SCRIPTNAME`]: a hook function that
11398/// `source`s a file on a worker would otherwise move the file PS4's `%x`
11399/// and the `funcstack` frames name on the shell thread.
11400static SCRIPTFILENAME: crate::thread_shell_state::ThreadMutex<Option<String>> =
11401 crate::thread_shell_state::ThreadMutex::new(None, || None);
11402
11403/// Port of `char *posixzero` from `Src/params.c:76`. The original
11404/// argv[0] preserved unchanged by later mutations. Used by
11405/// `argzerogetfn` for `$0` under `isset(POSIXARGZERO)`.
11406static POSIXZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
11407
11408// error flag: bits from enum errflag_bits // c:124
11409/// Port of `int errflag` from `Src/init.c`. Tracks whether an
11410/// error has been raised (`ERRFLAG_ERROR = 1`) or break/return
11411/// is in flight (`ERRFLAG_INT = 2`).
11412///
11413/// Direct global access: `errflag.load(Ordering::Relaxed)` reads
11414/// the current value, `errflag.fetch_or(ERRFLAG_ERROR, …)` matches
11415/// C's `errflag |= ERRFLAG_ERROR`, `errflag.store(0, …)` matches
11416/// C's `errflag = 0`.
11417///
11418/// Storage is [`crate::errflag_cell::ErrflagCell`], not a bare
11419/// `AtomicI32`: C's `errflag` is a per-PROCESS `int` and C's background
11420/// work is a forked child, so a child's errors never reach the parent's
11421/// line editor. zshrs runs that work on threads, so the flag is
11422/// process-global on the shell thread and thread-local everywhere else —
11423/// which is what `fork()` gave C. See that module for the ^G-abort
11424/// corruption this prevents.
11425#[allow(non_upper_case_globals)]
11426pub static errflag: crate::errflag_cell::ErrflagCell = crate::errflag_cell::ErrflagCell::new();
11427
11428/// Port of `int noerrs` from `Src/init.c`. Counter — when `> 0`,
11429/// suppresses error printing. `noerrs >= 2` also suppresses the
11430/// `errflag` set inside `zerr`/`zerrnam`.
11431static NOERRS: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
11432
11433/// Port of `int locallevel` from `Src/init.c`. Function-call depth
11434/// (0 = top-level, 1+ = inside a fn). `zwarning()` checks this in
11435/// the script-prefix path (utils.c:150).
11436// `LOCALLEVEL` removed — see `locallevel_lock` removal comment
11437// below. Canonical static lives in `LOCALLEVEL`
11438// (port of params.c:54).
11439
11440/// Port of `int lineno` from `Src/init.c`. Current line number;
11441/// `zerrmsg()` includes it in the diagnostic when locallevel > 0
11442/// or shinstdin is unset (utils.c:301).
11443static LINENO: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
11444
11445/// Port of the `isset(SHINSTDIN)` check from utils.c:150. C reads
11446/// the SHINSTDIN option directly; the Rust port caches it here so
11447/// callers don't pull in the whole option-table for every error.
11448static SHINSTDIN_OPT: std::sync::OnceLock<Mutex<bool>> = std::sync::OnceLock::new();
11449
11450/// `ERRFLAG_ERROR` from `Src/zsh.h`. Set on `zerr`/`zerrnam` to
11451/// signal a fatal error has occurred.
11452pub const ERRFLAG_ERROR: i32 = 1;
11453
11454/// Port of `mod_export int incompfunc` from `Src/utils.c:46`.
11455/// Set non-zero while a `comp*` builtin is dispatching from inside
11456/// a user-defined completion function — guards `comparguments` /
11457/// `compset` / `compadd` / `compdescribe` / `comptags` / `compvalues`
11458/// / `compfiles` / `compgroups` / `compquote` against being called
11459/// outside the `compfunc` shfunc context (each builtin checks
11460/// `INCOMPFUNC` early and emits "can only be called from completion
11461/// function" when zero).
11462pub static INCOMPFUNC: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:46
11463
11464/// Port of `mod_export int resetneeded` from `Src/utils.c:1821`.
11465/// Set when the editor needs a redraw — incremented by widgets +
11466/// signal handlers (e.g. SIGWINCH); the next prompt loop tick clears
11467/// it after running zrefresh.
11468pub static RESETNEEDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1821
11469
11470/// Port of `mod_export int winchanged` from `Src/utils.c:1827`.
11471/// Set by the SIGWINCH handler — the next refresh re-queries the
11472/// terminal size and re-renders, then clears the flag.
11473pub static WINCHANGED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1827
11474
11475/// Port of C's `GETKEY_*` flag enumeration from `Src/zsh.h:3143`.
11476/// Passed to `getkeystring_with` to alter escape interpretation:
11477/// - GETKEY_OCTAL_ESC: `\NNN` octal escapes are interpreted (1<<0)
11478/// - GETKEY_EMACS: unknown `\<char>` drops the backslash (1<<1)
11479/// - GETKEY_BACKSLASH_C: `\c` truncates the result (1<<3)
11480pub const GETKEY_OCTAL_ESC: u32 = 1 << 0; // c:zsh.h:3143
11481/// `GETKEY_EMACS` constant.
11482pub const GETKEY_EMACS: u32 = 1 << 1; // c:zsh.h:3150
11483/// `GETKEY_CTRL` constant.
11484pub const GETKEY_CTRL: u32 = 1 << 2; // c:zsh.h:3152
11485/// `GETKEY_BACKSLASH_C` constant.
11486pub const GETKEY_BACKSLASH_C: u32 = 1 << 3; // c:zsh.h:3154
11487
11488/// `GETKEYS_PRINT = GETKEY_OCTAL_ESC | GETKEY_BACKSLASH_C |
11489/// GETKEY_EMACS` per Src/zsh.h:3185 — the flag set `bin_print` uses
11490/// when interpreting escapes (with EMACS: unknown `\<c>` → `<c>`).
11491pub const GETKEYS_PRINT: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_BACKSLASH_C; // c:zsh.h:3185
11492
11493/// `GETKEYS_ECHO = GETKEY_BACKSLASH_C` per Src/zsh.h:3178 — the flag
11494/// set `echo` uses (and `print -e`). NOT GETKEY_EMACS: unknown
11495/// `\<c>` is preserved as literal `\<c>`. NOT GETKEY_OCTAL_ESC:
11496/// `\NNN` is NOT interpreted as octal. Only `\a \b \e \E \f \n \r
11497/// \t \v \\` (plus `\c` truncation) are handled.
11498///
11499/// Per builtin.c:4754-4760 — `bin_print` selects this for BIN_ECHO
11500/// or when `-e` is set; otherwise GETKEYS_PRINT.
11501pub const GETKEYS_ECHO: u32 = GETKEY_BACKSLASH_C; // c:zsh.h:3178
11502
11503/// `GETKEYS_BINDKEY = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL`
11504/// per Src/zsh.h:3187 — the flag set `print -b` uses. Like GETKEYS_PRINT
11505/// it has EMACS (so `\C-`/`\M-` backslash escapes are processed and
11506/// unknown `\<c>` collapses to `<c>`) but adds GETKEY_CTRL (enabling the
11507/// `^X` caret notation) and drops GETKEY_BACKSLASH_C (no `\c` truncation).
11508pub const GETKEYS_BINDKEY: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL; // c:zsh.h:3187
11509
11510/// Static `int ep` from Src/utils.c:4775 — sticky flag suppressing the
11511/// `can't set tty pgrp` warning after the first failure.
11512static ATTACHTTY_EP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:4775
11513
11514// =====================================================================
11515// !!! WARNING: RUST-ONLY HELPERS — NO DIRECT C COUNTERPART !!!
11516// =====================================================================
11517//
11518// These three ported (`fdtable_lock`, `fdtable_get`, `fdtable_set`) DO
11519// NOT EXIST as functions in `Src/utils.c`. The C source declares
11520// `fdtable` as a bare `unsigned char *` global (Src/utils.c:~63) and
11521// every call site reads/writes it as a direct array index:
11522//
11523// if (fdtable[fd] != FDT_UNUSED) ...
11524// fdtable[fd] = FDT_EXTERNAL;
11525//
11526// Rust can't hand out raw mutable indexes through a `Mutex<Vec<u8>>`
11527// safely without a borrow scope, so the Rust port wraps the same
11528// slot access in three `pub fn`s. Each call site to these helpers
11529// corresponds 1:1 to a `fdtable[fd] = X` or `fdtable[fd] != X`
11530// statement in the C source — they are NOT new policy, they only
11531// adapt the storage shape from `unsigned char *` to a growable
11532// `Mutex<Vec<i32>>`.
11533//
11534// Also adapts `growfdtable` (Src/utils.c:1965) by lazily growing the
11535// Vec inside `fdtable_set` instead of a separate `growfdtable`
11536// call — the C source calls `growfdtable(fd)` immediately before
11537// every `fdtable[fd] = X` write anyway.
11538//
11539// !!! Do NOT use these for any state that the C source does not
11540// already store in `fdtable[]`. Adding a new "fd kind" here is a
11541// scope expansion, not a port. !!!
11542// =====================================================================
11543
11544static FDTABLE: std::sync::OnceLock<Mutex<Vec<i32>>> = std::sync::OnceLock::new();
11545
11546/// Port of `int max_zsh_fd` global from `Src/exec.c:201`.
11547/// "The highest fd we know zsh is using" — bumped by `fdtable_set`
11548/// callers, shrunk by `zclose` when trailing slots fall to UNUSED.
11549pub static MAX_ZSH_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
11550
11551/// Port of `int fdtable_flocks` global from `Src/exec.c:204`.
11552/// Count of `FDT_FLOCK`-tagged fds; consulted by `closem()` to
11553/// decide whether to skip the flock-fd sweep.
11554pub static FDTABLE_FLOCKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
11555
11556/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11557/// standalone function.
11558/// C assigns the `char *scriptname` global (`Src/utils.c:36`) directly, e.g.
11559/// `scriptname = ...` in `bin_dot`. Rust needs a setter over the `OnceLock<
11560/// Mutex<Option<String>>>` that holds it.
11561/// Setter for `scriptname`. Called from `bin_dot` / `source`
11562/// when entering a script.
11563pub fn set_scriptname(name: Option<String>) {
11564 *scriptname_lock().lock().unwrap() = name;
11565}
11566
11567/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11568/// standalone function.
11569/// Reader for the same storage; C dereferences the `char *scriptname`
11570/// global at `Src/utils.c:36` in place.
11571/// Read `scriptname` — direct mirror of the C file-scope
11572/// `char *scriptname;` at `Src/utils.c:36`. Exposed for the
11573/// prompt expander (`%N`), `bin_dot`, and ZLE source tracking.
11574pub fn scriptname_get() -> Option<String> {
11575 scriptname_lock().lock().unwrap().clone()
11576}
11577
11578/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11579/// standalone function.
11580/// Setter for the storage mirroring C's `char *scriptfilename` global at
11581/// `Src/utils.c:41`, which C assigns directly.
11582/// Setter for `scriptfilename` — direct mirror of the C
11583/// `scriptfilename` global at `Src/init.c` (zsh.export:356).
11584/// Called from `bin_dot` at init.c:1592, restored at c:1667.
11585pub fn set_scriptfilename(name: Option<String>) {
11586 *scriptfilename_lock().lock().unwrap() = name;
11587}
11588
11589/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11590/// standalone function.
11591/// Reader for the same storage; C dereferences `scriptfilename`
11592/// (`Src/utils.c:41`) in place, e.g. PS4's `%x` in `Src/prompt.c`.
11593/// Read `scriptfilename` — direct mirror of the C global.
11594/// Used by PS4's `%x` (`Src/prompt.c:937`) and the function-call
11595/// frame at `Src/exec.c:1613` (`fstack.filename = scriptfilename`).
11596pub fn scriptfilename_get() -> Option<String> {
11597 scriptfilename_lock().lock().unwrap().clone()
11598}
11599
11600/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11601/// standalone function.
11602/// Reader for the `int locallevel` global declared at `Src/params.c:54`;
11603/// C reads the global directly.
11604/// Read `locallevel` — function nesting depth. Routes through the
11605/// canonical `LOCALLEVEL` static (port of
11606/// `Src/params.c:54`). The prior Rust port stored a duplicate
11607/// `Mutex<i32>` here that split state from params.rs; both copies
11608/// were never kept in sync.
11609pub fn locallevel() -> i32 {
11610 LOCALLEVEL.load(Ordering::Relaxed)
11611}
11612
11613/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11614/// standalone function.
11615/// C writes `locallevel++` inline in `startparamscope` (`Src/params.c`);
11616/// the global is declared at `Src/params.c:54`.
11617/// Bump `locallevel` (called by `startparamscope`).
11618pub fn inc_locallevel() {
11619 // c:Src/params.c:5837 `locallevel++` assumes one thread of shell
11620 // execution. An `async_precmd` batch runs hook functions on a pool worker
11621 // against this same counter and the same paramtab, so a scope opened here
11622 // while it runs gets its level-stamped params deleted by the worker's
11623 // `endparamscope` (c:5904-5907) — a widget's `$BUFFER` among them. Let the
11624 // batch finish (or withdraw it) before the shell thread opens a scope.
11625 crate::async_precmd::quiesce();
11626 LOCALLEVEL.fetch_add(1, Ordering::Relaxed);
11627}
11628
11629/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11630/// standalone function.
11631/// C writes `locallevel--` inline in `endparamscope` (`Src/params.c`);
11632/// the global is declared at `Src/params.c:54`.
11633/// Decrement `locallevel` (called by `endparamscope`).
11634pub fn dec_locallevel() {
11635 LOCALLEVEL.fetch_sub(1, Ordering::Relaxed);
11636}
11637
11638/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11639/// standalone function.
11640/// Setter for the `char *argzero` global declared at `Src/params.c:72`,
11641/// which C assigns directly from `parseargs`.
11642/// Setter for `argzero`. Called once at shell init from `parseargs`.
11643pub fn set_argzero(name: Option<String>) {
11644 // c:271 (init.c) — `argv0 = argzero = posixzero = *argv++;`. At
11645 // shell init both argzero and posixzero share the same source.
11646 // posixzero is preserved unchanged after later mutations (exec -a,
11647 // function frames) — argzero is what gets rewritten. If posixzero
11648 // hasn't been independently set yet, mirror argzero so the
11649 // POSIXARGZERO branch in `argzerogetfn` returns something useful.
11650 let mut posix_lock = posixzero_lock().lock().unwrap();
11651 if posix_lock.is_none() {
11652 *posix_lock = name.clone();
11653 }
11654 drop(posix_lock);
11655 *argzero_lock().lock().unwrap() = name;
11656}
11657
11658/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11659/// standalone function.
11660/// Reader for the same `char *argzero` global (`Src/params.c:72`);
11661/// C's `argzerogetfn` returns the global itself.
11662/// Read `argzero`. Used by `argzerogetfn` for `$0`.
11663pub fn argzero() -> Option<String> {
11664 argzero_lock().lock().unwrap().clone()
11665}
11666
11667/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11668/// standalone function.
11669/// Setter for the `char *posixzero` global declared at `Src/params.c:76`;
11670/// C assigns it directly in the init path.
11671/// Port of `char *posixzero` from `Src/params.c:76`. The original
11672/// `argv[0]` preserved from shell startup, unchanged by later `exec -a`
11673/// or function-call rewrites. C's `argzerogetfn` (params.c:4958)
11674/// returns this instead of `argzero` when `isset(POSIXARGZERO)`.
11675///
11676/// Set once at init via `set_posixzero` (called from the init path
11677/// in C at init.c:271/297/321). `set_argzero` mirrors the value here
11678/// on first call so the POSIXARGZERO branch has something to read
11679/// even if the init path hasn't run yet.
11680pub fn set_posixzero(name: Option<String>) {
11681 *posixzero_lock().lock().unwrap() = name;
11682}
11683
11684/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11685/// standalone function.
11686/// Reader for the same `char *posixzero` global (`Src/params.c:76`),
11687/// which C's `argzerogetfn` returns at `Src/params.c:4957`.
11688/// Read `posixzero`. C: `posixzero` (params.c:76 global). Used by
11689/// `argzerogetfn` when `isset(POSIXARGZERO)`.
11690pub fn posixzero() -> Option<String> {
11691 posixzero_lock().lock().unwrap().clone()
11692}
11693
11694/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11695/// standalone function.
11696/// Writer for the `int noerrs` global declared at `Src/exec.c:117`;
11697/// C does `noerrs++` / `noerrs--` on the global in place.
11698/// Setter for `noerrs`. Increment to suppress error output;
11699/// decrement to restore.
11700pub fn set_noerrs(v: i32) {
11701 *noerrs_lock().lock().unwrap() = v;
11702}
11703
11704/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11705/// standalone function.
11706/// Writer for the `int locallevel` global declared at `Src/params.c:54`.
11707/// Setter for `locallevel`. Called by function-call entry/exit.
11708pub fn set_locallevel(v: i32) {
11709 LOCALLEVEL.store(v, Ordering::Relaxed);
11710}
11711
11712/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11713/// standalone function.
11714/// Writer for the `zlong lineno` global declared at `Src/params.c:123`;
11715/// C assigns the global directly from the lexer/parser.
11716/// Setter for `lineno`. Called by the parser as it advances.
11717pub fn set_lineno(v: i32) {
11718 *lineno_lock().lock().unwrap() = v;
11719}
11720
11721/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11722/// standalone function.
11723/// Rust-only cache of the SHINSTDIN option bit (option table entry at
11724/// `Src/options.c:248`) so error emission does not have to reach into the
11725/// option table. C tests `isset(SHINSTDIN)` against `opts[]` directly.
11726/// Setter for the cached SHINSTDIN flag. Called by the option
11727/// machinery whenever `setopt shinstdin` / `unsetopt shinstdin`
11728/// fires.
11729pub fn set_shinstdin(v: bool) {
11730 *shinstdin_lock().lock().unwrap() = v;
11731}
11732
11733/// Check if string is a valid identifier
11734// `isident` DELETED — fake duplicate (cited `c:params.c:1288` from
11735// utils.rs, location mismatch). Canonical port is
11736// `isident` at `params.rs:2056`, which
11737// correctly handles namespace prefix (`ns.var`) per the real C
11738// body — the utils.rs copy dropped that arm. Callers now route
11739// through `isident` directly.
11740
11741// QuoteType enum + impl deleted — the canonical quote-type values are
11742// the bare `QT_*: i32` constants at `zsh_h.rs:175` (port of anonymous
11743// `enum { QT_NONE, QT_BACKSLASH, … }` from `Src/zsh.h:253-298`).
11744// `quotestring()` takes `quote_type: i32` matching C's signature.
11745
11746/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11747/// standalone function.
11748/// C has no such function: the `(q)`-count to `QT_*` mapping is done inline
11749/// in `paramsubst`'s flag loop at `Src/subst.c:2236`, where repeated `q`
11750/// flags run `quotemod++, quotetype++` (c:2252) over the `QT_*` enum.
11751/// Map a `(q)` flag count to a `QT_*` value.
11752/// Port of the q-flag dispatch in `Src/subst.c` `paramsubst()` —
11753/// `(q)`=`QT_BACKSLASH`, `(qq)`=`QT_SINGLE`, `(qqq)`=`QT_DOUBLE`,
11754/// `(qqqq+)`=`QT_DOLLARS`.
11755pub fn qflag_quotetype(count: u32) -> i32 {
11756 match count {
11757 0 => QT_NONE,
11758 1 => QT_BACKSLASH,
11759 2 => QT_SINGLE,
11760 3 => QT_DOUBLE,
11761 _ => QT_DOLLARS,
11762 }
11763}
11764
11765/// Port of `ispecial()` from `Src/ztype.h:59`.
11766///
11767/// Port of `ispecial()` macro from `Src/ztype.h:59` — `zistype(X,
11768/// ISPECIAL)`. C populates the ISPECIAL typtab bit at
11769/// `Src/utils.c:4253-4262`:
11770/// - Every byte in `SPECCHARS` (`Src/zsh.h:228` =
11771/// `"#$^*()=|{}[]\`<>?~;&\\n\\t \\\\\\'\\""`)
11772/// - `,` when `typtab_flags & ZTF_SP_COMMA` (set by
11773/// `makecommaspecial(1)` per c:4271)
11774/// - `bangchar` (default `!`) when `BANGHIST` is set AND
11775/// `ZTF_INTERACT` is set (interactive shell)
11776///
11777/// Reads the LIVE table, which is the whole point of the macro: the
11778/// set is not a constant. `inittyptab` recomputes it from options on
11779/// every relevant change, and two members exist only at runtime —
11780/// `,` under `makecommaspecial` and `bangchar` under
11781/// BANGHIST+interactive.
11782///
11783/// This used to enumerate `SPECCHARS` as a literal `matches!`, which
11784/// made both runtime members unreachable: `quotestring` is the only
11785/// caller, so `${(q)v}` on `v='a!b'` published `a!b` where an
11786/// interactive zsh publishes `a\!b`, and `makecommaspecial` could not
11787/// affect quoting at all. The bit was being computed correctly all
11788/// along (utils.c:4257-4259, and `makebangspecial` at c:4283) — it
11789/// just had no reader.
11790///
11791/// C indexes `typtab` by `unsigned char`, so anything outside 0..=255
11792/// has no entry; return false rather than truncating a `char` into a
11793/// byte that would alias an unrelated slot.
11794fn ispecial(c: char) -> bool {
11795 // c:59 (Src/ztype.h) — `zistype(X, ISPECIAL)`.
11796 let cp = c as u32;
11797 if cp > 0xff {
11798 return false;
11799 }
11800 crate::ported::ztype_h::ispecial(cp as u8)
11801}
11802
11803/// Port of `convbase()` from `Src/params.c:5632`.
11804///
11805/// Convert integer to string with specified base — re-export of
11806/// the canonical port at `convbase_param`.
11807///
11808/// The previous Rust port at this file was a SECOND, divergent
11809/// implementation of `convbase`:
11810/// - Used LOWERCASE digit chars (`"0123456789abc..."`) for
11811/// bases 11..36, while C uses UPPERCASE (`(dig - 10) + 'A'`
11812/// at Src/params.c:5621).
11813/// - Skipped CBASES + OCTALZEROES prefix handling at c:5598-5604
11814/// (`0x`/`0` prefixes when option-gated).
11815/// - Returned wrong format for bases not handled by the explicit
11816/// match arms.
11817///
11818/// The canonical port lives at `params.rs::convbase_ptr` (c:5588
11819/// faithful body) + `params.rs::convbase` (c:5634 wrapper). Route
11820/// through there so utils.rs / params.rs agree byte-for-byte and
11821/// no divergent duplicate stays alive.
11822pub fn convbase(val: i64, base: u32) -> String {
11823 // c:5634 (Src/params.c)
11824 convbase_param(val, base)
11825}
11826
11827// `checkglobqual` DELETED — fake bracket-depth scanner cited
11828// `c:glob.c:1158` from utils.rs. Canonical port is
11829// `crate::ported::glob::checkglobqual(str, sl, nobareglob, sp)` at
11830// `glob.rs:813`, which matches the real C signature
11831// `(char *str, int sl, int nobareglob, char **sp)`. The utils.rs
11832// fake reduced the signature to a single bool and discarded the
11833// `sp` glob-qualifier-start out-pointer that callers need. Zero
11834// callers used the utils.rs version.
11835
11836// `dupstrpfx` DELETED — fake duplicate cited `c:string.c:161` from
11837// utils.rs (wrong location). Canonical port is
11838// `dupstrpfx` at `string.rs:161`. Zero
11839// callers used the utils.rs version.
11840
11841/// Port of `statuidprint()` from `Src/Modules/stat.c:132`.
11842/// !!! DIVERGENT DUPLICATE !!! The faithful port of that C body (with the
11843/// `STF_RAW` / `STF_STRING` flag arms and the no-`getpwuid` fallback) is
11844/// `statuidprint(uid, flags) -> String` at `src/ported/modules/stat.rs:112`.
11845/// This copy implements only the `getpwuid(uid) -> pw_name` arm (c:142-144)
11846/// and has no caller; it should be deleted in favour of the stat.rs port.
11847///
11848/// Get username from UID (from utils.c getpwuid handling)
11849pub fn statuidprint(uid: u32) -> Option<String> {
11850 #[cfg(unix)]
11851 {
11852 let pwd = unsafe { libc::getpwuid(uid) };
11853 if pwd.is_null() {
11854 return None;
11855 }
11856 let name = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_name) };
11857 name.to_str().ok().map(|s| s.to_string())
11858 }
11859 #[cfg(not(unix))]
11860 {
11861 let _ = uid;
11862 None
11863 }
11864}
11865
11866// `ztrdup` / `dyncat` / `tricat` / `bicat` DELETED — these were
11867// fake duplicates of the canonical ports in `src/ported/string.rs`
11868// (`Src/string.c:62`, `:131`, `:98`, `:145`). The utils.rs copies
11869// admitted they were not in utils.c via `c:string.c:NNN`
11870// annotations; PORT.md Rule 1 disallows the duplicate location.
11871// Callers now route through `crate::ported::string::{ztrdup,
11872// dyncat, tricat, bicat}` directly.
11873
11874// `iwsep` / `iwsep_byte` DELETED — these were fake hardcoded
11875// `c==' '||c=='\t'||c=='\n'` checks claiming to port `iwsep` from
11876// "Src/zsh.h", but the real macro lives at `Src/ztype.h:61`
11877// (`#define iwsep(X) zistype(X, IWSEP)`) and consults the `typtab[]`
11878// lookup — which mutates when IFS is reassigned. The canonical port
11879// is `iwsep(u8) -> bool` at `ztype_h.rs:133`.
11880// Internal utils.rs callers (skipwsep / spacesplit / findword) now
11881// route through it directly.
11882
11883// `imeta(c: char)` DELETED — fake `char`-arg wrapper cited
11884// `c:ztype.h:60` from utils.rs (wrong location, wrong signature).
11885// Canonical port is `crate::ported::ztype_h::imeta(u8) -> bool` at
11886// `ztype_h.rs:130`. C `imeta(X)` takes a byte; wrapping with `char`
11887// invented an `> 0xff` early-out that C doesn't have. Migrated 1
11888// caller (`zle/computil.rs:6194`).
11889
11890/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11891/// standalone function.
11892/// C calls the libc `gethostname(2)` directly at `Src/params.c:874`
11893/// (`gethostname(hostnam, 256);`). The zsh function of this name
11894/// (`Src/compat.c:64`) is the `#ifndef HAVE_GETHOSTNAME` fallback built on
11895/// `uname()`, which this does NOT implement — this wraps libc's real one.
11896/// Get hostname
11897pub fn gethostname() -> String {
11898 #[cfg(unix)]
11899 {
11900 let mut buf = vec![0u8; 256];
11901 unsafe {
11902 if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) == 0 {
11903 let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
11904 return String::from_utf8_lossy(&buf[..len]).to_string();
11905 }
11906 }
11907 }
11908 // gethostname(2) failure fallback. C consults `$HOST` /
11909 // `cached_hostname`. Read paramtab; fall back to "localhost".
11910 getsparam("HOST")
11911 .or_else(|| getsparam("HOSTNAME"))
11912 .unwrap_or_else(|| "localhost".to_string())
11913}
11914
11915/// Get current working directory — re-export of the canonical
11916/// port at `compat::zgetcwd` (Src/compat.c:559).
11917// `zgetcwd` DELETED — fake `Option<String>` wrapper around the
11918// canonical `compat::zgetcwd() -> String` port (`Src/compat.c:559`).
11919// C `zgetcwd` never returns NULL (falls back to `"."` per the
11920// `dupstring(".")` arm), so the `Option` was caller-API churn, not
11921// a port. Callers route through `crate::ported::compat::zgetcwd()`
11922// directly.
11923
11924// `zchdir` duplicate deleted — canonical port at compat.rs:253
11925// matches C's `int zchdir(char *dir)` signature (returns i32, not
11926// bool). Zero callers used the utils.rs bool variant.
11927
11928// `realpath` / `mkdir` / `symlink` / `readlink` / `getenv` DELETED —
11929// five Rust-only convenience wrappers around std::fs / std::env /
11930// std::os::unix::fs with ZERO callers across the tree. Each was a
11931// thin std-lib re-export with no zsh C counterpart:
11932// - realpath → std::fs::canonicalize (libc realpath(3) — not in
11933// utils.c; the zsh source uses chrealpath at hist.c:1971)
11934// - mkdir → std::fs::create_dir (libc mkdir(2) — zsh's analogue
11935// is Modules/files.c::bin_mkdir, not a utils.c helper)
11936// - symlink → std::os::unix::fs::symlink (libc symlink(2))
11937// - readlink → std::fs::read_link (libc readlink(2))
11938// - getenv → std::env::var (libc getenv(3) — zsh's analogue is
11939// zgetenv in compat.c)
11940// PORT.md Rule 0/A: names must exist in upstream zsh C source as a
11941// `<name>(` function. None of these qualify; with zero callers they
11942// were dead code by definition. Callers needing the same semantics
11943// route through the canonical port (chrealpath / bin_mkdir / etc.)
11944// or std::fs directly.
11945
11946/// Per-prompt callback registry.
11947/// Port of the static `prepromptfns` LinkList in Src/utils.c:1313.
11948/// Holds the bare-fn pointers `addprepromptfn`/`delprepromptfn`
11949/// register and `preprompt()` walks.
11950static PREPROMPT_FNS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
11951
11952/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11953/// standalone function.
11954/// C has no `setenv` of its own — `Src/params.c`'s `zputenv` maintains the
11955/// `environ` array with metafied names. This is a plain
11956/// `std::env::set_var` wrapper for Rust-side callers, not that port.
11957/// Set environment variable
11958pub fn setenv(name: &str, value: &str) {
11959 std::env::set_var(name, value);
11960}
11961
11962/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11963/// standalone function.
11964/// Counterpart of `setenv` above; C's environment removal goes through
11965/// `Src/params.c`'s `zputenv`/`delenv`, not a function of this shape.
11966/// Unset environment variable
11967pub fn unsetenv(name: &str) {
11968 std::env::remove_var(name);
11969}
11970
11971/// Time-ordered timed-function registry.
11972/// Port of the `timedfns` LinkList Src/utils.c:1365 keeps for the
11973/// `sched` builtin. Sorted ascending by `when` (epoch seconds);
11974/// `addtimedfn` does an insertion sort (matches the C source's
11975/// `for (;;)` walk at lines 1394-1411).
11976pub static TIMED_FNS: Mutex<Vec<(i64, fn())>> = Mutex::new(Vec::new()); // c:1365 timedfns (mod_export in C)
11977
11978/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11979/// standalone function.
11980/// Wrapper over libc `getuid(2)`, which C calls directly at its use sites.
11981/// zsh has no `getuid` function of its own.
11982/// Get current user ID
11983pub fn getuid() -> u32 {
11984 #[cfg(unix)]
11985 unsafe {
11986 libc::getuid()
11987 }
11988 #[cfg(not(unix))]
11989 0
11990}
11991
11992/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
11993/// standalone function.
11994/// Wrapper over libc `geteuid(2)`, which C calls directly at its use sites.
11995/// Get effective user ID
11996pub fn geteuid() -> u32 {
11997 #[cfg(unix)]
11998 unsafe {
11999 libc::geteuid()
12000 }
12001 #[cfg(not(unix))]
12002 0
12003}
12004
12005/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12006/// standalone function.
12007/// Wrapper over libc `getgid(2)`, which C calls directly at its use sites.
12008/// Get current group ID
12009pub fn getgid() -> u32 {
12010 #[cfg(unix)]
12011 unsafe {
12012 libc::getgid()
12013 }
12014 #[cfg(not(unix))]
12015 0
12016}
12017
12018/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12019/// standalone function.
12020/// Wrapper over libc `getegid(2)`, which C calls directly at its use sites.
12021/// Get effective group ID
12022pub fn getegid() -> u32 {
12023 #[cfg(unix)]
12024 unsafe {
12025 libc::getegid()
12026 }
12027 #[cfg(not(unix))]
12028 0
12029}
12030
12031/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12032/// standalone function.
12033/// Wrapper over libc `getpid(2)`, which C calls directly at its use sites.
12034/// Get process ID
12035pub fn getpid() -> i32 {
12036 std::process::id() as i32
12037}
12038
12039/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12040/// standalone function.
12041/// Wrapper over libc `getppid(2)`, which C calls directly at its use sites.
12042/// Get parent process ID
12043pub fn getppid() -> i32 {
12044 #[cfg(unix)]
12045 unsafe {
12046 libc::getppid()
12047 }
12048 #[cfg(not(unix))]
12049 0
12050}
12051
12052/// Port of `getkeystring()` from `Src/utils.c:6915`.
12053///
12054/// `getkeystring` with the C `how` parameter (Src/utils.c:6915).
12055/// The plain `getkeystring(s)` shim above defaults `how=0` for
12056/// non-EMACS callers (zbeep, dollar-quote — they keep unknown
12057/// `\<char>` as literal `\<char>`). Pass `GETKEYS_PRINT` to get
12058/// the print/echo behavior (drop backslash on unknown).
12059/// `getkeystring` with the C `how` parameter AND the
12060/// `GETKEY_UPDATE_OFFSET` cursor-offset out-param (Src/utils.c:6915).
12061///
12062/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from
12063/// `Src/utils.c:6915`, specifically its `GETKEY_UPDATE_OFFSET` path.
12064/// When `how` sets `GETKEY_UPDATE_OFFSET` and `misc` is `Some`, `*misc`
12065/// is the C `*misc` completion offset — a byte index into `s` — and is
12066/// updated as escapes positioned before the cursor collapse:
12067/// - `-1` per collapsed `\<char>` escape whose backslash precedes the
12068/// cursor, `s - sstart < *misc` (c:6987); undone if the backslash is
12069/// kept literal in the default (non-EMACS) arm (c:7181-7183);
12070/// - additional `-6` for `\u`, `-10` (`-4`+`-6`) for `\U` (c:7073-7084),
12071/// then `+N` for the N output bytes emitted (c:7123-7124).
12072/// Octal/hex escapes get only the base `-1`, matching C's own incomplete
12073/// `/* HERE: GETKEY_UPDATE_OFFSET? */` handling (c:7155).
12074///
12075/// The offset is a byte index. For ASCII `$'...'` content (one byte per
12076/// char) it matches the C metafied-byte semantics exactly; the byte-vs-
12077/// char divergence for non-ASCII content is the pre-existing shared-cursor
12078/// limitation documented on `set_comp_sep` (compcore.rs), not new here.
12079///
12080/// Callers with no offset to track pass `misc = None`.
12081pub fn getkeystring_with(s: &str, how: u32, mut misc: Option<&mut i32>) -> (String, usize) {
12082 // c:utils.c:6915
12083 let update_off = (how & crate::ported::zsh_h::GETKEY_UPDATE_OFFSET as u32) != 0;
12084 let bash_dollar_quote = (how & crate::ported::zsh_h::GETKEY_DOLLAR_QUOTE as u32) != 0
12085 && crate::dash_mode::bash_mode();
12086 let mut result = String::new();
12087 let mut chars = s.chars().peekable();
12088 let mut consumed = 0;
12089 // c:7194 — C's `^` handler is `control = 1; continue;`, so the control bit
12090 // stays PENDING across loop iterations and a following `\M`/`\C` escape
12091 // chains onto it (`^\M-a` → c:7034 `meta = 1 + control` → meta==2 → 0x81).
12092 // This port's `^` arm consumes its base character inline, which cannot
12093 // express that, so the flag carries the state to the modifier arm instead.
12094 let mut pending_control = false;
12095 // (output length, control, meta) recorded by a `\C`/`\M` escape.
12096 let mut pending_mask: Option<(usize, u32, u32)> = None;
12097 // !!! WARNING: C applies a pending `\C`/`\M` modifier to `t[-1]` at the
12098 // bottom of the loop iteration that emitted the next character
12099 // (c:Src/utils.c:7261-7275), after that iteration decoded its own escape
12100 // (`\130`, `\x58`, `\e` …). The escape arms here leave the loop body from
12101 // many places, so the masks are applied to the last emitted unit at the
12102 // start of the next iteration and after the loop instead.
12103 let apply_pending_mask = |result: &mut String, pending: &mut Option<(usize, u32, u32)>| {
12104 let Some((mark, control, meta)) = *pending else {
12105 return;
12106 };
12107 if result.len() <= mark {
12108 return;
12109 }
12110 *pending = None;
12111 let mut units: Vec<char> = result.chars().collect();
12112 let Some(last) = units.pop() else {
12113 return;
12114 };
12115 let metafied = units.last() == Some(&'\u{83}');
12116 let mut byte = if metafied { (last as u32) ^ 32 } else { last as u32 };
12117 if byte > 0xff {
12118 return; // a multibyte character: left as emitted
12119 }
12120 if metafied {
12121 units.pop();
12122 }
12123 if meta == 2 {
12124 byte |= 0x80; // c:7261-7264
12125 }
12126 if control == 1 {
12127 byte = if byte == '?' as u32 { 0x7f } else { byte & 0x9f }; // c:7265-7271
12128 }
12129 if meta == 1 {
12130 byte |= 0x80; // c:7272-7275
12131 }
12132 *result = units.into_iter().collect();
12133 let b_ = byte as u8;
12134 if b_ < 0x80 {
12135 result.push(b_ as char);
12136 } else {
12137 // c:7289-7294 — a byte >= 0x80 is metafied.
12138 result.push('\u{83}');
12139 result.push(char::from(b_ ^ 32));
12140 }
12141 };
12142 // c:Src/utils.c:7276-7298 — under GETKEY_DOLLAR_QUOTE each chunk decoded
12143 // into the temporary buffer is copied out by a loop that, when
12144 // POSIX_STRINGS is set, stops at an embedded NUL and sets `ignoring`
12145 // (c:6920), which drops every later buffered chunk. `\u`/`\U`
12146 // (c:7117-7131) and tokens (c:7240-7255) bypass the buffer. `tbuf_mark`
12147 // is where the pending chunk starts in `result`.
12148 let posix_cut = (how & crate::ported::zsh_h::GETKEY_DOLLAR_QUOTE as u32) != 0
12149 && isset(crate::ported::zsh_h::POSIXSTRINGS);
12150 let mut ignoring = false;
12151 let mut tbuf_mark: Option<usize> = None;
12152 let flush_tbuf = |result: &mut String, mark: &mut Option<usize>, ignoring: &mut bool| {
12153 let Some(m) = mark.take() else { return };
12154 if !posix_cut || m > result.len() {
12155 return;
12156 }
12157 if *ignoring {
12158 result.truncate(m); // c:7286-7287
12159 } else if let Some(nul) = result[m..].find('\0') {
12160 *ignoring = true; // c:7284-7285
12161 result.truncate(m + nul);
12162 }
12163 };
12164 while let Some(c) = chars.next() {
12165 apply_pending_mask(&mut result, &mut pending_mask);
12166 flush_tbuf(&mut result, &mut tbuf_mark, &mut ignoring);
12167 if !(0xe184..=0xe1a1).contains(&(c as u32))
12168 && !(c == '\\' && matches!(chars.peek(), Some('u') | Some('U')))
12169 {
12170 tbuf_mark = Some(result.len());
12171 }
12172 consumed += c.len_utf8();
12173 // c:utils.c:7194 — `^X` caret notation. A bare `^` (not a backslash
12174 // escape) followed by any char applies the control mask to it, but
12175 // ONLY under GETKEY_CTRL — i.e. `print -b` / bindkey. Plain print
12176 // (GETKEYS_PRINT, no GETKEY_CTRL) keeps `^A` literal.
12177 if c == '^' && !pending_control && (how & GETKEY_CTRL) != 0 {
12178 // A `\M`/`\C` escape after the caret must be handled by the
12179 // modifier arm with control already pending — C reaches it via
12180 // `continue` rather than consuming the base char here. Gated on
12181 // GETKEY_EMACS because that is what enables the modifier arm at
12182 // all (c:7031/7043); without it C's `case 'M'` emits a literal
12183 // backslash instead.
12184 let mut la = chars.clone();
12185 if (how & GETKEY_EMACS) != 0
12186 && la.next() == Some('\\')
12187 && matches!(la.next(), Some('M') | Some('C'))
12188 {
12189 pending_control = true;
12190 continue;
12191 }
12192 if let Some(base) = chars.next() {
12193 consumed += base.len_utf8();
12194 let mut byte = base as u32;
12195 // c:7261-7267 — `^?` → 0x7f (DEL), else clear bits 5-6.
12196 if byte == '?' as u32 {
12197 byte = 0x7f;
12198 } else if byte <= 0xff {
12199 byte &= 0x9f;
12200 }
12201 if byte <= 0xff {
12202 let b_ = byte as u8;
12203 if b_ < 0x80 {
12204 result.push(b_ as char);
12205 } else {
12206 result.push('\u{83}');
12207 result.push(char::from(b_ ^ 32));
12208 }
12209 } else if let Some(ch) = char::from_u32(byte) {
12210 result.push(ch);
12211 }
12212 continue;
12213 }
12214 // c:7194 `s[1]` guard — a trailing `^` with no char is literal.
12215 result.push(c);
12216 continue;
12217 }
12218 if c != '\\' {
12219 result.push(c);
12220 continue;
12221 }
12222 // c is a backslash; record its byte start offset for
12223 // GETKEY_UPDATE_OFFSET (`s - sstart` in C).
12224 let bs_off = consumed - c.len_utf8();
12225 // c:utils.c:6987 — base offset decrement for a collapsed `\<char>`
12226 // escape whose backslash precedes the cursor. C gates on
12227 // `*s == '\\' && s[1]`, so a following char must exist (peek).
12228 let mut miscadded = false;
12229 if update_off && chars.peek().is_some() {
12230 if let Some(m) = misc.as_deref_mut() {
12231 if (bs_off as i32) < *m {
12232 *m -= 1;
12233 miscadded = true;
12234 }
12235 }
12236 }
12237 match chars.next() {
12238 Some('n') => {
12239 result.push('\n');
12240 consumed += 1;
12241 }
12242 Some('t') => {
12243 result.push('\t');
12244 consumed += 1;
12245 }
12246 Some('r') => {
12247 result.push('\r');
12248 consumed += 1;
12249 }
12250 // c:utils.c:7018-7024 — `\E` is EMACS-gated:
12251 // case 'E':
12252 // if (!(how & GETKEY_EMACS)) {
12253 // *t++ = '\\', s--;
12254 // if (miscadded) (*misc)++;
12255 // continue;
12256 // }
12257 // /* FALL THROUGH */
12258 // case 'e':
12259 // *t++ = '\033';
12260 // Without EMACS `\E` stays a literal backslash + `E`; only `\e`
12261 // is unconditional. Folding `E` in with `e` made `${(g::)v}` on
12262 // `\E` yield ESC where zsh yields `\E`. Unlike the octal
12263 // `continue` arm (c:7159), C restores the miscadded decrement
12264 // here, since the backslash is kept.
12265 Some('E') if (how & GETKEY_EMACS) == 0 => {
12266 consumed += 1;
12267 if miscadded {
12268 if let Some(m) = misc.as_deref_mut() {
12269 *m += 1;
12270 }
12271 }
12272 result.push('\\');
12273 result.push('E');
12274 }
12275 Some('e') | Some('E') => {
12276 result.push('\x1b');
12277 consumed += 1;
12278 }
12279 Some('a') => {
12280 result.push('\x07');
12281 consumed += 1;
12282 }
12283 Some('b') => {
12284 result.push('\x08');
12285 consumed += 1;
12286 }
12287 Some('f') => {
12288 result.push('\x0c');
12289 consumed += 1;
12290 }
12291 Some('v') => {
12292 result.push('\x0b');
12293 consumed += 1;
12294 }
12295 // c:utils.c:7140-7152 — `\\` (and `\'` under
12296 // GETKEY_DOLLAR_QUOTE) explicitly emit the trailing byte
12297 // bare. Outside DOLLAR_QUOTE, `\'` FALLTHROUGHs to default.
12298 // `\\` stays special because the default arm (c:7180-7184)
12299 // skips the leading-backslash emission iff `*s == '\\'`,
12300 // so `\\` reduces to `\` regardless of GETKEY_EMACS.
12301 //
12302 // Previous Rust port also had `Some('\'')` and `Some('"')`
12303 // arms that unconditionally dropped the backslash. That
12304 // contradicted C: under GETKEYS_ECHO (no GETKEY_EMACS),
12305 // `\'` must remain `\'` because echo doesn't strip
12306 // unknown escapes. Removed those arms — `\'` and `\"`
12307 // now flow through the default arm and honour GETKEY_EMACS.
12308 //
12309 // Root cause of `echo "${(qq)s}"` for `s="a'b"` emitting
12310 // `'a'''b'` instead of zsh's `'a'\''b'`.
12311 Some('\\') => {
12312 result.push('\\');
12313 consumed += 1;
12314 }
12315 // c:utils.c:7058-7063 — `\-` under GETKEY_BACKSLASH_MINUS:
12316 // case '-':
12317 // if (how & GETKEY_BACKSLASH_MINUS) {
12318 // *misc = 1;
12319 // break;
12320 // }
12321 // goto def;
12322 // The escape emits NOTHING; it only reports its presence. Without
12323 // the flag it falls to the default arm below.
12324 Some('-') if (how & crate::ported::zsh_h::GETKEY_BACKSLASH_MINUS as u32) != 0 => {
12325 consumed += 1;
12326 if let Some(m) = misc.as_deref_mut() {
12327 *m = 1;
12328 }
12329 }
12330 // c:utils.c:7072-7138 — `\u` (4-hex) / `\U` (8-hex) Unicode
12331 // codepoint escapes. `case 'U':` falls through into `case 'u':`
12332 // (c:7072-7076); only the digit count and the
12333 // GETKEY_UPDATE_OFFSET bookkeeping differ. The value is converted
12334 // by `ucs4tomb(wval, t)` (c:7101), i.e. in the CURRENT LOCALE, not
12335 // as blind UTF-8: under `LC_ALL=C` it fails with "character not
12336 // in range" and the escape is not decoded.
12337 Some(uc @ ('u' | 'U')) => {
12338 consumed += 1;
12339 // !!! BASH-MODE (no C counterpart) !!! bash keeps `\u` / `\U`
12340 // verbatim in `$'...'` when no hex digit follows.
12341 if bash_dollar_quote && !chars.peek().is_some_and(|c| c.is_ascii_hexdigit()) {
12342 result.push('\\');
12343 result.push(uc);
12344 continue;
12345 }
12346 // c:utils.c:7073-7084 — `\U` extra `-4` (c:7073) then the
12347 // shared `-6` (c:7077), both gated at offset bs_off+1.
12348 if update_off {
12349 if let Some(m) = misc.as_deref_mut() {
12350 if ((bs_off + 1) as i32) < *m {
12351 *m -= if uc == 'U' { 10 } else { 6 };
12352 }
12353 }
12354 }
12355 // c:7085-7095 — `wval = 0;` then up to 4 / 8 hex digits.
12356 // ZERO digits still emits codepoint 0: `\u` → one NUL byte,
12357 // `\uzz` → NUL + "zz".
12358 let mut wval: u32 = 0;
12359 for _ in 0..(if uc == 'u' { 4 } else { 8 }) {
12360 match chars.peek().and_then(|c| c.to_digit(16)) {
12361 Some(d) => {
12362 chars.next();
12363 consumed += 1;
12364 wval = wval.wrapping_mul(16).wrapping_add(d);
12365 }
12366 None => break,
12367 }
12368 }
12369 // c:7101 — `count = ucs4tomb(wval, t);`
12370 let mut tbuf = [0u8; 16];
12371 let count = ucs4tomb(wval, &mut tbuf);
12372 if count == -1 {
12373 // c:7102-7121 — the conversion failed (ucs4tomb already
12374 // raised the zerr). Under GETKEY_DOLLAR_QUOTE the rest of
12375 // the input is copied through undecoded (c:7104-7115);
12376 // otherwise the output ends here (c:7117-7119).
12377 if (how & crate::ported::zsh_h::GETKEY_DOLLAR_QUOTE as u32) != 0 {
12378 for rest in chars.by_ref() {
12379 // c:7105-7108 — `if (s - sstart > *misc) (*misc)++;`
12380 if update_off {
12381 if let Some(m) = misc.as_deref_mut() {
12382 if (consumed as i32) > *m {
12383 *m += 1;
12384 }
12385 }
12386 }
12387 consumed += rest.len_utf8();
12388 result.push(rest);
12389 }
12390 }
12391 flush_tbuf(&mut result, &mut tbuf_mark, &mut ignoring);
12392 return (result, consumed); // c:7121
12393 }
12394 // c:7122-7124 — add one per output byte, checked at the last
12395 // consumed input byte.
12396 if update_off {
12397 if let Some(m) = misc.as_deref_mut() {
12398 if ((consumed - 1) as i32) < *m {
12399 *m += count;
12400 }
12401 }
12402 }
12403 // c:7127-7137 — the converted bytes are metafied (`imeta`)
12404 // on the way out. Well-formed UTF-8 is a char a `String`
12405 // holds as-is; any other byte >= 0x80 (a non-UTF-8 locale's
12406 // encoding, or a surrogate's UTF-8 form) gets Meta + byte^32.
12407 let bytes = &tbuf[..count as usize];
12408 match std::str::from_utf8(bytes) {
12409 Ok(v) => result.push_str(v),
12410 Err(_) => {
12411 for &b in bytes {
12412 if b < 0x80 {
12413 result.push(b as char);
12414 } else {
12415 result.push('\u{83}');
12416 result.push(char::from(b ^ 32));
12417 }
12418 }
12419 }
12420 }
12421 }
12422 // c:utils.c:7156-7178 — the NUMERIC-ESCAPE branch. C handles
12423 // `\x`, `\NNN` and `\0NNN` in ONE arm, and the shared tail is
12424 // what makes them agree, so this port keeps them together too:
12425 //
12426 // if ((idigit(*s) && *s < '8') || *s == 'x') {
12427 // if (!(how & GETKEY_OCTAL_ESC)) {
12428 // if (*s == '0')
12429 // s++;
12430 // else if (*s != 'x') {
12431 // *t++ = '\\', s--;
12432 // continue;
12433 // }
12434 // }
12435 // if (s[1] && s[2] && s[3]) {
12436 // svchar = s[3]; s[3] = '\0'; u = s;
12437 // }
12438 // *t++ = zstrtol(s + (*s == 'x'), &s,
12439 // (*s == 'x') ? 16 : 8);
12440 // ...
12441 // s--;
12442 // }
12443 //
12444 // Three behaviours here were each missing when these were three
12445 // separate arms:
12446 //
12447 // 1. Without GETKEY_OCTAL_ESC a NON-ZERO digit is not an escape
12448 // at all (c:7159): C emits a literal backslash and backs up so
12449 // the digit is re-read as an ordinary char. This is INSIDE the
12450 // numeric branch, so — unlike the default arm — GETKEY_EMACS
12451 // does not suppress the backslash: `${(g:e:)v}` on `\101` is
12452 // `\101`, not `101`.
12453 // 2. The value goes through `zstrtol`, which SKIPS leading blanks
12454 // and accepts a `+`/`-` sign (c:2444-2450), and whose end
12455 // pointer lands past everything it consumed — blanks included.
12456 // So `\0 1` is byte 0o1, `\x 41` is 0x04 then `1`, `\x y` is
12457 // NUL then `y`, and `\0-1` is 0xff (a NEGATIVE parse truncated
12458 // to a byte). Peeking for "is the next char a digit" stopped at
12459 // the blank and left it in the output.
12460 // 3. The base is chosen from the char AT `s` AFTER the `\0`
12461 // introducer was skipped, so `\0x41` is HEX (`A`), not NUL
12462 // followed by `x41`.
12463 //
12464 // c:7166-7168 windows the parse by NUL-ing `s[3]` when three chars
12465 // follow `s`, capping it at 3 significant chars (2 after an `x`),
12466 // then restores the byte — so `\1234` is 0o123 then `4`.
12467 //
12468 // `*t++ = zstrtol(...)` assigns a zlong to a char, truncating to
12469 // the low byte: `\777` (511) is 0xff, `\400` (256) is NUL. Parsing
12470 // straight into u8 made those an Err and emitted nothing at all.
12471 Some(d) if d.is_digit(8) || d == 'x' => {
12472 consumed += 1;
12473 // !!! BASH-MODE (no C counterpart) !!! bash keeps `\x` verbatim
12474 // in `$'...'` when no hex digit follows (zsh decodes a NUL).
12475 if bash_dollar_quote && d == 'x' && !chars.peek().is_some_and(|c| c.is_ascii_hexdigit()) {
12476 result.push_str("\\x");
12477 continue;
12478 }
12479 // c:7157-7161 — the pre-checks that run only without
12480 // GETKEY_OCTAL_ESC. `\0` is the octal introducer (`s++`);
12481 // any other bare digit is not an escape.
12482 let zero_intro = (how & GETKEY_OCTAL_ESC) == 0 && d == '0';
12483 if (how & GETKEY_OCTAL_ESC) == 0 && d != '0' && d != 'x' {
12484 // c:7159 `*t++ = '\\', s--; continue;`
12485 result.push('\\');
12486 result.push(d);
12487 continue;
12488 }
12489 // C's `s` after the pre-checks: the char AFTER the `\0`
12490 // introducer, else the matched char itself.
12491 //
12492 // Four chars of lookahead is all C can read: the window test
12493 // reaches `s[3]`, and the parse never sees past it. Bounding
12494 // the clone keeps this O(1) per escape — collecting the whole
12495 // tail here would make decoding an escape-dense string O(n²).
12496 let rest: Vec<char> = chars.clone().take(4).collect();
12497 let (p, after): (Option<char>, &[char]) = if zero_intro {
12498 (rest.first().copied(), rest.get(1..).unwrap_or(&[]))
12499 } else {
12500 (Some(d), &rest[..])
12501 };
12502 // c:7166 `if (s[1] && s[2] && s[3])` — needs three chars after
12503 // `s`; the window then spans `s` plus two more.
12504 let win: &[char] = if after.len() >= 3 { &after[..2] } else { after };
12505 let mut vis: Vec<char> = Vec::with_capacity(3);
12506 if let Some(pc) = p {
12507 vis.push(pc);
12508 }
12509 vis.extend_from_slice(win);
12510 // c:7170 `zstrtol(s + (*s == 'x'), &s, (*s == 'x') ? 16 : 8)`.
12511 let (base, skip_p) = if p == Some('x') { (16, 1) } else { (8, 0) };
12512 let parse_src: String = vis[skip_p.min(vis.len())..].iter().collect();
12513 let (num, tail) = zstrtol(&parse_src, base);
12514 let used_chars = parse_src[..parse_src.len() - tail.len()].chars().count();
12515 // Advance past what C consumed. `vis[0]` is the already-taken
12516 // match char unless the `\0` introducer moved `s` forward.
12517 let vis_used = skip_p + used_chars;
12518 let advance = if zero_intro {
12519 vis_used
12520 } else {
12521 vis_used.saturating_sub(1)
12522 };
12523 for _ in 0..advance {
12524 chars.next();
12525 consumed += 1;
12526 }
12527 let val = (num as u64 & 0xff) as u8;
12528 // c:Src/utils.c — the escape is one raw BYTE; metafy high
12529 // bytes (c:7289-7294) so `\377` unmetafies to a single 0xff,
12530 // not the UTF-8 pair c3 bf.
12531 if val < 0x80 {
12532 result.push(val as char);
12533 } else {
12534 result.push('\u{83}');
12535 result.push(char::from(val ^ 32));
12536 }
12537 // c:7172-7173 — under GETKEY_PRINTF_PERCENT a numeric escape
12538 // producing `%` gets a second `%`.
12539 if (how & crate::ported::zsh_h::GETKEY_PRINTF_PERCENT as u32) != 0 && val == b'%' {
12540 result.push('%');
12541 }
12542 }
12543 // c:utils.c:7029-7052 + c:7255-7275 — `\C` / `\M` set the
12544 // control / meta modifiers (bindkey-style key escapes), an
12545 // optional `-` separator, then the next base char (possibly
12546 // through chained `\C`/`\M`) gets the mask applied:
12547 // control → `& 0x9f` (or `\C-?` → 0x7f), meta → `| 0x80`.
12548 // Gated on GETKEY_EMACS (c:7031/7043 `if (how & GETKEY_EMACS)`):
12549 // print / print -b / $'…' have it, echo does not (so echo keeps
12550 // `\C-a` literal via the default arm). Mirrors the same handler
12551 // already present in `getkeystring` (utils.rs) and
12552 // `getkeystring_dollar_quote` (lex.rs); print's `getkeystring_with`
12553 // path lacked it, so `print "\C-a"` emitted literal `C-a`.
12554 Some(mod_letter @ ('C' | 'M')) if (how & GETKEY_EMACS) != 0 => {
12555 consumed += 1;
12556 // c:7034 `meta = 1 + control; /* preserve the order of ^ and
12557 // meta */` — meta is a COUNT, not a flag: 2 when `\M` was seen
12558 // while control was already pending, which makes c:7261-7264
12559 // apply `|0x80` BEFORE the control mask instead of after. The
12560 // two orders differ for `?`: `\M-\C-?` is 0xff, `\C-\M-?` is
12561 // 0x9f. Booleans could not express that.
12562 // A `^` already seen (c:7194 `control = 1; continue;`) arrives
12563 // here as pending state, so `^\M-a` gives meta = 1 + 1 = 2.
12564 let mut control: u32 = if pending_control {
12565 pending_control = false;
12566 1
12567 } else {
12568 0
12569 };
12570 let mut meta: u32 = 0;
12571 if mod_letter == 'C' {
12572 control = 1; // c:7046
12573 } else {
12574 meta = 1 + control; // c:7034
12575 }
12576 // c:7032-7033 / 7044-7045 — `if (s[1] == '-') s++;` consumes at
12577 // most ONE separator per modifier. Looping over a RUN of dashes
12578 // ate the base character of `\M--` (meta applied to `-`, 0xad),
12579 // leaving an empty binding.
12580 if chars.peek() == Some(&'-') {
12581 chars.next();
12582 consumed += 1;
12583 }
12584 // Chained `\C`/`\M`, and the caret form after a modifier.
12585 loop {
12586 let mut iter_clone = chars.clone();
12587 if iter_clone.next() == Some('\\') {
12588 if let Some(nx) = iter_clone.next() {
12589 if nx == 'C' || nx == 'M' {
12590 chars.next(); // consume '\'
12591 chars.next(); // consume C/M
12592 consumed += 2;
12593 if nx == 'C' {
12594 control = 1; // c:7046
12595 } else {
12596 meta = 1 + control; // c:7034
12597 }
12598 if chars.peek() == Some(&'-') {
12599 chars.next();
12600 consumed += 1;
12601 }
12602 continue;
12603 }
12604 }
12605 }
12606 // c:7194 — `else if (*s == '^' && !control && (how &
12607 // GETKEY_CTRL) && s[1]) { control = 1; continue; }`. C's
12608 // `case 'M'` only sets `meta` and `continue`s, so the MAIN
12609 // loop reaches this `^` handler with meta still pending:
12610 // `\M-^?` is control+meta applied to `?` (0xff). This arm
12611 // consumed the base char itself, so `^` became the base and
12612 // `\M-^?` bound TWO bytes (0xde 0x3f) — breaking the very
12613 // common `bindkey '\M-^?' backward-kill-word`.
12614 if control == 0 && (how & GETKEY_CTRL) != 0 && chars.peek() == Some(&'^') {
12615 let mut it2 = chars.clone();
12616 it2.next();
12617 if it2.next().is_some() {
12618 // c:7194 `s[1]` guard
12619 chars.next();
12620 consumed += 1;
12621 control = 1;
12622 continue;
12623 }
12624 }
12625 break;
12626 }
12627 // c:7044-7047 — `control = 1; continue;` (and `meta = 1 +
12628 // control`, c:7034): the modifier does not read its base
12629 // character itself. The next loop iteration decodes whatever
12630 // follows — a plain char or a full escape such as `\130` /
12631 // `\x58` / `\e` — and c:7261-7275 masks the byte it emitted.
12632 // Reading a restricted base here turned `\C-\130` into
12633 // `\C-\1` + `30` (0x11 `3` `0`) where zsh gives 0x18.
12634 pending_mask = Some((result.len(), control, meta));
12635 }
12636 // c:utils.c:7180-7184 — default arm. With GETKEY_EMACS
12637 // set, drop the backslash; otherwise keep `\<char>`.
12638 Some(c) => {
12639 consumed += 1;
12640 // c:utils.c:7045 — `\c` under GETKEY_BACKSLASH_C
12641 // means TRUNCATE: drop the rest of the input,
12642 // suppress the trailing newline. Used by `echo`
12643 // (and `print` without -r). Set TLS flag so the
12644 // caller can detect + suppress the newline.
12645 if c == 'c' && (how & GETKEY_BACKSLASH_C) != 0 {
12646 GETKEY_TRUNCATED.with(|cell| cell.set(true));
12647 break;
12648 }
12649 // !!! BASH-MODE (no C counterpart) !!! bash `$'\cX'` is the
12650 // control character (`\c?` is DEL); any other unknown escape
12651 // keeps its backslash (`$'\z'` is `\z`, zsh drops it).
12652 if bash_dollar_quote {
12653 if c == 'c' {
12654 if let Some(x) = chars.next() {
12655 consumed += 1;
12656 result.push(if x == '?' { '\u{7f}' } else { ((x.to_ascii_uppercase() as u8) ^ 0x40) as char });
12657 continue;
12658 }
12659 }
12660 if !matches!(c, '\'' | '"' | '?' | '\\') {
12661 result.push('\\');
12662 result.push(c);
12663 continue;
12664 }
12665 }
12666 if (how & GETKEY_EMACS) == 0 {
12667 // c:utils.c:7181-7183 — backslash kept literal (no EMACS),
12668 // so the string does not collapse here: undo the base
12669 // GETKEY_UPDATE_OFFSET decrement applied above.
12670 if miscadded {
12671 if let Some(m) = misc.as_deref_mut() {
12672 *m += 1;
12673 }
12674 }
12675 result.push('\\');
12676 }
12677 result.push(c);
12678 }
12679 None => {
12680 result.push('\\');
12681 }
12682 }
12683 }
12684 apply_pending_mask(&mut result, &mut pending_mask);
12685 flush_tbuf(&mut result, &mut tbuf_mark, &mut ignoring);
12686 (result, consumed)
12687}
12688
12689thread_local! {
12690 /// Sticky flag set by `getkeystring_with` when a `\c` escape
12691 /// truncates the input under `GETKEY_BACKSLASH_C`. The caller
12692 /// (bin_print / bin_echo) reads + clears via `getkey_truncated_take()`
12693 /// to suppress the trailing newline and skip any subsequent args
12694 /// in the print pass.
12695 pub static GETKEY_TRUNCATED: std::cell::Cell<bool> =
12696 const { std::cell::Cell::new(false) };
12697}
12698
12699/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12700/// standalone function.
12701/// C returns the `\c`-truncation signal through `getkeystring`'s `int *misc`
12702/// out-parameter (`Src/utils.c:6915`). Rust's `getkeystring_with` records it
12703/// in a thread-local instead; this reads and clears that flag.
12704/// Read + clear the `\c`-truncated flag set by the previous
12705/// `getkeystring_with` call. Returns true once after a truncation
12706/// fires, false otherwise.
12707pub fn getkey_truncated_take() -> bool {
12708 GETKEY_TRUNCATED.with(|c| {
12709 let v = c.get();
12710 c.set(false);
12711 v
12712 })
12713}
12714
12715/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12716/// standalone function.
12717/// Equivalent to the C expression `fdtable[fd]` — a read of the
12718/// `unsigned char *fdtable` global declared at `Src/exec.c:179` and grown by
12719/// `growfdtable` (`Src/utils.c:1965`). C indexes the array directly.
12720/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
12721/// the C expression `fdtable[fd]` (read). Returns `FDT_UNUSED` for
12722/// any fd that has not been explicitly set, matching the C source's
12723/// post-`growfdtable` zero-fill behaviour at Src/utils.c:1979.
12724pub fn fdtable_get(fd: i32) -> i32 {
12725 // c:utils.c:fdtable[fd]
12726 if fd < 0 {
12727 return FDT_UNUSED;
12728 }
12729 let g = fdtable_lock().lock().unwrap();
12730 g.get(fd as usize).copied().unwrap_or(FDT_UNUSED)
12731}
12732
12733/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12734/// standalone function.
12735/// Equivalent to the C statement `fdtable[fd] = kind;` over the
12736/// `unsigned char *fdtable` global (`Src/exec.c:179`), with the
12737/// `growfdtable` call (`Src/utils.c:1965`) inlined.
12738/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
12739/// the C statement `fdtable[fd] = kind;`. Inlines the `growfdtable`
12740/// call from Src/utils.c:1965 since C always invokes it immediately
12741/// before the assignment anyway. Also updates `MAX_ZSH_FD` to track
12742/// the highest assigned slot — C does this inside `check_fd_table`
12743/// at `Src/utils.c:1982` (`max_zsh_fd = fd;`). Previously the Rust
12744/// `fdtable_set` skipped this, leaving `max_zsh_fd = 0` after any
12745/// `addlockfd` / `addmodulefd` call, which broke `zcloselockfd`'s
12746/// `if (fd > max_zsh_fd)` guard.
12747pub fn fdtable_set(fd: i32, kind: i32) {
12748 // c:utils.c:fdtable[fd]
12749 if fd < 0 {
12750 return;
12751 }
12752 let mut g = fdtable_lock().lock().unwrap();
12753 if (fd as usize) >= g.len() {
12754 g.resize((fd as usize) + 1, FDT_UNUSED);
12755 }
12756 g[fd as usize] = kind;
12757 // c:1982 — `max_zsh_fd = fd;` (with `if (fd <= max_zsh_fd) return;`
12758 // guard at c:1971). Inline equivalent: bump only when this fd
12759 // exceeds the current max.
12760 let cur = MAX_ZSH_FD.load(Ordering::Relaxed);
12761 if fd > cur {
12762 MAX_ZSH_FD.store(fd, Ordering::Relaxed);
12763 }
12764}
12765
12766/// Port of `imeta()` from `Src/ztype.h:60`.
12767///
12768/// Port of `imeta()` macro from `Src/ztype.h:60` — `zistype(X, IMETA)`.
12769///
12770/// IMETA is set in the typtab at `Src/utils.c:4195-4201` for:
12771/// - `'\0'` (c:4195)
12772/// - `Meta` (0x83) (c:4196)
12773/// - `Marker` (0xa2) (c:4197)
12774/// - `Pound..=LAST_NORMAL_TOK` = `0x84..=0x9c` (c:4198)
12775/// - `Snull..=Nularg` = `0x9d..=0xa1` (c:4200)
12776///
12777/// The CANONICAL set is `{0x00, 0x83..=0xa2}`. The previous Rust
12778/// port used `b >= 0x83` (every byte 0x83..=0xff) which was WRONG
12779/// for bytes `0xa3..=0xff`: C `imeta()` returns false for those,
12780/// so metafy passes them through unchanged. Rust's broader range
12781/// caused `metafy` to corrupt UTF-8 multibyte content (e.g.,
12782/// `é` = `0xc3 0xa9`: both bytes are NOT IMETA in C, but Rust
12783/// escaped both as `Meta + (byte ^ 32)`, mangling the encoding
12784/// and breaking every downstream consumer that expects the raw
12785/// UTF-8 bytes to round-trip).
12786///
12787/// Use the closed-range form rather than the typtab lookup so
12788/// `metafy` / `imeta_byte` work in test contexts where the typtab
12789/// isn't initialised, AND so the fast-path doesn't go through a
12790/// Mutex lock per byte (`metafy` is called per-character on every
12791/// shell input line).
12792#[inline]
12793pub fn imeta_byte(b: u8) -> bool {
12794 // c:4195-4201 — canonical IMETA range.
12795 b == 0 || (0x83..=0xa2).contains(&b)
12796}
12797
12798// `pub fn convfloat` and `pub fn convfloat_underscore` re-export
12799// wrappers DELETED per PORT.md Rule C. C defines both in
12800// `Src/params.c:5690` and `:5765` — the canonical Rust home is
12801// `src/ported/params.rs` (`convfloat` at line 7356,
12802// `convfloat_underscore` matching). Caller convenience cost is
12803// `use crate::ported::params::convfloat;` instead of `utils::`;
12804// no Rust-side re-export needed.
12805
12806// ===========================================================
12807// Methods moved verbatim from src/ported/vm_helper because their
12808// C counterpart's source file maps 1:1 to this Rust module.
12809// Phase: drift
12810// ===========================================================
12811
12812// BEGIN moved-from-exec-rs
12813// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
12814
12815// END moved-from-exec-rs
12816
12817// ===========================================================
12818// Free ported moved verbatim from src/ported/vm_helper.
12819// ===========================================================
12820// BEGIN moved-from-exec-rs (free ported)
12821/// Port of `base64_decode()` from `Src/Zle/termquery.c:571`.
12822/// The only base64 decoder in zsh's C tree — it is NOT a utils.c function.
12823///
12824/// !!! DIVERGENT DUPLICATE !!! The faithful line-by-line port of that C body
12825/// lives at `src/ported/zle/termquery.rs:451`. This copy arrived with the
12826/// `vm_helper` move (see the marker above), decodes in fixed 4-byte chunks
12827/// rather than C's byte-at-a-time `while (len && (c = src[i]) != '=')` loop,
12828/// and has one caller (`src/fusevm_bridge.rs:11864`).
12829pub(crate) fn base64_decode(s: &str) -> Vec<u8> {
12830 let decode_char = |c: u8| -> Option<u8> {
12831 match c {
12832 b'A'..=b'Z' => Some(c - b'A'),
12833 b'a'..=b'z' => Some(c - b'a' + 26),
12834 b'0'..=b'9' => Some(c - b'0' + 52),
12835 b'+' => Some(62),
12836 b'/' => Some(63),
12837 _ => None,
12838 }
12839 };
12840 let bytes = s.as_bytes();
12841 let mut out = Vec::with_capacity(s.len() / 4 * 3);
12842 let mut i = 0;
12843 while i + 4 <= bytes.len() {
12844 let chunk = &bytes[i..i + 4];
12845 let pad = chunk.iter().filter(|&&c| c == b'=').count();
12846 let v0 = decode_char(chunk[0]).unwrap_or(0) as u32;
12847 let v1 = decode_char(chunk[1]).unwrap_or(0) as u32;
12848 let v2 = decode_char(chunk[2]).unwrap_or(0) as u32;
12849 let v3 = decode_char(chunk[3]).unwrap_or(0) as u32;
12850 let n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
12851 out.push(((n >> 16) & 0xff) as u8);
12852 if pad < 2 {
12853 out.push(((n >> 8) & 0xff) as u8);
12854 }
12855 if pad < 1 {
12856 out.push((n & 0xff) as u8);
12857 }
12858 i += 4;
12859 }
12860 out
12861}
12862// END moved-from-exec-rs (free ported)
12863
12864// ===========================================================
12865// Utility helpers moved from src/ported/vm_helper.
12866// All correspond to Src/utils.c logic (path/string/bslashquote helpers).
12867// ===========================================================
12868
12869// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12870// ─── RUST-ONLY ACCESSORS ───
12871//
12872// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
12873// RwLock<T>>` globals declared above. C zsh uses direct global
12874// access; Rust needs these wrappers because `OnceLock::get_or_init`
12875// is the only way to lazily construct shared state. These ported sit
12876// here so the body of this file reads in C source order without
12877// the accessor wrappers interleaved between real port ported.
12878// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12879
12880// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12881// ─── RUST-ONLY ACCESSORS ───
12882//
12883// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
12884// RwLock<T>>` globals declared above. C zsh uses direct global
12885// access; Rust needs these wrappers because `OnceLock::get_or_init`
12886// is the only way to lazily construct shared state. These ported sit
12887// here so the body of this file reads in C source order without
12888// the accessor wrappers interleaved between real port ported.
12889// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12890
12891// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12892// ─── RUST-ONLY ACCESSORS ───
12893//
12894// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
12895// RwLock<T>>` globals declared above. C zsh uses direct global
12896// access; Rust needs these wrappers because `OnceLock::get_or_init`
12897// is the only way to lazily construct shared state. These ported sit
12898// here so the body of this file reads in C source order without
12899// the accessor wrappers interleaved between real port ported.
12900// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12901
12902// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12903// ─── RUST-ONLY ACCESSORS ───
12904//
12905// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
12906// RwLock<T>>` globals declared above. C zsh uses direct global
12907// access; Rust needs these wrappers because `OnceLock::get_or_init`
12908// is the only way to lazily construct shared state. These ported sit
12909// here so the body of this file reads in C source order without
12910// the accessor wrappers interleaved between real port ported.
12911// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12912
12913// WARNING: NOT IN UTILS.C — Rust-only OnceLock get-or-init
12914// helpers. C dereferences each global directly.
12915/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12916/// standalone function.
12917/// `OnceLock` get-or-init accessor for the storage behind C's
12918/// `char *scriptname` global (`Src/utils.c:36`). C dereferences the global.
12919fn scriptname_lock() -> &'static crate::thread_shell_state::ThreadMutex<Option<String>> {
12920 &SCRIPTNAME
12921}
12922
12923// WARNING: NOT IN UTILS.C — see scriptname_lock above.
12924/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12925/// standalone function.
12926/// `OnceLock` get-or-init accessor for the storage behind C's
12927/// `char *argzero` global (`Src/params.c:72`).
12928fn argzero_lock() -> &'static Mutex<Option<String>> {
12929 ARGZERO.get_or_init(|| Mutex::new(None))
12930}
12931
12932// WARNING: NOT IN UTILS.C — see scriptname_lock above.
12933/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12934/// standalone function.
12935/// `OnceLock` get-or-init accessor for the storage behind C's
12936/// `char *scriptfilename` global (`Src/utils.c:41`).
12937fn scriptfilename_lock() -> &'static crate::thread_shell_state::ThreadMutex<Option<String>> {
12938 &SCRIPTFILENAME
12939}
12940
12941// WARNING: NOT IN UTILS.C — Rust-only OnceLock accessor for `posixzero`.
12942// C's `posixzero` lives in params.c:76; the canonical storage lives
12943// here for OnceLock initialisation parity with `argzero`.
12944/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12945/// standalone function.
12946/// `OnceLock` get-or-init accessor for the storage behind C's
12947/// `char *posixzero` global (`Src/params.c:76`).
12948fn posixzero_lock() -> &'static Mutex<Option<String>> {
12949 POSIXZERO.get_or_init(|| Mutex::new(None))
12950}
12951
12952// WARNING: NOT IN UTILS.C — see scriptname_lock above.
12953/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12954/// standalone function.
12955/// `OnceLock` get-or-init accessor for the storage behind C's
12956/// `int noerrs` global (`Src/exec.c:117`).
12957pub fn noerrs_lock() -> &'static Mutex<i32> {
12958 NOERRS.get_or_init(|| Mutex::new(0))
12959}
12960
12961// `locallevel_lock` removed — duplicate of canonical
12962// `LOCALLEVEL` (port of params.c:54). All
12963// accessors here now route through the canonical AtomicI32.
12964// WARNING: NOT IN UTILS.C — see scriptname_lock above.
12965/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12966/// standalone function.
12967/// `OnceLock` get-or-init accessor for the storage behind C's
12968/// `zlong lineno` global (`Src/params.c:123`).
12969fn lineno_lock() -> &'static Mutex<i32> {
12970 LINENO.get_or_init(|| Mutex::new(0))
12971}
12972
12973// WARNING: NOT IN UTILS.C — Rust-only cache for the SHINSTDIN
12974// option flag so error-emission doesn't pull in the option-table.
12975/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12976/// standalone function.
12977/// `OnceLock` get-or-init accessor for the SHINSTDIN option cache; the
12978/// option itself is table entry `Src/options.c:248`.
12979fn shinstdin_lock() -> &'static Mutex<bool> {
12980 SHINSTDIN_OPT.get_or_init(|| Mutex::new(false))
12981}
12982
12983/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
12984/// standalone function.
12985/// `OnceLock` get-or-init accessor for the storage behind C's
12986/// `unsigned char *fdtable` global (`Src/exec.c:179`).
12987/// !!! RUST-ONLY HELPER — see WARNING block above. C source uses bare
12988/// `unsigned char *fdtable` global from Src/utils.c:~63.
12989fn fdtable_lock() -> &'static Mutex<Vec<i32>> {
12990 FDTABLE.get_or_init(|| Mutex::new(Vec::new()))
12991}
12992
12993#[cfg(test)]
12994mod tests {
12995 use super::*;
12996
12997 /// checkmailpath port (utils.c:1620). Drives mtime/atime/size through
12998 /// `libc::utimes` so the new-mail condition is deterministic in headless CI.
12999 #[test]
13000 fn checkmailpath_new_mail_conditions() {
13001 use std::io::Write as _;
13002 let _g = crate::test_util::global_state_lock();
13003
13004 // Unique temp file with content (size != 0).
13005 let dir = std::env::temp_dir();
13006 let path = dir.join(format!("zshrs_mailtest_{}", std::process::id()));
13007 {
13008 let mut f = std::fs::File::create(&path).unwrap();
13009 f.write_all(b"new message\n").unwrap();
13010 }
13011 let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
13012 // atime (times[0]) <= mtime (times[1]); both well past 0.
13013 let set_times = |atime: i64, mtime: i64| {
13014 let tv = [
13015 libc::timeval {
13016 tv_sec: atime as libc::time_t,
13017 tv_usec: 0,
13018 },
13019 libc::timeval {
13020 tv_sec: mtime as libc::time_t,
13021 tv_usec: 0,
13022 },
13023 ];
13024 assert_eq!(unsafe { libc::utimes(cpath.as_ptr(), tv.as_ptr()) }, 0);
13025 };
13026
13027 let saved_shout = *crate::ported::init::shout.lock().unwrap();
13028 let saved_lmc = LAST_MAILCHECK.load(Ordering::Relaxed);
13029 *crate::ported::init::shout.lock().unwrap() = 1; // interactive
13030 LAST_MAILCHECK.store(0, Ordering::Relaxed);
13031
13032 let p = path.to_str().unwrap().to_string();
13033
13034 // c:1671-1675 — unread + newer than last check → "You have new mail."
13035 set_times(1_000_000, 2_000_000);
13036 assert_eq!(
13037 checkmailpath(&[p.clone()]),
13038 vec!["You have new mail.".to_string()]
13039 );
13040
13041 // c:1676-1698 — `PATH?message`: the message (here a literal) is emitted.
13042 assert_eq!(
13043 checkmailpath(&[format!("{}?custom note", p)]),
13044 vec!["custom note".to_string()]
13045 );
13046
13047 // c:1672 — mtime older than the last check → nothing.
13048 LAST_MAILCHECK.store(9_000_000, Ordering::Relaxed);
13049 assert!(checkmailpath(&[p.clone()]).is_empty());
13050
13051 // c:1670 — non-interactive (no shout) → nothing, even when fresh.
13052 LAST_MAILCHECK.store(0, Ordering::Relaxed);
13053 *crate::ported::init::shout.lock().unwrap() = 0;
13054 assert!(checkmailpath(&[p.clone()]).is_empty());
13055
13056 // c:1634-1636 — empty component is reported (stderr), yields no message.
13057 *crate::ported::init::shout.lock().unwrap() = 1;
13058 assert!(checkmailpath(&["?orphan".to_string()]).is_empty());
13059
13060 *crate::ported::init::shout.lock().unwrap() = saved_shout;
13061 LAST_MAILCHECK.store(saved_lmc, Ordering::Relaxed);
13062 let _ = std::fs::remove_file(&path);
13063 }
13064
13065 /// zsh_errno_msg / zerrmsg `%e` rendering (utils.c:348-365). EINTR is the
13066 /// literal "interrupt"; EIO is verbatim strerror (keeps its capital); every
13067 /// other errno lowercases the first letter (`tulower`). The old
13068 /// io::Error-based path kept the capital and appended " (os error N)".
13069 #[test]
13070 fn zsh_errno_msg_matches_c_percent_e() {
13071 // c:355-358 — EINTR → "interrupt".
13072 assert_eq!(zsh_errno_msg(libc::EINTR), "interrupt");
13073
13074 // c:366 — non-EIO errno lowercases the first letter and never carries
13075 // the Rust "(os error N)" suffix.
13076 let acc = zsh_errno_msg(libc::EACCES);
13077 assert!(
13078 acc.chars().next().is_some_and(|c| c.is_lowercase()),
13079 "EACCES first letter must be lowercased: {acc:?}"
13080 );
13081 assert!(
13082 !acc.contains("(os error"),
13083 "must be strerror, not io::Error: {acc:?}"
13084 );
13085
13086 // c:359-364 — EIO is emitted verbatim (its message reads wrong
13087 // lowercased), so its first letter stays capitalized.
13088 let eio = zsh_errno_msg(libc::EIO);
13089 assert!(
13090 eio.chars().next().is_some_and(|c| c.is_uppercase()),
13091 "EIO must be verbatim strerror (capitalized): {eio:?}"
13092 );
13093 }
13094
13095 /// c:351-354 — zerrmsg's `%e` arm sets errflag |= ERRFLAG_ERROR on EINTR.
13096 #[test]
13097 fn zerrmsg_eintr_sets_errflag() {
13098 let _g = crate::test_util::global_state_lock();
13099 let saved = errflag.load(Ordering::Relaxed);
13100 errflag.store(0, Ordering::Relaxed);
13101 zerrmsg("test", Some(libc::EINTR)); // writes to stderr; side-effect is the flag
13102 assert_ne!(
13103 errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
13104 0,
13105 "EINTR must set ERRFLAG_ERROR"
13106 );
13107 // A non-EINTR errno must NOT set the flag.
13108 errflag.store(0, Ordering::Relaxed);
13109 zerrmsg("test", Some(libc::EACCES));
13110 assert_eq!(errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR, 0);
13111 errflag.store(saved, Ordering::Relaxed);
13112 }
13113
13114 /// zjoin port (utils.c:3622). ASCII delimiters join byte-for-byte; a meta
13115 /// delimiter (NUL is `imeta` per inittyptab c:4195) must be written in
13116 /// metafied form (`Meta` + `delim^32`), never as a raw byte.
13117 #[test]
13118 fn zjoin_ascii_and_meta_delims() {
13119 let v = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
13120
13121 // c:3632-3641 — ordinary (non-meta) delimiter: plain interposition.
13122 assert_eq!(zjoin(&v(&["a", "b", "c"]), ' '), "a b c");
13123 assert_eq!(zjoin(&v(&["foo"]), ':'), "foo"); // single elem, no delim
13124 // c:3629-3630 — empty array → "".
13125 assert_eq!(zjoin(&v(&[]), ' '), "");
13126
13127 // c:3634-3637 — NUL is imeta, so the separator is metafied, NOT a raw
13128 // NUL byte (the prior `arr.join("\0")` bug). The Meta byte (0x83) can't
13129 // survive the UTF-8 String boundary, but the raw NUL must be gone.
13130 let joined = zjoin(&v(&["a", "b"]), '\0');
13131 assert!(
13132 !joined.as_bytes().contains(&0u8),
13133 "NUL delim must be metafied, not emitted as a raw NUL byte: {joined:?}"
13134 );
13135 assert_ne!(joined, "a\u{0}b", "must not raw-join on a meta delimiter");
13136 assert!(joined.starts_with('a') && joined.ends_with('b'));
13137 }
13138
13139 /// findsep port (utils.c:3784). Pure function — no global/cwd state.
13140 #[test]
13141 fn findsep_default_ifs_separators() {
13142 inittyptab(); // ISEP bits are set by the type table (c:4155)
13143 // c:3814-3817 — advance to the first ISEP char, return 1.
13144 let mut s = "foo bar".to_string();
13145 let mut pos = 0usize;
13146 assert_eq!(findsep(&mut s, &mut pos, None, false), 1);
13147 assert_eq!(pos, 3); // points at the space
13148 assert_eq!(&s[..pos], "foo");
13149
13150 // c:3821 — already sitting on a separator: return 0, no advance.
13151 let mut s = " foo".to_string();
13152 let mut pos = 0usize;
13153 assert_eq!(findsep(&mut s, &mut pos, None, false), 0);
13154 assert_eq!(pos, 0);
13155 }
13156
13157 #[test]
13158 fn findsep_quote_strips_escaped_separator() {
13159 inittyptab(); // ISEP bits are set by the type table (c:4155)
13160 // c:3795-3804 — `\<sep>` is not a separator; the backslash is
13161 // stripped in place and the bare char is consumed into the word.
13162 let mut s = "foo\\ bar".to_string(); // foo<bslash><space>bar
13163 let mut pos = 0usize;
13164 let r = findsep(&mut s, &mut pos, None, true);
13165 assert_eq!(r, 1);
13166 assert_eq!(s, "foo bar"); // backslash removed
13167 assert_eq!(pos, s.len()); // whole thing is one word, no real sep
13168 }
13169
13170 #[test]
13171 fn findsep_quote_collapses_double_backslash() {
13172 // c:3796-3799 — `\\` collapses to a single literal backslash.
13173 let mut s = "a\\\\b".to_string(); // a <bslash><bslash> b
13174 let mut pos = 0usize;
13175 let r = findsep(&mut s, &mut pos, None, true);
13176 assert_eq!(r, 1);
13177 assert_eq!(s, "a\\b"); // one backslash left
13178 assert_eq!(pos, s.len()); // no separator present
13179 }
13180
13181 #[test]
13182 fn findsep_literal_multichar_separator() {
13183 // c:3836-3841 — explicit multi-byte literal separator.
13184 let mut s = "a::b".to_string();
13185 let mut pos = 0usize;
13186 assert_eq!(findsep(&mut s, &mut pos, Some(b"::"), false), 1);
13187 assert_eq!(pos, 1); // points at the "::"
13188 assert_eq!(&s[..pos], "a");
13189
13190 // c:3844 — separator absent → -1.
13191 let mut s = "abc".to_string();
13192 let mut pos = 0usize;
13193 assert_eq!(findsep(&mut s, &mut pos, Some(b"x"), false), -1);
13194 }
13195
13196 #[test]
13197 fn findsep_empty_separator_advances_one_char() {
13198 // c:3825-3834 — empty sep just steps past one character.
13199 let mut s = "ab".to_string();
13200 let mut pos = 0usize;
13201 assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), 1);
13202 assert_eq!(pos, 1);
13203
13204 let mut s = String::new();
13205 let mut pos = 0usize;
13206 assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), -1); // c:3833
13207 }
13208
13209 /// c:4033 — `subst_string_by_func` returns `getaparam("reply")`
13210 /// after the hook function finishes. The previous Rust port read
13211 /// `env::var("reply")` and split on NUL — wrong because `reply`
13212 /// is a shell-local PM_ARRAY in paramtab, never exported. This
13213 /// pin sets `reply` via `setaparam` (the paramtab write path)
13214 /// and exercises `subst_string_by_hook` end-to-end isn't viable
13215 /// in unit tests without a function-name hook, so we exercise
13216 /// just the getaparam plumbing.
13217 #[test]
13218 fn getaparam_reads_reply_from_paramtab_not_env() {
13219 let _g = crate::test_util::global_state_lock();
13220 // Stash any prior `reply` value.
13221 let saved = getaparam("reply");
13222
13223 // Write a known reply array via the canonical setaparam path.
13224 let payload = vec!["abbreviated".to_string(), "11".to_string()];
13225 let _ = setaparam("reply", payload.clone());
13226
13227 // The reply array must be reachable via getaparam — not env.
13228 // (env::var would return Err because setaparam never exports
13229 // an un-flagged array to env.)
13230 assert_eq!(
13231 getaparam("reply"),
13232 Some(payload),
13233 "getaparam(\"reply\") must return paramtab array"
13234 );
13235
13236 // Restore.
13237 let _ = setaparam("reply", saved.unwrap_or_default());
13238 }
13239
13240 /// c:1133-1134 — `finddir` reads the global `home` variable (the
13241 /// canonical `$HOME` storage, not `getenv("HOME")`). The global
13242 /// is updated by `homesetfn` (Src/params.c:5118) whenever the user
13243 /// assigns `HOME=...` inside the shell.
13244 /// Regression target: a previous Rust port read `env::var("HOME")`
13245 /// so an in-shell `HOME=...` assignment would not retarget
13246 /// `~`-abbreviation until the user re-exported HOME.
13247 #[test]
13248 fn finddir_uses_paramtab_home_not_env() {
13249 let _g = crate::test_util::global_state_lock();
13250 // C's `homesetfn` (params.c:5118) UNUSED(Param pm) — the
13251 // function ignores its param-pointer argument and writes the
13252 // canonical `char *home` global directly. zshrs's port mirrors
13253 // that: `params::homesetfn(_pm, x)` ignores _pm and updates
13254 // `home_lock()`. So we pass a stack-default param; the paramtab
13255 // wiring (PM_SPECIAL dispatch) is not on the test path.
13256 let mut pm = crate::ported::zsh_h::param::default();
13257 let saved = crate::ported::params::homegetfn(&pm);
13258 let sentinel = "/tmp/zshrs-finddir-pin".to_string();
13259 homesetfn(&mut pm, sentinel.clone());
13260
13261 // `/tmp/zshrs-finddir-pin/x` must abbreviate to `~/x`.
13262 let abbrev = finddir(&format!("{}/x", sentinel));
13263 assert_eq!(
13264 abbrev.as_deref(),
13265 Some("~/x"),
13266 "finddir must consult canonical HOME (got {:?})",
13267 abbrev
13268 );
13269
13270 // Restore.
13271 homesetfn(&mut pm, saved);
13272 }
13273
13274 #[test]
13275 fn test_sepsplit() {
13276 let _g = crate::test_util::global_state_lock();
13277 assert_eq!(sepsplit("a:b:c", Some(":"), false), vec!["a", "b", "c"]);
13278 assert_eq!(sepsplit("a::b", Some(":"), false), vec!["a", "b"]);
13279 assert_eq!(sepsplit("a::b", Some(":"), true), vec!["a", "", "b"]);
13280 }
13281
13282 #[test]
13283 fn test_unmetafy_no_meta_byte_passes_through() {
13284 let _g = crate::test_util::global_state_lock();
13285 // No Meta byte → buffer unchanged, length unchanged.
13286 let mut buf = b"hello".to_vec();
13287 let n = unmetafy(&mut buf);
13288 assert_eq!(n, 5);
13289 assert_eq!(&buf, b"hello");
13290 }
13291
13292 #[test]
13293 fn test_unmetafy_collapses_meta_escapes() {
13294 let _g = crate::test_util::global_state_lock();
13295 // C: Meta byte (0x83) followed by `'a' ^ 32` (0x41 = 'A')
13296 // unmetafies to a single byte 'a' (0x61).
13297 // i.e. {0x83, 'a' ^ 32} → {'a'}.
13298 let mut buf = vec![0x83, b'a' ^ 32];
13299 let n = unmetafy(&mut buf);
13300 assert_eq!(n, 1);
13301 assert_eq!(buf, vec![b'a']);
13302 }
13303
13304 #[test]
13305 fn test_unmetafy_mixed_prefix_then_meta() {
13306 let _g = crate::test_util::global_state_lock();
13307 // Plain prefix, then Meta-escaped 0xFF (0x83, 0xFF ^ 32 = 0xDF).
13308 let mut buf = vec![b'X', b'Y', 0x83, 0xFF ^ 32, b'Z'];
13309 let n = unmetafy(&mut buf);
13310 assert_eq!(n, 4);
13311 assert_eq!(buf, vec![b'X', b'Y', 0xFF, b'Z']);
13312 }
13313
13314 #[test]
13315 fn test_unmetafy_returns_self_value() {
13316 let _g = crate::test_util::global_state_lock();
13317 // C returns `s` (the buffer) for chaining; Rust returns
13318 // the new length. Verify length matches a call that
13319 // collapses two Meta-escapes.
13320 let mut buf = vec![
13321 b'A',
13322 0x83,
13323 b'B' ^ 32, // → 'B'
13324 0x83,
13325 b'C' ^ 32, // → 'C'
13326 b'D',
13327 ];
13328 let n = unmetafy(&mut buf);
13329 assert_eq!(n, 4);
13330 assert_eq!(buf, b"ABCD".to_vec());
13331 }
13332
13333 #[test]
13334 fn test_imeta_byte_threshold() {
13335 let _g = crate::test_util::global_state_lock();
13336 // Canonical IMETA per Src/utils.c:4195-4201:
13337 // - 0x00 (c:4195)
13338 // - 0x83..=0xa2 (Meta through Marker — c:4196-4200)
13339 //
13340 // The previous version of this test asserted `imeta_byte(0xFF) == true`
13341 // based on the WRONG `b >= Meta` predicate the Rust port
13342 // originally used. C `imeta()` reads the typtab; 0xa3..=0xff
13343 // have NO typtab assignment so they're NOT IMETA, and
13344 // `metafy` must NOT escape them.
13345 assert!(imeta_byte(0x00), "c:4195 — NUL is IMETA");
13346 assert!(!imeta_byte(0x82), "0x82 is NOT IMETA (below Meta)");
13347 assert!(imeta_byte(Meta), "c:4196 — Meta (0x83) is IMETA");
13348 assert!(imeta_byte(0xa2), "c:4197 — Marker (0xa2) is IMETA");
13349 // c:4198-4200 upper bound — Nularg is the last IMETA byte.
13350 assert!(imeta_byte(0xa1), "c:4200 — Nularg (0xa1) is IMETA");
13351 // 0xa3..=0xff are NOT IMETA — UTF-8 multi-byte content
13352 // must pass through `metafy` unchanged.
13353 assert!(!imeta_byte(0xa3), "0xa3 NOT IMETA (above Marker)");
13354 assert!(!imeta_byte(0xc3), "0xc3 NOT IMETA (UTF-8 'é' lead)");
13355 assert!(!imeta_byte(0xFF), "0xFF NOT IMETA");
13356 }
13357
13358 #[test]
13359 fn test_meta_constant_value() {
13360 let _g = crate::test_util::global_state_lock();
13361 // Locked at 0x83 by Src/zsh.h. If this test fails, zsh
13362 // bumped the Meta sentinel and the encoding mapping needs
13363 // a full audit.
13364 assert_eq!(Meta, 0x83);
13365 }
13366
13367 #[test]
13368 #[cfg(unix)]
13369 fn test_mode_to_octal_canonical_bits() {
13370 let _g = crate::test_util::global_state_lock();
13371 // rwx for owner = 0o700.
13372 let mode = (S_IRUSR | S_IWUSR | S_IXUSR) as u32;
13373 assert_eq!(mode_to_octal(mode), 0o700);
13374 // rwx all = 0o777.
13375 let all = (S_IRUSR | S_IWUSR | S_IXUSR) as u32 * (1 + 8 + 64);
13376 // Use libc constants individually for portability.
13377 let m = (S_IRUSR
13378 | S_IWUSR
13379 | S_IXUSR
13380 | S_IRGRP
13381 | S_IWGRP
13382 | S_IXGRP
13383 | S_IROTH
13384 | S_IWOTH
13385 | S_IXOTH) as u32;
13386 assert_eq!(mode_to_octal(m), 0o777);
13387 let _ = all;
13388 }
13389
13390 #[test]
13391 #[cfg(unix)]
13392 fn test_mode_to_octal_setuid_setgid_sticky() {
13393 let _g = crate::test_util::global_state_lock();
13394 assert_eq!(mode_to_octal(S_ISUID as u32), 0o4000);
13395 assert_eq!(mode_to_octal(S_ISGID as u32), 0o2000);
13396 assert_eq!(mode_to_octal(S_ISVTX as u32), 0o1000);
13397 // All three: 0o7000.
13398 let all = (S_ISUID | S_ISGID | S_ISVTX) as u32;
13399 assert_eq!(mode_to_octal(all), 0o7000);
13400 }
13401
13402 #[test]
13403 #[cfg(unix)]
13404 fn test_mailstat_plain_file_returns_native_stat() {
13405 let _g = crate::test_util::global_state_lock();
13406 // Plain file path → *st fields mirror native stat,
13407 // not the maildir aggregation.
13408 let mut st: libc::stat = unsafe { std::mem::zeroed() };
13409 let rc = mailstat("/etc/hosts", &mut st);
13410 if rc == 0 {
13411 assert_eq!(
13412 st.st_nlink as u64,
13413 fs::metadata("/etc/hosts").unwrap().nlink()
13414 );
13415 }
13416 }
13417
13418 #[test]
13419 fn test_mailstat_nonexistent_returns_neg1() {
13420 let _g = crate::test_util::global_state_lock();
13421 let mut st: libc::stat = unsafe { std::mem::zeroed() };
13422 assert_eq!(mailstat("/nonexistent/path/does/not/exist", &mut st), -1);
13423 }
13424
13425 #[test]
13426 fn test_mailstat_directory_without_maildir_subdirs() {
13427 let _g = crate::test_util::global_state_lock();
13428 // /tmp is a directory but not a maildir (no cur/tmp/new) —
13429 // returns the partial aggregate (top dir's atime/mtime,
13430 // size=0 since cur/ wasn't found before we'd start summing).
13431 let mut st: libc::stat = unsafe { std::mem::zeroed() };
13432 let rc = mailstat("/tmp", &mut st);
13433 assert_eq!(rc, 0);
13434 assert_eq!(st.st_nlink, 1);
13435 assert_eq!(st.st_size, 0);
13436 assert_eq!(st.st_blocks, 0);
13437 // S_IFDIR bit should be cleared, S_IFREG set.
13438 #[cfg(unix)]
13439 {
13440 assert_eq!(st.st_mode & libc::S_IFDIR, 0);
13441 assert_ne!(st.st_mode & libc::S_IFREG, 0);
13442 }
13443 }
13444
13445 #[test]
13446 fn test_dupstrpfx_byte_counted() {
13447 let _g = crate::test_util::global_state_lock(); // c:161
13448 // 5 bytes of ASCII = 5 chars, identical. Canonical
13449 // port lives at `string.rs:161`; pin from utils.rs's test
13450 // module via the qualified path.
13451 assert_eq!(dupstrpfx("hello", 3), "hel"); // c:161
13452 assert_eq!(dupstrpfx("hi", 10), "hi"); // c:161
13453 assert_eq!(dupstrpfx("anything", 0), ""); // c:161
13454 }
13455
13456 #[test]
13457 fn test_metafy_passes_through_ascii() {
13458 let _g = crate::test_util::global_state_lock();
13459 // ASCII bytes (< 0x83) stay untouched.
13460 assert_eq!(metafy("hello"), "hello");
13461 assert_eq!(metafy(""), "");
13462 }
13463
13464 #[test]
13465 fn test_metafy_imeta_predicate_matches_c_macro() {
13466 let _g = crate::test_util::global_state_lock();
13467 // Canonical C IMETA per Src/utils.c:4195-4201:
13468 // - 0x00 (c:4195)
13469 // - 0x83 (Meta, c:4196)
13470 // - 0x84..=0x9c (Pound..LAST_NORMAL_TOK=Bang, c:4198)
13471 // - 0x9d..=0xa1 (Snull..Nularg, c:4200)
13472 // - 0xa2 (Marker, c:4197)
13473 //
13474 // The closed set is {0x00, 0x83..=0xa2}. Every other byte
13475 // (0x01..=0x82, 0xa3..=0xff) is NOT IMETA in C and so must
13476 // NOT be Meta-encoded by `metafy`. The previous Rust
13477 // hardcoded predicate `b >= 0x83` falsely marked
13478 // 0xa3..=0xff as IMETA, corrupting UTF-8 multibyte
13479 // content.
13480
13481 // NUL is IMETA (c:4195).
13482 assert!(imeta_byte(0x00), "c:4195 — '\\0' IS imeta");
13483
13484 // 0x01..=0x82 are NOT IMETA (no typtab assignment for them).
13485 for b in 0x01u8..=0x82 {
13486 assert!(!imeta_byte(b), "byte {:#x} should NOT be imeta", b);
13487 }
13488
13489 // 0x83..=0xa2 ARE IMETA (the canonical full range).
13490 for b in 0x83u8..=0xa2 {
13491 assert!(imeta_byte(b), "c:4196-4200 — byte {:#x} IS imeta", b);
13492 }
13493
13494 // 0xa3..=0xff are NOT IMETA. C `imeta()` returns false
13495 // for these so `metafy` must pass them through unchanged.
13496 // UTF-8 continuation bytes (0x80..=0xbf) and multi-byte
13497 // leads (0xc0..=0xff) live here and must round-trip.
13498 for b in 0xa3u8..=0xff {
13499 assert!(
13500 !imeta_byte(b),
13501 "byte {:#x} should NOT be imeta (no c:4195-4201 assignment)",
13502 b
13503 );
13504 }
13505 }
13506
13507 /// Pin: `metafy` passes UTF-8 multibyte bytes through unchanged
13508 /// when they fall outside the canonical IMETA range. Previously
13509 /// the broader `b >= 0x83` predicate corrupted every UTF-8
13510 /// continuation byte and lead byte that wasn't a token marker.
13511 #[test]
13512 fn metafy_preserves_utf8_high_bytes_outside_imeta_range() {
13513 let _g = crate::test_util::global_state_lock();
13514 // 'é' = U+00E9 = UTF-8 0xc3 0xa9. Both bytes are >= 0x83 BUT
13515 // both are also > 0xa2, so they're NOT IMETA per the typtab.
13516 // C `metafy` passes them through unchanged.
13517 let input = std::str::from_utf8(&[0xC3, 0xA9]).unwrap();
13518 let out = metafy(input);
13519 // Should round-trip the two bytes exactly (no Meta escape).
13520 let out_bytes = out.as_bytes();
13521 assert_eq!(
13522 out_bytes,
13523 &[0xC3, 0xA9],
13524 "c:4196-4200 — UTF-8 bytes 0xc3/0xa9 outside IMETA range must pass through"
13525 );
13526 }
13527
13528 #[test]
13529 fn test_ztrcmp_meta_aware() {
13530 let _g = crate::test_util::global_state_lock();
13531 // Two identical metafied strings → Equal.
13532 assert_eq!(ztrcmp("foo", "foo"), std::cmp::Ordering::Equal);
13533 // "foo" < "foz".
13534 assert_eq!(ztrcmp("foo", "foz"), std::cmp::Ordering::Less);
13535 // Prefix comparison: shorter < longer.
13536 assert_eq!(ztrcmp("foo", "foobar"), std::cmp::Ordering::Less);
13537 // Meta-encoded comparison: {0x83, 'a'^32} should compare as 'a'.
13538 let s_meta = unsafe { std::str::from_utf8_unchecked(&[0x83, b'a' ^ 32]) };
13539 let s_plain = "a";
13540 // Meta-encoded "a" should compare equal to plain "a".
13541 assert_eq!(ztrcmp(s_meta, s_plain), std::cmp::Ordering::Equal);
13542 }
13543
13544 #[test]
13545 fn test_skipwsep_skips_runs() {
13546 let _g = crate::test_util::global_state_lock();
13547 // 3 spaces + 'x' → returns ("x", 3).
13548 let (rest, n) = skipwsep(" x");
13549 assert_eq!(rest, "x");
13550 assert_eq!(n, 3);
13551 // No leading whitespace → 0 skipped.
13552 let (rest, n) = skipwsep("foo");
13553 assert_eq!(rest, "foo");
13554 assert_eq!(n, 0);
13555 // Mix of space/tab/newline.
13556 let (rest, n) = skipwsep(" \t\nbar");
13557 assert_eq!(rest, "bar");
13558 assert_eq!(n, 3);
13559 }
13560
13561 #[test]
13562 fn test_imeta_macro_threshold() {
13563 let _g = crate::test_util::global_state_lock(); // c:60
13564 // `Src/ztype.h:60` `imeta(X) zistype(X, IMETA)` — typtab-driven
13565 // predicate. Per `Src/utils.c:4195-4201`, IMETA is set on:
13566 // NUL, Meta=0x83, Marker=0xa2, and the Pound..Nularg ITOK range
13567 // (0x84..=0xa2). Routes through canonical
13568 // `ztype_h::imeta(u8)`; init the typtab first since the
13569 // canonical port reads through it.
13570 inittyptab(); // c:4148
13571 assert!(imeta(0x00), "c:4195 — NUL is IMETA"); // c:4195
13572 assert!(imeta(Meta), "c:4196 — Meta (0x83) is IMETA"); // c:4196
13573 assert!(imeta(0xa2), "c:4197 — Marker (0xa2) is IMETA"); // c:4197
13574 assert!(
13575 imeta(0x84),
13576 "c:4199-4201 — Pound (0x84) is IMETA via ITOK range"
13577 );
13578 assert!(
13579 imeta(0x9b),
13580 "c:4199-4201 — Dash sentinel within ITOK range is IMETA"
13581 );
13582 assert!(!imeta(0x82), "c:4170 — 0x82 is ICNTRL, not IMETA"); // c:4170
13583 assert!(!imeta(0xa3), "byte 0xa3 is past Marker — NOT IMETA");
13584 assert!(
13585 !imeta(0xff),
13586 "byte 0xff is NOT IMETA (the prior `>= Meta` over-report)"
13587 );
13588 assert!(!imeta(b' '), "space is not IMETA");
13589 assert!(!imeta(b'A'), "'A' is not IMETA");
13590 }
13591
13592 #[test]
13593 fn test_unmeta_routes_through_unmetafy() {
13594 let _g = crate::test_util::global_state_lock();
13595 // unmeta wraps the in-place unmetafy via a byte-vector
13596 // copy; the no-Meta fast path returns the source as-is.
13597 assert_eq!(unmeta("plain"), "plain");
13598 }
13599
13600 #[test]
13601 fn test_iwsep_includes_newline() {
13602 let _g = crate::test_util::global_state_lock(); // c:61
13603 // The previous port omitted '\n' which broke wordcount on
13604 // multi-line input. Routes through canonical
13605 // `ztype_h::iwsep` (`Src/ztype.h:61`).
13606 assert!(iwsep(b'\n')); // c:61
13607 assert!(iwsep(b'\t')); // c:61
13608 assert!(iwsep(b' ')); // c:61
13609 assert!(!iwsep(b'a')); // c:61
13610 }
13611
13612 #[test]
13613 fn test_mailstat_aggregates_maildir() {
13614 let _g = crate::test_util::global_state_lock();
13615 // Create a temp maildir layout with 2 messages in new/ and 1
13616 // in cur/, verify the aggregate.
13617 let tmp = std::env::temp_dir().join(format!("zshrs_mailstat_test_{}", std::process::id()));
13618 let _ = fs::remove_dir_all(&tmp);
13619 fs::create_dir_all(tmp.join("cur")).unwrap();
13620 fs::create_dir_all(tmp.join("new")).unwrap();
13621 fs::create_dir_all(tmp.join("tmp")).unwrap();
13622 let mut f = fs::File::create(tmp.join("new").join("msg1")).unwrap();
13623 f.write_all(b"hello").unwrap();
13624 let mut f = fs::File::create(tmp.join("new").join("msg2")).unwrap();
13625 f.write_all(b"world!").unwrap();
13626 let mut f = fs::File::create(tmp.join("cur").join("msg3")).unwrap();
13627 f.write_all(b"third").unwrap();
13628 let mut st: libc::stat = unsafe { std::mem::zeroed() };
13629 let rc = mailstat(tmp.to_str().unwrap(), &mut st);
13630 assert_eq!(rc, 0, "maildir should stat");
13631 assert_eq!(st.st_blocks, 3, "3 messages total across new/ + cur/");
13632 assert_eq!(st.st_size, 5 + 6 + 5, "5+6+5 bytes total");
13633 let _ = fs::remove_dir_all(&tmp);
13634 }
13635
13636 #[test]
13637 fn test_spacesplit() {
13638 let _g = crate::test_util::global_state_lock();
13639 assert_eq!(spacesplit("a b c", false, false), vec!["a", "b", "c"]);
13640 assert_eq!(spacesplit("a b", false, false), vec!["a", "b"]);
13641 }
13642
13643 /// `Src/utils.c:3741-3743` + `findsep` (c:3795-3811) — with `quote` a
13644 /// backslash before a separator keeps it inside the field (and is
13645 /// stripped), `\\` collapses to `\`, and without `quote` the backslash is
13646 /// an ordinary byte. This is the split `vared` applies to an array
13647 /// (zle_main.c:1878, `spacesplit(t, 1, 0, 1)`).
13648 #[test]
13649 fn spacesplit_quote_keeps_escaped_separators_in_the_field() {
13650 let _g = crate::test_util::global_state_lock();
13651 assert_eq!(spacesplit("a\\ b c", true, true), vec!["a b", "c"]);
13652 assert_eq!(spacesplit("a\\\\b c", true, true), vec!["a\\b", "c"]);
13653 assert_eq!(spacesplit("a\\ b c", true, false), vec!["a\\", "b", "c"]);
13654 }
13655
13656 #[test]
13657 fn test_sepjoin() {
13658 let _g = crate::test_util::global_state_lock();
13659 assert_eq!(
13660 sepjoin(&["a".into(), "b".into(), "c".into()], Some(":")),
13661 "a:b:c"
13662 );
13663 assert_eq!(sepjoin(&["a".into(), "b".into()], None), "a b");
13664 }
13665
13666 #[test]
13667 fn test_isident() {
13668 let _g = crate::test_util::global_state_lock(); // c:1288
13669 // Canonical port lives at `params.rs:2056` (`Src/params.c:1288`).
13670 assert!(isident("foo")); // c:1288
13671 assert!(isident("_bar")); // c:1288
13672 assert!(isident("baz123")); // c:1288
13673 assert!(!isident("123abc")); // c:1288
13674 assert!(!isident("foo-bar")); // c:1288
13675 }
13676
13677 #[test]
13678 fn test_nicechar() {
13679 let _g = crate::test_util::global_state_lock();
13680 assert_eq!(nicechar('\n'), "\\n");
13681 assert_eq!(nicechar('\t'), "\\t");
13682 assert_eq!(nicechar('a'), "a");
13683 }
13684
13685 #[test]
13686 fn test_quotedzputs_single_quote_wrap() {
13687 let _g = crate::test_util::global_state_lock();
13688 assert_eq!(quotedzputs("simple"), "simple");
13689 assert_eq!(quotedzputs("has space"), "'has space'");
13690 assert_eq!(quotedzputs("it's"), "'it'\\''s'");
13691 }
13692
13693 #[test]
13694 fn test_quotestring_backslash() {
13695 let _g = crate::test_util::global_state_lock();
13696 assert_eq!(quotestring("hello", QT_BACKSLASH), "hello");
13697 assert_eq!(quotestring("has space", QT_BACKSLASH), "has\\ space");
13698 assert_eq!(quotestring("$var", QT_BACKSLASH), "\\$var");
13699 }
13700
13701 /// Pin: `ispecial(c)` matches the canonical SPECCHARS set at
13702 /// `Src/zsh.h:228` exactly: `"#$^*()=|{}[]\`<>?~;&\n\t \\'\""`.
13703 /// Previously the Rust local `ispecial` included `!` unconditionally
13704 /// which diverged from C — C only ISPECIAL-tags bangchar (default
13705 /// `!`) under BANGHIST + interactive (per `Src/utils.c:4257-4261`).
13706 ///
13707 /// This test exercises the path indirectly via `quotestring` with
13708 /// `QT_BACKSLASH` (which prepends `\` before every ispecial char).
13709 #[test]
13710 fn quotestring_backslash_only_specchars_no_bang_in_default() {
13711 let _g = crate::test_util::global_state_lock();
13712 // `!` is NOT in canonical SPECCHARS — should NOT be backslashed
13713 // in default non-interactive mode (matches C).
13714 assert_eq!(
13715 quotestring("a!b", QT_BACKSLASH),
13716 "a!b",
13717 "c:228 — `!` is not in SPECCHARS; only bangchar+BANGHIST adds it"
13718 );
13719 // `,` is NOT in canonical SPECCHARS until makecommaspecial(1).
13720 assert_eq!(
13721 quotestring("a,b", QT_BACKSLASH),
13722 "a,b",
13723 "c:228 — `,` not in SPECCHARS until makecommaspecial(1)"
13724 );
13725 // `^` IS in canonical SPECCHARS (per c:228 `"#$^..."`).
13726 assert_eq!(
13727 quotestring("a^b", QT_BACKSLASH),
13728 "a\\^b",
13729 "c:228 — `^` is in SPECCHARS"
13730 );
13731 // Open and close braces are in canonical SPECCHARS.
13732 assert_eq!(
13733 quotestring("a{b", QT_BACKSLASH),
13734 "a\\{b",
13735 "c:228 — open-brace is in SPECCHARS"
13736 );
13737 assert_eq!(
13738 quotestring("a}b", QT_BACKSLASH),
13739 "a\\}b",
13740 "c:228 — close-brace is in SPECCHARS"
13741 );
13742 // `#` is in canonical SPECCHARS (first char of c:228).
13743 assert_eq!(
13744 quotestring("a#b", QT_BACKSLASH),
13745 "a\\#b",
13746 "c:228 — `#` is the first char of SPECCHARS"
13747 );
13748 // `\\` (literal backslash) is in canonical SPECCHARS.
13749 assert_eq!(
13750 quotestring("a\\b", QT_BACKSLASH),
13751 "a\\\\b",
13752 "c:228 — `\\\\` in SPECCHARS"
13753 );
13754 }
13755
13756 /// c:6131-6134 — "Most quote styles other than backslash assume the
13757 /// quotes are to be added outside quotestring()." QT_SINGLE therefore
13758 /// returns the BODY: the caller (subst.c:4085-4087, text.c:1086-1088)
13759 /// supplies the `'…'` pair. The inner apostrophe still takes the
13760 /// close-reopen form from c:6372-6377.
13761 #[test]
13762 fn test_quotestring_single() {
13763 let _g = crate::test_util::global_state_lock();
13764 assert_eq!(quotestring("hello", QT_SINGLE), "hello");
13765 assert_eq!(quotestring("it's", QT_SINGLE), "it'\\''s");
13766 }
13767
13768 /// c:6131-6134 + c:6311-6312 — body only; `"` is backslash-escaped and
13769 /// the surrounding pair is the caller's (subst.c:4085-4087).
13770 #[test]
13771 fn test_quotestring_double() {
13772 let _g = crate::test_util::global_state_lock();
13773 assert_eq!(quotestring("hello", QT_DOUBLE), "hello");
13774 assert_eq!(quotestring("say \"hi\"", QT_DOUBLE), "say \\\"hi\\\"");
13775 }
13776
13777 /// c:6131-6134 — body only. C's caller writes the quote pair and then
13778 /// `val[0] = '$'` (subst.c:4085-4090), so `$'` never appears here.
13779 #[test]
13780 fn test_quotestring_dollars() {
13781 let _g = crate::test_util::global_state_lock();
13782 assert_eq!(quotestring("hello", QT_DOLLARS), "hello");
13783 assert_eq!(quotestring("line\nbreak", QT_DOLLARS), "line\\nbreak");
13784 assert_eq!(quotestring("tab\there", QT_DOLLARS), "tab\\there");
13785 }
13786
13787 #[test]
13788 fn test_quotestring_pattern() {
13789 let _g = crate::test_util::global_state_lock();
13790 assert_eq!(quotestring("*.txt", QT_BACKSLASH_PATTERN), "\\*.txt");
13791 assert_eq!(quotestring("file[1]", QT_BACKSLASH_PATTERN), "file\\[1\\]");
13792 }
13793
13794 #[test]
13795 fn test_quotetype_from_q_count() {
13796 let _g = crate::test_util::global_state_lock();
13797 assert_eq!(qflag_quotetype(1), QT_BACKSLASH);
13798 assert_eq!(qflag_quotetype(2), QT_SINGLE);
13799 assert_eq!(qflag_quotetype(3), QT_DOUBLE);
13800 assert_eq!(qflag_quotetype(4), QT_DOLLARS);
13801 }
13802
13803 #[test]
13804 fn test_tulower_tuupper() {
13805 let _g = crate::test_util::global_state_lock();
13806 assert_eq!(tulower('A'), 'a');
13807 assert_eq!(tuupper('a'), 'A');
13808 assert_eq!(tulower('1'), '1');
13809 }
13810
13811 #[test]
13812 fn test_wordcount_ifs_default() {
13813 let _g = crate::test_util::global_state_lock();
13814 // C: wordcount("a b c", NULL, 0) -> 3
13815 assert_eq!(wordcount("a b c", None, 0), 3);
13816 // Leading/trailing whitespace coalesced when mul <= 0.
13817 assert_eq!(wordcount(" a b ", None, 0), 2);
13818 // Empty string with mul == 0 -> 0 words.
13819 assert_eq!(wordcount("", None, 0), 0);
13820 // Single word, no separators.
13821 assert_eq!(wordcount("foo", None, 0), 1);
13822 }
13823
13824 #[test]
13825 fn test_wordcount_with_explicit_sep() {
13826 let _g = crate::test_util::global_state_lock();
13827 // C: wordcount("a:b:c", ":", 0) -> 3 (3 fields, 2 separators)
13828 assert_eq!(wordcount("a:b:c", Some(":"), 0), 3);
13829 // Empty fields counted when mul != 0.
13830 assert_eq!(wordcount("a::b", Some(":"), 1), 3);
13831 // Without mul, consecutive empties collapse: a, b => 2... but
13832 // C's "if ((c || mul) && (sl || *(s+sl)))" — second `:` has
13833 // c=0 and mul=0 so doesn't increment. Result: a, b => 2.
13834 assert_eq!(wordcount("a::b", Some(":"), 0), 2);
13835 }
13836
13837 #[test]
13838 fn test_ucs4tomb_ascii() {
13839 let _g = crate::test_util::global_state_lock();
13840 let mut buf = [0u8; 8];
13841 // 'A' = 0x41, ASCII, single byte in any locale.
13842 let n = ucs4tomb('A' as u32, &mut buf);
13843 // wctomb may return 1 in C/POSIX locale; in UTF-8 locale also 1.
13844 assert_eq!(n, 1);
13845 assert_eq!(buf[0], b'A');
13846 }
13847
13848 #[test]
13849 fn test_is_mb_niceformat_plain_ascii() {
13850 let _g = crate::test_util::global_state_lock();
13851 // Plain printable ASCII — nothing needs nicechar escaping.
13852 assert_eq!(is_mb_niceformat("hello world"), 0);
13853 }
13854
13855 #[test]
13856 fn test_is_mb_niceformat_with_control_char() {
13857 let _g = crate::test_util::global_state_lock();
13858 // Tab is control (< 0x20) — needs nice escaping.
13859 assert_eq!(is_mb_niceformat("a\tb"), 1);
13860 // Bell character.
13861 assert_eq!(is_mb_niceformat("a\x07b"), 1);
13862 }
13863
13864 /// c:4856/4954 — `metafy` + `unmetafy` MUST round-trip for ASCII.
13865 /// A regression in the round-trip corrupts every metafied buffer
13866 /// the lexer/param-subst pipeline produces.
13867 #[test]
13868 fn metafy_unmetafy_round_trips_for_ascii() {
13869 let _g = crate::test_util::global_state_lock();
13870 let s = "hello world";
13871 let m = metafy(s);
13872 let mut buf = m.into_bytes();
13873 unmetafy(&mut buf);
13874 assert_eq!(std::str::from_utf8(&buf).unwrap(), s);
13875 }
13876
13877 /// `ztrlen` counts metafied characters (Meta-pairs as 1).
13878 /// Regression that double-counts Meta-pair bytes would break
13879 /// every fixed-width column path (printf %s).
13880 #[test]
13881 fn ztrlen_counts_ascii_one_per_byte() {
13882 let _g = crate::test_util::global_state_lock();
13883 assert_eq!(ztrlen(""), 0);
13884 assert_eq!(ztrlen("a"), 1);
13885 assert_eq!(ztrlen("hello"), 5);
13886 }
13887
13888 /// `Src/utils.c:5141-5149` — Meta-byte pair counts as 1 char.
13889 /// `*s++ == Meta` advances 2 bytes per iteration but increments
13890 /// `l` only once. Pin: a string with one Meta+X pair counts as 1.
13891 #[test]
13892 fn ztrlen_counts_meta_pair_as_one() {
13893 let _g = crate::test_util::global_state_lock();
13894 // META (0x83) + 0x20 = unmetafies to one '\0' byte (or a NUL).
13895 let meta = char::from_u32(Meta as u32).unwrap();
13896 let s: String = [meta, '\x20'].iter().collect();
13897 assert_eq!(
13898 ztrlen(&s),
13899 1,
13900 "c:5141-5148 — Meta+X pair counts as ONE char, not two"
13901 );
13902 // Mixed: "a" + META + "x" + "b" = 3 unmetafied chars.
13903 let mixed: String = ['a', meta, '\x20', 'b'].iter().collect();
13904 assert_eq!(
13905 ztrlen(&mixed),
13906 3,
13907 "c:5141 — three unmetafied chars from 'a' + Meta+X + 'b'"
13908 );
13909 }
13910
13911 /// `Src/utils.c:2579-2618` — `setblock_fd(turnonblocking, fd, modep)`.
13912 /// Signature pin: previously the Rust port had a 2-arg
13913 /// `(fd, blocking: bool) -> bool` signature, swapping the
13914 /// turnonblocking/fd argument order vs C AND collapsing the
13915 /// 3rd `*modep` out-param entirely. The fix restored canonical
13916 /// C order `(turnonblocking, fd)` with a `(bool, c_long)` tuple
13917 /// return mirroring `int return + long *modep`.
13918 ///
13919 /// Also pin the c:2599 regular-file short-circuit: C only
13920 /// operates on non-regular fds (pipes, sockets, ttys). A regular
13921 /// file returns `(false, -1)` immediately.
13922 #[cfg(unix)]
13923 #[test]
13924 fn setblock_fd_skips_regular_files_per_c_2599() {
13925 let _g = crate::test_util::global_state_lock();
13926 // Open a regular tempfile.
13927 let dir = tempfile::TempDir::new().unwrap();
13928 let f = fs::File::create(dir.path().join("regular")).unwrap();
13929 let fd = f.as_raw_fd();
13930 let (changed, mode) = setblock_fd(true, fd);
13931 assert!(
13932 !changed,
13933 "c:2599 — regular files short-circuit; setblock_fd must NOT report a change"
13934 );
13935 assert_eq!(mode, -1, "c:2614 — `*modep = -1` for regular files");
13936 }
13937
13938 /// `Src/utils.c:2606-2611` — `setblock_fd(turnonblocking=true, fd)`
13939 /// clears O_NONBLOCK on a pipe (non-regular fd). Returns
13940 /// `(true, prior_flags)` if the state was changed.
13941 #[cfg(unix)]
13942 #[test]
13943 fn setblock_fd_clears_o_nonblock_on_pipe() {
13944 let _g = crate::test_util::global_state_lock();
13945 // Create a pipe — non-regular fd.
13946 let mut pipefd: [libc::c_int; 2] = [0; 2];
13947 let r = unsafe { libc::pipe(pipefd.as_mut_ptr()) };
13948 assert_eq!(r, 0, "pipe(2) must succeed");
13949 let read_fd = pipefd[0];
13950 let write_fd = pipefd[1];
13951 // Force NONBLOCK on the read end.
13952 let cur = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
13953 unsafe {
13954 libc::fcntl(read_fd, libc::F_SETFL, cur | libc::O_NONBLOCK);
13955 }
13956 // Verify NONBLOCK is set.
13957 let now = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
13958 assert_ne!(
13959 now & libc::O_NONBLOCK,
13960 0,
13961 "test setup: NONBLOCK should be set"
13962 );
13963 // Call setblock_fd to ENABLE blocking (clear NONBLOCK).
13964 let (changed, _mode) = setblock_fd(true, read_fd);
13965 assert!(
13966 changed,
13967 "c:2611 — turnonblocking=true on a NONBLOCK pipe must report state change"
13968 );
13969 // Verify NONBLOCK is now cleared.
13970 let after = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
13971 assert_eq!(
13972 after & libc::O_NONBLOCK,
13973 0,
13974 "c:2611 — O_NONBLOCK must be cleared after turnonblocking=true"
13975 );
13976 // Cleanup.
13977 unsafe {
13978 libc::close(read_fd);
13979 libc::close(write_fd);
13980 }
13981 }
13982
13983 /// `Src/utils.c:2620-2625` — `setblock_stdin()`. C body
13984 /// `setblock_fd(1, 0, &mode)` enables BLOCKING on fd 0 (stdin).
13985 /// Previously the Rust port called `setblock_fd(0, false)`
13986 /// which DISABLES blocking — exact opposite. Pin via real fd
13987 /// inspection.
13988 #[cfg(unix)]
13989 #[test]
13990 fn setblock_stdin_enables_blocking_on_fd_zero() {
13991 let _g = crate::test_util::global_state_lock();
13992 // Set stdin to NONBLOCKING first to verify the function
13993 // ACTUALLY switches it back to blocking. Skip if stdin is
13994 // not a normal fd (some CI configurations).
13995 let cur = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
13996 if cur < 0 {
13997 return;
13998 }
13999 // Force NONBLOCK first.
14000 unsafe {
14001 libc::fcntl(0, libc::F_SETFL, cur | libc::O_NONBLOCK);
14002 }
14003 let after_set_nb = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
14004 if after_set_nb & libc::O_NONBLOCK == 0 {
14005 // System rejected the change (regular file? CI tty?) — skip.
14006 unsafe {
14007 libc::fcntl(0, libc::F_SETFL, cur);
14008 }
14009 return;
14010 }
14011 // Call setblock_stdin — should CLEAR O_NONBLOCK.
14012 setblock_stdin();
14013 let after_setblock = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
14014 assert_eq!(
14015 after_setblock & libc::O_NONBLOCK,
14016 0,
14017 "c:2624 — setblock_stdin must CLEAR O_NONBLOCK (enable blocking)"
14018 );
14019 // Restore original flags.
14020 unsafe {
14021 libc::fcntl(0, libc::F_SETFL, cur);
14022 }
14023 }
14024
14025 /// `Src/utils.c:2437-2519` — `zstrtol_underscore(s, base, false)`.
14026 /// Base-10 (explicit) parses canonical decimal.
14027 #[test]
14028 fn zstrtol_underscore_base_10_parses_decimal() {
14029 let _g = crate::test_util::global_state_lock();
14030 let (v, rest) = zstrtol_underscore("12345", 10, false);
14031 assert_eq!(v, 12345, "c:2471 — decimal accumulator");
14032 assert_eq!(rest, "", "rest is empty after full consumption");
14033 // With trailing non-digit.
14034 let (v, rest) = zstrtol_underscore("100abc", 10, false);
14035 assert_eq!(v, 100);
14036 assert_eq!(
14037 rest, "abc",
14038 "c:2467 — loop exits at first non-digit; rest carries on"
14039 );
14040 }
14041
14042 /// c:2452-2461 — base==0 autodetect: `0x`→16, `0b`→2, leading
14043 /// `0`→8 (always, unlike `zstrtoul_underscore` which honors
14044 /// OCTALZEROES). `zstrtol_underscore` does NOT consult
14045 /// OCTALZEROES — pure prefix detection.
14046 #[test]
14047 fn zstrtol_underscore_base_zero_autodetects_prefix() {
14048 let _g = crate::test_util::global_state_lock();
14049 // Hex.
14050 assert_eq!(
14051 zstrtol_underscore("0xff", 0, false).0,
14052 255,
14053 "c:2455 — 0x → base 16"
14054 );
14055 assert_eq!(zstrtol_underscore("0XFF", 0, false).0, 255);
14056 // Binary.
14057 assert_eq!(
14058 zstrtol_underscore("0b1010", 0, false).0,
14059 10,
14060 "c:2457 — 0b → base 2"
14061 );
14062 // Leading 0 → octal (always, no OCTALZEROES gate).
14063 assert_eq!(
14064 zstrtol_underscore("0777", 0, false).0,
14065 511,
14066 "c:2460 — leading 0 → octal (no OCTALZEROES gate for zstrtol)"
14067 );
14068 // Decimal default.
14069 assert_eq!(zstrtol_underscore("12345", 0, false).0, 12345);
14070 }
14071
14072 /// c:2447-2450 — leading `-` and `+` consumed; `-` triggers
14073 /// negation at the end (c:2497-2498).
14074 #[test]
14075 fn zstrtol_underscore_handles_sign_chars() {
14076 let _g = crate::test_util::global_state_lock();
14077 assert_eq!(
14078 zstrtol_underscore("-42", 10, false).0,
14079 -42,
14080 "c:2447 — leading `-` → negate"
14081 );
14082 assert_eq!(
14083 zstrtol_underscore("+42", 10, false).0,
14084 42,
14085 "c:2449 — leading `+` consumed, no negation"
14086 );
14087 // Mixed whitespace + sign.
14088 assert_eq!(
14089 zstrtol_underscore(" -100", 10, false).0,
14090 -100,
14091 "c:2444 — leading whitespace skipped, then sign"
14092 );
14093 }
14094
14095 /// c:2466-2492 — base 16 (hex) accumulator: digits 0-9 + letters
14096 /// a-f / A-F via `idigit(*s)` || `'a' ≤ *s < 'a'+base-10`.
14097 /// Pin both upper- and lower-case.
14098 #[test]
14099 fn zstrtol_underscore_base_16_accepts_letters() {
14100 let _g = crate::test_util::global_state_lock();
14101 assert_eq!(
14102 zstrtol_underscore("ff", 16, false).0,
14103 255,
14104 "c:2485 — base-16 'ff' → 255"
14105 );
14106 assert_eq!(
14107 zstrtol_underscore("FF", 16, false).0,
14108 255,
14109 "c:2485 — base-16 'FF' → 255 (upper case via `*s & 0x1f`)"
14110 );
14111 assert_eq!(
14112 zstrtol_underscore("DEADbeef", 16, false).0,
14113 0xDEADBEEF,
14114 "c:2485 — mixed case 32-bit hex"
14115 );
14116 }
14117
14118 /// c:2468/2482 — `underscore` flag enables digit-separator. With
14119 /// underscore=false, `_` is treated as end-of-digits. With
14120 /// underscore=true, `_` is skipped (c:2469-2470).
14121 #[test]
14122 fn zstrtol_underscore_underscore_flag() {
14123 let _g = crate::test_util::global_state_lock();
14124 // underscore=false: `_` terminates parse.
14125 let (v, rest) = zstrtol_underscore("1_000", 10, false);
14126 assert_eq!(v, 1, "c:2467 — underscore=false: `_` terminates");
14127 assert_eq!(rest, "_000");
14128 // underscore=true: `_` accepted and skipped.
14129 let (v, rest) = zstrtol_underscore("1_000_000", 10, true);
14130 assert_eq!(
14131 v, 1_000_000,
14132 "c:2469-2470 — underscore=true: `_` skipped during accumulation"
14133 );
14134 assert_eq!(rest, "", "fully consumed including `_`s");
14135 }
14136
14137 /// `Src/utils.c:2750-2774` — `timespec_diff_us(t1, t2)` returns
14138 /// the signed microsecond delta `t2 - t1`. Pin both sign
14139 /// conventions: t2 after t1 → positive; t2 before t1 → negative.
14140 #[test]
14141 fn timespec_diff_us_sign_matches_c_t2_minus_t1() {
14142 let _g = crate::test_util::global_state_lock();
14143 let t1 = std::time::Instant::now();
14144 std::thread::sleep(std::time::Duration::from_millis(2));
14145 let t2 = std::time::Instant::now();
14146 // c:2759 — `diff_sec = t2 - t1` for the t2 > t1 path. Result
14147 // positive (microseconds elapsed).
14148 let d = timespec_diff_us(&t1, &t2);
14149 assert!(d > 0, "c:2759 — t2 after t1 → positive delta");
14150 assert!(d >= 1000, "expected >= 1ms (1000us); got {}us", d);
14151 // c:2754 — reversed (t1 > t2): result negative.
14152 let r = timespec_diff_us(&t2, &t1);
14153 assert!(r < 0, "c:2770 — t1 after t2 → negative delta");
14154 assert_eq!(d, -r, "swap of args negates the result");
14155 }
14156
14157 /// c:2752 — `timespec_diff_us(t, t)` is 0 (identical Instants).
14158 #[test]
14159 fn timespec_diff_us_same_instant_returns_zero() {
14160 let _g = crate::test_util::global_state_lock();
14161 let t = std::time::Instant::now();
14162 assert_eq!(
14163 timespec_diff_us(&t, &t),
14164 0,
14165 "c:2752 — identical Instants → 0"
14166 );
14167 }
14168
14169 /// `Src/utils.c:6743-6779` — `ucs4toutf8(dest, wval)`. Encodes
14170 /// a Unicode codepoint to UTF-8. Pin canonical 1-, 2-, 3-, and
14171 /// 4-byte encodings.
14172 #[test]
14173 fn ucs4toutf8_encodes_canonical_lengths() {
14174 let _g = crate::test_util::global_state_lock();
14175 // c:6750 — 1 byte: ASCII range [0, 0x80).
14176 assert_eq!(
14177 ucs4toutf8(0x41),
14178 Some(b"A".to_vec()),
14179 "c:6750 — 0x41 → 'A' (1 byte)"
14180 );
14181 // c:6752 — 2 bytes: [0x80, 0x800). 'é' = U+00E9.
14182 assert_eq!(
14183 ucs4toutf8(0xe9),
14184 Some("é".as_bytes().to_vec()),
14185 "c:6752 — U+00E9 → 'é' (2 bytes)"
14186 );
14187 // c:6754 — 3 bytes: [0x800, 0x10000). '字' = U+5B57.
14188 assert_eq!(
14189 ucs4toutf8(0x5B57),
14190 Some("字".as_bytes().to_vec()),
14191 "c:6754 — U+5B57 → '字' (3 bytes)"
14192 );
14193 // c:6756 — 4 bytes: [0x10000, 0x200000). '𝄞' = U+1D11E.
14194 assert_eq!(
14195 ucs4toutf8(0x1D11E),
14196 Some("𝄞".as_bytes().to_vec()),
14197 "c:6756 — U+1D11E → '𝄞' (4 bytes)"
14198 );
14199 }
14200
14201 /// `Src/utils.c:6762-6764` — values `>= 0x80000000` are
14202 /// out-of-range; C emits `zerr("character not in range")` and
14203 /// returns `-1`. Surrogates (U+D800..U+DFFF) and values in
14204 /// 0x110000..0x80000000 are *encoded as raw bytes* by C
14205 /// (matches `wctomb(3)` extended UCS-4 range).
14206 #[test]
14207 fn ucs4toutf8_rejects_invalid_codepoints() {
14208 let _g = crate::test_util::global_state_lock();
14209 // c:6762-6764 — value >= 0x80000000 → None.
14210 assert_eq!(
14211 ucs4toutf8(0xFFFF_FFFE),
14212 None,
14213 "c:6763 — values >= 0x80000000 → None"
14214 );
14215 assert_eq!(
14216 ucs4toutf8(0x8000_0000),
14217 None,
14218 "c:6760-6764 — exactly 0x80000000 is out of range"
14219 );
14220 // Surrogates encode as 3-byte sequences per C bit-pattern
14221 // (c:6754 → len=3). The C source has no Unicode-validity
14222 // gate; that's a wctomb-only concern in callers.
14223 assert!(
14224 ucs4toutf8(0xD800).is_some(),
14225 "c:6754 — surrogates encode as 3-byte UTF-8 in C (no Unicode validity check)"
14226 );
14227 }
14228
14229 /// `Src/utils.c:6648-6723` — `dquotedztrdup(s)`. Default (non-
14230 /// CSHJUNKIEQUOTES) arm: wrap whole string in `"..."`. Backslash
14231 /// doubles, `"`/`$`/`` ` `` get escaped.
14232 #[test]
14233 fn dquotedztrdup_default_path_wraps_whole_string() {
14234 let _g = crate::test_util::global_state_lock();
14235 if isset(CSHJUNKIEQUOTES) {
14236 return; // Default-only test.
14237 }
14238 // c:6690 — `*p++ = '"';` then iterate.
14239 assert_eq!(
14240 dquotedztrdup("hello"),
14241 "\"hello\"",
14242 "c:6690+6711+6718 — wrap plain text in double quotes"
14243 );
14244 // c:6703-6708 — `$` gets `\$`.
14245 assert_eq!(
14246 dquotedztrdup("$var"),
14247 "\"\\$var\"",
14248 "c:6703-6708 — `$` → `\\$`"
14249 );
14250 // c:6703-6708 — `"` gets `\"`.
14251 assert_eq!(
14252 dquotedztrdup("a\"b"),
14253 "\"a\\\"b\"",
14254 "c:6703-6708 — `\"` → `\\\"`"
14255 );
14256 }
14257
14258 /// c:6697-6701 — backslash handling with `pending` state.
14259 /// Single `\` followed by an ordinary char emits just `\` (no
14260 /// doubling). The "pending" extra `\` fires only when the next
14261 /// char is itself a backslash or one of the escaped specials
14262 /// (`"`/`$`/`` ` ``), OR at end-of-string before the closing `"`.
14263 #[test]
14264 fn dquotedztrdup_pending_backslash_only_doubles_when_needed() {
14265 let _g = crate::test_util::global_state_lock();
14266 if isset(CSHJUNKIEQUOTES) {
14267 return;
14268 }
14269 // Mid-string `\` before ordinary char: stays as single `\`.
14270 assert_eq!(
14271 dquotedztrdup("a\\b"),
14272 "\"a\\b\"",
14273 "c:6711+6712 — `\\b` mid-string: no extra (pending=0 after b)"
14274 );
14275 // Trailing `\` triggers pending quirk: c:6716-6717 emits
14276 // an extra `\` before the closing `"`.
14277 let r = dquotedztrdup("a\\");
14278 assert!(
14279 r.ends_with("\\\\\""),
14280 "c:6716-6717 — trailing `\\` → emit extra `\\` before closing `\"`: got {:?}",
14281 r
14282 );
14283 }
14284
14285 /// `Src/utils.c:6464-6549` — `quotedzputs(s)`. Three branches:
14286 /// - empty → `''`.
14287 /// - has special chars (via `hasspecial`) → wrap in `'...'`
14288 /// with `'\''` escape for embedded apostrophes.
14289 /// - else → return unchanged.
14290 /// Pin all three.
14291 #[test]
14292 fn quotedzputs_empty_string_returns_double_quotes() {
14293 let _g = crate::test_util::global_state_lock();
14294 assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
14295 }
14296
14297 /// c:6514-6543 — needs-quote path: wrap in single quotes,
14298 /// escape embedded `'` as `'\''`.
14299 #[test]
14300 fn quotedzputs_wraps_specials_in_single_quotes() {
14301 let _g = crate::test_util::global_state_lock();
14302 inittyptab();
14303 // Space is special → wrap.
14304 assert_eq!(quotedzputs("hello world"), "'hello world'");
14305 // `=` is special → wrap.
14306 assert_eq!(quotedzputs("foo=bar"), "'foo=bar'");
14307 // Embedded apostrophe — `'\''` escape.
14308 assert_eq!(
14309 quotedzputs("it's"),
14310 "'it'\\''s'",
14311 "c:6517-6519 — apostrophe → '\\''"
14312 );
14313 }
14314
14315 /// c:6512 — no specials → return unchanged.
14316 #[test]
14317 fn quotedzputs_plain_alnum_returns_unchanged() {
14318 let _g = crate::test_util::global_state_lock();
14319 inittyptab();
14320 assert_eq!(
14321 quotedzputs("hello"),
14322 "hello",
14323 "c:6512 — no specials, no wrap"
14324 );
14325 assert_eq!(quotedzputs("abc123"), "abc123");
14326 }
14327
14328 /// c:6578-6637 — Bourne path "avoids empty quoted strings" by
14329 /// tracking `inquote`. Input `'x` should not produce empty `''`
14330 /// at the front; `x'` should not produce trailing `''`.
14331 #[test]
14332 fn quotedzputs_avoids_empty_quoted_runs_on_boundaries() {
14333 let _g = crate::test_util::global_state_lock();
14334 inittyptab();
14335 // Leading `'`: c:6587 with inquote=0 skips the close, emits
14336 // `\'`. Then `x`: c:6604 opens a quote, emits `x`, end emits
14337 // closing `'`. Result: `\''x'` — the visible doubled `''` is
14338 // `\'` (escaped) + `'` (opening single-quote), NOT an empty
14339 // single-quoted string.
14340 assert_eq!(
14341 quotedzputs("'x"),
14342 "\\''x'",
14343 "c:6587-6610 — leading `'` produces `\\'` + opening `'`"
14344 );
14345 // Trailing `'`: open quote at `x`, see `'` with inquote=1 → close
14346 // (`'`), emit `\'`. End-of-string: inquote=0, no trailing `'`.
14347 // Result: `'x'\\'` — NO trailing `''`.
14348 assert_eq!(
14349 quotedzputs("x'"),
14350 "'x'\\'",
14351 "c:6631-6637 — trailing `'` doesn't generate empty `''`"
14352 );
14353 }
14354
14355 /// c:6478-6492 — `is_mb_niceformat` arm of quotedzputs uses
14356 /// mb_niceformat with NICEFLAG_QUOTE so embedded `'` becomes
14357 /// `\'` inside the `$'...'` wrapper (preventing the quote from
14358 /// terminating the wrap). Previously sb_niceformat without
14359 /// NICEFLAG_QUOTE was used, which would have left `'` raw.
14360 #[test]
14361 fn quotedzputs_dollar_quote_escapes_apostrophe_inside_wrap() {
14362 let _g = crate::test_util::global_state_lock();
14363 inittyptab();
14364 // String with embedded `'` AND a control char so the
14365 // is_mb_niceformat arm is taken (controls trigger it).
14366 let r = quotedzputs("a\nb'c");
14367 // Expected: `$'a\nb\'c'` — the embedded `'` is `\'` inside
14368 // dollar-quotes, NOT terminating the wrap.
14369 assert!(
14370 r.starts_with("$'") && r.ends_with("'"),
14371 "c:6488 — must be wrapped in $'...' got {:?}",
14372 r
14373 );
14374 assert!(
14375 r.contains("\\'"),
14376 "c:5413-5414 — embedded `'` must be `\\'` not raw `'`: got {:?}",
14377 r
14378 );
14379 }
14380
14381 /// c:6533-6576 — RCQUOTES branch: wrap everything in `'…'`,
14382 /// double embedded `'` as `''`.
14383 #[test]
14384 fn quotedzputs_rcquotes_doubles_apostrophe() {
14385 let _g = crate::test_util::global_state_lock();
14386 inittyptab();
14387 let prev = crate::ported::options::opt_state_get("rcquotes").unwrap_or(false);
14388 crate::ported::options::opt_state_set("rcquotes", true);
14389 // RCQUOTES: `it's` → `'it''s'` (doubled `'`, NOT `'\''`).
14390 let got = quotedzputs("it's");
14391 crate::ported::options::opt_state_set("rcquotes", prev);
14392 assert_eq!(
14393 got, "'it''s'",
14394 "c:6548-6553 — RCQUOTES: `'` → `''` (doubled)"
14395 );
14396 }
14397
14398 /// `Src/utils.c:2331-2337` — `strucpy(s, t)`. Appends `t` to
14399 /// `*s` and leaves `*s` pointing at the NUL terminator (Rust
14400 /// equivalent: append-in-place; `.len()` gives the new end).
14401 /// Pin: empty input → no-op; non-empty input → append.
14402 #[test]
14403 fn strucpy_appends_t_to_dest() {
14404 let _g = crate::test_util::global_state_lock();
14405 let mut dest = String::from("prefix-");
14406 strucpy(&mut dest, "suffix");
14407 assert_eq!(
14408 dest, "prefix-suffix",
14409 "c:2335 — strucpy appends t (NOT case-changes — c-name 'u' is pointer-walk, not upper)"
14410 );
14411 // Empty t — no change.
14412 let mut dest = String::from("alone");
14413 strucpy(&mut dest, "");
14414 assert_eq!(dest, "alone");
14415 // Empty dest + non-empty t.
14416 let mut dest = String::new();
14417 strucpy(&mut dest, "hello");
14418 assert_eq!(dest, "hello");
14419 }
14420
14421 /// `Src/utils.c:2341-2350` — `struncpy(s, t, n)`. Appends up to
14422 /// `n` bytes of `t` to `*s`. Pin: n=0 no-op; n < len(t) clips;
14423 /// n >= len(t) appends all.
14424 #[test]
14425 fn struncpy_appends_up_to_n_bytes() {
14426 let _g = crate::test_util::global_state_lock();
14427 // n < len(t).
14428 let mut dest = String::from("X");
14429 struncpy(&mut dest, "abcdef", 3);
14430 assert_eq!(dest, "Xabc", "c:2345 — clipped to n=3 bytes of `abcdef`");
14431 // n >= len(t).
14432 let mut dest = String::from("Y");
14433 struncpy(&mut dest, "abc", 100);
14434 assert_eq!(dest, "Yabc", "c:2345 — full copy when n >= len");
14435 // n=0 → no append.
14436 let mut dest = String::from("Z");
14437 struncpy(&mut dest, "abc", 0);
14438 assert_eq!(dest, "Z", "c:2345 — n=0 stops the copy loop immediately");
14439 }
14440
14441 /// `Src/utils.c:2280-2288` — `has_token(s)`. Returns true iff
14442 /// any byte in `s` triggers `itok()`. Pin: ASCII strings →
14443 /// false; strings containing token bytes (Pound 0x84, Bang
14444 /// 0x9c, Nularg 0xa1) → true.
14445 #[test]
14446 fn has_token_detects_typtab_token_bytes() {
14447 let _g = crate::test_util::global_state_lock();
14448 inittyptab();
14449 // c:2285 — pure ASCII has no token bytes.
14450 assert!(!has_token(""), "empty: no tokens");
14451 assert!(
14452 !has_token("hello world"),
14453 "c:2285 — ASCII text has no token bytes"
14454 );
14455 // Pound (0x84) — first token byte.
14456 let s: String = std::iter::once(crate::token_char::token_char_from_byte(0x84).unwrap()).collect();
14457 assert!(
14458 has_token(&s),
14459 "c:2285 — Pound (0x84) is itok → has_token=true"
14460 );
14461 // Bang (0x9c) — last_normal_tok.
14462 let s: String = std::iter::once(crate::token_char::token_char_from_byte(0x9c).unwrap()).collect();
14463 assert!(has_token(&s), "c:2285 — Bang (0x9c) is itok");
14464 // Nularg (0xa1) — upper bound.
14465 let s: String = std::iter::once(crate::token_char::token_char_from_byte(0xa1).unwrap()).collect();
14466 assert!(has_token(&s), "c:2285 — Nularg (0xa1) is itok");
14467 }
14468
14469 /// `Src/utils.c:2280` — Meta byte (0x83) is NOT a token. Pin
14470 /// the regression: previously the Rust port hardcoded `0x83`
14471 /// as a token byte. Now correctly excludes it.
14472 #[test]
14473 fn has_token_excludes_meta_byte() {
14474 let _g = crate::test_util::global_state_lock();
14475 inittyptab();
14476 // 0x83 is Meta, NOT itok. Should return false.
14477 let s: String = std::iter::once(0x83u8 as char).collect();
14478 assert!(
14479 !has_token(&s),
14480 "c:2285 — Meta (0x83) is NOT itok; previous hardcoded 0x83 list misfired"
14481 );
14482 }
14483
14484 /// `Src/utils.c:2284-2285` — C scans BYTES because its input is
14485 /// METAFIED (a raw >= 0x80 byte is stored as Meta + `byte ^ 32`), so a
14486 /// bare 0x84..=0xA1 there is always a token. zshrs keeps UTF-8 `str`
14487 /// and stores tokens as PUA CHARS (crate::token_char) — the representation
14488 /// `untokenize` (lex.rs:5990) tests with `c as u32` — so the byte walk
14489 /// mistook a UTF-8 CONTINUATION byte for a token. `—` U+2014 is E2 80
14490 /// 94, `→` U+2192 is E2 86 92, `“` U+201C is E2 80 9C: 0x94, 0x86,
14491 /// 0x92 and 0x9C all answer `itok`. This asserted-false set is what
14492 /// `s.bytes().any(itok)` got wrong; `lex.rs:6165` already scanned
14493 /// chars, this copy did not.
14494 #[test]
14495 fn has_token_ignores_utf8_continuation_bytes_in_itok_range() {
14496 let _g = crate::test_util::global_state_lock();
14497 inittyptab();
14498 for s in ["a—b", "a→b", "a“b”c", "—", "→"] {
14499 assert!(
14500 s.bytes().any(itok),
14501 "{s:?} must actually contain an ITOK-range BYTE, else this pins nothing"
14502 );
14503 assert!(
14504 !has_token(s),
14505 "c:2285 — {s:?} holds no token CHAR (crate::token_char); only \
14506 UTF-8 continuation bytes that happen to land in the ITOK range"
14507 );
14508 }
14509 // Still true for a genuine token char, so the fix isn't a blanket
14510 // "non-ASCII is never a token".
14511 assert!(has_token("plain\u{e19c}"), "c:2285 — Bang (U+e19C) is a token");
14512 }
14513
14514 /// `Src/utils.c:5271` — `else if (itok(*s)) { s++; continue; }`. C may
14515 /// scan bytes there because its `s` is metafied; zshrs keeps UTF-8
14516 /// `str` with tokens as CHARS, so the byte walk silently DROPPED the
14517 /// continuation byte of any multibyte glyph landing in `0x84..=0xa1`.
14518 /// `functions f` printed `print "a<E2><80>b"` for a body holding `—`
14519 /// (U+2014 = E2 80 94, 0x94 = `Inang`). Same defect as
14520 /// `has_token_ignores_utf8_continuation_bytes_in_itok_range` above,
14521 /// at the printing site.
14522 #[test]
14523 fn zputs_ignores_utf8_continuation_bytes_in_itok_range() {
14524 let _g = crate::test_util::global_state_lock();
14525 inittyptab();
14526 for s in ["a—b", "a→b", "a“b”c", "字符", "aéb"] {
14527 let mut out: Vec<u8> = Vec::new();
14528 assert_eq!(zputs(s, &mut out), 0, "c:5280 — zputs returns 0");
14529 assert_eq!(
14530 out,
14531 s.as_bytes(),
14532 "c:5271 — {s:?} holds no token CHAR (crate::token_char); zputs \
14533 must emit it verbatim, not drop UTF-8 continuation bytes"
14534 );
14535 }
14536 // Genuine token chars are still skipped (c:5271-5272), and a
14537 // Meta pair still decodes to the escaped byte (c:5269-5270), so
14538 // the fix is not a blanket "pass everything through".
14539 let mut out: Vec<u8> = Vec::new();
14540 let _ = zputs("a\u{e19c}b", &mut out);
14541 assert_eq!(out, b"ab", "c:5271 — Bang (U+e19C) is a token, skipped");
14542 let mut out: Vec<u8> = Vec::new();
14543 let _ = zputs("a\u{83}\u{c1}b", &mut out);
14544 assert_eq!(
14545 out,
14546 b"a\xe1b",
14547 "c:5270 — Meta + (0xe1 ^ 32) decodes back to the raw 0xe1"
14548 );
14549 }
14550
14551 /// `Src/utils.c:6082-6124` — `addunprintable(c)`. Renders bytes
14552 /// using shell-compatible C-string escapes (`\a`/`\b`/`\f`/`\n`/
14553 /// `\r`/`\t`/`\v` for the named controls + `\nnn` octal for
14554 /// others + `\0` for NUL). Previously emitted ZLE caret form —
14555 /// wrong convention.
14556 #[test]
14557 fn addunprintable_named_control_escapes() {
14558 let _g = crate::test_util::global_state_lock();
14559 // c:6106-6112 — named-escape per byte.
14560 assert_eq!(addunprintable('\x07'), "\\a", "c:6106 — BEL → \\a");
14561 assert_eq!(addunprintable('\x08'), "\\b", "c:6107 — BS → \\b");
14562 assert_eq!(addunprintable('\x0c'), "\\f", "c:6108 — FF → \\f");
14563 assert_eq!(addunprintable('\n'), "\\n", "c:6109 — LF → \\n");
14564 assert_eq!(addunprintable('\r'), "\\r", "c:6110 — CR → \\r");
14565 assert_eq!(addunprintable('\t'), "\\t", "c:6111 — TAB → \\t");
14566 assert_eq!(addunprintable('\x0b'), "\\v", "c:6112 — VT → \\v");
14567 }
14568
14569 /// c:6097-6103 — NUL renders as `\0`. (C peeks next byte for
14570 /// `\000` disambiguation; Rust port works one char at a time
14571 /// so emits the short `\0` form.)
14572 #[test]
14573 fn addunprintable_nul_renders_as_backslash_zero() {
14574 let _g = crate::test_util::global_state_lock();
14575 assert_eq!(addunprintable('\0'), "\\0", "c:6097-6098 — NUL → \\0");
14576 }
14577
14578 /// c:6114-6119 — `\nnn` 3-digit octal fallback for un-named
14579 /// control bytes. Pin the format: each digit is one octal digit
14580 /// (0-7), zero-padded to 3 positions.
14581 #[test]
14582 fn addunprintable_octal_fallback_for_unnamed_controls() {
14583 let _g = crate::test_util::global_state_lock();
14584 // 0x01 (SOH) = 001 octal.
14585 assert_eq!(addunprintable('\x01'), "\\001", "c:6116-6118 — SOH → \\001");
14586 // 0x1b (ESC) = 033 octal.
14587 assert_eq!(addunprintable('\x1b'), "\\033", "c:6116-6118 — ESC → \\033");
14588 // 0x7f (DEL) = 177 octal.
14589 assert_eq!(addunprintable('\x7f'), "\\177", "c:6116-6118 — DEL → \\177");
14590 // 0xff (high) = 377 octal.
14591 assert_eq!(
14592 addunprintable(char::from_u32(0xff).unwrap()),
14593 "\\377",
14594 "c:6116-6118 — 0xff → \\377"
14595 );
14596 }
14597
14598 /// `Src/utils.c:6072-6082` — `hasspecial(s)`. Pin canonical
14599 /// special chars from SPECCHARS (`Src/zsh.h:228`): `#$^*()=|{}[]
14600 /// \`<>?~;&\\n\\t \\\\\\'\\"`.
14601 #[test]
14602 fn hasspecial_recognises_canonical_special_chars() {
14603 let _g = crate::test_util::global_state_lock();
14604 // typtab access is read-only here (no flag mutation); concurrent
14605 // inittyptab() rebuilds are idempotent for the default flag set.
14606 inittyptab();
14607 // c:6075 — every char in SPECCHARS triggers true.
14608 for c in "#$^*()=|{}[]<>?~;&".chars() {
14609 let s = c.to_string();
14610 assert!(
14611 hasspecial(&s),
14612 "c:6075 — '{}' is in SPECCHARS, must be special",
14613 c
14614 );
14615 }
14616 // Whitespace specials.
14617 assert!(hasspecial(" "), "space in SPECCHARS");
14618 assert!(hasspecial("\t"), "tab in SPECCHARS");
14619 assert!(hasspecial("\n"), "newline in SPECCHARS");
14620 // Non-special chars → false.
14621 assert!(
14622 !hasspecial("hello"),
14623 "c:6075 — plain alphanumerics are NOT special"
14624 );
14625 assert!(!hasspecial("ABC012"));
14626 }
14627
14628 /// `Src/utils.c:6072-6075` — Meta-byte handling: `*s == Meta ?
14629 /// *++s ^ 32 : *s`. Pin Meta+X pair gets decoded before special-
14630 /// check. A Meta+'A' decodes to 'a' (not special), so a string
14631 /// containing only Meta+'A' returns false.
14632 #[test]
14633 fn hasspecial_decodes_meta_byte_before_check() {
14634 let _g = crate::test_util::global_state_lock();
14635 // Same as above — read-only after inittyptab.
14636 inittyptab();
14637 // Meta + 0x41 ('A') → 'a' (0x61) → not special.
14638 let bytes: Vec<u8> = vec![Meta, 0x41u8];
14639 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14640 assert!(
14641 !hasspecial(s),
14642 "c:6075 — Meta+0x41 decodes to 'a' which is NOT special"
14643 );
14644 }
14645
14646 /// `Src/utils.c:5849-5910` — `sb_niceformat(s)`. Pin: ASCII
14647 /// printable passes through unchanged; controls escape.
14648 /// C-equivalent call shape: `char *out; sb_niceformat(s, NULL, &out, 0);`
14649 #[test]
14650 fn sb_niceformat_passes_printable_ascii() {
14651 let _g = crate::test_util::global_state_lock();
14652 let mut out: Option<String> = None;
14653 let l = sb_niceformat("hello", None, Some(&mut out), 0);
14654 assert_eq!(
14655 out.as_deref(),
14656 Some("hello"),
14657 "c:5886 — nicechar_sel passes printable through"
14658 );
14659 assert_eq!(l, 5, "c:5928 — return length equals output bytes");
14660
14661 let mut out: Option<String> = None;
14662 sb_niceformat("", None, Some(&mut out), 0);
14663 assert_eq!(out.as_deref(), Some(""));
14664
14665 let mut out: Option<String> = None;
14666 sb_niceformat("ABC012!?@", None, Some(&mut out), 0);
14667 assert_eq!(out.as_deref(), Some("ABC012!?@"));
14668 }
14669
14670 /// c:5886 — controls get `\n`/`\t`/`^X`/`^?` escapes via nicechar_sel.
14671 #[test]
14672 fn sb_niceformat_escapes_controls() {
14673 let _g = crate::test_util::global_state_lock();
14674 let mut out: Option<String> = None;
14675 sb_niceformat("a\nb", None, Some(&mut out), 0);
14676 assert_eq!(out.as_deref(), Some("a\\nb"), "c:5886 — newline → \\n");
14677
14678 let mut out: Option<String> = None;
14679 sb_niceformat("a\tb", None, Some(&mut out), 0);
14680 assert_eq!(out.as_deref(), Some("a\\tb"));
14681
14682 let mut out: Option<String> = None;
14683 sb_niceformat("\x01", None, Some(&mut out), 0);
14684 assert_eq!(
14685 out.as_deref(),
14686 Some("^A"),
14687 "c:5886 — control char → ^X form"
14688 );
14689
14690 let mut out: Option<String> = None;
14691 sb_niceformat("\x7f", None, Some(&mut out), 0);
14692 assert_eq!(out.as_deref(), Some("^?"));
14693 }
14694
14695 /// `Src/utils.c:5872` — unmetafy step before formatting. Pin:
14696 /// metafied Meta+X pair gets unescaped first, then run through
14697 /// nicechar_sel.
14698 #[test]
14699 fn sb_niceformat_unmetafies_before_formatting() {
14700 let _g = crate::test_util::global_state_lock();
14701 // META + 0x41 ('A') → decodes to 'a' (0x61). 'a' is printable
14702 // → passes through unchanged.
14703 let bytes: Vec<u8> = vec![Meta, 0x41u8];
14704 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14705 let mut out: Option<String> = None;
14706 sb_niceformat(s, None, Some(&mut out), 0);
14707 assert_eq!(
14708 out.as_deref(),
14709 Some("a"),
14710 "c:5872 — unmetafy first: Meta+0x41 → 'a' → printable passthrough"
14711 );
14712 // META + 0x20 → decodes to NUL → "^@" form.
14713 let bytes: Vec<u8> = vec![Meta, 0x20u8];
14714 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14715 let mut out: Option<String> = None;
14716 sb_niceformat(s, None, Some(&mut out), 0);
14717 let r = out.unwrap_or_default();
14718 assert!(
14719 !r.is_empty(),
14720 "c:5872 — Meta+0x20 → NUL → must emit some escape, not empty"
14721 );
14722 }
14723
14724 /// `Src/utils.c:5937-5959` — `is_sb_niceformat(s)`. Returns 1
14725 /// if sb_niceformat would change the input, else 0.
14726 #[test]
14727 fn is_sb_niceformat_true_for_strings_with_controls() {
14728 let _g = crate::test_util::global_state_lock();
14729 assert_eq!(is_sb_niceformat("\n"), 1, "newline is nice");
14730 assert_eq!(is_sb_niceformat("a\tb"), 1);
14731 assert_eq!(is_sb_niceformat("\x01"), 1);
14732 // Non-control ASCII → 0.
14733 assert_eq!(is_sb_niceformat("hello"), 0);
14734 assert_eq!(is_sb_niceformat(""), 0);
14735 }
14736
14737 /// `Src/utils.c:5810-5826` — `metacharlenconv(x, c)`. Returns
14738 /// `(2, decoded)` for Meta+X pair, `(1, byte)` for plain byte.
14739 /// Pin both branches.
14740 #[test]
14741 fn metacharlenconv_plain_ascii_returns_one_byte() {
14742 let _g = crate::test_util::global_state_lock();
14743 // c:5823-5825 — plain byte.
14744 let (n, c) = metacharlenconv("a");
14745 assert_eq!((n, c), (1, Some('a')), "c:5823 — plain byte → (1, byte)");
14746 let (n, c) = metacharlenconv("X");
14747 assert_eq!((n, c), (1, Some('X')));
14748 }
14749
14750 /// c:5818-5821 — Meta+X pair: `*c = x[1] ^ 32; return 2`.
14751 #[test]
14752 fn metacharlenconv_meta_pair_xor_decodes() {
14753 let _g = crate::test_util::global_state_lock();
14754 // Meta + 0x41 ('A') → 'A' ^ 32 = 'a'.
14755 let bytes: Vec<u8> = vec![Meta, 0x41u8];
14756 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14757 let (n, c) = metacharlenconv(s);
14758 assert_eq!(
14759 (n, c),
14760 (2, Some('a')),
14761 "c:5820 — Meta+0x41 → 'a' (XOR 32), consumed 2 bytes"
14762 );
14763 // Empty input.
14764 let (n, c) = metacharlenconv("");
14765 assert_eq!((n, c), (0, None));
14766 }
14767
14768 /// `Src/utils.c:5832-5843` — `charlenconv(x, len, c)`. Returns
14769 /// `(0, NUL)` when len=0, `(1, byte)` otherwise.
14770 #[test]
14771 fn charlenconv_zero_len_returns_zero() {
14772 let _g = crate::test_util::global_state_lock();
14773 // c:5834 — `if (!len) { *c = '\0'; return 0; }`.
14774 let (n, c) = charlenconv("abc", 0);
14775 assert_eq!((n, c), (0, None), "c:5834-5837 — len=0 returns 0");
14776 }
14777
14778 /// c:5840-5842 — non-zero len: read one byte, return (1, byte).
14779 #[test]
14780 fn charlenconv_returns_first_byte_for_nonzero_len() {
14781 let _g = crate::test_util::global_state_lock();
14782 let (n, c) = charlenconv("abc", 3);
14783 assert_eq!((n, c), (1, Some('a')), "c:5841 — *c = *x; return 1");
14784 let (n, c) = charlenconv("xy", 2);
14785 assert_eq!((n, c), (1, Some('x')));
14786 }
14787
14788 /// `Src/utils.c:5474-5524` — `is_mb_niceformat(s)`. Predicate:
14789 /// would mb_niceformat produce a different output? Pin canonical
14790 /// branches:
14791 /// - Pure ASCII printable → false (no escape needed).
14792 /// - Contains control char → true.
14793 /// - Pure UTF-8 wide chars → false under default PRINTEIGHTBIT-off
14794 /// since is_wcs_nicechar treats them as printable.
14795 #[test]
14796 fn is_mb_niceformat_false_for_pure_printable_ascii() {
14797 let _g = crate::test_util::global_state_lock();
14798 assert_eq!(
14799 is_mb_niceformat("hello"),
14800 0,
14801 "c:5509 — printable ASCII needs no nice-format"
14802 );
14803 assert_eq!(
14804 is_mb_niceformat(""),
14805 0,
14806 "c:5486 — empty string has no chars to flag"
14807 );
14808 assert_eq!(is_mb_niceformat("ABC012!?@"), 0);
14809 }
14810
14811 /// c:5509 + `is_wcs_nicechar` — newline/tab/control chars trigger
14812 /// the predicate.
14813 #[test]
14814 fn is_mb_niceformat_true_for_strings_with_controls() {
14815 let _g = crate::test_util::global_state_lock();
14816 assert_eq!(is_mb_niceformat("a\nb"), 1, "c:5509 — newline is nice");
14817 assert_eq!(is_mb_niceformat("\t"), 1);
14818 assert_eq!(is_mb_niceformat("\x01"), 1, "c:5509 — control char is nice");
14819 assert_eq!(is_mb_niceformat("\x7f"), 1, "c:5509 — DEL is nice");
14820 }
14821
14822 /// `Src/utils.c:5366-5460` — `mb_niceformat(s)`. Multibyte-aware
14823 /// nice-formatter. Calls `wcs_nicechar` per char. Test uses
14824 /// C-equivalent call shape: `char *out; mb_niceformat(s, NULL, &out, 0);`
14825 #[test]
14826 fn mb_niceformat_preserves_printable_wide_chars() {
14827 let _g = crate::test_util::global_state_lock();
14828 let mut out: Option<String> = None;
14829 mb_niceformat("hello", None, Some(&mut out), 0);
14830 assert_eq!(
14831 out.as_deref(),
14832 Some("hello"),
14833 "c:5407 — printable ASCII passes through"
14834 );
14835
14836 let mut out: Option<String> = None;
14837 mb_niceformat("café", None, Some(&mut out), 0);
14838 assert_eq!(
14839 out.as_deref(),
14840 Some("café"),
14841 "c:5407 — Latin-1 'é' must NOT byte-mask to \\M-X"
14842 );
14843
14844 let mut out: Option<String> = None;
14845 mb_niceformat("字", None, Some(&mut out), 0);
14846 assert_eq!(
14847 out.as_deref(),
14848 Some("字"),
14849 "c:5407 — CJK printable passes through"
14850 );
14851
14852 let mut out: Option<String> = None;
14853 mb_niceformat("abcéxyz", None, Some(&mut out), 0);
14854 assert_eq!(out.as_deref(), Some("abcéxyz"));
14855 }
14856
14857 /// `Src/utils.c:5407` + `wcs_nicechar` — controls still escape.
14858 #[test]
14859 fn mb_niceformat_escapes_controls() {
14860 let _g = crate::test_util::global_state_lock();
14861 let mut out: Option<String> = None;
14862 mb_niceformat("\n", None, Some(&mut out), 0);
14863 assert_eq!(
14864 out.as_deref(),
14865 Some("\\n"),
14866 "c:5407 → wcs_nicechar c:625 — newline escapes"
14867 );
14868
14869 let mut out: Option<String> = None;
14870 mb_niceformat("\t", None, Some(&mut out), 0);
14871 assert_eq!(
14872 out.as_deref(),
14873 Some("\\t"),
14874 "c:5407 → wcs_nicechar c:628 — tab escapes"
14875 );
14876
14877 let mut out: Option<String> = None;
14878 mb_niceformat("a\nb", None, Some(&mut out), 0);
14879 assert_eq!(out.as_deref(), Some("a\\nb"));
14880 }
14881
14882 /// `Src/utils.c:4971-4983` — `metalen(s, len)`. **Input `len` is
14883 /// the UNMETAFIED char count; output is the METAFIED byte count.**
14884 /// Pin a metafied string with one Meta+X pair: 3 unmetafied chars
14885 /// → 4 metafied bytes.
14886 #[test]
14887 fn metalen_returns_metafied_byte_count() {
14888 let _g = crate::test_util::global_state_lock();
14889 // Pure ASCII: 5 chars → 5 bytes (no Meta encountered).
14890 assert_eq!(
14891 metalen("hello", 5),
14892 5,
14893 "c:4972 — ASCII: metafied bytes == unmetafied chars"
14894 );
14895 // [a, Meta, X, b] = 4 metafied bytes representing 3 chars.
14896 let bytes: Vec<u8> = vec![b'a', Meta, 0x41, b'b'];
14897 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14898 assert_eq!(
14899 metalen(s, 3),
14900 4,
14901 "c:4978 — 3 unmetafied chars + 1 Meta = 4 metafied bytes"
14902 );
14903 // Two Meta pairs: [Meta, X, Meta, Y] = 4 bytes for 2 chars.
14904 let bytes: Vec<u8> = vec![Meta, 0x41, Meta, 0x42];
14905 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14906 assert_eq!(
14907 metalen(s, 2),
14908 4,
14909 "c:4978 — 2 unmetafied chars (both Meta+X) = 4 metafied bytes"
14910 );
14911 }
14912
14913 /// c:4974 — `mlen = len;` is the initial value (no Meta found,
14914 /// returns the input length unchanged). Pin len=0 and pure-ASCII.
14915 #[test]
14916 fn metalen_returns_input_for_no_meta_chars() {
14917 let _g = crate::test_util::global_state_lock();
14918 assert_eq!(metalen("", 0), 0, "c:4974 — empty input returns 0");
14919 assert_eq!(
14920 metalen("abc", 3),
14921 3,
14922 "c:4974 — no Meta chars: output == input len"
14923 );
14924 }
14925
14926 /// `Src/utils.c:5070` — `if (!in || !*in) return 0;`.
14927 /// Empty input returns `('\0', 0)`. Pin: empty &str → 0 bytes consumed.
14928 #[test]
14929 fn unmeta_one_empty_input_returns_zero() {
14930 let _g = crate::test_util::global_state_lock();
14931 let (c, n) = unmeta_one("");
14932 assert_eq!(c, '\0', "c:5070 — empty input returns NUL char");
14933 assert_eq!(n, 0, "c:5070 — empty input consumes 0 bytes");
14934 }
14935
14936 /// `Src/utils.c:5081-5082` — Non-Meta byte: `*sz = 1; wc = byte`.
14937 #[test]
14938 fn unmeta_one_plain_ascii_consumes_one_byte() {
14939 let _g = crate::test_util::global_state_lock();
14940 for c in "aA0!~".chars() {
14941 let s = c.to_string();
14942 let (got, n) = unmeta_one(&s);
14943 assert_eq!(got, c, "c:5082 — '{}' decodes to itself", c);
14944 assert_eq!(n, 1, "c:5081 — non-Meta byte consumes 1");
14945 }
14946 }
14947
14948 /// `Src/utils.c:5077-5079` — Meta byte: `*sz = 2; wc = in[1] ^ 32`.
14949 /// Pin via constructed Meta byte sequence.
14950 #[test]
14951 fn unmeta_one_meta_pair_decodes_xor_32() {
14952 let _g = crate::test_util::global_state_lock();
14953 // META + 0x41 ('A') → 'A' ^ 32 = 0x61 ('a'). 2 bytes consumed.
14954 let bytes: Vec<u8> = vec![Meta, 0x41u8];
14955 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14956 let (got, n) = unmeta_one(s);
14957 assert_eq!(got, 'a', "c:5079 — Meta+0x41 decodes to 0x41^32 = 'a'");
14958 assert_eq!(n, 2, "c:5078 — Meta pair consumes 2 bytes");
14959 // META + 0x20 = 0x20^32 = 0x00 (NUL).
14960 let bytes: Vec<u8> = vec![Meta, 0x20u8];
14961 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14962 let (got, n) = unmeta_one(s);
14963 assert_eq!(
14964 got, '\0',
14965 "c:5079 — Meta+0x20 decodes to NUL (the canonical metafy-NUL pattern)"
14966 );
14967 assert_eq!(n, 2);
14968 }
14969
14970 /// Edge case: bare Meta byte at end with no follower. C source
14971 /// would read past the buffer (UB); Rust port's `bytes.len() > 1`
14972 /// guard handles this by falling through to the non-Meta arm.
14973 #[test]
14974 fn unmeta_one_trailing_meta_byte_falls_through() {
14975 let _g = crate::test_util::global_state_lock();
14976 let bytes: Vec<u8> = vec![Meta];
14977 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
14978 let (_, n) = unmeta_one(s);
14979 // Defensive: Rust returns the Meta byte itself as char + 1.
14980 assert_eq!(n, 1, "trailing Meta — no panic, no read past buffer");
14981 }
14982
14983 /// `Src/utils.c:5185-5203` — `ztrsub(t, s)`. Count of unmetafied
14984 /// chars between two pointers in the same metafied string.
14985 /// Rust port takes (buf, start, end) byte-offsets since raw
14986 /// pointer subtraction across distinct &str isn't safe.
14987 /// Pin pure-ASCII subrange (no Meta) → returns end-start.
14988 #[test]
14989 fn ztrsub_ascii_no_meta_returns_byte_distance() {
14990 let _g = crate::test_util::global_state_lock();
14991 // "hello world" — substring [0..5) → "hello" → 5 chars.
14992 assert_eq!(
14993 ztrsub("hello world", 0, 5),
14994 5,
14995 "c:5189 — no Meta: returns t-s byte distance"
14996 );
14997 // [6..11) → "world" → 5 chars.
14998 assert_eq!(ztrsub("hello world", 6, 11), 5);
14999 // Empty range.
15000 assert_eq!(ztrsub("hello", 2, 2), 0);
15001 }
15002
15003 /// c:5191-5199 — each Meta+X pair decrements `l` by 1 (since the
15004 /// initial l = byte-distance counts both bytes but the pair is
15005 /// one logical char). Pin via constructed Meta string.
15006 #[test]
15007 fn ztrsub_subtracts_one_per_meta_pair() {
15008 let _g = crate::test_util::global_state_lock();
15009 // Build "a" + Meta + 0x20 + "b" = 4 bytes total.
15010 // Unmetafied: "a" + 1-char-from-meta + "b" = 3 chars.
15011 let meta_byte = Meta;
15012 let buf_bytes: Vec<u8> = vec![b'a', meta_byte, 0x20, b'b'];
15013 let buf = unsafe { std::str::from_utf8_unchecked(&buf_bytes) };
15014 // [0..4) byte distance = 4; Meta pair found → l-- → 3.
15015 assert_eq!(
15016 ztrsub(buf, 0, 4),
15017 3,
15018 "c:5192-5199 — Meta pair decrements distance by 1"
15019 );
15020 }
15021
15022 /// c:5189 — `int l = t - s;` start of count. Pin edge case
15023 /// where start > end (caller bug) doesn't underflow or panic;
15024 /// Rust port clamps via `start.min(end)`.
15025 #[test]
15026 fn ztrsub_clamps_inverted_range_to_zero() {
15027 let _g = crate::test_util::global_state_lock();
15028 assert_eq!(
15029 ztrsub("hello", 4, 2),
15030 0,
15031 "inverted range start>end → 0 (defensive; not in C contract)"
15032 );
15033 // Beyond-buffer end gets clamped.
15034 assert_eq!(
15035 ztrsub("hi", 0, 100),
15036 2,
15037 "end > buf.len() clamps to buffer length"
15038 );
15039 }
15040
15041 /// `Src/utils.c:5141` — trailing Meta byte with no following char
15042 /// is a malformed-string edge case. C has a DPUTS debug-only
15043 /// fprintf for "unexpected end" but otherwise the loop terminates
15044 /// because `*s` is now NUL. Rust port must not panic on this
15045 /// truncated input.
15046 #[test]
15047 fn ztrlen_handles_trailing_meta_byte_without_panic() {
15048 let _g = crate::test_util::global_state_lock();
15049 let meta = char::from_u32(Meta as u32).unwrap();
15050 let trailing: String = ['a', meta].iter().collect();
15051 // c:5141-5149 — increment l, then encounter Meta, then loop
15052 // condition `*s` is empty → terminate. Count: 'a' + Meta = 2
15053 // (Rust port counts Meta as 1 char when no follower).
15054 let r = ztrlen(&trailing);
15055 assert!(r >= 1, "trailing Meta must not panic; got {}", r);
15056 }
15057
15058 /// c:params.c:1288 — `isident` rejects digit-leading names.
15059 /// A regression accepting them lets `typeset 1foo=bar` install
15060 /// a poisoned param that no later expansion can address.
15061 #[test]
15062 fn isident_rejects_digit_leading_names() {
15063 let _g = crate::test_util::global_state_lock(); // c:1288
15064 // c:1315-1319 — C `isident`: if first char is digit, ALL
15065 // chars must be digit (all-digit names are valid positional
15066 // params like $99). So `1foo` is rejected (digit-then-alpha)
15067 // but `99` is accepted as a valid positional-param name.
15068 // The previous utils.rs fake rejected all-digit names too,
15069 // which was non-faithful — the canonical `params::isident`
15070 // at `params.rs:2056` matches the C semantics.
15071 assert!(!isident("1foo")); // c:1318
15072 assert!(
15073 isident("99"), // c:1318
15074 "c:1315-1319 — all-digit names are valid positional params"
15075 );
15076 assert!(!isident("")); // c:1290
15077 }
15078
15079 /// `isident` MUST accept underscore-leading + alpha-leading names —
15080 /// `_foo`, `Foo_BAR_42` are valid POSIX shell idents.
15081 #[test]
15082 fn isident_accepts_underscore_and_alpha_leading() {
15083 let _g = crate::test_util::global_state_lock(); // c:1288
15084 assert!(isident("foo")); // c:1288
15085 assert!(isident("_foo")); // c:1288
15086 assert!(isident("Foo_BAR_42")); // c:1288
15087 assert!(isident("a")); // c:1288
15088 }
15089
15090 /// `isident` rejects whitespace/punctuation/$. A regression
15091 /// accepting them would let assignments install names no
15092 /// subsequent lookup can address (`typeset 'foo bar'=baz`).
15093 #[test]
15094 fn isident_rejects_special_chars() {
15095 let _g = crate::test_util::global_state_lock(); // c:1288
15096 // c:1320-1322 — non-digit-leading names use itype_end with
15097 // INAMESPC bits (alnum + `_` + `.`); other chars terminate.
15098 // The previous utils.rs fake rejected `foo.` outright, which
15099 // is incorrect — C accepts trailing `.` (it's an INAMESPC
15100 // member). Whitespace, `-`, and `$` are NOT INAMESPC chars
15101 // and remain rejected.
15102 assert!(!isident("foo bar")); // c:1322
15103 assert!(!isident("foo-bar")); // c:1322
15104 assert!(!isident("a$b")); // c:1322
15105 }
15106
15107 /// `convbase(0, 10)` MUST produce `"0"`, not `""`. The literal-zero
15108 /// edge case is a real C divergence point — regression here would
15109 /// make `$(( 0 ))` print nothing.
15110 #[test]
15111 fn convbase_zero_renders_as_zero_literal() {
15112 let _g = crate::test_util::global_state_lock();
15113 assert_eq!(convbase(0, 10), "0");
15114 }
15115
15116 /// `convbase` uses zsh's canonical `BASE#NUMBER` syntax for
15117 /// non-decimal output (matches `$(( [#16] 255 ))` → `16#FF`).
15118 /// This is the format `printf "%X"` and `$(( ... ))` both produce.
15119 #[test]
15120 fn convbase_uses_base_prefix_syntax_for_non_decimal() {
15121 let _g = crate::test_util::global_state_lock();
15122 assert_eq!(convbase(255, 16), "16#FF");
15123 assert_eq!(convbase(8, 8), "8#10");
15124 assert_eq!(convbase(5, 2), "2#101");
15125 // Base 10 has no prefix.
15126 assert_eq!(convbase(42, 10), "42");
15127 }
15128
15129 /// Negative number renders with leading `-`. Regression that drops
15130 /// the sign would silently flip arithmetic semantics in `$((...))`.
15131 #[test]
15132 fn convbase_preserves_negative_sign() {
15133 let _g = crate::test_util::global_state_lock();
15134 assert_eq!(convbase(-42, 10), "-42");
15135 }
15136
15137 /// `Src/utils.c:837` — `slashsplit` keeps an EMPTY leading segment
15138 /// when the input has a leading `/`. The caller `xsymlinks`
15139 /// (c:879) reads this empty as the signal "absolute path, start
15140 /// xbuf from /". Previous Rust test asserted filtering of all
15141 /// empties (matching the previous buggy port); that contract was
15142 /// wrong relative to C. Updated to assert the C contract.
15143 #[test]
15144 fn slashsplit_keeps_leading_empty_segment() {
15145 let _g = crate::test_util::global_state_lock();
15146 // c:851 with t==s on first iter → empty prefix segment.
15147 assert_eq!(
15148 slashsplit("/usr/local/bin"),
15149 vec![
15150 "".to_string(),
15151 "usr".to_string(),
15152 "local".to_string(),
15153 "bin".to_string()
15154 ],
15155 "c:851 — leading `/` produces empty first segment"
15156 );
15157 // Single `/`: one empty segment only.
15158 assert_eq!(
15159 slashsplit("/"),
15160 vec!["".to_string()],
15161 "c:854-857 — trailing `/` after first iter returns ['']"
15162 );
15163 // Consecutive slashes collapse (c:852-853 inner while-loop).
15164 assert_eq!(
15165 slashsplit("//foo"),
15166 vec!["".to_string(), "foo".to_string()],
15167 "c:852-853 — consecutive `/` collapse, leading empty kept"
15168 );
15169 // Empty input still empty result.
15170 assert_eq!(
15171 slashsplit(""),
15172 Vec::<String>::new(),
15173 "c:842 — empty input → empty array"
15174 );
15175 }
15176
15177 /// `Src/utils.c:837` — relative paths (no leading `/`) start
15178 /// at the first non-slash; trailing `/` doesn't add a segment
15179 /// (c:854-857 returns before tail emit).
15180 #[test]
15181 fn slashsplit_relative_path_no_trailing_empty() {
15182 let _g = crate::test_util::global_state_lock();
15183 assert_eq!(
15184 slashsplit("a/b/"),
15185 vec!["a".to_string(), "b".to_string()],
15186 "c:854-857 — trailing `/` doesn't add segment"
15187 );
15188 // Mid-slash collapse in relative path.
15189 assert_eq!(
15190 slashsplit("a//b"),
15191 vec!["a".to_string(), "b".to_string()],
15192 "c:852-853 — mid `//` collapses"
15193 );
15194 }
15195
15196 /// `equalsplit("foo=bar")` returns Some(("foo","bar")). Used by
15197 /// `typeset NAME=val` parsing. Regression returning None on a
15198 /// well-formed assignment would silently break every export/typeset.
15199 #[test]
15200 fn equalsplit_returns_first_equals_split() {
15201 let _g = crate::test_util::global_state_lock();
15202 assert_eq!(
15203 equalsplit("foo=bar"),
15204 Some(("foo".to_string(), "bar".to_string()))
15205 );
15206 assert_eq!(
15207 equalsplit("a=b=c"),
15208 Some(("a".to_string(), "b=c".to_string())),
15209 "splits on FIRST `=` only"
15210 );
15211 }
15212
15213 /// `equalsplit("foo")` returns None — no `=` to split on. Catches
15214 /// a regression returning Some(("foo","")) which would let bare
15215 /// names install with empty values silently.
15216 #[test]
15217 fn equalsplit_no_equals_returns_none() {
15218 let _g = crate::test_util::global_state_lock();
15219 assert_eq!(equalsplit("foo"), None);
15220 assert_eq!(equalsplit(""), None);
15221 }
15222
15223 /// c:5106 — `ztrcmp` is the canonical zsh string compare. Same
15224 /// inputs MUST produce same Ordering (deterministic). Regression
15225 /// to a randomised hash-order would mis-sort `${(o)array}` output.
15226 #[test]
15227 fn ztrcmp_deterministic_and_lexicographic() {
15228 let _g = crate::test_util::global_state_lock();
15229 assert_eq!(ztrcmp("abc", "abc"), std::cmp::Ordering::Equal);
15230 assert_eq!(ztrcmp("abc", "abd"), std::cmp::Ordering::Less);
15231 assert_eq!(ztrcmp("abd", "abc"), std::cmp::Ordering::Greater);
15232 // Empty strings.
15233 assert_eq!(ztrcmp("", ""), std::cmp::Ordering::Equal);
15234 assert_eq!(ztrcmp("", "a"), std::cmp::Ordering::Less);
15235 }
15236
15237 /// c:5106 — shorter-as-prefix sorts before longer (like strcmp).
15238 /// Catches a regression where length-then-content would mis-sort.
15239 #[test]
15240 fn ztrcmp_shorter_prefix_is_less() {
15241 let _g = crate::test_util::global_state_lock();
15242 assert_eq!(ztrcmp("a", "ab"), std::cmp::Ordering::Less);
15243 assert_eq!(ztrcmp("foo", "foob"), std::cmp::Ordering::Less);
15244 }
15245
15246 /// `Src/utils.c:5117-5122` — Meta+X pair decodes to `X ^ 32`
15247 /// for comparison purposes. Pin: two strings differing only in
15248 /// the metafied byte's decoded value compare correctly.
15249 #[test]
15250 fn ztrcmp_decodes_meta_byte_for_comparison() {
15251 let _g = crate::test_util::global_state_lock();
15252 // META+0x21 ('!') = NUL? Let me use safer chars.
15253 // META+'A' (0x41) decodes to 'A' ^ 32 = 'a' (0x61).
15254 // So a "META a" pair represents the byte 'a' (0x61).
15255 // Compare "Ma" (M=Meta+a-byte-pair) vs "b" (>a).
15256 let meta_byte = Meta;
15257 let s1_bytes: Vec<u8> = vec![meta_byte, 0x41u8]; // decodes to 0x61 = 'a'
15258 let s2_bytes: Vec<u8> = vec![b'b'];
15259 let s1 = unsafe { std::str::from_utf8_unchecked(&s1_bytes) };
15260 let s2 = unsafe { std::str::from_utf8_unchecked(&s2_bytes) };
15261 // 'a' (0x61) < 'b' (0x62) → Less.
15262 assert_eq!(
15263 ztrcmp(s1, s2),
15264 std::cmp::Ordering::Less,
15265 "c:5117-5118 — Meta+0x41 decodes to 'a' which < 'b'"
15266 );
15267 }
15268
15269 /// `Src/utils.c:531-539` — `is_nicechar`. Returns true for chars
15270 /// needing escape-formatting:
15271 /// - c:534 — printable ASCII (0x20-0x7e) → false.
15272 /// - c:536 — high-bit byte (>= 0x80) → !PRINTEIGHTBIT.
15273 /// - c:538 — DEL/\n/\t/<0x20 → true.
15274 /// Pin the three branches.
15275 #[test]
15276 fn is_nicechar_printable_ascii_is_not_nice() {
15277 let _g = crate::test_util::global_state_lock();
15278 // c:534 — letters, digits, punctuation in 0x20-0x7e all NOT nice.
15279 for c in "abcXYZ012!?@~".chars() {
15280 assert!(
15281 !is_nicechar(c),
15282 "c:534 — '{}' (ASCII printable) must NOT be nice",
15283 c
15284 );
15285 }
15286 // c:534 — space (0x20) is printable.
15287 assert!(!is_nicechar(' '));
15288 }
15289
15290 /// c:538 — control chars (DEL, newline, tab, <0x20) ARE nice.
15291 #[test]
15292 fn is_nicechar_control_chars_are_nice() {
15293 let _g = crate::test_util::global_state_lock();
15294 assert!(is_nicechar('\n'), "c:538 — newline is nice");
15295 assert!(is_nicechar('\t'), "c:538 — tab is nice");
15296 assert!(is_nicechar('\x7f'), "c:538 — DEL is nice");
15297 assert!(is_nicechar('\x00'), "c:538 — NUL is nice (<0x20)");
15298 assert!(is_nicechar('\x07'), "c:538 — BEL is nice (<0x20)");
15299 assert!(is_nicechar('\x1b'), "c:538 — ESC is nice (<0x20)");
15300 assert!(is_nicechar('\x1f'), "c:538 — boundary 0x1f is nice");
15301 }
15302
15303 /// `Src/utils.c:2528-2570` — `zstrtoul_underscore(s)`. Pins
15304 /// the base-prefix dispatch: `0x` → 16, `0b` → 2, leading `0`
15305 /// only when OCTALZEROES is set. Tests the default state.
15306 #[test]
15307 fn zstrtoul_underscore_recognises_hex_binary_decimal() {
15308 let _g = crate::test_util::global_state_lock();
15309 // c:2538 — hex.
15310 assert_eq!(zstrtoul_underscore("0xff"), Some(255));
15311 assert_eq!(zstrtoul_underscore("0XFF"), Some(255));
15312 // c:2540 — binary.
15313 assert_eq!(zstrtoul_underscore("0b1010"), Some(10));
15314 assert_eq!(zstrtoul_underscore("0B11"), Some(3));
15315 // c:2537 — pure decimal (no leading zero).
15316 assert_eq!(zstrtoul_underscore("12345"), Some(12345));
15317 // c:2543 — leading-zero with OCTALZEROES off (default) → decimal.
15318 if !isset(OCTALZEROES) {
15319 assert_eq!(
15320 zstrtoul_underscore("0777"),
15321 Some(777),
15322 "c:2543 — OCTALZEROES off: leading-0 parses as decimal"
15323 );
15324 assert_eq!(zstrtoul_underscore("010"), Some(10));
15325 }
15326 }
15327
15328 /// `Src/utils.c:2547-2548` — underscores are stripped before
15329 /// parsing. C: `if (*s == '_') continue;`. Used to support
15330 /// human-readable big numbers like `1_000_000`.
15331 #[test]
15332 fn zstrtoul_underscore_strips_underscores() {
15333 let _g = crate::test_util::global_state_lock();
15334 assert_eq!(
15335 zstrtoul_underscore("1_000_000"),
15336 Some(1_000_000),
15337 "c:2547-2548 — `_` stripped from numeric input"
15338 );
15339 assert_eq!(zstrtoul_underscore("0xff_ff"), Some(0xffff));
15340 assert_eq!(zstrtoul_underscore("0b1010_1010"), Some(0xaa));
15341 }
15342
15343 /// `Src/utils.c:2533-2534` — leading `+` sign is consumed.
15344 /// `zulong` is unsigned so `-` is not handled here (caller
15345 /// handles negation at `zstrtol_underscore`).
15346 #[test]
15347 fn zstrtoul_underscore_consumes_leading_plus() {
15348 let _g = crate::test_util::global_state_lock();
15349 assert_eq!(zstrtoul_underscore("+42"), Some(42));
15350 assert_eq!(zstrtoul_underscore("+0xff"), Some(255));
15351 }
15352
15353 /// `Src/utils.c:467` — `if (ZISPRINT(c)) goto done;`. Printable
15354 /// ASCII passes through nicechar_sel unchanged. Pin a single-char
15355 /// printable input → single-char output.
15356 #[test]
15357 fn nicechar_sel_passes_printable_ascii_unchanged() {
15358 let _g = crate::test_util::global_state_lock();
15359 for c in "aA0!~".chars() {
15360 assert_eq!(
15361 nicechar_sel(c, false),
15362 c.to_string(),
15363 "c:467 — printable ASCII '{}' passes through",
15364 c
15365 );
15366 }
15367 }
15368
15369 /// `Src/utils.c:487-492` — `\n` becomes `\n` (backslash-n) and
15370 /// `\t` becomes `\t`. Both two-char outputs. Pin the canonical
15371 /// escapes.
15372 #[test]
15373 fn nicechar_sel_escapes_newline_and_tab() {
15374 let _g = crate::test_util::global_state_lock();
15375 assert_eq!(
15376 nicechar_sel('\n', false),
15377 "\\n",
15378 "c:487-489 — newline escape"
15379 );
15380 assert_eq!(nicechar_sel('\t', false), "\\t", "c:490-492 — tab escape");
15381 }
15382
15383 /// `Src/utils.c:493-501` — control chars (<0x20 except \n/\t)
15384 /// get `^X` prefix in non-quotable mode and `\C-X` in quotable.
15385 /// c:500 — `c += 0x40` adds 0x40 to map 0x01 → 0x41 ('A').
15386 #[test]
15387 fn nicechar_sel_control_chars_use_caret_or_c_prefix() {
15388 let _g = crate::test_util::global_state_lock();
15389 // Non-quotable: ^A
15390 assert_eq!(
15391 nicechar_sel('\x01', false),
15392 "^A",
15393 "c:499-500 — \\x01 → ^A non-quotable"
15394 );
15395 // Quotable: \C-A
15396 assert_eq!(
15397 nicechar_sel('\x01', true),
15398 "\\C-A",
15399 "c:495-500 — \\x01 → \\C-A quotable"
15400 );
15401 // ^G (BEL = 0x07 → 0x47 = 'G')
15402 assert_eq!(nicechar_sel('\x07', false), "^G");
15403 // ^[ (ESC = 0x1b → 0x5b = '[')
15404 assert_eq!(nicechar_sel('\x1b', false), "^[");
15405 }
15406
15407 /// `Src/utils.c:479-486` — DEL (0x7f) renders as `^?` or `\C-?`.
15408 #[test]
15409 fn nicechar_sel_del_renders_as_caret_question() {
15410 let _g = crate::test_util::global_state_lock();
15411 assert_eq!(nicechar_sel('\x7f', false), "^?", "c:485-486 — DEL is `^?`");
15412 assert_eq!(
15413 nicechar_sel('\x7f', true),
15414 "\\C-?",
15415 "c:481-486 — DEL is `\\C-?` quotable"
15416 );
15417 }
15418
15419 /// `Src/utils.c:469-478` — high-bit bytes (>= 0x80) under default
15420 /// PRINTEIGHTBIT-off path: write `\M-` then mask to low ASCII.
15421 /// If the masked char is printable, output is `\M-<char>`. Pin
15422 /// the canonical 0xc1 → \M-A and 0xe1 → \M-a cases.
15423 #[test]
15424 fn nicechar_sel_highbit_uses_meta_prefix_when_printeightbit_off() {
15425 let _g = crate::test_util::global_state_lock();
15426 if isset(PRINTEIGHTBIT) {
15427 return; // Test only valid when PRINTEIGHTBIT off (default).
15428 }
15429 // 0xc1 = 'A' + 0x80 → \M-A
15430 let c = char::from_u32(0xc1).unwrap();
15431 assert_eq!(
15432 nicechar_sel(c, false),
15433 "\\M-A",
15434 "c:472-477 — high-bit 0xc1 → \\M-A under PRINTEIGHTBIT-off"
15435 );
15436 // 0xe1 = 'a' + 0x80 → \M-a
15437 let c = char::from_u32(0xe1).unwrap();
15438 assert_eq!(nicechar_sel(c, false), "\\M-a");
15439 }
15440
15441 /// `Src/utils.c:593-705` — `wcs_nicechar_sel`. Wide-char variant
15442 /// of `nicechar_sel`: control chars escape, printable wides
15443 /// emit raw UTF-8, large codepoints get `\u`/`\U` hex escape.
15444 /// Pin every branch. Previously delegated to `nicechar_sel`
15445 /// which byte-masked via `c & 0xff` — every UTF-8 codepoint
15446 /// emerged mangled.
15447 #[test]
15448 fn wcs_nicechar_sel_printable_wide_emits_utf8() {
15449 let _g = crate::test_util::global_state_lock();
15450 // c:644-678 — printable wide char emits raw UTF-8.
15451 assert_eq!(
15452 wcs_nicechar_sel('a', None, None, false),
15453 "a",
15454 "ASCII printable"
15455 );
15456 assert_eq!(
15457 wcs_nicechar_sel('é', None, None, false),
15458 "é",
15459 "Latin-1 printable"
15460 );
15461 assert_eq!(
15462 wcs_nicechar_sel('字', None, None, false),
15463 "字",
15464 "CJK printable"
15465 );
15466 }
15467
15468 /// c:625-630 — `\n` and `\t` escape (same as nicechar_sel for ASCII).
15469 #[test]
15470 fn wcs_nicechar_sel_escapes_newline_and_tab() {
15471 let _g = crate::test_util::global_state_lock();
15472 assert_eq!(wcs_nicechar_sel('\n', None, None, false), "\\n");
15473 assert_eq!(wcs_nicechar_sel('\t', None, None, false), "\\t");
15474 }
15475
15476 /// c:617-623 — DEL (0x7f) renders as `^?` (non-quotable) or
15477 /// `\C-?` (quotable). Same as the byte version.
15478 #[test]
15479 fn wcs_nicechar_sel_del_uses_caret_question() {
15480 let _g = crate::test_util::global_state_lock();
15481 assert_eq!(wcs_nicechar_sel('\x7f', None, None, false), "^?");
15482 assert_eq!(wcs_nicechar_sel('\x7f', None, None, true), "\\C-?");
15483 }
15484
15485 /// c:631-638 — control chars (0x01-0x1f, except \n/\t) emit
15486 /// `^X` or `\C-X` with the +0x40 offset.
15487 #[test]
15488 fn wcs_nicechar_sel_control_chars_use_caret_prefix() {
15489 let _g = crate::test_util::global_state_lock();
15490 assert_eq!(wcs_nicechar_sel('\x01', None, None, false), "^A");
15491 assert_eq!(wcs_nicechar_sel('\x07', None, None, false), "^G");
15492 assert_eq!(wcs_nicechar_sel('\x1b', None, None, false), "^[");
15493 assert_eq!(wcs_nicechar_sel('\x01', None, None, true), "\\C-A");
15494 }
15495
15496 /// `Src/utils.c:656-663` — large non-printable codepoints get
15497 /// hex escape: `\U%.8x` (>= 0x10000), `\u%.4x` (>= 0x100).
15498 /// Both lowercase per C's `%x`. Use a non-printable wide char
15499 /// to test (most BMP chars are printable; pick a control area
15500 /// like U+0085 NEXT LINE).
15501 #[test]
15502 fn wcs_nicechar_sel_large_nonprintable_uses_hex_escape() {
15503 let _g = crate::test_util::global_state_lock();
15504 // U+0085 NEL — control in C1 range, not iswprint.
15505 let nel = char::from_u32(0x85).unwrap();
15506 // Since 0x85 < 0x100, falls into nicechar_sel fallback.
15507 // After u9_iswprint says false, cv < 0x80 is false (cv=0x85),
15508 // PRINTEIGHTBIT off → enters !is_printable && cv < 0x80 branch?
15509 // Actually 0x85 >= 0x80 → !print_eightbit true → enters non-print branch.
15510 // 0x85 not 0x7f, not \n/\t, not <0x20. Falls past all if-elses to
15511 // post-branch logic → !is_printable, cv >= 0x100 false, cv >= 0x100 false
15512 // → falls to nicechar_sel fallback.
15513 let r = wcs_nicechar_sel(nel, None, None, false);
15514 assert!(!r.is_empty(), "must emit something for U+0085");
15515 // U+200B ZERO WIDTH SPACE — non-printable, 0x100-0xffff range.
15516 let zwsp = char::from_u32(0x200B).unwrap();
15517 let r = wcs_nicechar_sel(zwsp, None, None, false);
15518 // u9_iswprint(0x200B) returns true via unicode-width 0-width.
15519 // Per the impl: u9_iswprint true → emit raw. So r should be
15520 // the raw zwsp char.
15521 assert!(!r.is_empty());
15522 }
15523
15524 /// `Src/utils.c:709-714` — `wcs_nicechar(c)` delegates to
15525 /// `wcs_nicechar_sel(c, widthp, swidep, 0)`. Pin the parity.
15526 #[test]
15527 fn wcs_nicechar_matches_wcs_nicechar_sel_with_zero() {
15528 let _g = crate::test_util::global_state_lock();
15529 for c in ['a', '\n', '\t', '\x7f', '\x01', 'é', '字'] {
15530 assert_eq!(
15531 wcs_nicechar(c, None, None),
15532 wcs_nicechar_sel(c, None, None, false),
15533 "c:711 — `wcs_nicechar(c, NULL, NULL)` must equal `wcs_nicechar_sel(c, NULL, NULL, 0)` for {:?}",
15534 c
15535 );
15536 }
15537 }
15538
15539 /// `Src/utils.c:1989-2012` — `movefd(fd)`. Three contracts:
15540 /// 1. fd == -1 → returned as-is (no syscalls).
15541 /// 2. fd >= 10 → returned unchanged (already high).
15542 /// 3. fd < 10 → dupped with F_DUPFD >= 10, original closed
15543 /// unconditionally (even on dup failure).
15544 #[test]
15545 #[cfg(unix)]
15546 fn movefd_returns_minus_one_unchanged() {
15547 let _g = crate::test_util::global_state_lock();
15548 // c:1992 — `if (fd != -1 && fd < 10)` skipped for fd=-1.
15549 assert_eq!(movefd(-1), -1, "fd=-1 must be returned unchanged");
15550 }
15551
15552 /// c:1992 — `fd >= 10` skips the dup-and-close path.
15553 #[test]
15554 #[cfg(unix)]
15555 fn movefd_returns_high_fd_unchanged() {
15556 let _g = crate::test_util::global_state_lock();
15557 // Use a known-open fd. /dev/null open with fd guaranteed to be
15558 // <10 because new fds get the lowest free slot — but we can
15559 // dup it to 10 directly to test the early-return path.
15560 let f = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15561 if f < 0 {
15562 return;
15563 } // Skip on unusual systems.
15564 let high = unsafe { libc::fcntl(f, libc::F_DUPFD, 20) };
15565 unsafe {
15566 libc::close(f);
15567 }
15568 if high < 0 {
15569 return;
15570 }
15571 // movefd(20) must return 20 unchanged (no dup, no close).
15572 let r = movefd(high);
15573 assert_eq!(r, high, "c:1992 — fd >= 10 returned unchanged");
15574 unsafe {
15575 libc::close(high);
15576 }
15577 }
15578
15579 /// `Src/utils.c:1968-1983` — `check_fd_table(fd)`. Grows the
15580 /// fdtable Vec to `fd+1` slots (filling new entries with
15581 /// FDT_UNUSED) and bumps `MAX_ZSH_FD` to `fd`. Pin: a small
15582 /// fd grows the table to `fd+1` length; MAX_ZSH_FD reflects it.
15583 /// fd ≤ cur_max early-returns without growing (c:1971-1972).
15584 #[test]
15585 fn check_fd_table_grows_to_fd_plus_one() {
15586 let _g = crate::test_util::global_state_lock();
15587 // Reset globals so the test is deterministic regardless
15588 // of prior ordering — other tests may have grown the
15589 // table or bumped MAX_ZSH_FD.
15590 MAX_ZSH_FD.store(-1, Ordering::Relaxed);
15591 {
15592 let mut g = fdtable_lock().lock().unwrap();
15593 g.clear();
15594 }
15595
15596 // c:1971-1972 — fd ≤ cur_max is the early-return path.
15597 // With cur_max=-1, fd=-1 hits `fd <= cur_max` → true.
15598 assert!(check_fd_table(-1));
15599 assert_eq!(
15600 MAX_ZSH_FD.load(Ordering::Relaxed),
15601 -1,
15602 "fd ≤ cur_max early-returns; MAX_ZSH_FD untouched"
15603 );
15604
15605 // c:1974-1981 — fd > cur_max grows fdtable to fd+1 slots.
15606 assert!(check_fd_table(7));
15607 assert_eq!(
15608 MAX_ZSH_FD.load(Ordering::Relaxed),
15609 7,
15610 "c:1982 — MAX_ZSH_FD := fd"
15611 );
15612 let len = {
15613 let g = fdtable_lock().lock().unwrap();
15614 g.len()
15615 };
15616 assert!(
15617 len >= 8,
15618 "c:1975-1979 — fdtable grew to ≥ fd+1 = 8 slots, got {}",
15619 len
15620 );
15621
15622 // Idempotence under fd ≤ cur_max.
15623 assert!(check_fd_table(3));
15624 assert_eq!(
15625 MAX_ZSH_FD.load(Ordering::Relaxed),
15626 7,
15627 "fd ≤ cur_max keeps MAX_ZSH_FD at 7"
15628 );
15629 }
15630
15631 /// `Src/utils.c:4364,4367` — `wcsitype(c, IWORD/ISEP)` reads
15632 /// from the `wordchars`/`ifs` GLOBALS (writable by
15633 /// `wordcharssetfn`/`ifssetfn`). Previously read from
15634 /// `std::env::var` — the libc process env — which never
15635 /// reflects runtime `WORDCHARS=…` shell-side assignments.
15636 /// Pin: set wordchars via `wordcharssetfn`, verify `wcsitype`
15637 /// returns true for chars in the new set.
15638 #[test]
15639 fn wcsitype_iword_reads_from_canonical_wordchars_global() {
15640 let _g = crate::test_util::global_state_lock();
15641 // Test only meaningful in MULTIBYTE mode for non-ASCII chars;
15642 // ASCII chars (< 0x80) route through TYPTAB directly.
15643 // Skip if MULTIBYTE off.
15644 if !isset(MULTIBYTE) {
15645 return;
15646 }
15647 // Save and set WORDCHARS to a single non-ASCII char.
15648 // Snapshot the store `wordcharssetfn` writes: the `wordchars` global
15649 // itself. A paramtab read finds no WORDCHARS node in a bare test
15650 // process and yields "", and restoring that left an EMPTY WORDCHARS
15651 // behind for every later typtab test.
15652 let saved = crate::ported::params::wordchars_lock()
15653 .lock()
15654 .map(|g| g.clone())
15655 .unwrap_or_default();
15656 // wordcharssetfn ignores its `_pm` arg and re-enters paramtab's
15657 // read lock via inittyptab (sibling of the IFS deadlock above).
15658 // Use a stack-local dummy pm instead of going through paramtab's
15659 // write lock so the test never holds two paramtab locks at once.
15660 let mut dummy = crate::ported::zsh_h::param::default();
15661 let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
15662 wordcharssetfn(dummy, val);
15663 };
15664 do_set(&mut dummy, "é".to_string());
15665 // 'é' is alphanumeric per Unicode → IWORD returns true via
15666 // is_alphanumeric short-circuit at c:4353. So we can't pin
15667 // the WORDCHARS-specific path with 'é'. Use a non-alnum char.
15668 do_set(&mut dummy, ":".to_string());
15669 // ':' is ASCII so wcsitype routes through TYPTAB (which now
15670 // has IWORD on ':' because wordcharssetfn called inittyptab).
15671 assert!(
15672 wcsitype(':', IWORD as u32),
15673 "c:4364 — wordchars membership through canonical global"
15674 );
15675 // Restore.
15676 *crate::ported::params::wordchars_lock()
15677 .lock()
15678 .expect("wordchars poisoned") = saved;
15679 crate::ported::utils::inittyptab();
15680 }
15681
15682 /// `Src/utils.c:2090-2097` — `addmodulefd(fd, fdt)`. Stores the
15683 /// provided fdt in the fdtable slot for the given fd. Previously
15684 /// Rust port hardcoded FDT_MODULE and added CLOEXEC unconditionally.
15685 /// Pin: pass FDT_EXTERNAL, verify slot stores FDT_EXTERNAL (not
15686 /// FDT_MODULE).
15687 #[cfg(unix)]
15688 #[test]
15689 fn addmodulefd_respects_fdt_parameter() {
15690 let _g = crate::test_util::global_state_lock();
15691 // Open a real fd to use.
15692 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15693 if fd < 0 {
15694 return;
15695 }
15696 addmodulefd(fd, FDT_EXTERNAL);
15697 assert_eq!(
15698 fdtable_get(fd),
15699 FDT_EXTERNAL,
15700 "c:2095 — fdt parameter must be stored verbatim"
15701 );
15702 // Switch to FDT_MODULE.
15703 addmodulefd(fd, FDT_MODULE);
15704 assert_eq!(
15705 fdtable_get(fd),
15706 FDT_MODULE,
15707 "c:2095 — second call overwrites with new fdt"
15708 );
15709 // Cleanup.
15710 unsafe {
15711 libc::close(fd);
15712 }
15713 }
15714
15715 /// `Src/utils.c:2093` — `if (fd >= 0)`. Negative fd is silently
15716 /// ignored (no fdtable update). Pin the guard.
15717 #[test]
15718 fn addmodulefd_ignores_negative_fd() {
15719 let _g = crate::test_util::global_state_lock();
15720 // Should not panic, no side effect.
15721 addmodulefd(-1, FDT_MODULE);
15722 // Nothing to assert on side-effect-free path; just verify no panic.
15723 }
15724
15725 /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns -1 for
15726 /// non-lock fds, 0 for FDT_FLOCK/FDT_FLOCK_EXEC. Previously the
15727 /// Rust port always returned 0 — broke `zsystem flock -u <fd>`
15728 /// distinguishing "fd never flocked" from "successfully released".
15729 #[cfg(unix)]
15730 #[test]
15731 fn zcloselockfd_returns_minus_one_for_non_lock_fd() {
15732 let _g = crate::test_util::global_state_lock();
15733 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15734 if fd < 0 {
15735 return;
15736 }
15737 // No addlockfd call → fd is NOT in the flock table.
15738 let r = zcloselockfd(fd);
15739 assert_eq!(r, -1, "c:2160-2161 — non-lock fd must return -1, NOT 0");
15740 unsafe {
15741 libc::close(fd);
15742 }
15743 }
15744
15745 /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns 0 for an
15746 /// fd that was registered via `addlockfd`. Pin the end-to-end
15747 /// round-trip: addlockfd → zcloselockfd returns 0.
15748 #[cfg(unix)]
15749 #[test]
15750 fn zcloselockfd_returns_zero_for_flock_fd_then_closes() {
15751 let _g = crate::test_util::global_state_lock();
15752 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15753 if fd < 0 {
15754 return;
15755 }
15756 addlockfd(fd, true);
15757 let r = zcloselockfd(fd);
15758 assert_eq!(r, 0, "c:2162-2163 — FDT_FLOCK fd → zclose + return 0");
15759 // After zcloselockfd, the underlying close() has fired.
15760 // close() returning -1 with EBADF would confirm that, but
15761 // that's a libc side-effect; just verify the return value.
15762 }
15763
15764 /// `Src/utils.c:1982` — `check_fd_table` sets `max_zsh_fd = fd`
15765 /// when fd is new. `fdtable_set` inlines this to keep the guard
15766 /// in `zcloselockfd` (`if (fd > max_zsh_fd) return -1`) working
15767 /// against fds populated by `addmodulefd`/`addlockfd`. Pin: after
15768 /// `fdtable_set(fd, FDT_FLOCK)`, MAX_ZSH_FD >= fd.
15769 #[cfg(unix)]
15770 #[test]
15771 fn fdtable_set_bumps_max_zsh_fd() {
15772 let _g = crate::test_util::global_state_lock();
15773 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15774 if fd < 0 {
15775 return;
15776 }
15777 fdtable_set(fd, FDT_FLOCK);
15778 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
15779 assert!(
15780 max_fd >= fd,
15781 "c:1982 — max_zsh_fd ({}) must be >= newly-set fd ({})",
15782 max_fd,
15783 fd
15784 );
15785 // Cleanup.
15786 fdtable_set(fd, FDT_UNUSED);
15787 unsafe {
15788 libc::close(fd);
15789 }
15790 }
15791
15792 /// `Src/utils.c:2111-2121` — `addlockfd(fd, cloexec)`. Selects
15793 /// between FDT_FLOCK (when cloexec=true, lock dies on exec) and
15794 /// FDT_FLOCK_EXEC (when cloexec=false, lock survives exec).
15795 /// Previously the Rust port did `fcntl(F_SETFD, CLOEXEC)` —
15796 /// totally wrong semantics. Pin the FDT slot.
15797 #[cfg(unix)]
15798 #[test]
15799 fn addlockfd_selects_flock_category_per_cloexec_flag() {
15800 let _g = crate::test_util::global_state_lock();
15801 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
15802 if fd < 0 {
15803 return;
15804 }
15805 // cloexec=true → FDT_FLOCK (lock dies on exec).
15806 addlockfd(fd, true);
15807 assert_eq!(
15808 fdtable_get(fd),
15809 FDT_FLOCK,
15810 "c:2117 — cloexec=true → FDT_FLOCK"
15811 );
15812 // cloexec=false → FDT_FLOCK_EXEC (lock survives exec).
15813 addlockfd(fd, false);
15814 assert_eq!(
15815 fdtable_get(fd),
15816 FDT_FLOCK_EXEC,
15817 "c:2119 — cloexec=false → FDT_FLOCK_EXEC"
15818 );
15819 // Cleanup.
15820 unsafe {
15821 libc::close(fd);
15822 }
15823 }
15824
15825 /// `Src/utils.c:1211` — `adduserdir` rejects paths `>= PATH_MAX`
15826 /// by removing the existing entry (treating as "can't use this
15827 /// value as a directory"). Pin with a path constructed to exceed
15828 /// PATH_MAX, verify no insertion happens.
15829 #[cfg(unix)]
15830 #[test]
15831 fn adduserdir_rejects_paths_at_or_above_path_max() {
15832 let _g = crate::test_util::global_state_lock();
15833 if !interact() {
15834 // c:1193 — non-interactive shells skip the table entirely.
15835 // Test inactive in non-interactive contexts.
15836 return;
15837 }
15838 // Ensure the entry doesn't exist beforehand.
15839 let name = "ZSHRS_TEST_PATHMAX_DIR";
15840 let _ = removenameddirnode(name);
15841 // Construct an oversized path.
15842 let mut over: String = "/".to_string();
15843 over.push_str(&"a".repeat(libc::PATH_MAX as usize));
15844 // adduserdir with the oversized path + AUTONAMEDIRS-off path
15845 // via always=true (so the AUTONAMEDIRS guard doesn't reject).
15846 adduserdir(name, &over, 0, true);
15847 // c:1211 — too-long → remove path triggered, no entry added.
15848 let tab = nameddirtab().lock().unwrap();
15849 assert!(
15850 !tab.contains_key(name),
15851 "c:1211 — strlen(t) >= PATH_MAX must NOT insert entry"
15852 );
15853 }
15854
15855 /// `Src/utils.c:760-786` — `pathprog`. Finds any regular file
15856 /// (not directory, no executable-bit check) in `$PATH`.
15857 /// Previously required `mode & 0o111 != 0` — silently missed
15858 /// non-executable autoload-function plaintext scripts. Pin
15859 /// using a known-existing file in /tmp.
15860 #[test]
15861 #[cfg(unix)]
15862 fn pathprog_finds_non_executable_files() {
15863 let _g = crate::test_util::global_state_lock();
15864 // Create a non-executable file in /tmp and add /tmp to PATH.
15865 let test_name = format!("zshrs_test_pathprog_{}", unsafe { libc::getpid() });
15866 let path = PathBuf::from("/tmp").join(&test_name);
15867 // Write content, NO executable bit.
15868 if fs::write(&path, b"plain content").is_err() {
15869 return; // /tmp may be unwritable on some systems.
15870 }
15871 // Verify it's not executable.
15872 let meta = fs::metadata(&path).unwrap();
15873 assert_eq!(
15874 meta.permissions().mode() & 0o111,
15875 0,
15876 "test setup: must be non-executable"
15877 );
15878 // Set PATH to /tmp.
15879 let saved_path = getsparam("PATH");
15880 assignsparam("PATH", "/tmp", 0);
15881 // pathprog should find the file.
15882 let r = pathprog(&test_name);
15883 // Cleanup.
15884 let _ = fs::remove_file(&path);
15885 if let Some(prev) = saved_path {
15886 assignsparam("PATH", &prev, 0);
15887 }
15888 assert_eq!(
15889 r,
15890 Some(path),
15891 "c:776 — pathprog finds non-executable files (only F_OK + !S_ISDIR)"
15892 );
15893 }
15894
15895 /// `Src/utils.c:776-778` — pathprog skips directories. Pin: a
15896 /// directory in $PATH with the queried name must NOT be returned.
15897 #[test]
15898 #[cfg(unix)]
15899 fn pathprog_skips_directories() {
15900 let _g = crate::test_util::global_state_lock();
15901 let test_name = format!("zshrs_test_pathprog_dir_{}", unsafe { libc::getpid() });
15902 let path = PathBuf::from("/tmp").join(&test_name);
15903 if fs::create_dir(&path).is_err() {
15904 return;
15905 }
15906 let saved_path = getsparam("PATH");
15907 assignsparam("PATH", "/tmp", 0);
15908 let r = pathprog(&test_name);
15909 // Cleanup.
15910 let _ = fs::remove_dir(&path);
15911 if let Some(prev) = saved_path {
15912 assignsparam("PATH", &prev, 0);
15913 }
15914 assert!(
15915 r.is_none(),
15916 "c:778 — pathprog must skip directories (!S_ISDIR)"
15917 );
15918 }
15919
15920 /// `Src/utils.c:4367` — `wcsitype(c, ISEP)` reads from canonical
15921 /// `ifs` global. Pin via `ifssetfn`. ASCII path routes through
15922 /// TYPTAB which gets ISEP bit from inittyptab's IFS walk
15923 /// (added earlier this session). End-to-end pin.
15924 #[test]
15925 fn wcsitype_isep_reads_from_canonical_ifs_global() {
15926 let _g = crate::test_util::global_state_lock();
15927 if !isset(MULTIBYTE) {
15928 return;
15929 }
15930 // Snapshot the store `ifssetfn` writes: the `ifs` global itself
15931 // (`None` is an unset IFS, c:Src/params.c:4748). A paramtab read finds
15932 // no IFS node in a bare test process and yields "", and restoring that
15933 // left an EMPTY IFS behind, so every later typtab test saw no
15934 // separators.
15935 let saved = crate::ported::params::ifs_lock()
15936 .lock()
15937 .map(|g| g.clone())
15938 .unwrap_or_default();
15939 // ifssetfn ignores its `_pm` arg (the C `gsu.s->setfn` receives
15940 // the param but the IFS callback only touches the global `ifs`
15941 // store + calls inittyptab). Calling through paramtab's write
15942 // lock would deadlock because inittyptab() re-takes paramtab's
15943 // read lock for the IFS walk (utils.rs:5028) — pthread rwlock
15944 // forbids same-thread write→read upgrade. Pass a stack-local
15945 // dummy pm instead so we never hold the paramtab lock during
15946 // ifssetfn.
15947 let mut dummy = crate::ported::zsh_h::param::default();
15948 let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
15949 ifssetfn(dummy, val);
15950 };
15951 do_set(&mut dummy, ":".to_string());
15952 assert!(
15953 wcsitype(':', ISEP as u32),
15954 "c:4367 — IFS membership through canonical global"
15955 );
15956 // Restore.
15957 *crate::ported::params::ifs_lock().lock().expect("ifs poisoned") = saved;
15958 crate::ported::utils::inittyptab();
15959 }
15960
15961 /// c:536 — high-bit byte (>= 0x80) is nice when PRINTEIGHTBIT is
15962 /// OFF (the default). Pin the default case; the alternate
15963 /// behavior under PRINTEIGHTBIT-on is harder to exercise from
15964 /// a unit test due to opt-state global mutation.
15965 #[test]
15966 fn is_nicechar_highbit_is_nice_when_printeightbit_off() {
15967 let _g = crate::test_util::global_state_lock();
15968 // Default state: PRINTEIGHTBIT is off → high-bit bytes are nice.
15969 // Use char with high-bit equivalent low byte.
15970 // 0xb5 (Meta+5 territory) — masked to 0xff → still 0xb5.
15971 let c = char::from_u32(0xb5).unwrap();
15972 // ZISPRINT(0xb5) is false (>0x7e). 0xb5 & 0x80 set → check PRINTEIGHTBIT.
15973 // Default PRINTEIGHTBIT off → returns true (nice).
15974 if !isset(PRINTEIGHTBIT) {
15975 assert!(
15976 is_nicechar(c),
15977 "c:536 — high-bit byte 0x{:x} must be nice when PRINTEIGHTBIT off",
15978 0xb5_u32
15979 );
15980 }
15981 }
15982
15983 /// `Src/utils.c:1969-1983` — `check_fd_table(fd)` grows the
15984 /// fdtable so it can index `fd` and bumps `max_zsh_fd`. The
15985 /// previous Rust port was a no-op shim that always returned true;
15986 /// real behavior is "fdtable.len() must be > fd" and
15987 /// "MAX_ZSH_FD >= fd" after the call.
15988 #[test]
15989 fn check_fd_table_grows_fdtable_and_bumps_max_zsh_fd() {
15990 let _g = crate::test_util::global_state_lock();
15991 // Snapshot prior state so we don't poison other tests.
15992 let saved_max = MAX_ZSH_FD.load(Ordering::Relaxed);
15993 // Pick a target fd well above the typical 0/1/2.
15994 let target = saved_max.max(50) + 7;
15995 check_fd_table(target);
15996 let new_max = MAX_ZSH_FD.load(Ordering::Relaxed);
15997 assert!(
15998 new_max >= target,
15999 "c:1982 — max_zsh_fd must be >= target after grow (got {})",
16000 new_max
16001 );
16002 // Calling again with a lower fd MUST NOT shrink max_zsh_fd
16003 // (c:1971-1972 early return).
16004 check_fd_table(target - 3);
16005 assert_eq!(
16006 MAX_ZSH_FD.load(Ordering::Relaxed),
16007 new_max,
16008 "c:1971 — fd <= max_zsh_fd path must not change max"
16009 );
16010 // Restore approximately — best we can do without exposing
16011 // fdtable internals; subsequent fdtable_set callers cope.
16012 MAX_ZSH_FD.store(saved_max, Ordering::Relaxed);
16013 }
16014
16015 /// `Src/utils.c:1971` — `if (fd <= max_zsh_fd) return;` early
16016 /// exit. Calling with a small fd (<= current max) must be a
16017 /// no-op.
16018 #[test]
16019 fn check_fd_table_small_fd_is_noop() {
16020 let _g = crate::test_util::global_state_lock();
16021 // Ensure max_zsh_fd is non-trivial.
16022 let _ = check_fd_table(100);
16023 let max_before = MAX_ZSH_FD.load(Ordering::Relaxed);
16024 check_fd_table(5);
16025 assert_eq!(
16026 MAX_ZSH_FD.load(Ordering::Relaxed),
16027 max_before,
16028 "c:1971 — small fd path must not touch max_zsh_fd"
16029 );
16030 }
16031
16032 /// `Src/utils.c:1969-1983` — defensive: negative fd shouldn't
16033 /// panic. C-side would have indexed past the start of `fdtable`
16034 /// (UB). Rust port should fail-soft.
16035 #[test]
16036 fn check_fd_table_negative_fd_does_not_panic() {
16037 let _g = crate::test_util::global_state_lock();
16038 // Should not panic; behavior beyond "no panic" is unspecified.
16039 let _ = check_fd_table(-1);
16040 let _ = check_fd_table(-100);
16041 }
16042
16043 /// `Src/zsh.h:3274` — under MULTIBYTE_SUPPORT, the C source
16044 /// defines `nicezputs(str, outs)` as a macro:
16045 /// `(void)mb_niceformat((str), (outs), NULL, 0)`.
16046 /// So Rust `nicezputs(s)` must equal `mb_niceformat(s)` for
16047 /// every input. Pins the macro-equivalence after fixing the
16048 /// previous `chars().map(nicechar)` impl (which corrupted
16049 /// non-ASCII multibyte codepoints into `\M-X` mangle).
16050 #[test]
16051 fn nicezputs_matches_mb_niceformat_under_multibyte() {
16052 let _g = crate::test_util::global_state_lock();
16053 // C-equivalent pattern: write to a stream, compare to
16054 // mb_niceformat outstrp-form output. Inline rather than via a
16055 // Rust-only helper.
16056 for input in ["hello", "a\nb", "é", ""] {
16057 let mut nz_buf: Vec<u8> = Vec::new();
16058 let _ = nicezputs(input, &mut nz_buf);
16059 let nz = String::from_utf8(nz_buf).expect("utf8");
16060
16061 let mut mb_out: Option<String> = None;
16062 let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
16063 assert_eq!(
16064 nz,
16065 mb_out.unwrap_or_default(),
16066 "nicezputs and mb_niceformat must agree for {:?}",
16067 input
16068 );
16069 }
16070 // \n must produce literal `\n` (backslash + n).
16071 let mut nz_buf: Vec<u8> = Vec::new();
16072 let _ = nicezputs("a\nb", &mut nz_buf);
16073 let nz = String::from_utf8(nz_buf).expect("utf8");
16074 assert!(nz.contains("\\n"), "nicechar emits `\\n`, not raw 0x0a");
16075 }
16076
16077 /// `Src/utils.c:5530` — under MULTIBYTE_SUPPORT, `nicedup(s, heap)`
16078 /// body is `(void)mb_niceformat(s, NULL, &retstr, …); return retstr;`
16079 /// so the returned string MUST equal mb_niceformat output.
16080 #[test]
16081 fn nicedup_matches_mb_niceformat_under_multibyte() {
16082 let _g = crate::test_util::global_state_lock();
16083 for input in ["hello", "a\nb", "é"] {
16084 let nd = nicedup(input, 0);
16085 let mut mb_out: Option<String> = None;
16086 let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
16087 assert_eq!(
16088 nd,
16089 mb_out.unwrap_or_default(),
16090 "nicedup must equal mb_niceformat outstrp form for {:?}",
16091 input
16092 );
16093 }
16094 // nicedupstring delegates to nicedup(s, 1).
16095 assert_eq!(nicedupstring("hé\nllo"), nicedup("hé\nllo", 1));
16096 }
16097
16098 /// `Src/utils.c:734-744` — `zwcwidth(wc)` returns 1 when MULTIBYTE
16099 /// option is unset (c:738), regardless of the codepoint's actual
16100 /// display width. The previous Rust port skipped the option gate
16101 /// and always used the Unicode width table. Pin the option-gated
16102 /// behavior end-to-end: unset MULTIBYTE → width 1 for CJK; set →
16103 /// width 2 for CJK.
16104 #[test]
16105 fn zwcwidth_returns_1_when_multibyte_unset() {
16106 let _g = crate::test_util::global_state_lock();
16107 let saved = isset(MULTIBYTE);
16108 // c:738 — unset(MULTIBYTE) path returns 1 for everything.
16109 dosetopt(MULTIBYTE, 0, 1);
16110 assert_eq!(zwcwidth('a'), 1, "c:738 — ASCII width 1 under nomultibyte");
16111 assert_eq!(
16112 zwcwidth('字'),
16113 1,
16114 "c:738 — CJK collapses to 1 under nomultibyte (was 2 via Unicode-width)"
16115 );
16116 assert_eq!(
16117 zwcwidth('\u{200B}'),
16118 1,
16119 "c:738 — zero-width space → 1 under nomultibyte (was 0 via Unicode-width)"
16120 );
16121 // c:740-744 — MULTIBYTE on: Unicode-width table applies.
16122 dosetopt(MULTIBYTE, 1, 1);
16123 assert_eq!(zwcwidth('a'), 1, "ASCII width is 1");
16124 assert_eq!(zwcwidth('字'), 2, "c:740 — CJK width 2 under multibyte");
16125 // Restore prior state.
16126 dosetopt(MULTIBYTE, if saved { 1 } else { 0 }, 1);
16127 }
16128
16129 /// `Src/utils.c:5613` — `if (!isset(MULTIBYTE) || (unsigned char) *s <=
16130 /// 0x7f)` returns a SINGLE byte. The port had only the multibyte arm, so
16131 /// every walk built on this function kept stepping whole characters in a
16132 /// mode where zsh steps bytes — `${gr[2]}`, `${(s::)v}` and `$(( ##c ))`
16133 /// all read the wrong unit.
16134 ///
16135 /// Both the length and the returned unit are asserted: a version that
16136 /// fixed the length but re-encoded the unit as UTF-8 would still emit
16137 /// `c3 8e` where zsh emits the single byte `ce`.
16138 #[test]
16139 fn mb_metacharlenconv_steps_bytes_when_multibyte_unset() {
16140 let _g = crate::test_util::global_state_lock();
16141 let saved = isset(MULTIBYTE);
16142 // `α` is ce b1 — one character, two bytes.
16143 let alpha: &[u8] = &[0xce, 0xb1];
16144
16145 dosetopt(MULTIBYTE, 1, 1);
16146 let (n, wc, unit) = mb_metacharlenconv(alpha);
16147 assert_eq!(n, 2, "c:5635 — multibyte on: the whole character");
16148 assert_eq!(wc, Some('α'));
16149 assert_eq!(unit.as_bytes(), alpha, "the unit is the character verbatim");
16150
16151 dosetopt(MULTIBYTE, 0, 1);
16152 let (n, wc, unit) = mb_metacharlenconv(alpha);
16153 assert_eq!(n, 1, "c:5613 — multibyte off: one byte");
16154 assert_eq!(wc, Some('\u{ce}'), "c:5616 — the byte value is the scalar");
16155 assert_eq!(
16156 crate::ported::utils::unmetafy_str(&unit),
16157 vec![0xceu8],
16158 "the unit must decode back to the single byte 0xce, not to U+00CE's \
16159 UTF-8 (c3 8e) — half a character cannot be held verbatim"
16160 );
16161
16162 // ASCII takes the same one-byte path in either mode (c:5613).
16163 dosetopt(MULTIBYTE, 1, 1);
16164 assert_eq!(mb_metacharlenconv(b"abc").0, 1);
16165 dosetopt(MULTIBYTE, 0, 1);
16166 assert_eq!(mb_metacharlenconv(b"abc").0, 1);
16167
16168 dosetopt(MULTIBYTE, if saved { 1 } else { 0 }, 1);
16169 }
16170
16171 /// `Src/utils.c:6478-6492` — `quotedzputs(s, NULL)` under
16172 /// MULTIBYTE_SUPPORT detects "needs nice-format" inputs via
16173 /// `is_mb_niceformat(s)` and emits `$'<nice-formatted body>'`
16174 /// (the dollar-quoted form so embedded `\n`/`\t`/escape sequences
16175 /// round-trip through `typeset -p`/`set`/`set -x`). The previous
16176 /// Rust port skipped this branch entirely and single-quoted such
16177 /// strings — POSIX `'…'` is *strong* (no escapes interpreted),
16178 /// breaking round-trip for control bytes.
16179 #[test]
16180 fn quotedzputs_uses_dollar_quotes_for_control_chars() {
16181 let _g = crate::test_util::global_state_lock();
16182 // Ensure typtab is initialised — hasspecial depends on ISPECIAL
16183 // bits which are populated by inittyptab. Without this the
16184 // SPECCHARS arm short-circuits to "no specials".
16185 inittyptab();
16186 // Empty stays `''`.
16187 assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
16188 // Plain alphanumeric: no specials, no controls → bare.
16189 assert_eq!(
16190 quotedzputs("hello"),
16191 "hello",
16192 "c:6511-6517 — no SPECCHARS member → return unchanged"
16193 );
16194 // Control char `\n` needs `$'…'` to round-trip.
16195 let r = quotedzputs("a\nb");
16196 assert!(
16197 r.starts_with("$'") && r.ends_with('\''),
16198 "c:6488 — control char must use $'…' form (got {:?})",
16199 r
16200 );
16201 // Tab and ESC also force the niceformat arm.
16202 let r = quotedzputs("\t");
16203 assert!(
16204 r.starts_with("$'"),
16205 "c:6488 — TAB forces $'…' arm (got {:?})",
16206 r
16207 );
16208 let r = quotedzputs("\u{1b}[31m");
16209 assert!(
16210 r.starts_with("$'"),
16211 "c:6488 — ESC sequence forces $'…' arm (got {:?})",
16212 r
16213 );
16214 // Single quote forces single-quote arm via SPECCHARS membership
16215 // (Src/zsh.h:228 — SPECCHARS includes `'`). Embedded `'`
16216 // rewrites to `'\''`.
16217 let r = quotedzputs("a'b");
16218 assert!(
16219 r.contains("'\\''"),
16220 "c:6573-6587 — embedded ' → '\\'' (got {:?})",
16221 r
16222 );
16223 }
16224
16225 /// `Src/utils.c:2923-2945` — `read_loop` returns the requested
16226 /// length on full read, or the partial count on EOF. Pin: writing
16227 /// a known buffer to a pipe and reading it back returns the same
16228 /// content + correct length. Drives the no-side-effect path that
16229 /// the diagnostic-message fix doesn't touch.
16230 #[test]
16231 #[cfg(unix)]
16232 fn read_loop_round_trips_pipe_bytes() {
16233 let _g = crate::test_util::global_state_lock();
16234 // Create a pipe; write 16 bytes; read them back.
16235 let mut fds: [libc::c_int; 2] = [0; 2];
16236 unsafe {
16237 assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
16238 }
16239 let payload = b"hello-world-1234";
16240 let written = unsafe {
16241 libc::write(
16242 fds[1],
16243 payload.as_ptr() as *const libc::c_void,
16244 payload.len(),
16245 )
16246 };
16247 assert_eq!(written, payload.len() as isize, "write all 16 bytes");
16248 unsafe {
16249 libc::close(fds[1]);
16250 }
16251 let mut buf = [0u8; 16];
16252 let got = read_loop(fds[0], &mut buf).expect("read_loop ok");
16253 assert_eq!(got, payload.len(), "c:2929 — read_loop returns full length");
16254 assert_eq!(
16255 &buf[..],
16256 &payload[..],
16257 "c:2940-2941 — buffer copied verbatim"
16258 );
16259 unsafe {
16260 libc::close(fds[0]);
16261 }
16262 }
16263
16264 /// `Src/utils.c:2949-2970` — `write_loop` returns the requested
16265 /// length when the kernel accepts all bytes (the common case for
16266 /// a pipe with room). Pin the no-side-effect happy path.
16267 #[test]
16268 #[cfg(unix)]
16269 fn write_loop_writes_all_bytes_to_pipe() {
16270 let _g = crate::test_util::global_state_lock();
16271 let mut fds: [libc::c_int; 2] = [0; 2];
16272 unsafe {
16273 assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
16274 }
16275 let payload = b"abcdef";
16276 let got = write_loop(fds[1], payload).expect("write_loop ok");
16277 assert_eq!(
16278 got,
16279 payload.len(),
16280 "c:2955-2956 — write_loop returns full length on accept"
16281 );
16282 // Read back to verify.
16283 unsafe {
16284 libc::close(fds[1]);
16285 }
16286 let mut buf = [0u8; 6];
16287 let _ = unsafe { libc::read(fds[0], buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
16288 assert_eq!(&buf[..], &payload[..]);
16289 unsafe {
16290 libc::close(fds[0]);
16291 }
16292 }
16293
16294 /// `Src/utils.c:2935-2936` — `read_loop` on a closed/invalid fd
16295 /// returns an io::Error (the C path returns `ret` and emits the
16296 /// `zwarn` to stderr). Pin the error propagation; the zwarn
16297 /// emission is a stderr side-effect tested only by inspecting
16298 /// log output (out of scope here).
16299 #[test]
16300 #[cfg(unix)]
16301 fn read_loop_returns_error_on_invalid_fd() {
16302 let _g = crate::test_util::global_state_lock();
16303 let mut buf = [0u8; 4];
16304 // fd 9999 is essentially guaranteed-not-open in a test
16305 // process; the canonical "bad fd" error path.
16306 let r = read_loop(9999, &mut buf);
16307 assert!(
16308 r.is_err(),
16309 "c:2935 — invalid fd → io::Error (and zwarn to stderr)"
16310 );
16311 }
16312
16313 /// `Src/utils.c:2972-2988` — `read1char(echo)` reads from SHTTY,
16314 /// not stdin. With SHTTY uninitialised (default -1 in test
16315 /// environment), the function MUST return -1 immediately rather
16316 /// than blocking on a stdin read. The previous Rust port read
16317 /// from stdin and returned None — which in some test runners
16318 /// would block waiting for input.
16319 #[test]
16320 #[cfg(unix)]
16321 fn read1char_returns_minus_one_when_shtty_unset() {
16322 let _g = crate::test_util::global_state_lock();
16323 // Test environment: SHTTY is -1 (no controlling tty bound
16324 // by the port). C-side would `read(-1, ...)` which fails;
16325 // Rust port should fail-fast with -1.
16326 let saved = SHTTY.load(Ordering::Relaxed);
16327 SHTTY.store(-1, Ordering::Relaxed);
16328 let got = read1char(0); // echo=0
16329 assert_eq!(got, -1, "c:2978 — SHTTY=-1 → read fails → return -1");
16330 // Restore.
16331 SHTTY.store(saved, Ordering::Relaxed);
16332 }
16333
16334 /// `Src/utils.c:1989-2012` — `movefd(fd)` dups fd to >= 10 (so it
16335 /// stays out of the user fd range 0..=9), closes the original,
16336 /// AND marks the new fd as `FDT_INTERNAL` in fdtable. Previous
16337 /// Rust port omitted the fdtable_set call — internal-fd tracking
16338 /// (`closeallelse`, forkexec) silently never saw zshrs-internal
16339 /// fds. Pin: after movefd, fdtable[new_fd] == FDT_INTERNAL.
16340 #[test]
16341 #[cfg(unix)]
16342 fn movefd_marks_fdtable_internal() {
16343 let _g = crate::test_util::global_state_lock();
16344 // Open /dev/null to get a small (< 10) fd...
16345 let dev_null = CString::new("/dev/null").unwrap();
16346 let fd = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16347 assert!(fd >= 0, "open /dev/null returned -1");
16348 let new_fd = movefd(fd);
16349 assert!(
16350 new_fd >= 10,
16351 "c:1992-1994 — movefd dups to fd >= 10 (got {})",
16352 new_fd
16353 );
16354 // c:2009 — fdtable[new_fd] := FDT_INTERNAL.
16355 let entry = fdtable_get(new_fd);
16356 assert_eq!(
16357 entry, FDT_INTERNAL,
16358 "c:2009 — movefd must mark new_fd as FDT_INTERNAL (got {})",
16359 entry
16360 );
16361 unsafe {
16362 libc::close(new_fd);
16363 }
16364 }
16365
16366 /// `Src/utils.c:1989` — `movefd(-1)` is a defensive no-op: the
16367 /// `fd != -1 && fd < 10` gate at c:1992 fails, the c:2007
16368 /// `if (fd != -1)` post-check skips, and -1 is returned
16369 /// unchanged. Pin so a refactor of the early-out doesn't
16370 /// accidentally call fcntl(-1, ...).
16371 #[test]
16372 #[cfg(unix)]
16373 fn movefd_minus_one_returns_minus_one() {
16374 let _g = crate::test_util::global_state_lock();
16375 assert_eq!(
16376 movefd(-1),
16377 -1,
16378 "c:1992 — movefd(-1) bypasses both gates and returns -1"
16379 );
16380 }
16381
16382 /// `Src/utils.c:2021-2068` — `redup(x, y)` after a successful
16383 /// `dup2(x, y)` must:
16384 /// * Copy fdtable[x] to fdtable[y] (c:2054).
16385 /// * Promote FDT_FLOCK/FDT_FLOCK_EXEC to FDT_INTERNAL on the
16386 /// dup target (c:2055-2056) — the lock doesn't transfer.
16387 /// * Then close x (c:2064).
16388 ///
16389 /// Previous Rust port skipped the fdtable updates entirely.
16390 /// Pin the ownership-transfer with two fds the test allocates.
16391 #[test]
16392 #[cfg(unix)]
16393 fn redup_copies_fdtable_ownership_to_target() {
16394 let _g = crate::test_util::global_state_lock();
16395 // Open two distinct fds — both will land in the fdtable.
16396 let dev_null = CString::new("/dev/null").unwrap();
16397 let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16398 let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16399 assert!(x >= 0 && y >= 0, "open /dev/null returned -1");
16400 assert_ne!(x, y);
16401
16402 // Mark x as FDT_INTERNAL so we can observe the copy to y.
16403 check_fd_table(x);
16404 check_fd_table(y);
16405 fdtable_set(x, FDT_INTERNAL);
16406 fdtable_set(y, FDT_UNUSED);
16407
16408 let ret = redup(x, y);
16409 assert_eq!(ret, y, "c:2067 — successful redup returns y");
16410 // c:2054 — fdtable[y] inherited from fdtable[x].
16411 assert_eq!(
16412 fdtable_get(y),
16413 FDT_INTERNAL,
16414 "c:2054 — fdtable[y] = fdtable[x] (FDT_INTERNAL)"
16415 );
16416 // x is closed by c:2064.
16417 unsafe {
16418 libc::close(y);
16419 }
16420 }
16421
16422 /// `Src/utils.c:872-908` — `xsymlinks(path)` resolves `.`/`..`
16423 /// AND follows ONE LEVEL of symlinks via `readlink(2)`. Pin the
16424 /// symlink-following behavior with a temp-dir-managed symlink
16425 /// (the actual bug-fix pin — previous Rust port just normalised
16426 /// `.`/`..` without ever calling readlink).
16427 #[test]
16428 #[cfg(unix)]
16429 fn xsymlinks_follows_one_level_of_symlinks() {
16430 let _g = crate::test_util::global_state_lock();
16431 let tmp = std::env::temp_dir();
16432 let target = tmp.join(format!("zshrs_xsymlinks_target_{}", std::process::id()));
16433 let link = tmp.join(format!("zshrs_xsymlinks_link_{}", std::process::id()));
16434 // Create target as a regular dir to symlink to.
16435 let _ = fs::create_dir(&target);
16436 let _ = fs::remove_file(&link);
16437 std::os::unix::fs::symlink(&target, &link).unwrap();
16438
16439 let got = xsymlinks(link.to_str().unwrap()).unwrap();
16440 assert_eq!(
16441 got,
16442 target.to_string_lossy(),
16443 "c:908 — xsymlinks must follow the symlink to its target"
16444 );
16445
16446 // Cleanup.
16447 let _ = fs::remove_file(&link);
16448 let _ = fs::remove_dir(&target);
16449 }
16450
16451 /// `Src/utils.c:881-882` — `.` components are skipped.
16452 /// `Src/utils.c:883-896` — `..` components walk back one
16453 /// `/`-segment of xbuf (unless xbuf is empty or `/`).
16454 ///
16455 /// Use a temp directory we create ourselves — we can't pin
16456 /// `/tmp/...` literally because the macOS sandbox symlinks
16457 /// `/tmp -> /private/tmp`, which `xsymlinks` correctly follows
16458 /// (proving the bug-fix works). Test with a directory under
16459 /// the env tempdir + a non-existent sub so readlink fails on
16460 /// every component and the test exercises ONLY the c:881-896
16461 /// `.` / `..` paths.
16462 #[test]
16463 #[cfg(unix)]
16464 fn xsymlinks_normalises_dot_and_dotdot() {
16465 let _g = crate::test_util::global_state_lock();
16466 let tmp = fs::canonicalize(std::env::temp_dir()).unwrap();
16467 let base_dir = tmp.join(format!("zshrs_xs_norm_{}", std::process::id()));
16468 let _ = fs::create_dir(&base_dir);
16469 // `<base>/.` should be `<base>` (c:881 — `.` skipped).
16470 let arg = format!("{}/./.", base_dir.display());
16471 let got = xsymlinks(&arg).unwrap();
16472 assert_eq!(
16473 got,
16474 base_dir.to_string_lossy(),
16475 "c:881 — `.` segments collapse"
16476 );
16477 // `<base>/foo/..` should be `<base>` (c:891-895 — `..` walks back).
16478 let arg = format!("{}/foo/..", base_dir.display());
16479 let got = xsymlinks(&arg).unwrap();
16480 assert_eq!(
16481 got,
16482 base_dir.to_string_lossy(),
16483 "c:891-895 — `..` walks back one segment"
16484 );
16485 let _ = fs::remove_dir(&base_dir);
16486 }
16487
16488 /// `Src/utils.c:2055-2056` — when fdtable[x] is FDT_FLOCK or
16489 /// FDT_FLOCK_EXEC, the dup'd fd y gets promoted to FDT_INTERNAL
16490 /// (the dup doesn't carry the flock). Pin the promotion.
16491 ///
16492 /// Note on fdtable_flocks: C's c:2062-2063 decrements the
16493 /// flock count in `redup` BEFORE `zclose(x)`, and zclose ALSO
16494 /// decrements when it sees fdtable[fd] == FDT_FLOCK (c:2135).
16495 /// So C double-decrements for an FDT_FLOCK source fd in redup —
16496 /// the C comment at c:2058-2061 calls this "isn't expected to
16497 /// happen" (FDT_FLOCK fds aren't normally redup'd). We mirror
16498 /// C exactly: test asserts the count drops by 2 (1 → -1) to
16499 /// pin the faithful-to-C behavior. Reporting this as a C bug
16500 /// upstream is the right path; the port preserves it verbatim.
16501 #[test]
16502 #[cfg(unix)]
16503 fn redup_promotes_flock_to_internal_on_target() {
16504 let _g = crate::test_util::global_state_lock();
16505 let dev_null = CString::new("/dev/null").unwrap();
16506 let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16507 let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
16508 assert!(x >= 0 && y >= 0);
16509 assert_ne!(x, y);
16510
16511 check_fd_table(x);
16512 check_fd_table(y);
16513 // Mark x as FDT_FLOCK.
16514 fdtable_set(x, FDT_FLOCK);
16515 FDTABLE_FLOCKS.store(2, Ordering::SeqCst); // start at 2 so double-decrement lands at 0
16516
16517 let _ = redup(x, y);
16518
16519 // c:2055-2056 — promoted to FDT_INTERNAL on y, NOT carried.
16520 assert_eq!(
16521 fdtable_get(y),
16522 FDT_INTERNAL,
16523 "c:2055-2056 — FDT_FLOCK on x promotes to FDT_INTERNAL on y"
16524 );
16525 // c:2062-2063 + zclose c:2135 — double decrement.
16526 assert_eq!(
16527 FDTABLE_FLOCKS.load(Ordering::SeqCst),
16528 0,
16529 "c:2062-2063 + c:2135 — flock count double-decremented (faithful to C)"
16530 );
16531 unsafe {
16532 libc::close(y);
16533 }
16534 }
16535
16536 /// `Src/utils.c:5217-5240` — `zreaddir(dir, ignoredots)` exposes
16537 /// the dot-filter as a parameter and returns one entry per call.
16538 /// Two paths:
16539 /// * `ignoredots=1` (the common case at c:590/655/1653/2884) —
16540 /// `.` and `..` are filtered out.
16541 /// * `ignoredots=0` (used at c:4648 for spelling correction) —
16542 /// `.` and `..` are RETAINED as valid candidates.
16543 #[test]
16544 #[cfg(unix)]
16545 fn zreaddir_honors_ignoredots_flag() {
16546 let _g = crate::test_util::global_state_lock();
16547 let tmp = std::env::temp_dir();
16548 let base = tmp.join(format!("zshrs_zreaddir_{}", std::process::id()));
16549 let _ = fs::create_dir(&base);
16550 // Create one real entry to make the test non-trivial.
16551 fs::write(base.join("file"), "x").unwrap();
16552
16553 // c:5232 — ignoredots=1: skip `.` and `..`, keep `file`.
16554 let mut dir = fs::read_dir(&base).unwrap();
16555 let mut with_skip: Vec<String> = Vec::new();
16556 while let Some(n) = zreaddir(&mut dir, 1) {
16557 with_skip.push(n);
16558 }
16559 assert!(
16560 with_skip.contains(&"file".to_string()),
16561 "c:5232 — real entry survives ignoredots=1"
16562 );
16563 assert!(
16564 !with_skip.contains(&".".to_string()),
16565 "c:5232 — `.` filtered with ignoredots=1"
16566 );
16567 assert!(
16568 !with_skip.contains(&"..".to_string()),
16569 "c:5232 — `..` filtered with ignoredots=1"
16570 );
16571
16572 // c:4648-equivalent — ignoredots=0: KEEP `.` and `..`.
16573 // (libstd's fs::read_dir filters them before exposing on
16574 // macOS/Linux, so the without_skip set is functionally
16575 // identical here — pin only that the API path doesn't error.)
16576 let mut dir2 = fs::read_dir(&base).unwrap();
16577 let mut without_skip: Vec<String> = Vec::new();
16578 while let Some(n) = zreaddir(&mut dir2, 0) {
16579 without_skip.push(n);
16580 }
16581 assert!(
16582 without_skip.contains(&"file".to_string()),
16583 "ignoredots=0 still yields real entries"
16584 );
16585
16586 let _ = fs::remove_file(base.join("file"));
16587 let _ = fs::remove_dir(&base);
16588 }
16589
16590 /// `Src/utils.c:1075` — `get_username()` uses `ztrdup_metafy` on
16591 /// `pswd->pw_name`. Previous Rust port returned the raw pw_name
16592 /// verbatim — fine for ASCII usernames, broken for high-bit
16593 /// bytes (downstream paramtab consumers assume metafied entries).
16594 /// Pin: ASCII usernames round-trip identically through metafy,
16595 /// AND the result is a non-empty string for the current uid.
16596 /// `Src/utils.c:3445-3460` — ztrftime zsh-specific %K/%L/%f
16597 /// extensions return values WITHOUT leading zeros (vs the
16598 /// strftime %H/%I/%d which pad).
16599 #[test]
16600 #[cfg(unix)]
16601 fn ztrftime_zsh_extensions_no_leading_zero() {
16602 let _g = crate::test_util::global_state_lock();
16603 // Pick a known time: Jan 5 2024 09:07:42.123456789 UTC.
16604 // Use SystemTime + an offset since we want deterministic values
16605 // independent of TZ; we just verify that the format substitutes
16606 // the right NUMBER OF DIGITS for the leading-zero case.
16607 use std::time::Duration;
16608 // 2024-01-05 09:07:42 UTC = 1704445662
16609 let t = UNIX_EPOCH + Duration::new(1704445662, 123_456_789);
16610 // %H gives leading zero "09"; %K should give "9" (in some TZ
16611 // hour will be different but the digit-count rule still holds).
16612 let h_padded = ztrftime("%H", t, false);
16613 let k_unpadded = ztrftime("%K", t, false);
16614 // Both represent the same hour. If h_padded starts with `0`,
16615 // k_unpadded must be 1-char and not start with `0`.
16616 if h_padded.starts_with('0') && h_padded.len() == 2 {
16617 assert!(
16618 !k_unpadded.starts_with('0'),
16619 "c:3445 — %K must strip the leading 0 from %H={}, got %K={}",
16620 h_padded,
16621 k_unpadded
16622 );
16623 assert_eq!(
16624 k_unpadded.len(),
16625 1,
16626 "c:3445 — %K should be 1 digit when hour < 10"
16627 );
16628 }
16629 // %f for day-of-month: t is Jan 5 → in any reasonable TZ
16630 // day is between 4 and 6; format should be 1 digit when day < 10.
16631 let d_padded = ztrftime("%d", t, false);
16632 let f_unpadded = ztrftime("%f", t, false);
16633 if d_padded.starts_with('0') && d_padded.len() == 2 {
16634 assert!(!f_unpadded.starts_with('0'));
16635 assert_eq!(
16636 f_unpadded.len(),
16637 1,
16638 "c:3457 — %f should be 1 digit when day < 10"
16639 );
16640 }
16641 // %3. fractional seconds: input nsec is 123456789 → first 3
16642 // digits should be 123. Note zsh's syntax is `%N.` where N is
16643 // the digit count (c:3374-3384 + c:3409).
16644 let frac = ztrftime("%3.", t, false);
16645 assert_eq!(
16646 frac, "123",
16647 "c:3409-3438 — %3. must emit first 3 digits of nsec"
16648 );
16649 // %. with no digit prefix defaults to 3 digits per c:3409.
16650 let frac_default = ztrftime("%.", t, false);
16651 assert_eq!(frac_default, "123", "c:3409 — %. defaults to 3 digits");
16652 }
16653
16654 #[test]
16655 #[cfg(unix)]
16656 fn get_username_returns_metafied_non_empty_string() {
16657 let _g = crate::test_util::global_state_lock();
16658 let name = get_username();
16659 // CI environments may have a username from `whoami(1)`. In
16660 // weird sandbox-only setups it might be empty, but on every
16661 // standard build it should be set.
16662 if name.is_empty() {
16663 return;
16664 }
16665 // ASCII round-trip via metafy is identity. If the username
16666 // had high-bit bytes (the bug case), the output would be
16667 // Meta-escaped — both forms are non-empty so this pin
16668 // doesn't reject either, just ensures the fn returns
16669 // SOMETHING usable (not an empty string).
16670 assert!(
16671 !name.is_empty(),
16672 "c:1086 — getpwuid result must yield a non-empty username"
16673 );
16674 // Sanity: metafy(name) == name for ASCII input (every byte
16675 // is below Meta range). Confirms the c:1086 step preserves
16676 // ASCII paths byte-for-byte.
16677 if name.bytes().all(|b| b < 0x80) {
16678 assert_eq!(
16679 metafy(&name),
16680 name,
16681 "c:1086 — ASCII metafy is identity (so pin holds for ASCII users)"
16682 );
16683 }
16684 }
16685
16686 // ═══════════════════════════════════════════════════════════════════
16687 // Pure-utility tests — zstrtol/zstrtoul (numeric parse) and
16688 // getkeystring (key-escape decode). Each test pinned against either
16689 // C zsh semantics or a direct zsh shell invocation where applicable.
16690 // ═══════════════════════════════════════════════════════════════════
16691
16692 // ── zstrtol: numeric string → (value, unconsumed_tail) ──────────
16693 /// `zstrtol("42", 10)` → (42, "") — full consumption.
16694 #[test]
16695 fn zstrtol_decimal_full_consumption() {
16696 let (v, t) = zstrtol("42", 10);
16697 assert_eq!(v, 42);
16698 assert_eq!(t, "");
16699 }
16700
16701 /// `zstrtol("42abc", 10)` → (42, "abc") — stops at first non-digit.
16702 #[test]
16703 fn zstrtol_decimal_stops_at_non_digit() {
16704 let (v, t) = zstrtol("42abc", 10);
16705 assert_eq!(v, 42);
16706 assert_eq!(t, "abc");
16707 }
16708
16709 /// `zstrtol("-7", 10)` → (-7, "") — sign handling.
16710 #[test]
16711 fn zstrtol_negative_decimal() {
16712 let (v, t) = zstrtol("-7", 10);
16713 assert_eq!(v, -7);
16714 assert_eq!(t, "");
16715 }
16716
16717 /// `zstrtol("+12", 10)` → (12, "") — explicit `+` sign.
16718 #[test]
16719 fn zstrtol_explicit_plus_sign_consumed() {
16720 let (v, t) = zstrtol("+12", 10);
16721 assert_eq!(v, 12);
16722 assert_eq!(t, "");
16723 }
16724
16725 /// `zstrtol("ff", 16)` → (255, "") — hex base.
16726 #[test]
16727 fn zstrtol_hex_base_16() {
16728 let (v, t) = zstrtol("ff", 16);
16729 assert_eq!(v, 255);
16730 assert_eq!(t, "");
16731 }
16732
16733 /// `zstrtol("FF", 16)` → (255, "") — case-insensitive hex.
16734 #[test]
16735 fn zstrtol_hex_uppercase() {
16736 let (v, t) = zstrtol("FF", 16);
16737 assert_eq!(v, 255);
16738 }
16739
16740 /// `zstrtol("1010", 2)` → (10, "") — binary base.
16741 #[test]
16742 fn zstrtol_binary_base_2() {
16743 let (v, t) = zstrtol("1010", 2);
16744 assert_eq!(v, 10);
16745 assert_eq!(t, "");
16746 }
16747
16748 /// `zstrtol("17", 8)` → (15, "") — octal base.
16749 #[test]
16750 fn zstrtol_octal_base_8() {
16751 let (v, t) = zstrtol("17", 8);
16752 assert_eq!(v, 15);
16753 }
16754
16755 /// `zstrtol("0x1A", 0)` → base-detect picks hex from `0x` prefix → 26.
16756 #[test]
16757 fn zstrtol_base_zero_detects_hex_prefix() {
16758 let (v, _) = zstrtol("0x1A", 0);
16759 assert_eq!(v, 26, "0x1A with base=0 → hex 26");
16760 }
16761
16762 /// `zstrtol("0b101", 0)` → base-detect picks binary → 5.
16763 #[test]
16764 fn zstrtol_base_zero_detects_binary_prefix() {
16765 let (v, _) = zstrtol("0b101", 0);
16766 assert_eq!(v, 5, "0b101 with base=0 → binary 5");
16767 }
16768
16769 /// `zstrtol("017", 0)` → base-detect: leading `0` → octal → 15.
16770 #[test]
16771 fn zstrtol_base_zero_leading_zero_means_octal() {
16772 let (v, _) = zstrtol("017", 0);
16773 assert_eq!(v, 15, "017 with base=0 → octal 15");
16774 }
16775
16776 /// `zstrtol(" 42", 10)` → (42, "") — leading whitespace skipped.
16777 #[test]
16778 fn zstrtol_leading_whitespace_skipped() {
16779 let (v, _) = zstrtol(" 42", 10);
16780 assert_eq!(v, 42);
16781 }
16782
16783 /// `zstrtol("0", 10)` → (0, "") — zero is valid input.
16784 #[test]
16785 fn zstrtol_zero_input() {
16786 let (v, t) = zstrtol("0", 10);
16787 assert_eq!(v, 0);
16788 assert_eq!(t, "");
16789 }
16790
16791 // ── zstrtol_underscore: digit-separator support ─────────────────
16792 /// `zstrtol_underscore("1_000_000", 10, true)` → (1_000_000, "")
16793 /// — underscores skipped during digit accumulation.
16794 #[test]
16795 fn zstrtol_underscore_separator_in_decimal() {
16796 let (v, _) = zstrtol_underscore("1_000_000", 10, true);
16797 assert_eq!(v, 1_000_000);
16798 }
16799
16800 /// Without underscore flag, `_` stops parsing.
16801 #[test]
16802 fn zstrtol_underscore_disabled_stops_at_underscore() {
16803 let (v, t) = zstrtol_underscore("123_456", 10, false);
16804 assert_eq!(v, 123);
16805 assert_eq!(t, "_456");
16806 }
16807
16808 // ── zstrtoul_underscore: unsigned variant ───────────────────────
16809 /// Parses a plain decimal unsigned.
16810 #[test]
16811 fn zstrtoul_underscore_basic_decimal() {
16812 let v = zstrtoul_underscore("12345");
16813 assert_eq!(v, Some(12345));
16814 }
16815
16816 /// Empty string → None (no number to parse).
16817 #[test]
16818 fn zstrtoul_underscore_empty_returns_none() {
16819 let v = zstrtoul_underscore("");
16820 assert_eq!(v, None);
16821 }
16822
16823 // ── getkeystring: shell-escape decode ───────────────────────────
16824 /// `\n` → newline, `\t` → tab, `\r` → CR.
16825 /// Anchor: `print -r -- $'\n\t\r'` produces the three bytes.
16826 #[test]
16827 fn getkeystring_decodes_common_escapes() {
16828 assert_eq!(getkeystring("\\n").0, "\n");
16829 assert_eq!(getkeystring("\\t").0, "\t");
16830 assert_eq!(getkeystring("\\r").0, "\r");
16831 }
16832
16833 /// `\\` → single backslash.
16834 #[test]
16835 fn getkeystring_double_backslash_yields_single() {
16836 assert_eq!(getkeystring("\\\\").0, "\\");
16837 }
16838
16839 /// `\xNN` hex escape → byte value.
16840 /// Anchor: `print -r -- $'\x41'` → "A" (0x41 = 65 = 'A').
16841 #[test]
16842 fn getkeystring_hex_escape_lowercase_x() {
16843 assert_eq!(getkeystring("\\x41").0, "A");
16844 assert_eq!(getkeystring("\\x7E").0, "~");
16845 }
16846
16847 /// `\u{NNNN}` Unicode escape → corresponding char.
16848 /// Anchor: `print -r -- $'é'` → "é".
16849 #[test]
16850 fn getkeystring_unicode_escape_u_four_digits() {
16851 assert_eq!(getkeystring("\\u00E9").0, "é");
16852 }
16853
16854 /// Plain text passes through unchanged.
16855 #[test]
16856 fn getkeystring_plain_text_passes_through() {
16857 assert_eq!(getkeystring("hello").0, "hello");
16858 assert_eq!(getkeystring("").0, "");
16859 }
16860
16861 /// Mixed plain + escapes → both handled.
16862 #[test]
16863 fn getkeystring_mixed_text_and_escapes() {
16864 assert_eq!(getkeystring("a\\nb").0, "a\nb");
16865 assert_eq!(
16866 getkeystring("line1\\nline2\\tindented").0,
16867 "line1\nline2\tindented"
16868 );
16869 }
16870
16871 // ── metafy / unmetafy round-trip on ASCII (identity) ────────────
16872 /// ASCII strings: metafy is identity (no meta-bytes to escape).
16873 #[test]
16874 fn metafy_then_unmetafy_ascii_roundtrips_to_input() {
16875 let s = "hello world";
16876 let m = metafy(s);
16877 assert_eq!(m, s);
16878 let mut bytes = m.into_bytes();
16879 let _len = unmetafy(&mut bytes);
16880 assert_eq!(String::from_utf8(bytes).unwrap(), "hello world");
16881 }
16882
16883 // ═══════════════════════════════════════════════════════════════════
16884 // quotestring — emit a quoted shell-safe form of the input string.
16885 // Each QT_* mode produces a different quoting style. Tests pin
16886 // each mode for both empty and non-empty input.
16887 // ═══════════════════════════════════════════════════════════════════
16888
16889 use crate::zsh_h::{
16890 QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_DOLLARS, QT_DOUBLE, QT_NONE,
16891 QT_SINGLE, QT_SINGLE_OPTIONAL,
16892 };
16893
16894 /// QT_NONE: no quoting; passes input through unchanged.
16895 #[test]
16896 fn quotestring_qt_none_passes_through_unchanged() {
16897 let _g = crate::test_util::global_state_lock();
16898 assert_eq!(quotestring("hello world", QT_NONE), "hello world");
16899 assert_eq!(quotestring("", QT_NONE), "");
16900 assert_eq!(quotestring("a*b?c", QT_NONE), "a*b?c");
16901 }
16902
16903 /// QT_NONE empty → empty.
16904 #[test]
16905 fn quotestring_qt_none_empty_is_empty() {
16906 let _g = crate::test_util::global_state_lock();
16907 assert_eq!(quotestring("", QT_NONE), "");
16908 }
16909
16910 /// QT_BACKSLASH on empty → "''" (single-quote pair).
16911 #[test]
16912 fn quotestring_qt_backslash_empty_yields_empty() {
16913 let _g = crate::test_util::global_state_lock();
16914 // c:6194 — shownull is 0 for plain QT_BACKSLASH, so empty → "".
16915 assert_eq!(quotestring("", QT_BACKSLASH), "");
16916 }
16917
16918 /// QT_BACKSLASH_SHOWNULL on empty → "''" too.
16919 #[test]
16920 fn quotestring_qt_backslash_shownull_empty_yields_empty_single_quotes() {
16921 let _g = crate::test_util::global_state_lock();
16922 assert_eq!(quotestring("", QT_BACKSLASH_SHOWNULL), "''");
16923 }
16924
16925 /// c:6194 — `if (!*s && shownull)`. `shownull` is 0 for QT_SINGLE (it is
16926 /// set only by QT_BACKSLASH_SHOWNULL at c:6165 and QT_SINGLE_OPTIONAL at
16927 /// c:6187), so an empty string yields an EMPTY body; the `''` comes from
16928 /// the caller's wrapper (subst.c:4085-4087).
16929 #[test]
16930 fn quotestring_qt_single_empty_yields_empty_body() {
16931 let _g = crate::test_util::global_state_lock();
16932 assert_eq!(quotestring("", QT_SINGLE), "");
16933 }
16934
16935 /// QT_SINGLE_OPTIONAL on empty → "''" too.
16936 #[test]
16937 fn quotestring_qt_single_optional_empty_yields_empty_single_quotes() {
16938 let _g = crate::test_util::global_state_lock();
16939 assert_eq!(quotestring("", QT_SINGLE_OPTIONAL), "''");
16940 }
16941
16942 /// c:6194 — `shownull` is 0 for QT_DOUBLE, so the body is empty and the
16943 /// `""` pair is added by the caller (subst.c:4085-4087).
16944 #[test]
16945 fn quotestring_qt_double_empty_yields_empty_body() {
16946 let _g = crate::test_util::global_state_lock();
16947 assert_eq!(quotestring("", QT_DOUBLE), "");
16948 }
16949
16950 /// c:6194 — same for QT_DOLLARS; C's caller adds `''` and then
16951 /// `val[0] = '$'` (subst.c:4085-4090).
16952 #[test]
16953 fn quotestring_qt_dollars_empty_yields_empty_body() {
16954 let _g = crate::test_util::global_state_lock();
16955 assert_eq!(quotestring("", QT_DOLLARS), "");
16956 }
16957
16958 /// QT_BACKSLASH_PATTERN escapes only pattern meta-chars.
16959 /// Input "a*b?c[d]" → "a\\*b\\?c\\[d\\]".
16960 #[test]
16961 fn quotestring_qt_backslash_pattern_escapes_glob_metas() {
16962 let _g = crate::test_util::global_state_lock();
16963 let r = quotestring("a*b?c[d]", QT_BACKSLASH_PATTERN);
16964 assert!(
16965 r.contains("\\*") && r.contains("\\?") && r.contains("\\[") && r.contains("\\]"),
16966 "all globs must be escaped; got {r:?}"
16967 );
16968 }
16969
16970 /// QT_BACKSLASH_PATTERN does NOT escape plain ASCII letters/digits.
16971 #[test]
16972 fn quotestring_qt_backslash_pattern_doesnt_escape_plain_chars() {
16973 let _g = crate::test_util::global_state_lock();
16974 let r = quotestring("plain", QT_BACKSLASH_PATTERN);
16975 assert_eq!(r, "plain", "plain text passes through");
16976 }
16977
16978 /// QT_BACKSLASH_PATTERN escapes `<`, `>`, `(`, `)`, `|`, `#`, `^`, `~`.
16979 #[test]
16980 fn quotestring_qt_backslash_pattern_escapes_all_meta_set() {
16981 let _g = crate::test_util::global_state_lock();
16982 for ch in ['<', '>', '(', ')', '|', '#', '^', '~'] {
16983 let s = format!("x{ch}y");
16984 let r = quotestring(&s, QT_BACKSLASH_PATTERN);
16985 assert!(
16986 r.contains(&format!("\\{ch}")),
16987 "{ch:?} must be escaped, got {r:?}"
16988 );
16989 }
16990 }
16991
16992 /// c:6131-6134 — QT_SINGLE returns the BODY; a word with nothing to
16993 /// escape passes straight through and the caller adds the `'…'` pair.
16994 #[test]
16995 fn quotestring_qt_single_returns_body_unwrapped() {
16996 let _g = crate::test_util::global_state_lock();
16997 assert_eq!(quotestring("simple", QT_SINGLE), "simple");
16998 }
16999
17000 // ═══════════════════════════════════════════════════════════════════
17001 // metafy / unmetafy edge cases — bytes that NEED meta-encoding
17002 // (NUL, Meta byte 0x83, Nularg 0xa1, etc.). Pin that metafy escapes
17003 // them via the Meta + (byte ^ 32) scheme and unmetafy reverses it.
17004 // ═══════════════════════════════════════════════════════════════════
17005
17006 /// `metafy` is identity for plain ASCII (no meta bytes).
17007 #[test]
17008 fn metafy_ascii_is_identity_byte_for_byte() {
17009 let s = "abc 123 XYZ!@#";
17010 assert_eq!(metafy(s), s);
17011 }
17012
17013 /// Empty string round-trips.
17014 #[test]
17015 fn metafy_empty_string_returns_empty() {
17016 assert_eq!(metafy(""), "");
17017 }
17018
17019 /// metafy → unmetafy is round-trip identity for ASCII input.
17020 #[test]
17021 fn metafy_unmetafy_roundtrip_with_punctuation() {
17022 let s = "Hello, World! 123 +-=";
17023 let m = metafy(s);
17024 let mut bytes = m.into_bytes();
17025 let _ = unmetafy(&mut bytes);
17026 let result = String::from_utf8(bytes).expect("valid utf-8");
17027 assert_eq!(result, s);
17028 }
17029
17030 /// Multi-byte UTF-8 chars get meta-encoded then unmetafied. The
17031 /// round-trip MUST preserve the original bytes. Inline the byte-
17032 /// level metafy here — `metafy()`'s String return goes through
17033 /// `from_utf8_lossy` for non-UTF-8 outputs (correct for String
17034 /// consumers like the lexer or pattern compile, but breaks byte-
17035 /// round-trip because U+FFFD inserts EF BF BD where the
17036 /// original Meta-escaped bytes lived). The unmetafy step
17037 /// reverses the Meta-escape to recover the source UTF-8 verbatim.
17038 #[test]
17039 fn metafy_unmetafy_roundtrip_with_utf8_multibyte_anchored() {
17040 let s = "日本語";
17041 // Inline of metafy() byte path (no String materialisation).
17042 let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
17043 for &b in s.as_bytes() {
17044 if imeta_byte(b) {
17045 bytes.push(Meta);
17046 bytes.push(b ^ 32);
17047 } else {
17048 bytes.push(b);
17049 }
17050 }
17051 let _ = unmetafy(&mut bytes);
17052 let result = String::from_utf8(bytes).expect("valid utf-8");
17053 assert_eq!(result, s, "UTF-8 round-trip must preserve content");
17054 }
17055
17056 // ─── zsh-corpus pins for quotestring per quote_type ─────────────
17057
17058 /// `quotestring(QT_NONE)` is identity on any input.
17059 #[test]
17060 fn quotestring_corpus_qt_none_is_identity() {
17061 let s = "anything `goes` $here";
17062 assert_eq!(quotestring(s, QT_NONE), s);
17063 }
17064
17065 /// c:6131-6134 — `quotestring(QT_SINGLE)` on a plain word is a
17066 /// pass-through: no `'` pair is added here (that is the caller's job at
17067 /// subst.c:4085-4087). Emitting the pair here is what broke completion
17068 /// inside a quoted word — see `multiquote_leaves_plain_word_unquoted`.
17069 #[test]
17070 fn quotestring_corpus_qt_single_returns_plain_word_verbatim() {
17071 let out = quotestring("hello", QT_SINGLE);
17072 assert_eq!(out, "hello", "QT_SINGLE returns the body, unwrapped");
17073 }
17074
17075 /// c:6131-6134 — same contract for QT_DOUBLE.
17076 #[test]
17077 fn quotestring_corpus_qt_double_returns_plain_word_verbatim() {
17078 let out = quotestring("hello", QT_DOUBLE);
17079 assert_eq!(out, "hello", "QT_DOUBLE returns the body, unwrapped");
17080 }
17081
17082 /// `quotestring(QT_SINGLE)` on string with apostrophe escapes the
17083 /// apostrophe by closing+escape+reopen: `it's` → `'it'\''s'`.
17084 #[test]
17085 fn quotestring_corpus_qt_single_escapes_apostrophe() {
17086 // Same precondition the sibling QT_BACKSLASH test states: `'` only
17087 // reaches the c:6313 `instring == QT_SINGLE && *u == '\''` arm after
17088 // passing c:6301 `ispecial(*u)`, which reads the live typtab. C
17089 // guarantees it is populated via the `inittyptab()` at init.c:1261
17090 // (ported at init.rs:1244); a unit test that takes neither
17091 // `global_state_lock()` nor this call sees a zeroed table.
17092 inittyptab();
17093 let out = quotestring("it's", QT_SINGLE);
17094 assert!(
17095 out.contains("\\'") || out.contains("'\\''"),
17096 "apostrophe gets escaped in single-quote form, got {out:?}",
17097 );
17098 }
17099
17100 /// `quotestring(QT_BACKSLASH)` escapes shell special chars.
17101 /// Pattern chars like `*`, `?`, `$`, `(`, `)` should be backslashed.
17102 #[test]
17103 fn quotestring_corpus_qt_backslash_escapes_glob_chars() {
17104 // `ispecial` reads the live typtab, as C's macro does, so the table
17105 // has to be populated first — C guarantees it via the `inittyptab`
17106 // call in `setupvals`. Same entry call the sibling typtab tests make.
17107 inittyptab();
17108 let out = quotestring("a*b?c", QT_BACKSLASH);
17109 assert!(out.contains("\\*"), "* gets backslashed, got {out:?}");
17110 assert!(out.contains("\\?"), "? gets backslashed, got {out:?}");
17111 }
17112
17113 /// c:6194 — `shownull` is 0 for QT_DOUBLE, so an empty input gives an
17114 /// empty body; the `""` pair comes from the caller.
17115 #[test]
17116 fn quotestring_corpus_qt_double_empty_yields_empty_body() {
17117 let out = quotestring("", QT_DOUBLE);
17118 assert_eq!(out, "", "empty double-quoted body is empty");
17119 }
17120
17121 /// c:6194 — same for QT_SINGLE.
17122 #[test]
17123 fn quotestring_corpus_qt_single_empty_yields_empty_body() {
17124 let out = quotestring("", QT_SINGLE);
17125 assert_eq!(out, "", "empty single-quoted body is empty");
17126 }
17127
17128 /// `quotestring(QT_BACKSLASH)` on plain alphanumeric is identity.
17129 /// "abc123" should round-trip with no backslashes added.
17130 #[test]
17131 fn quotestring_corpus_qt_backslash_plain_word_unchanged() {
17132 let out = quotestring("abc123", QT_BACKSLASH);
17133 assert_eq!(
17134 out, "abc123",
17135 "plain alphanumeric needs no escape, got {out:?}"
17136 );
17137 }
17138
17139 /// `ztrlen("hello")` = 5 (no meta chars).
17140 #[test]
17141 fn ztrlen_corpus_plain_ascii_byte_count() {
17142 assert_eq!(ztrlen("hello"), 5);
17143 }
17144
17145 /// `ztrlen("")` = 0.
17146 #[test]
17147 fn ztrlen_corpus_empty_is_zero() {
17148 assert_eq!(ztrlen(""), 0);
17149 }
17150
17151 // ═══════════════════════════════════════════════════════════════════
17152 // itype_end C-parity tests — pin `utils.c:4395-4495` per-itype-flag
17153 // behavior. One assertion per flag/path so a regression in the
17154 // char-class walk points at the exact branch that drifted.
17155 //
17156 // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"]
17157 // so the bug stays surfaced but CI doesn't fail until the fix.
17158 // ═══════════════════════════════════════════════════════════════════
17159
17160 /// `itype_end("abc123", IIDENT, false)` walks the full identifier.
17161 /// C utils.c:4395 — IIDENT matches letters/digits/`_` via TYPTAB.
17162 /// Requires `inittyptab()` for TYPTAB population — call at entry.
17163 #[test]
17164 fn itype_end_ident_walks_full_identifier() {
17165 let _g = crate::test_util::global_state_lock();
17166 inittyptab();
17167 use crate::ported::ztype_h::IIDENT;
17168 assert_eq!(itype_end("abc123", IIDENT as u32, false), 6);
17169 }
17170
17171 /// `itype_end("abc-def", IIDENT, false)` stops at `-` (non-IIDENT).
17172 #[test]
17173 fn itype_end_ident_stops_at_dash() {
17174 let _g = crate::test_util::global_state_lock();
17175 inittyptab();
17176 use crate::ported::ztype_h::IIDENT;
17177 assert_eq!(itype_end("abc-def", IIDENT as u32, false), 3);
17178 }
17179
17180 /// `itype_end("a", IIDENT, true)` with `once=true` stops after 1.
17181 #[test]
17182 fn itype_end_once_stops_after_first_match() {
17183 let _g = crate::test_util::global_state_lock();
17184 inittyptab();
17185 use crate::ported::ztype_h::IIDENT;
17186 assert_eq!(itype_end("abc", IIDENT as u32, true), 1);
17187 }
17188
17189 /// `itype_end("", IIDENT, false)` on empty string returns 0.
17190 #[test]
17191 fn itype_end_empty_string_returns_zero() {
17192 let _g = crate::test_util::global_state_lock();
17193 inittyptab();
17194 use crate::ported::ztype_h::IIDENT;
17195 assert_eq!(itype_end("", IIDENT as u32, false), 0);
17196 }
17197
17198 /// `itype_end(":", IIDENT, false)` on non-IIDENT first char = 0.
17199 #[test]
17200 fn itype_end_non_ident_first_char_returns_zero() {
17201 let _g = crate::test_util::global_state_lock();
17202 inittyptab();
17203 use crate::ported::ztype_h::IIDENT;
17204 assert_eq!(itype_end(":foo", IIDENT as u32, false), 0);
17205 }
17206
17207 /// `itype_end("ns.foo", INAMESPC, false)` — C utils.c:4399-4413
17208 /// INAMESPC special-cases ksh93 namespace `.` separators. With
17209 /// non-POSIXIDENTIFIERS / non-KSH emulation, dotted names should
17210 /// walk through the `.` to consume `ns.foo` as a single name.
17211 /// The Rust port may stop at `.` since dotted-namespace support
17212 /// requires POSIXIDENTIFIERS check + ksh emulation gating that
17213 /// isn't fully wired.
17214 #[test]
17215 fn itype_end_inamespc_walks_through_ksh93_dot() {
17216 let _g = crate::test_util::global_state_lock();
17217 inittyptab();
17218 use crate::ported::ztype_h::INAMESPC;
17219 // Expected: full "ns.foo" = 6 bytes walked.
17220 assert_eq!(itype_end("ns.foo", INAMESPC, false), 6);
17221 }
17222
17223 /// `itype_end("a b c", ISEP, false)` from a space char.
17224 /// ISEP matches IFS chars. Default IFS includes ` \t\n`.
17225 /// Walk should consume only the leading separator(s).
17226 #[test]
17227 fn itype_end_isep_walks_separator_run() {
17228 let _g = crate::test_util::global_state_lock();
17229 inittyptab();
17230 use crate::ported::ztype_h::ISEP;
17231 // " \t " then 'a' — 3 sep bytes.
17232 assert_eq!(itype_end(" \t a", ISEP as u32, false), 3);
17233 }
17234
17235 // ═══════════════════════════════════════════════════════════════════
17236 // unmetafy C-parity tests — pin `utils.c:4954-4983`. Meta-byte
17237 // collapse (0x83 + (b^0x20) → b) must be in-place + return new len.
17238 // ═══════════════════════════════════════════════════════════════════
17239
17240 /// `unmetafy("abc")` is a no-op for non-metafied input.
17241 #[test]
17242 fn unmetafy_pure_ascii_unchanged() {
17243 let _g = crate::test_util::global_state_lock();
17244 let mut v = b"abc".to_vec();
17245 let n = unmetafy(&mut v);
17246 assert_eq!(n, 3);
17247 assert_eq!(v, b"abc");
17248 }
17249
17250 /// `unmetafy("a\x83a")` → "a\x01" because Meta(0x83) + next ^ 0x20
17251 /// = 'a' ^ 0x20 = 0x41 ^ 0x20 = 0x61... wait, 'a'=0x61, 0x61 ^ 0x20
17252 /// = 0x41 = 'A'. So unmetafy("a\x83a") → "aA" (2 bytes).
17253 #[test]
17254 fn unmetafy_meta_pair_collapses_to_xor_byte() {
17255 let _g = crate::test_util::global_state_lock();
17256 // 'a' + Meta(0x83) + 'a' → 'a' + ('a' ^ 0x20) = 'a' + 'A' = "aA"
17257 let mut v: Vec<u8> = vec![b'a', 0x83, b'a'];
17258 let n = unmetafy(&mut v);
17259 assert_eq!(n, 2);
17260 assert_eq!(v, vec![b'a', b'A']);
17261 }
17262
17263 /// `unmetafy("")` returns 0 with empty buffer.
17264 #[test]
17265 fn unmetafy_empty_returns_zero() {
17266 let _g = crate::test_util::global_state_lock();
17267 let mut v: Vec<u8> = Vec::new();
17268 let n = unmetafy(&mut v);
17269 assert_eq!(n, 0);
17270 }
17271
17272 /// `unmetafy` with trailing lone Meta byte. C `unmetafy` checks
17273 /// `p < s.len()` before reading the byte after Meta — trailing
17274 /// Meta at EOF is preserved as a single literal Meta byte.
17275 #[test]
17276 fn unmetafy_trailing_lone_meta_preserved() {
17277 let _g = crate::test_util::global_state_lock();
17278 let mut v: Vec<u8> = vec![b'a', 0x83];
17279 let n = unmetafy(&mut v);
17280 // Rust port copies the Meta byte to t, then loop exits with
17281 // p == s.len() → no second-byte read. Final = "a\x83" (2).
17282 assert_eq!(n, 2);
17283 assert_eq!(v, vec![b'a', 0x83]);
17284 }
17285
17286 // ═══════════════════════════════════════════════════════════════════
17287 // nicechar / wcs_nicechar C-parity tests — pin display-form
17288 // conversion. Required by stradd, prompt expansion, completion etc.
17289 // ═══════════════════════════════════════════════════════════════════
17290
17291 /// `nicechar('a')` returns the printable char unchanged.
17292 #[test]
17293 fn nicechar_printable_ascii_passes_through() {
17294 let _g = crate::test_util::global_state_lock();
17295 assert_eq!(nicechar('a'), "a");
17296 }
17297
17298 /// `nicechar('\n')` returns `\n` escape sequence per C `nicechar`
17299 /// (utils.c:464+ — control chars get `^M` style or `\n` escape).
17300 /// zsh emits `\n` as literal `\n` (two chars) for non-quotable
17301 /// nicechar; the `nicechar_sel(c, false)` form is non-quotable.
17302 #[test]
17303 fn nicechar_newline_emits_escape() {
17304 let _g = crate::test_util::global_state_lock();
17305 let out = nicechar('\n');
17306 assert!(
17307 out == "\\n" || out == "\n",
17308 "newline should be escaped or literal; got {out:?}"
17309 );
17310 }
17311
17312 /// `wcs_nicechar('A', None, None)` returns "A" for printable wide
17313 /// chars per utils.c:689.
17314 #[test]
17315 fn wcs_nicechar_printable_ascii_passes_through() {
17316 let _g = crate::test_util::global_state_lock();
17317 assert_eq!(wcs_nicechar('A', None, None), "A");
17318 }
17319
17320 // ═══════════════════════════════════════════════════════════════════
17321 // Additional C-parity tests for Src/utils.c nicechar dispatch.
17322 // ═══════════════════════════════════════════════════════════════════
17323
17324 /// c:462 — nicechar for printable ASCII passes through.
17325 #[test]
17326 fn nicechar_printable_ascii_passes_through_pin() {
17327 let _g = crate::test_util::global_state_lock();
17328 assert_eq!(nicechar('a'), "a");
17329 assert_eq!(nicechar('Z'), "Z");
17330 assert_eq!(nicechar('5'), "5");
17331 assert_eq!(nicechar(' '), " ");
17332 }
17333
17334 /// c:487 — nicechar('\\n') returns '\\n' (backslash + n).
17335 #[test]
17336 fn nicechar_newline_returns_backslash_n() {
17337 let _g = crate::test_util::global_state_lock();
17338 assert_eq!(nicechar('\n'), "\\n");
17339 }
17340
17341 /// c:490 — nicechar('\\t') returns '\\t' (backslash + t).
17342 #[test]
17343 fn nicechar_tab_returns_backslash_t() {
17344 let _g = crate::test_util::global_state_lock();
17345 assert_eq!(nicechar('\t'), "\\t");
17346 }
17347
17348 /// c:479 — nicechar(0x7f) returns '^?' (DEL → ^?).
17349 #[test]
17350 fn nicechar_del_returns_caret_question() {
17351 let _g = crate::test_util::global_state_lock();
17352 assert_eq!(nicechar('\x7f'), "^?");
17353 }
17354
17355 /// c:493 — nicechar(0x01) returns '^A' (Ctrl-A).
17356 #[test]
17357 fn nicechar_ctrl_a_returns_caret_A() {
17358 let _g = crate::test_util::global_state_lock();
17359 assert_eq!(nicechar('\x01'), "^A");
17360 }
17361
17362 /// c:493 — nicechar(0x1b) returns '^[' (ESC).
17363 #[test]
17364 fn nicechar_esc_returns_caret_bracket() {
17365 let _g = crate::test_util::global_state_lock();
17366 assert_eq!(nicechar('\x1b'), "^[");
17367 }
17368
17369 /// c:462 — nicechar_sel quotable=true uses '\\C-' instead of '^'
17370 /// for control chars.
17371 #[test]
17372 fn nicechar_sel_quotable_uses_backslash_C() {
17373 let _g = crate::test_util::global_state_lock();
17374 let r = nicechar_sel('\x01', true);
17375 assert!(
17376 r.contains("\\C-") || r.contains("^"),
17377 "quotable should emit \\C- form, got {:?}",
17378 r
17379 );
17380 }
17381
17382 /// c:531 — is_nicechar('a') returns FALSE: printable chars don't
17383 /// NEED nice (escape) representation, they pass through verbatim.
17384 /// Per the C comment in utils.c:531: "Return whether the char needs
17385 /// nice representation".
17386 #[test]
17387 fn is_nicechar_printable_returns_false() {
17388 let _g = crate::test_util::global_state_lock();
17389 assert!(!is_nicechar('a'), "printable doesn't NEED nice repr");
17390 assert!(!is_nicechar('Z'));
17391 assert!(!is_nicechar('5'));
17392 }
17393
17394 /// c:531 — is_nicechar for any byte 0..127 is safe (no panic).
17395 #[test]
17396 fn is_nicechar_all_ascii_safe() {
17397 let _g = crate::test_util::global_state_lock();
17398 for c in 0u8..=127 {
17399 let _ = is_nicechar(c as char); // no panic
17400 }
17401 }
17402
17403 /// c:462 — nicechar deterministic.
17404 #[test]
17405 fn nicechar_is_deterministic() {
17406 let _g = crate::test_util::global_state_lock();
17407 for c in ['a', '\n', '\t', '\x7f', '\x01', '\x1b'] {
17408 let first = nicechar(c);
17409 for _ in 0..5 {
17410 assert_eq!(nicechar(c), first, "{:?} must be pure", c);
17411 }
17412 }
17413 }
17414
17415 /// c:706 — `pathprog("")` returns None (empty path invalid).
17416 #[test]
17417 fn pathprog_empty_returns_none() {
17418 let _g = crate::test_util::global_state_lock();
17419 assert!(pathprog("").is_none());
17420 }
17421
17422 /// c:776 — `pathprog("/nonexistent/zshrs_xyz")` returns None.
17423 #[test]
17424 fn pathprog_nonexistent_returns_none() {
17425 let _g = crate::test_util::global_state_lock();
17426 let r = pathprog("/__never_exists_zshrs_pathprog_xyz");
17427 assert!(r.is_none());
17428 }
17429
17430 // ═══════════════════════════════════════════════════════════════════
17431 // Additional C-parity tests for Src/utils.c
17432 // c:99 set_widearray / c:447 nicechar_sel / c:521 is_nicechar /
17433 // c:742 zwcwidth / c:836 findpwd / c:928 slashsplit /
17434 // c:1258 get_username / c:1426 getnameddir / c:1475 dircmp /
17435 // c:1576 callhookfunc
17436 // ═══════════════════════════════════════════════════════════════════
17437
17438 /// c:99 — `set_widearray("")` empty returns empty Vec.
17439 #[test]
17440 fn set_widearray_empty_returns_empty() {
17441 let _g = crate::test_util::global_state_lock();
17442 assert!(set_widearray("").is_empty());
17443 }
17444
17445 /// c:99 — `set_widearray` returns Vec<char>.
17446 #[test]
17447 fn set_widearray_returns_vec_char_type() {
17448 let _g = crate::test_util::global_state_lock();
17449 let _: Vec<char> = set_widearray("a");
17450 }
17451
17452 /// c:447 — `nicechar_sel('a', false)` returns "a" (printable pass-through).
17453 #[test]
17454 fn nicechar_sel_ascii_letter_passthrough() {
17455 assert_eq!(nicechar_sel('a', false), "a", "printable letter as-is");
17456 }
17457
17458 /// c:447 — `nicechar_sel` is pure.
17459 #[test]
17460 fn nicechar_sel_is_pure() {
17461 for c in ['a', '\t', '\n', '\x1b', '\x7f', '日'] {
17462 let first = nicechar_sel(c, false);
17463 for _ in 0..3 {
17464 assert_eq!(
17465 nicechar_sel(c, false),
17466 first,
17467 "nicechar_sel({:?}, false) must be pure",
17468 c
17469 );
17470 }
17471 }
17472 }
17473
17474 /// c:742 — `zwcwidth` returns usize (compile-time type pin).
17475 #[test]
17476 fn zwcwidth_returns_usize_type() {
17477 let _: usize = zwcwidth('a');
17478 }
17479
17480 /// c:742 — `zwcwidth('a')` ASCII letter returns 1.
17481 #[test]
17482 fn zwcwidth_ascii_returns_one() {
17483 assert_eq!(zwcwidth('a'), 1, "ASCII letter width = 1");
17484 }
17485
17486 /// c:836 — `findpwd("")` empty returns Option<String>.
17487 #[test]
17488 fn findpwd_empty_returns_option_string_type() {
17489 let _g = crate::test_util::global_state_lock();
17490 let _: Option<String> = findpwd("");
17491 }
17492
17493 /// c:928 — `slashsplit("")` empty returns empty Vec.
17494 #[test]
17495 fn slashsplit_empty_returns_empty() {
17496 assert!(slashsplit("").is_empty());
17497 }
17498
17499 /// c:928 — `slashsplit("a/b/c")` splits to 3 elements.
17500 #[test]
17501 fn slashsplit_simple_three_components() {
17502 let r = slashsplit("a/b/c");
17503 assert_eq!(r, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
17504 }
17505
17506 /// c:1475 — `dircmp("", "")` both empty returns bool (type pin + safe).
17507 #[test]
17508 fn dircmp_both_empty_returns_bool_type() {
17509 let _g = crate::test_util::global_state_lock();
17510 let _: bool = dircmp("", "");
17511 }
17512
17513 /// c:1426 — `getnameddir("")` empty returns None.
17514 #[test]
17515 fn getnameddir_empty_returns_none() {
17516 let _g = crate::test_util::global_state_lock();
17517 assert!(getnameddir("").is_none(), "empty → None");
17518 }
17519
17520 /// c:1258 — `get_username` returns String (compile-time type pin).
17521 #[test]
17522 fn get_username_returns_string_type() {
17523 let _g = crate::test_util::global_state_lock();
17524 let _: String = get_username();
17525 }
17526
17527 /// c:1921-1935 — adjustwinsize(1) seeds `$LINES`/`$COLUMNS` from the
17528 /// TIOCGWINSZ probe of SHTTY. The port gated the setiparam calls on
17529 /// LINES/COLUMNS being in the OS environment (a misread of C's
17530 /// zgetenv guard, which only covers env re-publication — the C param
17531 /// is always live via its zterm_lines valptr alias), so every
17532 /// interactive shell ran with LINES=0 → `$(( LINES / 3 ))` division
17533 /// by zero in history-search-multi-word pagination (_hsmw_main:81).
17534 #[test]
17535 fn adjustwinsize_seeds_lines_columns_from_shtty() {
17536 let _g = crate::test_util::global_state_lock();
17537 unsafe {
17538 let mut master: libc::c_int = -1;
17539 let mut slave: libc::c_int = -1;
17540 let mut ws: libc::winsize = std::mem::zeroed();
17541 ws.ws_row = 33;
17542 ws.ws_col = 77;
17543 if libc::openpty(
17544 &mut master,
17545 &mut slave,
17546 std::ptr::null_mut(),
17547 std::ptr::null_mut(),
17548 &mut ws,
17549 ) != 0
17550 {
17551 return; // no pty available (sandboxed CI) — skip
17552 }
17553 let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
17554 crate::ported::init::SHTTY.store(slave, Ordering::Relaxed);
17555 let _ = adjustwinsize(1);
17556 crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
17557 libc::close(slave);
17558 libc::close(master);
17559 }
17560 assert_eq!(
17561 crate::ported::params::getiparam("LINES"),
17562 33,
17563 "LINES must reflect the SHTTY winsize probe"
17564 );
17565 assert_eq!(
17566 crate::ported::params::getiparam("COLUMNS"),
17567 77,
17568 "COLUMNS must reflect the SHTTY winsize probe"
17569 );
17570 }
17571
17572 /// c:Src/utils.c:1858 / c:Src/params.c:362 — `zterm_columns` is ONE
17573 /// cell. `$COLUMNS` is that cell (`IPDEF5("COLUMNS", &zterm_columns,
17574 /// zlevar_gsu)`), and the TIOCGWINSZ probe that refreshes it lives
17575 /// only in `adjustwinsize` (c:1902). So a window that has been
17576 /// resized but whose SIGWINCH has not been handled yet — the normal
17577 /// state, since C blocks SIGWINCH by default (`Src/init.c:1458`
17578 /// `winch_block()`) — must still report the OLD geometry to every
17579 /// reader, script-side and C-side alike.
17580 ///
17581 /// `adjustcolumns()`/`adjustlines()` used to run their own
17582 /// `ioctl(1, TIOCGWINSZ)` on every call, so the two answers split:
17583 /// `_git`'s `${(r.COLUMNS-4.)…}` (c:Completion/Unix/Command/_git:6890)
17584 /// built its display strings at the pre-resize width while
17585 /// `calclist` (c:Src/Zle/compresult.c:1646/1701) counted them at the
17586 /// post-resize width. `git <TAB>` across a 24x80 -> 24x60 resize
17587 /// asked "see all 164 possibilities (251 lines)?" for 164 matches
17588 /// that each occupied one line.
17589 #[test]
17590 fn adjustcolumns_does_not_resample_the_tty_between_adjustwinsize_calls() {
17591 let _g = crate::test_util::global_state_lock();
17592 let saved_lines = ZTERM_LINES.load(Ordering::SeqCst);
17593 let saved_columns = ZTERM_COLUMNS.load(Ordering::SeqCst);
17594 let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
17595 let mut published: Option<(usize, usize)> = None;
17596 let mut after_resize: Option<(usize, usize)> = None;
17597 let mut after_second_probe: Option<(usize, usize)> = None;
17598 let mut params_after_resize: Option<(i64, i64)> = None;
17599 unsafe {
17600 let mut master: libc::c_int = -1;
17601 let mut slave: libc::c_int = -1;
17602 let mut ws: libc::winsize = std::mem::zeroed();
17603 ws.ws_row = 24;
17604 ws.ws_col = 80;
17605 if libc::openpty(
17606 &mut master,
17607 &mut slave,
17608 std::ptr::null_mut(),
17609 std::ptr::null_mut(),
17610 &mut ws,
17611 ) != 0
17612 {
17613 return; // no pty available (sandboxed CI) — skip
17614 }
17615 // The old implementation probed fd 1, so fd 1 has to BE the
17616 // pty for this to pin anything: otherwise a CI runner's piped
17617 // stdout makes the stale-fallback path accidentally agree.
17618 let saved_stdout = libc::dup(1);
17619 libc::dup2(slave, 1);
17620 crate::ported::init::SHTTY.store(slave, Ordering::Relaxed);
17621
17622 let _ = adjustwinsize(1);
17623 published = Some((adjustlines(), adjustcolumns()));
17624
17625 // Resize the window WITHOUT running the SIGWINCH handler.
17626 let mut ws2: libc::winsize = std::mem::zeroed();
17627 ws2.ws_row = 12;
17628 ws2.ws_col = 60;
17629 libc::ioctl(master, libc::TIOCSWINSZ, &ws2);
17630 after_resize = Some((adjustlines(), adjustcolumns()));
17631 params_after_resize = Some((
17632 crate::ported::params::getiparam("LINES"),
17633 crate::ported::params::getiparam("COLUMNS"),
17634 ));
17635
17636 // c:469 — the handler's `adjustwinsize(1)` is the one thing
17637 // that may move the number.
17638 let _ = adjustwinsize(1);
17639 after_second_probe = Some((adjustlines(), adjustcolumns()));
17640
17641 if saved_stdout >= 0 {
17642 libc::dup2(saved_stdout, 1);
17643 libc::close(saved_stdout);
17644 }
17645 crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
17646 libc::close(slave);
17647 libc::close(master);
17648 }
17649 ZTERM_LINES.store(saved_lines, Ordering::SeqCst);
17650 ZTERM_COLUMNS.store(saved_columns, Ordering::SeqCst);
17651
17652 assert_eq!(
17653 published,
17654 Some((24, 80)),
17655 "adjustwinsize(1) publishes the probed geometry"
17656 );
17657 assert_eq!(
17658 after_resize,
17659 Some((24, 80)),
17660 "an unhandled resize must NOT move zterm_lines/zterm_columns — \
17661 the probe belongs to adjustwinsize (c:1902), not to the readers"
17662 );
17663 assert_eq!(
17664 params_after_resize,
17665 Some((24, 80)),
17666 "$LINES/$COLUMNS and the accessors are one cell (c:Src/params.c:362-363)"
17667 );
17668 assert_eq!(
17669 after_second_probe,
17670 Some((12, 60)),
17671 "the SIGWINCH handler's adjustwinsize(1) is what republishes it (c:469)"
17672 );
17673 }
17674
17675 /// c:Src/params.c:4230 — `zlevarsetfn` does `*p = x` where `p` is
17676 /// `&zterm_columns` itself, so an explicit `COLUMNS=N` is visible to
17677 /// every C-side reader immediately, with no tty involved. The mirror
17678 /// has to live in `zlevarsetfn` and not in `adjustwinsize`, which
17679 /// early-returns at c:1900-1901 whenever `SHTTY == -1` — exactly the
17680 /// non-interactive case where `COLUMNS=N` is most often set.
17681 #[test]
17682 fn explicit_columns_assignment_reaches_the_zterm_columns_readers() {
17683 let _g = crate::test_util::global_state_lock();
17684 let saved_columns = ZTERM_COLUMNS.load(Ordering::SeqCst);
17685 let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
17686 crate::ported::init::SHTTY.store(-1, Ordering::Relaxed); // c:1900
17687 // A sentinel the assignment must overwrite. Without the c:4230
17688 // mirror the readers keep answering with this, whatever `$COLUMNS`
17689 // says.
17690 ZTERM_COLUMNS.store(999, Ordering::SeqCst);
17691
17692 setiparam("COLUMNS", 61); // c:4230
17693 let mirrored = ZTERM_COLUMNS.load(Ordering::SeqCst);
17694 let seen = adjustcolumns();
17695
17696 crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
17697 ZTERM_COLUMNS.store(saved_columns, Ordering::SeqCst);
17698
17699 assert_eq!(
17700 mirrored, 61,
17701 "c:Src/params.c:4230 — `*p = x` writes zterm_columns itself"
17702 );
17703 assert_eq!(
17704 seen, 61,
17705 "COLUMNS=61 must be what the zterm_columns readers see (c:Src/params.c:362)"
17706 );
17707 }
17708
17709 /// The same contract as the test above, but with `zterm_columns` ALREADY
17710 /// seeded — which is the only state that can catch the real defect.
17711 ///
17712 /// `adjustcolumns` (c:Src/utils.c:1858) is a cached read: a non-zero
17713 /// `zterm_columns` short-circuits every fallback. So with the global at 0
17714 /// the test above passes even when the assignment never reaches it,
17715 /// because `adjustcolumns` then falls through to `getsparam("COLUMNS")`
17716 /// and finds the parameter. Seed the global first and the fallback is
17717 /// unreachable, so the only way to observe 61 is the `setfn` dispatch
17718 /// C makes at `Src/params.c:2874` (`pm->gsu.i->setfn(pm, val.u.l)` →
17719 /// `zlevarsetfn`, c:4226-4230, whose `*p = x` IS the global).
17720 ///
17721 /// That dispatch was missing: `setnumvalue`'s PM_INTEGER arm stored
17722 /// `pm.u_val` directly, leaving `zlevarsetfn` with zero call sites in the
17723 /// tree. Any test that ran after one which probed a tty (there is one
17724 /// eight tests earlier in this very module) saw the stale probe value.
17725 #[test]
17726 fn explicit_columns_assignment_overrides_an_already_cached_zterm_columns() {
17727 let _g = crate::test_util::global_state_lock();
17728 let saved_columns = ZTERM_COLUMNS.load(Ordering::SeqCst);
17729 let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
17730 crate::ported::init::SHTTY.store(-1, Ordering::Relaxed); // c:1900
17731
17732 // Stand in for a completed `adjustwinsize` probe (c:1862).
17733 ZTERM_COLUMNS.store(77, Ordering::SeqCst);
17734 setiparam("COLUMNS", 61); // c:4230 via c:2874 setfn dispatch
17735 let seen = adjustcolumns();
17736 let global = ZTERM_COLUMNS.load(Ordering::SeqCst);
17737
17738 crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
17739 ZTERM_COLUMNS.store(saved_columns, Ordering::SeqCst);
17740
17741 assert_eq!(
17742 global, 61,
17743 "the assignment must land in the zterm_columns GLOBAL, not just \
17744 the parameter (c:Src/params.c:4230 `*p = x`)"
17745 );
17746 assert_eq!(
17747 seen, 61,
17748 "a cached zterm_columns must not survive an explicit COLUMNS="
17749 );
17750 }
17751}
17752
17753// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
17754// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
17755// pipeline. Extracted body of zerrmsg's `%e` arm (Src/utils.c:355-366):
17756// EINTR → "interrupt", EIO verbatim, else lowercased strerror.
17757// zerrmsg's errno branch routes through this so the prefix
17758// formatting matches C; module callers (zsh/system) embed the
17759// string in their own zwarnnam messages where C uses `%e`.
17760/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
17761/// standalone function.
17762/// C has no such function: the errno-to-message rendering is the `case 'e':`
17763/// arm inside `zerrmsg` at `Src/utils.c:352-369` — EINTR becomes the literal
17764/// `interrupt`, EIO is printed verbatim, everything else is `strerror(num)`
17765/// with `tulower(errmsg[0])` applied to the first byte (c:366). Rust needs it
17766/// as a value-returning function because callers format their own output.
17767/// Render an errno the way zsh's `%e` warning format does —
17768/// `zerrmsg` case `'e'` at `Src/utils.c:352-368`:
17769///
17770/// - `EINTR` → the literal string `interrupt` (C also sets
17771/// `errflag |= ERRFLAG_ERROR` and truncates the rest of the
17772/// format; every zshrs call site uses `%e` as the final
17773/// conversion so the truncation is a no-op here).
17774/// - `EIO` → `strerror(EIO)` verbatim ("I/O" reads wrong
17775/// lowercased, per the c:361-364 comment).
17776/// - everything else → `strerror(num)` with the first letter
17777/// lowercased (`tulower(errmsg[0])`, c:366).
17778///
17779/// `std::io::Error::from_raw_os_error(n)` Display is NOT this
17780/// format — it appends " (os error N)" and keeps the capital.
17781/// That mismatch was the residual byte-diff in zsh/system's
17782/// `sysopen`/`zsystem flock` warnings (docs/BUGS.md #316).
17783pub fn zsh_errno_msg(num: i32) -> String {
17784 if num == libc::EINTR {
17785 return "interrupt".to_string(); // c:355-358
17786 }
17787 let msg = unsafe {
17788 let p = libc::strerror(num); // c:360
17789 if p.is_null() {
17790 return String::new();
17791 }
17792 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
17793 };
17794 if num == libc::EIO {
17795 return msg; // c:363-364
17796 }
17797 // c:366 — `fputc(tulower(errmsg[0]), file);`
17798 let mut chars = msg.chars();
17799 match chars.next() {
17800 Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
17801 None => msg,
17802 }
17803}
17804
17805// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
17806// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
17807// pipeline. Byte-exact inverse of metafy (Src/utils.c:4954 unmetafy):
17808// decodes Meta (\u{83}) + c^32 pairs back to raw bytes. C
17809// unmetafy works in place on char*; Rust strings are UTF-8 so
17810// the decode returns Vec<u8> for byte-exact writes.
17811/// !!! WARNING: RUST-ONLY HELPER !!! No C counterpart exists as a
17812/// standalone function.
17813/// Byte-exact inverse of `metafy` for Rust's char-level metafied `String`.
17814/// C's `unmetafy` (`Src/utils.c:4954`) rewrites a `char *` in place; a UTF-8
17815/// `String` cannot hold raw bytes, so this returns `Vec<u8>` instead.
17816/// Decode a char-level metafied String back to RAW bytes for a write
17817/// or exec boundary. c:Src/utils.c:4954 unmetafy — `if (*t++ == Meta
17818/// && *p) t[-1] = *p++ ^ 32;` — transposed onto the char-level
17819/// encoding `meta_encode_byte` produces: U+0083 followed by a scalar
17820/// in U+0080..=U+00FF yields the single raw byte `payload ^ 32`;
17821/// everything else emits its UTF-8 bytes verbatim. A U+0083 followed
17822/// by anything outside that range is not our encoding and passes
17823/// through untouched. Bug #127.
17824pub fn unmetafy_str(s: &str) -> Vec<u8> {
17825 let mut out = Vec::with_capacity(s.len());
17826 let mut it = s.chars().peekable();
17827 let mut buf = [0u8; 4];
17828 while let Some(c) = it.next() {
17829 if c == '\u{83}' {
17830 if let Some(&n) = it.peek() {
17831 let nu = n as u32;
17832 if (0x80..=0xff).contains(&nu) {
17833 out.push((nu as u8) ^ 32);
17834 it.next();
17835 continue;
17836 }
17837 }
17838 }
17839 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
17840 }
17841 out
17842}