zsh/ported/utils.rs
1//! Utility functions for zshrs
2//!
3//! Port from zsh/Src/utils.c
4//!
5//! Provides miscellaneous utilities: error handling, file operations,
6//! string utilities, and character classification.
7
8use std::ffi::CString;
9use std::fs;
10use std::io::{self, Read, Write};
11use std::os::unix::fs::{MetadataExt, PermissionsExt};
12use std::os::unix::io::{AsRawFd, RawFd};
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicI64, Ordering};
15use std::sync::Mutex;
16use std::time::UNIX_EPOCH;
17
18use crate::init::zleentry;
19use crate::params::getsparam_u;
20use crate::ported::builtin::{BUILTINS, SFCONTEXT, STOPMSG};
21use crate::ported::compat::{u9_iswprint, zchdir, zgetdir};
22use crate::ported::hashnameddir::{nameddirtab, removenameddirnode};
23use crate::ported::hashtable::shfunctab_lock;
24use crate::ported::hist::{bangchar, chrealpath};
25use crate::ported::init::SHTTY;
26use crate::ported::modules::clone::{coprocin, coprocout, mypgrp};
27use crate::{DPUTS, DPUTS1};
28use libc::{
29 S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP,
30 S_IXOTH, S_IXUSR,
31};
32// SHTTY imported under an alias to avoid collision with the
33// `SHTTY: i32` function parameters at fdsettyinfo/fdgettyinfo
34// (Rule E — C uses SHTTY as both the global and the parameter name).
35use crate::ported::lex::{lineno, untokenize};
36use crate::ported::options::{dosetopt, opt_state_set};
37use crate::ported::params::{
38 assignsparam, convbase as convbase_param, getaparam, getsparam, homesetfn, ifsgetfn, ifssetfn,
39 isident, locallevel as LOCALLEVEL, setaparam, setiparam, setsparam, wordcharsgetfn,
40 wordcharssetfn,
41};
42use crate::ported::signals::{queue_signals, unqueue_signals};
43use crate::ported::string::dupstrpfx;
44use crate::ported::zsh_h::{
45 dirsav, hashnode, interact, isset, jobbing, nameddir, opt_name, shfunc, unset, Dash, Marker,
46 Meta, Nularg, Pound, Snull, AUTONAMEDIRS, BANGHIST, BEEP, CHASELINKS, CSHJUNKIEQUOTES,
47 DEFAULT_IFS, DVORAK, EMULATE_KSH, EMULATE_SH, EMULATION, EXTENDEDGLOB, FDT_EXTERNAL, FDT_FLOCK,
48 FDT_FLOCK_EXEC, FDT_INTERNAL, FDT_MODULE, FDT_UNUSED, GLOBDOTS, HISTFLAG_NOEXEC,
49 LAST_NORMAL_TOK, MAGICEQUALSUBST, MULTIBYTE, ND_NOABBREV, ND_USERNAME, NICEFLAG_HEAP,
50 NICEFLAG_NODUP, NICEFLAG_QUOTE, OCTALZEROES, PATCHARS, POSIXIDENTIFIERS, PRINTEIGHTBIT,
51 QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_BACKTICK, QT_DOLLARS, QT_DOUBLE,
52 QT_NONE, QT_SINGLE, QT_SINGLE_OPTIONAL, RCQUOTES, RMSTARWAIT, SFC_SUBST, SHINSTDIN, SPECCHARS,
53 XTRACE, ZLE_CMD_TRASH,
54};
55use crate::ported::zsh_system_h::DEFAULT_WORDCHARS;
56use crate::ported::ztype_h::{
57 imeta, itok, iwsep, IALNUM, IALPHA, IBLANK, ICNTRL, IDIGIT, IIDENT, IMETA, INBLANK, INULL,
58 IPATTERN, ISEP, ISPECIAL, ITOK, IUSER, IWORD, IWSEP, TYPTAB, TYPTAB_FLAGS, ZISPRINT,
59 ZTF_BANGCHAR, ZTF_INIT, ZTF_INTERACT, ZTF_SP_COMMA,
60};
61
62/// Set a wide-char array from a multibyte source string.
63/// Port of `set_widearray(char *mb_array, Widechar_array wca)` from `Src/utils.c:69`.
64///
65/// ```c
66/// static void set_widearray(char *mb_array, Widechar_array wca) {
67/// if (wca->chars) free(wca->chars);
68/// wca->len = 0;
69/// if (mb_array) {
70/// while (*mb_array) {
71/// if (unsigned char *mb_array <= 0x7f) {
72/// *wcptr++ = (wchar_t)*mb_array++;
73/// continue;
74/// }
75/// mblen = mb_metacharlenconv(mb_array, &wci);
76/// if (!mblen) break;
77/// if (wci == WEOF) return; // any non-convertible aborts
78/// *wcptr++ = (wchar_t)wci;
79/// mb_array += mblen;
80/// }
81/// wca->chars = malloc(...); wca->len = wcptr - tmpwcs;
82/// }
83/// }
84/// ```
85///
86/// Build a wide-char array from a metafied multibyte source string.
87/// C uses `mb_metacharlenconv()` to walk Meta-encoded sequences;
88/// Rust port unmetafies first, then collects chars (the
89/// equivalent: walk Unicode codepoints).
90///
91/// Returns the new vec; caller assigns to the appropriate slot
92/// (`WORDCHARS_w`, `IFS_w`, etc.). C aborts on non-convertible
93/// chars (returns without setting `wca->chars`); Rust port mirrors
94/// by returning empty Vec when conversion fails.
95/// WARNING: param names don't match C — Rust=(mb_array) vs C=(mb_array, wca)
96// Rust idiom replacement: `unmetafy` + `str::chars` covers the C
97// `mbsrtowcs`+`mbstate_t` conversion; the C `wca` out-param drops
98// since the Vec is returned by value.
99/// `set_widearray` — see implementation.
100pub fn set_widearray(mb_array: &str) -> Vec<char> {
101 let mut bytes = mb_array.as_bytes().to_vec();
102 unmetafy(&mut bytes);
103 match std::str::from_utf8(&bytes) {
104 Ok(s) => s.chars().collect(),
105 Err(_) => Vec::new(),
106 }
107}
108
109// =====================================================================
110// Port of `zwarning(const char *cmd, const char *fmt, va_list ap)` from `Src/utils.c:142`.
111// =====================================================================
112
113/// Port of `zwarning(const char *cmd, const char *fmt, va_list ap)` from `Src/utils.c:142`.
114///
115/// Internal helper that builds the diagnostic prefix and emits it +
116/// the formatted message to stderr. Direct C body translation:
117///
118/// ```c
119/// if (isatty(2)) zleentry(ZLE_CMD_TRASH);
120/// char *prefix = scriptname ? scriptname : (argzero ? argzero : "");
121/// if (cmd) {
122/// if (unset(SHINSTDIN) || locallevel) {
123/// nicezputs(prefix, stderr);
124/// fputc(':', stderr);
125/// }
126/// nicezputs(cmd, stderr);
127/// fputc(':', stderr);
128/// } else {
129/// nicezputs((isset(SHINSTDIN) && !locallevel) ? "zsh" : prefix, stderr);
130/// fputc(':', stderr);
131/// }
132/// zerrmsg(stderr, fmt, ap);
133/// ```
134/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd, fmt, ap)
135fn zwarning(cmd: Option<&str>, msg: &str) {
136 // c:96 — `if (isatty(2)) zleentry(ZLE_CMD_TRASH);`
137 // Flush any in-flight ZLE redraw state before the warning lands
138 // on stderr — without this, half-painted edit lines bleed into
139 // the diagnostic. Previously: `let _ = isatty(2);` (the result
140 // was discarded; the canonical zleentry port at init.rs:905 was
141 // never actually called from the warning path).
142 if unsafe { libc::isatty(2) } != 0 {
143 // c:96
144 let _ = zleentry(
145 // c:96
146 ZLE_CMD_TRASH, // c:96
147 );
148 }
149 let scriptname = scriptname_lock().lock().unwrap().clone();
150 let argzero = argzero_lock().lock().unwrap().clone();
151 let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
152 let prefix: String = scriptname.or(argzero).unwrap_or_default();
153 let stderr_handle = io::stderr();
154 let mut stderr_lock = stderr_handle.lock();
155 if let Some(cmd) = cmd {
156 // c:107-110 — `if (unset(SHINSTDIN) || locallevel) {
157 // nicezputs(prefix, stderr); fputc(':', stderr); }`
158 if unset(SHINSTDIN) || locallevel != 0 {
159 let _ = nicezputs(&prefix, &mut stderr_lock); // c:108
160 let _ = stderr_lock.write_all(b":");
161 }
162 let _ = nicezputs(cmd, &mut stderr_lock); // c:111
163 let _ = stderr_lock.write_all(b":");
164 } else {
165 // c:114 — `nicezputs((isset(SHINSTDIN) && !locallevel) ? "zsh" : prefix, stderr);`
166 let to_emit = if isset(SHINSTDIN) && locallevel == 0 {
167 "zsh"
168 } else {
169 prefix.as_str()
170 };
171 let _ = nicezputs(to_emit, &mut stderr_lock); // c:114
172 let _ = stderr_lock.write_all(b":");
173 }
174 // c:116 — `zerrmsg(stderr, fmt, ap)` — lineno prefix + message.
175 // Pre-built `msg: &str` covers C's va_list; zerrmsg port hasn't
176 // had its `(file, fmt, ap)` signature wired yet so the lineno
177 // prefix + write is inlined here against the same `unset(SHINSTDIN)`
178 // gate C uses at c:301.
179 let lineno = lineno() as i32;
180 if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
181 let _ = stderr_lock.write_all(format!("{}: ", lineno).as_bytes());
182 } else {
183 let _ = stderr_lock.write_all(b" ");
184 }
185 let _ = stderr_lock.write_all(msg.as_bytes());
186 let _ = stderr_lock.write_all(b"\n");
187 let _ = stderr_lock.flush();
188}
189
190// =====================================================================
191// Port of `zerr(VA_ALIST1(const char *fmt))` / `zerrnam` / `zwarn` / `zwarnnam` from utils.c:173
192// onward. Each is a thin wrapper: check errflag/noerrs guards, set
193// `ERRFLAG_ERROR` on the fatal variants, call `zwarning`.
194// =====================================================================
195
196/// Port of `zerr(VA_ALIST1(const char *fmt))` from `Src/utils.c:173`.
197///
198/// ```c
199/// if (errflag || noerrs) {
200/// if (noerrs < 2) errflag |= ERRFLAG_ERROR;
201/// return;
202/// }
203/// errflag |= ERRFLAG_ERROR;
204/// zwarning(NULL, fmt, ap);
205/// ```
206/// WARNING: param names don't match C — Rust=(msg) vs C=()
207pub fn zerr(msg: &str) {
208 // c:173
209 let noerrs = *noerrs_lock().lock().unwrap();
210 if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
211 // c:175
212 if noerrs < 2 {
213 // c:176
214 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:176
215 }
216 return; // c:177
217 }
218 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:194
219 zwarning(None, msg); // c:194
220}
221
222/// Port of `zerrnam(VA_ALIST2(const char *cmd, const char *fmt))` from `Src/utils.c:194`.
223///
224/// ```c
225/// if (errflag || noerrs) return;
226/// errflag |= ERRFLAG_ERROR;
227/// zwarning(cmd, fmt, ap);
228/// ```
229/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd)
230pub fn zerrnam(cmd: &str, msg: &str) {
231 // c:194
232 let noerrs = *noerrs_lock().lock().unwrap();
233 if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
234 // c:196
235 return;
236 }
237 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:214
238 zwarning(Some(cmd), msg); // c:214
239}
240
241/// Port of `zwarn(VA_ALIST1(const char *fmt))` from `Src/utils.c:214`.
242///
243/// ```c
244/// if (errflag || noerrs) return;
245/// zwarning(NULL, fmt, ap);
246/// ```
247/// WARNING: param names don't match C — Rust=(msg) vs C=()
248pub fn zwarn(msg: &str) {
249 // c:214
250 let noerrs = *noerrs_lock().lock().unwrap();
251 if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
252 // c:216
253 return;
254 }
255 zwarning(None, msg); // c:231
256}
257
258/// Port of `zwarnnam(VA_ALIST2(const char *cmd, const char *fmt))` from `Src/utils.c:231`.
259///
260/// ```c
261/// if (errflag || noerrs) return;
262/// zwarning(cmd, fmt, ap);
263/// ```
264/// WARNING: param names don't match C — Rust=(cmd, msg) vs C=(cmd)
265pub fn zwarnnam(cmd: &str, msg: &str) {
266 // c:231
267 let noerrs = *noerrs_lock().lock().unwrap();
268 if errflag.load(Ordering::Relaxed) != 0 || noerrs != 0 {
269 // c:233
270 return;
271 }
272 zwarning(Some(cmd), msg); // c:235
273}
274
275/// Port of `dputs(VA_ALIST1(const char *message))` from `Src/utils.c:253`.
276///
277/// C body (c:253-270):
278/// ```c
279/// mod_export void dputs(const char *message)
280/// {
281/// char *filename;
282/// FILE *file;
283/// if ((filename = getsparam_u("ZSH_DEBUG_LOG")) != NULL &&
284/// (file = fopen(filename, "a")) != NULL) {
285/// zerrmsg(file, message, ap);
286/// fclose(file);
287/// } else
288/// zerrmsg(stderr, message, ap);
289/// }
290/// ```
291///
292/// Previous Rust port was a FAKE — `eprintln!("BUG: {}", msg)` ignored
293/// `$ZSH_DEBUG_LOG` entirely (which is the whole point of dputs — to
294/// route debug output to a user-specified log file). Re-port now
295/// consults paramtab via `getsparam_u`, opens the log in append mode,
296/// and routes the message there; falls back to stderr per c:268.
297///
298/// Rust signature drift: takes `&str` instead of va_args; callers
299/// pre-format via Rust's `format!` (same pattern as `zerrmsg`).
300pub fn dputs(msg: &str) {
301 // c:253
302 // c:263 — `getsparam_u("ZSH_DEBUG_LOG")`. The `_u` variant
303 // unmetafies the result (utils.rs's getsparam_u port at
304 // params.rs:3831 wraps getsparam + unmeta).
305 let log_file = getsparam_u("ZSH_DEBUG_LOG"); // c:263
306 // c:264 — `fopen(filename, "a")` — append mode.
307 let opened = log_file.as_ref().and_then(|p| {
308 // c:264
309 fs::OpenOptions::new()
310 .create(true)
311 .append(true)
312 .open(p)
313 .ok()
314 });
315 // Shared format logic: lineno prefix + msg + newline, matching
316 // zerrmsg at c:296-308. Built once, written to file or stderr.
317 let lineno = lineno() as i32;
318 let shinstdin = isset(SHINSTDIN);
319 let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
320 let prefix = if (!shinstdin || locallevel != 0) && lineno != 0 {
321 format!("{}: ", lineno)
322 } else {
323 String::new()
324 };
325 let line = format!("{}{}\n", prefix, msg);
326 if let Some(mut f) = opened {
327 // c:265 zerrmsg(file, ...)
328 let _ = f.write_all(line.as_bytes()); // c:265
329 // c:266 — `fclose(file)` — handled by Drop when `f` goes out of scope.
330 } else {
331 // c:267 else stderr
332 let _ = io::stderr().write_all(line.as_bytes()); // c:268 zerrmsg(stderr, ...)
333 }
334}
335
336// ---------------------------------------------------------------------------
337// Remaining 33 missing utils.c functions
338// ---------------------------------------------------------------------------
339
340// `zwarning` is defined earlier in this file as the real port of
341// utils.c:142 (private helper invoked by `zerr`/`zerrnam`/`zwarn`/
342// `zwarnnam`). The duplicate stub previously here has been deleted
343// — callers use the four public entry points instead.
344
345/// Port of `void zz_plural_z_alpha(void)` from Src/utils.c:282.
346///
347/// Cygwin-only no-op symbol the C source emits to work around a
348/// dllwrap bug that drops the last alphabetically-sorted exported
349/// symbol (zwarnnam). The Rust port has no equivalent linker
350/// problem; this is preserved as a no-op for symbol-table parity.
351pub fn zz_plural_z_alpha() {} // c:282
352
353/// Port of `zerrmsg(FILE *file, const char *fmt, va_list ap)` from `Src/utils.c:289`.
354///
355/// C body emits the formatted message + (when locallevel > 0 or
356/// SHINSTDIN unset) the line number prefix + "\n". The Rust port
357/// is invoked indirectly through `zwarning` — direct callers pass
358/// pre-formatted strings.
359/// WARNING: param names don't match C — Rust=(msg, errno) vs C=(file, fmt, ap)
360// Rust idiom replacement: pre-formatted `msg: &str` covers the C
361// va_list expansion; the C `file`+`fmt`+`ap` triplet collapses
362// because callers (zerr/zwarning) pre-format via Rust's `format!`.
363/// `zerrmsg` — see implementation.
364pub fn zerrmsg(msg: &str, errno: Option<i32>) {
365 // c:289
366 // c:296 — `lineno` is the parser-advanced line counter. The
367 // previous Rust port read `lineno_lock` (a Mutex<i32> in utils.rs)
368 // that ONLY fusevm_bridge::set_lineno wrote to; the actual
369 // parser uses lex::LEX_LINENO (thread_local Cell<u64>) updated
370 // by every newline scan. Result: error messages emitted from
371 // parse-time always printed line 0 (or last fusevm_bridge value)
372 // rather than the actual line of the syntax error.
373 //
374 // Route through lex::lineno() so the parser-advanced counter
375 // drives the error prefix.
376 let lineno = lineno() as i32;
377 let locallevel = LOCALLEVEL.load(Ordering::Relaxed);
378 // c:301-308 — `if ((unset(SHINSTDIN) || locallevel) && lineno)
379 // fprintf(file, "%d: ", lineno); else fputc(' ', file);`
380 if (unset(SHINSTDIN) || locallevel != 0) && lineno != 0 {
381 eprint!("{}: ", lineno);
382 } else {
383 eprint!(" ");
384 }
385 if let Some(e) = errno {
386 // c:348-365 — `%e`: render the errno exactly as C's zerrmsg does,
387 // through `zsh_errno_msg` (EINTR→"interrupt", EIO verbatim, else
388 // strerror with a lowercased first letter). The previous
389 // `io::Error::from_raw_os_error` kept the capital and appended
390 // " (os error N)" — the byte-diff tracked as docs/BUGS.md #316.
391 if e == libc::EINTR {
392 // c:351-354 — EINTR sets the error flag and stops formatting.
393 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
394 }
395 eprintln!("{}: {}", msg, zsh_errno_msg(e));
396 } else {
397 eprintln!("{}", msg);
398 }
399}
400
401/// Port of `void zsetupterm(void)` from Src/utils.c:390.
402///
403/// Reference-counts terminfo's `cur_term` setup. The C source
404/// guards with `#ifdef HAVE_SETUPTERM`; without terminfo this is a
405/// pure no-op. Rust's `term`-style state lives elsewhere (modules
406/// like `zle/termcap` initialize on demand), so this is a no-op
407/// counter for symbol-table parity.
408pub fn zsetupterm() {
409 // c:390
410 static TERM_COUNT: std::sync::atomic::AtomicI32 = // c:402
411 std::sync::atomic::AtomicI32::new(0);
412 let tc = TERM_COUNT.load(Ordering::Relaxed);
413 // c:395-396 — DPUTS(term_count < 0 || (term_count > 0 && !cur_term),
414 // "inconsistent term_count and/or cur_term");
415 // The Rust port has no `cur_term` analogue (terminfo state lives
416 // in zle/termcap module), so the second condition reduces to
417 // `term_count > 0 && true` if we treat cur_term as the count
418 // itself. Simplified to the bare `term_count < 0` invariant.
419 DPUTS!(tc < 0, "inconsistent term_count and/or cur_term"); // c:395-396
420 TERM_COUNT.fetch_add(1, Ordering::Relaxed); // c:402
421}
422
423/// Port of `void zdeleteterm(void)` from Src/utils.c:409.
424pub fn zdeleteterm() {
425 // c:409
426 static TERM_COUNT: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
427 let tc = TERM_COUNT.load(Ordering::Relaxed);
428 // c:412-413 — DPUTS(term_count < 1 || !cur_term,
429 // "inconsistent term_count and/or cur_term");
430 DPUTS!(tc < 1, "inconsistent term_count and/or cur_term"); // c:412-413
431 if tc > 0 {
432 TERM_COUNT.fetch_sub(1, Ordering::Relaxed); // c:414
433 }
434}
435
436/// Port of `int putraw(int c)` from Src/utils.c:424. Writes a
437/// single byte to stdout for the termcap library, returning 0.
438pub fn putraw(c: char) -> i32 {
439 // c:424
440 print!("{}", c); // c:434
441 0 // c:434
442}
443
444/// Port of `int putshout(int c)` from Src/utils.c:434. Writes a
445/// single byte to `shout` (zsh's interactive stdout — falls back
446/// to stdout in zshrs's static-link path), returning 0.
447pub fn putshout(c: char) -> i32 {
448 // c:434
449 print!("{}", c); // c:434
450 0 // c:437
451}
452
453/// Port of `nicechar_sel(int c, int quotable)` from `Src/utils.c:462`.
454/// Renders one byte as its `^X` / `M-X` / `\\n` / `\\t` display form;
455/// `quotable=true` emits `\\C-X` instead of `^X` so the result is
456/// shell-quotable.
457pub fn nicechar_sel(c: char, quotable: bool) -> String {
458 // c:462
459 // c:466 — `c &= 0xff;` mask to byte before any classification.
460 let mut c = (c as u32) & 0xff;
461 let mut out = String::new();
462 // c:467 — `if (ZISPRINT(c)) goto done;`. Use the canonical
463 // `ZISPRINT` predicate (port of ztype.h:89 — IPRINT typtab bit
464 // AND != 0x7f). Previously used a custom `is_print` closure that
465 // wrongly accepted 0xa0+ as printable — diverged from C ZISPRINT
466 // for every high-bit byte under PRINTEIGHTBIT-off, breaking
467 // `\M-X` escape generation.
468 if !ZISPRINT(c as u8) {
469 // c:467
470 if c & 0x80 != 0 {
471 // c:469
472 if isset(PRINTEIGHTBIT) { // c:470
473 // c:471 — goto done (output raw); c unchanged.
474 } else {
475 out.push_str("\\M-"); // c:472-474
476 c &= 0x7f; // c:475
477 if ZISPRINT(c as u8) {
478 // c:476-477
479 // c:477 — goto done after writing \M- + ASCII char.
480 if let Some(ch) = char::from_u32(c) {
481 out.push(ch);
482 }
483 return out;
484 }
485 }
486 }
487 if c == 0x7f {
488 // c:479
489 out.push_str(if quotable { "\\C-" } else { "^" }); // c:481-486
490 c = b'?' as u32;
491 } else if c == b'\n' as u32 {
492 // c:487
493 out.push('\\');
494 c = b'n' as u32;
495 } else if c == b'\t' as u32 {
496 // c:490
497 out.push('\\');
498 c = b't' as u32;
499 } else if c < 0x20 {
500 // c:493
501 out.push_str(if quotable { "\\C-" } else { "^" }); // c:494-499
502 c += 0x40;
503 }
504 }
505 if let Some(ch) = char::from_u32(c) {
506 // c:511
507 out.push(ch);
508 }
509 out
510}
511
512/// Port of `nicechar(int c)` from Src/utils.c:520. C body:
513/// `return nicechar_sel(c, 0);`
514pub fn nicechar(c: char) -> String {
515 // c:520
516 nicechar_sel(c, false) // c:523
517}
518
519/// Port of `is_nicechar(int c)` from `Src/utils.c:531-539`.
520/// ```c
521/// c &= 0xff;
522/// if (ZISPRINT(c)) return 0;
523/// if (c & 0x80) return !isset(PRINTEIGHTBIT);
524/// return (c == 0x7f || c == '\n' || c == '\t' || c < 0x20);
525/// ```
526/// "Nice" means "needs escape-formatting when printed" — so
527/// returns true for control chars + (under PRINTEIGHTBIT off)
528/// high-bit bytes. The previous Rust port treated all `!c.is_ascii()`
529/// as nice unconditionally — divergent for users running with
530/// `setopt printeightbit` (very common for non-ASCII filenames).
531pub fn is_nicechar(c: char) -> bool {
532 let cu = (c as u32) & 0xff;
533 // c:534 — `if (ZISPRINT(c)) return 0;` — printable ASCII is not nice.
534 if ZISPRINT(cu as u8) {
535 return false;
536 }
537 // c:536 — high-bit byte path.
538 if (cu & 0x80) != 0 {
539 return !isset(PRINTEIGHTBIT);
540 }
541 // c:538 — ASCII control chars (DEL/\n/\t/<0x20).
542 cu == 0x7f || cu == b'\n' as u32 || cu == b'\t' as u32 || cu < 0x20
543}
544
545/// Initialize multibyte state (from utils.c mb_charinit) - no-op in Rust
546/// Port of `mb_charinit` from `Src/utils.c:553`.
547pub fn mb_charinit() {
548 // Rust handles UTF-8 natively
549}
550
551/// Port of `wcs_nicechar_sel(wchar_t c, size_t *widthp, char **swidep,
552/// int quotable)` from `Src/utils.c:593-705`. Four branches per C:
553/// 1. c < 0x80 and not printable: control-char escape (\n, \t, ^X, \C-X)
554/// — same as `nicechar_sel`.
555/// 2. c >= 0x80 printable AND PRINTEIGHTBIT set: emit raw UTF-8 bytes.
556/// 3. c >= 0x80 fits in UTF-8: emit UTF-8 bytes (default-on for MULTIBYTE).
557/// 4. c >= 0x10000: `\U%.8x` 8-digit hex; c >= 0x100: `\u%.4x` 4-digit hex.
558///
559/// Param mapping (Rule S1, faithful to C):
560/// - `widthp: Option<&mut usize>` ← C `size_t *widthp` (`None` ≡ `NULL`).
561/// Set to the display column width of the produced sequence.
562/// - `swidep: Option<&mut usize>` ← C `char **swidep` (`None` ≡ `NULL`).
563/// Set to the byte index in the returned string where the post-wide
564/// meta-prefixed bytes begin (i.e. boundary between display-width
565/// characters and trailing Meta+X encoding bytes that don't add to
566/// column position).
567pub fn wcs_nicechar_sel(
568 c: char,
569 widthp: Option<&mut usize>,
570 swidep: Option<&mut usize>,
571 quotable: bool,
572) -> String {
573 // c:593
574 let cv = c as u32;
575 // c:616 — `if (!WC_ISPRINT(c) && (c < 0x80 || !isset(PRINTEIGHTBIT)))`.
576 // The non-printable + (low-ASCII or PRINTEIGHTBIT-off) branch.
577 let print_eightbit = isset(PRINTEIGHTBIT);
578 let is_printable = u9_iswprint(c);
579 let buf: String;
580 if !is_printable && (cv < 0x80 || !print_eightbit) {
581 if cv == 0x7f {
582 // c:617-624 — DEL: `^?` / `\C-?`
583 buf = if quotable {
584 "\\C-?".to_string()
585 } else {
586 "^?".to_string()
587 };
588 // c:686 widthp = (s - buf) + wcw (wcw for '?' = 1).
589 if let Some(wp) = widthp {
590 *wp = buf.chars().count();
591 }
592 if let Some(sp) = swidep {
593 *sp = buf.len();
594 }
595 return buf;
596 } else if c == '\n' {
597 // c:625-627 — `\n` literal.
598 buf = "\\n".to_string();
599 if let Some(wp) = widthp {
600 *wp = 2;
601 }
602 if let Some(sp) = swidep {
603 *sp = buf.len();
604 }
605 return buf;
606 } else if c == '\t' {
607 // c:628-630 — `\t` literal.
608 buf = "\\t".to_string();
609 if let Some(wp) = widthp {
610 *wp = 2;
611 }
612 if let Some(sp) = swidep {
613 *sp = buf.len();
614 }
615 return buf;
616 } else if cv < 0x20 {
617 // c:631-638 — ^X / \C-X for controls (excluding \n, \t).
618 let cc = (cv + 0x40) as u8 as char;
619 buf = if quotable {
620 format!("\\C-{}", cc)
621 } else {
622 format!("^{}", cc)
623 };
624 if let Some(wp) = widthp {
625 *wp = buf.chars().count();
626 }
627 if let Some(sp) = swidep {
628 *sp = buf.len();
629 }
630 return buf;
631 }
632 // c:639-641 — c >= 0x80 non-printable falls through to ret=-1
633 // path (hex escape).
634 } else if cv < 0x80 {
635 // c:644-704 — printable ASCII: emit raw char. wcw=1.
636 buf = c.to_string();
637 if let Some(wp) = widthp {
638 *wp = 1;
639 }
640 if let Some(sp) = swidep {
641 *sp = buf.len();
642 }
643 return buf;
644 }
645 // c:644-678 — high-bit char: try UTF-8 encode first.
646 if u9_iswprint(c) {
647 // c:681-693 — printable wide char: emit raw UTF-8.
648 buf = c.to_string();
649 let wcw = zwcwidth(c) as usize;
650 if let Some(wp) = widthp {
651 *wp = wcw;
652 }
653 if let Some(sp) = swidep {
654 *sp = buf.len();
655 }
656 return buf;
657 }
658 // c:656-678 — non-printable wide: hex escape (or fall back to byte
659 // nicechar for c < 0x100).
660 if cv >= 0x10000 {
661 // c:656-659 — `\U%.8x` (10 chars).
662 buf = format!("\\U{:08x}", cv);
663 if let Some(wp) = widthp {
664 *wp = 10;
665 }
666 if let Some(sp) = swidep {
667 *sp = buf.len();
668 }
669 buf
670 } else if cv >= 0x100 {
671 // c:660-663 — `\u%.4x` (6 chars).
672 buf = format!("\\u{:04x}", cv);
673 if let Some(wp) = widthp {
674 *wp = 6;
675 }
676 if let Some(sp) = swidep {
677 *sp = buf.len();
678 }
679 buf
680 } else {
681 // c:664-674 — fall back to byte nicechar_sel.
682 buf = nicechar_sel(c, quotable);
683 // c:670-671 — `*widthp = ztrlen(buf);` (display width respects
684 // metafied chars). `ztrlen` counts visible cells.
685 if let Some(wp) = widthp {
686 *wp = ztrlen(&buf);
687 }
688 // c:672-673 — `*swidep = buf + strlen(buf);` (no trailing meta
689 // since nicechar_sel ASCII output).
690 if let Some(sp) = swidep {
691 *sp = buf.len();
692 }
693 buf
694 }
695}
696
697/// Port of `wcs_nicechar(wchar_t c, size_t *widthp, char **swidep)` from `Src/utils.c:709`.
698/// C body: `return wcs_nicechar_sel(c, widthp, swidep, 0);`
699pub fn wcs_nicechar(c: char, widthp: Option<&mut usize>, swidep: Option<&mut usize>) -> String {
700 // c:709
701 wcs_nicechar_sel(c, widthp, swidep, false) // c:711
702}
703
704/// Port of `int is_wcs_nicechar(wchar_t c)` from Src/utils.c:720.
705///
706/// "Return 1 if wcs_nicechar() would reformat this character for
707/// display." Mirrors the C condition: non-printable AND (low ASCII
708/// OR PRINTEIGHTBIT unset) for control chars; for high bytes, true
709/// when ≥0x100 or `is_nicechar` says so.
710pub fn is_wcs_nicechar(c: char) -> bool {
711 // c:720
712 let cv = c as u32;
713 let printable = !c.is_control() && cv >= 0x20;
714 let print_eight = isset(PRINTEIGHTBIT); // c:722
715 if !printable && (cv < 0x80 || !print_eight) {
716 if cv == 0x7f || c == '\n' || c == '\t' || cv < 0x20 {
717 // c:734
718 return true;
719 }
720 if cv >= 0x80 {
721 // c:734
722 return cv >= 0x100 || is_nicechar(c); // c:734
723 }
724 }
725 false // c:734
726}
727
728/// Get wide character width (from utils.c zwcwidth)
729/// Port of `int zwcwidth(wint_t wc)` from `Src/utils.c:734`.
730///
731/// C body (c:734-745):
732/// ```c
733/// int wcw;
734/// /* assume a single-byte character if not valid */
735/// if (wc == WEOF || unset(MULTIBYTE)) // c:738
736/// return 1;
737/// wcw = WCWIDTH(wc); // c:740
738/// /* if not printable, assume width 1 */
739/// if (wcw < 0) // c:742
740/// return 1;
741/// return wcw; // c:744
742/// ```
743///
744/// The previous Rust port skipped the `unset(MULTIBYTE)` early-
745/// return (c:738). When the MULTIBYTE option is OFF (set via
746/// `setopt nomultibyte` / `set +o multibyte`), C zsh treats every
747/// codepoint as a single-byte char and returns width 1 unconditionally.
748/// Without the option check, the Rust port would still report the
749/// Unicode-width-table answer (2 for CJK, 0 for combining marks)
750/// in single-byte mode — diverging from prompt/refresh layout that
751/// relies on the option as the source of truth.
752pub fn zwcwidth(wc: char) -> usize {
753 // c:734
754 // c:738 — `if (wc == WEOF || unset(MULTIBYTE)) return 1;`. WEOF
755 // path is Rust-impossible (char is always a valid scalar). The
756 // MULTIBYTE option gate maps to the canonical option register.
757 if !isset(MULTIBYTE) {
758 // c:738
759 return 1; // c:739
760 }
761 // c:740-744 — WCWIDTH(wc); negative result → width 1.
762 unicode_width::UnicodeWidthChar::width(wc).unwrap_or(1) // c:740-744
763}
764
765/// Port of `pathprog(char *prog, char **namep)` from `Src/utils.c:760-786`.
766/// ```c
767/// for (pp = path; *pp; pp++) {
768/// sprintf(buf, "%s/%s", *pp, prog);
769/// funmeta = unmeta(buf);
770/// if (access(funmeta, F_OK) == 0 && stat(funmeta, &st) >= 0 &&
771/// !S_ISDIR(st.st_mode)) {
772/// return funmeta;
773/// }
774/// }
775/// return NULL;
776/// ```
777/// C checks: (1) F_OK = exists, (2) stat succeeds, (3) NOT a directory.
778/// NO executable-bit check. Used by autoload / `which` paths that
779/// need to find any file in PATH, not just executables.
780///
781/// Previously the Rust port added a `mode & 0o111 != 0` executable
782/// check — divergent, made every `pathprog` lookup miss non-executable
783/// files (e.g. autoload-function plaintext scripts that don't have
784/// +x set).
785/// WARNING: param names don't match C — Rust=(prog) vs C=(prog, namep)
786pub fn pathprog(prog: &str) -> Option<PathBuf> {
787 // c:760
788 // The early-return on `prog` containing '/' is a Rust-port
789 // convenience NOT in C's pathprog. C unconditionally walks $PATH
790 // and prefixes each entry. The convenience is harmless since
791 // C's caller (`findcmd`) handles slashes separately before
792 // calling pathprog.
793 if prog.contains('/') {
794 let p = PathBuf::from(prog);
795 return if p.exists() { Some(p) } else { None };
796 }
797 if let Some(path_var) = getsparam("PATH") {
798 for dir in path_var.split(':') {
799 // c:773
800 let full_path = PathBuf::from(dir).join(prog);
801 // c:776 — `funmeta = unmeta(buf)`. The previous Rust port
802 // passed the raw composed path to `fs::metadata`, missing
803 // the unmeta step. Paths containing Meta-encoded bytes
804 // (from PATH entries or prog name with metafy lead bytes)
805 // would silently miss valid executables.
806 let unmeta_path = unmeta(full_path.to_str().unwrap_or("")); // c:776 unmeta(buf)
807 // c:777-779 — `access(F_OK) == 0 && stat >= 0 && !S_ISDIR`.
808 // is_file() folds existence + stat + not-dir into one.
809 if let Ok(meta) = fs::metadata(&unmeta_path) {
810 if meta.is_file() {
811 // Return the unmeta'd path since that's what
812 // C does (funmeta is the returned value).
813 return Some(PathBuf::from(unmeta_path));
814 }
815 }
816 }
817 }
818 None
819}
820
821/// Port of `findpwd(char *s)` from `Src/utils.c:792`.
822///
823/// ```c
824/// char *findpwd(char *s)
825/// {
826/// char *t;
827/// if (*s == '/')
828/// return xsymlink(s, 0);
829/// s = tricat((pwd[1]) ? pwd : "", "/", s);
830/// t = xsymlink(s, 0);
831/// zsfree(s);
832/// return t;
833/// }
834/// ```
835///
836/// Resolve `s` to its canonical form. Absolute paths route through
837/// `xsymlink` directly; relative paths get prefixed with the
838/// current `pwd` first.
839///
840/// Signature note: C takes `s: char *`. The previous Rust port had
841/// no parameter (returning the cwd) — completely wrong. New port
842/// matches C: takes `&str`, returns `Option<String>` (xsymlink can
843/// return NULL).
844// get a symlink-free pathname for s relative to PWD // c:792
845/// `findpwd` — see implementation.
846pub fn findpwd(s: &str) -> Option<String> {
847 // c:792
848 if s.starts_with('/') {
849 // c:792
850 return xsymlink(s); // c:797
851 }
852 // C: tricat((pwd[1]) ? pwd : "", "/", s) — uses the global
853 // `pwd` (logical cwd; differs from realpath when chasing
854 // symlinks is disabled). The Rust port reads `$PWD` since
855 // shell-set `PWD` mirrors C's `pwd` global; falls back to
856 // `getcwd()` when unset.
857 let pwd = getsparam("PWD")
858 .or_else(|| {
859 std::env::current_dir()
860 .ok()
861 .map(|p| p.to_string_lossy().into_owned())
862 })
863 .unwrap_or_default(); // c:798
864 let prefix: &str = if pwd.len() > 1 { &pwd } else { "" }; // c:798 pwd[1]
865 let combined = format!("{}/{}", prefix, s); // c:798
866 xsymlink(&combined) // c:799
867}
868
869/// Validate an inherited `$PWD` exactly like zsh's ispwd() at
870/// src/zsh/Src/utils.c:809: PWD must be absolute, must stat to the
871/// same dev+inode as ".", and must contain no `.` or `..` components.
872/// When this returns false, callers should fall back to `getcwd()`.
873pub(crate) fn ispwd(pwd: &str) -> bool {
874 if !pwd.starts_with('/') {
875 return false;
876 }
877 let pwd_meta = match fs::metadata(pwd) {
878 Ok(m) => m,
879 Err(_) => return false,
880 };
881 let dot_meta = match fs::metadata(".") {
882 Ok(m) => m,
883 Err(_) => return false,
884 };
885 if pwd_meta.dev() != dot_meta.dev() || pwd_meta.ino() != dot_meta.ino() {
886 return false;
887 }
888 // Reject any component that is exactly `.` or `..` — the same loop
889 // zsh runs after the dev/ino check.
890 for comp in pwd.split('/') {
891 if comp == "." || comp == ".." {
892 return false;
893 }
894 }
895 true
896}
897
898// ---------------------------------------------------------------------------
899// Missing utility functions ported from utils.c
900// ---------------------------------------------------------------------------
901
902/// Split path into components (from utils.c slashsplit).
903///
904/// Port of `static char **slashsplit(char *s)` from `Src/utils.c:837`.
905///
906/// C body (c:837-863):
907/// ```c
908/// if (!*s) return zshcalloc(...); // c:842 — empty input → empty
909/// for (t = s, t0 = 0; *t; t++) // c:845 — count slashes
910/// if (*t == '/') t0++;
911/// q = r = zalloc(sizeof(char*) * (t0 + 2));
912/// while ((t = strchr(s, '/'))) { // c:850
913/// *q++ = ztrduppfx(s, t - s); // c:851 — emit prefix
914/// while (*t == '/') t++; // c:852-853 — collapse runs
915/// if (!*t) { *q = NULL; return r; } // c:854-857 — trailing `/` ends
916/// s = t;
917/// }
918/// *q++ = ztrdup(s); // c:860 — final tail
919/// ```
920///
921/// Three behaviors that the previous `split('/').filter(non_empty)`
922/// Rust port got WRONG:
923/// 1. **Leading `/` keeps an empty segment** (c:851 with `t == s`).
924/// C: `slashsplit("/usr")` → `["", "usr"]`; previous Rust dropped
925/// the empty, returning `["usr"]`. Caller `xsymlinks` (c:879)
926/// iterates the result building `xbuf` byte-by-byte — the
927/// empty leading segment is how it knows to start from `/`.
928/// Without it, absolute-path resolution silently became relative.
929/// 2. **Consecutive slashes collapse** (c:852-853 inner while-loop).
930/// C: `"a//b"` → `["a", "b"]` (drop empty between). Filter-on-
931/// empty Rust gets this right by coincidence.
932/// 3. **Trailing `/` drops** (c:854-857). C: `"a/b/"` → `["a", "b"]`.
933/// Filter-on-empty Rust gets this right by coincidence.
934///
935/// Pin: the leading-empty-segment behavior IS the C contract, not
936/// an oversight to filter out. Matches `xsymlinks` and any future
937/// path-walker port that reads slashsplit output.
938pub fn slashsplit(s: &str) -> Vec<String> {
939 // c:837
940 if s.is_empty() {
941 // c:842
942 return Vec::new();
943 }
944 let mut result = Vec::new();
945 let mut rest = s;
946 // c:850 — `while ((t = strchr(s, '/')))`.
947 while let Some(pos) = rest.find('/') {
948 result.push(rest[..pos].to_string()); // c:851 ztrduppfx
949 rest = &rest[pos..];
950 // c:852-853 — `while (*t == '/') t++;` collapse run.
951 while let Some(rest_after) = rest.strip_prefix('/') {
952 rest = rest_after;
953 }
954 // c:854-857 — if walked off end, return without emitting tail.
955 if rest.is_empty() {
956 return result;
957 }
958 }
959 // c:860 — `*q++ = ztrdup(s);` emit final tail.
960 result.push(rest.to_string());
961 result
962}
963
964/// Port of `static int xsymlinks(char *s)` from `Src/utils.c:872`.
965///
966/// Expands `.` and `..` components AND follows ONE LEVEL of
967/// symlinks (per C source comment at c:865-867: "expands .. or .
968/// expressions and one level of symlinks"). Used by the `:A`/`:P`
969/// modifier paths via `subst.rs:6896`.
970///
971/// The previous Rust port did `.`/`..` normalization ONLY, with a
972/// stale doc-comment claiming "Does NOT follow symlinks (matches
973/// the `physical = 0` mode in C)." That claim is FALSE: C
974/// `xsymlinks` IS the symlink-following form (it calls `readlink(2)`
975/// at c:908). The `physical = 0` no-symlink path is a different
976/// fn (`xsymlink` at utils.c:971, without the trailing `s`). Result:
977/// `:A` modifier output never resolved actual symlinks — `:A` on
978/// `/tmp/link -> /usr` returned `/tmp/link` instead of `/usr`.
979///
980/// Rewrite: walk components; for each non-`.`/`..` component, try
981/// `readlink` on the accumulated path. If it succeeds, the target
982/// replaces (or prepends to) the accumulator; otherwise the
983/// component appends as-is. C handles re-rooting (absolute symlink
984/// target replaces buf) and component-level concat.
985pub fn xsymlinks(s: &str) -> io::Result<String> {
986 // c:872
987 if s.is_empty() {
988 return Ok(String::new());
989 }
990
991 let path = if !s.starts_with('/') {
992 // c:879 — slashsplit
993 let cwd = std::env::current_dir()?;
994 format!("{}/{}", cwd.display(), s)
995 } else {
996 s.to_string()
997 };
998
999 let components: Vec<&str> = path.split('/').collect();
1000 // c:877 — `xbuflen = strlen(xbuf)`. Start with empty xbuf for
1001 // absolute paths (the leading "" from slashsplit handles that).
1002 let mut xbuf = String::new();
1003 for comp in components {
1004 match comp {
1005 "" | "." => continue, // c:881
1006 ".." => {
1007 // c:883
1008 if xbuf == "/" || xbuf.is_empty() {
1009 // c:886-889
1010 continue;
1011 }
1012 // c:891-895 — walk back one `/`-delimited component.
1013 if let Some(pos) = xbuf.rfind('/') {
1014 xbuf.truncate(pos);
1015 }
1016 }
1017 c => {
1018 // c:905-907 — `memcpy xbuf2, xbuf` then append `/comp`.
1019 let candidate = format!("{}/{}", xbuf, c); // c:907
1020 // c:908 — `readlink(unmeta(xbuf2), xbuf3, PATH_MAX)`.
1021 #[cfg(unix)]
1022 {
1023 match fs::read_link(&candidate) {
1024 // c:908
1025 Ok(target) => {
1026 // c:918-933 — successful readlink: target is
1027 // either absolute (replaces xbuf wholesale) or
1028 // relative (appends with `/`).
1029 let t = target.to_string_lossy().into_owned();
1030 if t.starts_with('/') {
1031 // c:927
1032 xbuf = t; // c:928
1033 } else {
1034 xbuf = format!("{}/{}", xbuf, t); // c:930-931
1035 }
1036 continue;
1037 }
1038 Err(_) => {
1039 // c:909-916 — readlink failed (not a symlink),
1040 // append the component verbatim.
1041 xbuf = candidate; // c:910-912
1042 }
1043 }
1044 }
1045 #[cfg(not(unix))]
1046 {
1047 xbuf = candidate;
1048 }
1049 }
1050 }
1051 }
1052
1053 if xbuf.is_empty() {
1054 Ok(if path.starts_with('/') {
1055 "/".to_string()
1056 } else {
1057 ".".to_string()
1058 })
1059 } else {
1060 Ok(xbuf)
1061 }
1062}
1063
1064/// Port of `xsymlink(char *s, int heap)` from `Src/utils.c:971`.
1065///
1066/// ```c
1067/// mod_export char *
1068/// xsymlink(char *s, int heap)
1069/// {
1070/// if (*s != '/')
1071/// return NULL;
1072/// *xbuf = '\0';
1073/// if (!chrealpath(&s, 'P', heap)) {
1074/// zwarn("path expansion failed, using root directory");
1075/// return heap ? dupstring("/") : ztrdup("/");
1076/// }
1077/// return s;
1078/// }
1079/// ```
1080///
1081/// Returns `Some(resolved)` on success, `None` if the path isn't
1082/// absolute (C: returns NULL). On resolve failure emits the same
1083/// "path expansion failed, using root directory" warning and
1084/// returns `Some("/")`.
1085///
1086/// C body (c:971-980):
1087/// ```c
1088/// char *xsymlink(char *s, int heap)
1089/// {
1090/// if (*s != '/') return NULL;
1091/// *xbuf = '\0';
1092/// if (!chrealpath(&s, 'P', heap)) {
1093/// zwarn("path expansion failed, using root directory");
1094/// return heap ? dupstring("/") : ztrdup("/");
1095/// }
1096/// return s;
1097/// }
1098/// ```
1099///
1100/// Previous Rust port was a FAKE — called `std::fs::canonicalize`
1101/// directly with the rationale "same semantics for symlink-
1102/// resolution". That bypassed the canonical `chrealpath` port
1103/// (hist.rs:2311) which handles the partial-prefix walk + xbuf
1104/// state that `fs::canonicalize` doesn't replicate. Re-port now
1105/// matches C line-by-line.
1106///
1107/// Rust signature drift: `heap` param dropped — Rust strings always
1108/// live on the heap; the C `heap` flag toggles between zhalloc
1109/// (heap arena) and ztrdup (process-wide) allocation which has no
1110/// distinction in Rust. Always defaults to `false` (ztrdup arm) at
1111/// the chrealpath call.
1112pub fn xsymlink(path: &str) -> Option<String> {
1113 // c:971
1114 // c:973 — `if (*s != '/') return NULL;`
1115 if !path.starts_with('/') {
1116 // c:973
1117 return None;
1118 }
1119 // c:974 — `*xbuf = '\0';` Reset the xbuf cursor; no-op in Rust
1120 // (xbuf is an internal chrealpath buffer not exposed at this level).
1121 // c:975 — `if (!chrealpath(&s, 'P', heap))`
1122 match chrealpath(path, b'P', false) {
1123 // c:975
1124 Some(r) => Some(r), // c:979 return s
1125 None => {
1126 // c:976 failure arm
1127 // c:977 — `zwarn("path expansion failed, using root directory");`
1128 zwarn("path expansion failed, using root directory"); // c:977
1129 // c:978 — `return heap ? dupstring("/") : ztrdup("/");`
1130 Some("/".to_string()) // c:978
1131 }
1132 }
1133}
1134
1135/// Port of `void print_if_link(char *s, int all)` from Src/utils.c:985.
1136///
1137/// "Print arrow + symlink target(s) iff `s` is an absolute path
1138/// pointing through symlinks." When `all` is set, follows the
1139/// chain (`xsymlinks` loop, c:992); otherwise emits just the final
1140/// realpath if it differs from the input. Always relative to
1141/// stdout. The Rust port writes via `print!` to mirror C's
1142/// `printf`/`zputs(stdout)` calls.
1143pub fn print_if_link(s: &str, all: bool) {
1144 // c:985
1145 if !s.starts_with('/') {
1146 // c:987
1147 return;
1148 }
1149 if all {
1150 // c:988
1151 let mut start = s.to_string();
1152 loop {
1153 // c:992
1154 match xsymlinks(&start) {
1155 Ok(target) if !target.is_empty() && target != start => {
1156 print!(" -> "); // c:994
1157 print!("{}", if target.is_empty() { "/" } else { &target }); // c:995
1158 start = target; // c:998-999
1159 }
1160 _ => break, // c:1002
1161 }
1162 }
1163 } else {
1164 // c:1006
1165 // c:1007-1011 — HAVE_MEMCCPY arm: copy s into a PATH_MAX-1 buffer
1166 // and DPUTS1 if the input overflows. Rust's String has no fixed
1167 // PATH_MAX limit but the C parity-check still fires when a
1168 // pathological caller passes a path that wouldn't fit C's
1169 // s_at_entry[PATH_MAX+1] buffer.
1170 DPUTS1!(
1171 // c:1009
1172 s.len() >= libc::PATH_MAX as usize, // c:1008 memccpy returns NULL
1173 "path longer than PATH_MAX: {}",
1174 s // c:1009
1175 );
1176 let s_at_entry = s.to_string(); // c:1013
1177 // c:1015 — `if (chrealpath(&s, 'P', 0) && strcmp(s, s_at_entry))`
1178 // The previous Rust port called std::fs::canonicalize directly —
1179 // a fake that bypassed the canonical chrealpath port (hist.rs:2311).
1180 let mut resolved = s.to_string(); // c:1015 &s in/out
1181 if let Some(r) = chrealpath(&resolved, b'P', false) {
1182 // c:1015
1183 resolved = r;
1184 if resolved != s_at_entry {
1185 // c:1015 strcmp(s, s_at_entry)
1186 print!(" -> "); // c:1016
1187 print!("{}", if resolved.is_empty() { "/" } else { &resolved });
1188 // c:1017
1189 }
1190 }
1191 }
1192 let _ = io::stdout().flush();
1193}
1194
1195/// Port of `void fprintdir(char *s, FILE *f)` from Src/utils.c:1031.
1196///
1197/// "print a directory" — abbreviates `s` via `finddir` (so a path
1198/// matching `$HOME` or a `nameddirtab` entry shows as `~name/...`)
1199/// and returns the rendering. The C source writes to a FILE*; Rust
1200/// returns the string for the caller to print.
1201pub fn fprintdir(s: &str) -> String {
1202 // c:1031
1203 match finddir(s) {
1204 // c:1031
1205 None => unmeta(s), // c:1036
1206 Some(rendered) => rendered, // c:1038-1040
1207 }
1208}
1209
1210/// Port of `char *substnamedir(char *s)` from Src/utils.c:1053.
1211///
1212/// C body (c:1053-1061):
1213/// ```c
1214/// Nameddir d = finddir(s);
1215/// if (!d)
1216/// return quotestring(s, QT_BACKSLASH);
1217/// return zhtricat("~", d->node.nam, quotestring(s + strlen(d->dir),
1218/// QT_BACKSLASH));
1219/// ```
1220///
1221/// The previous Rust port was a FAKE — it called `finddir(s)` (Rust
1222/// signature returns `Option<String>` of the already-formatted
1223/// `~name/rest`) and returned that string unchanged in the Some-arm,
1224/// missing the `quotestring(..., QT_BACKSLASH)` C applies to the
1225/// residue. C's `finddir` returns a `Nameddir` pointer; the
1226/// `~name` + quoted-residue join lives here in `substnamedir`.
1227///
1228/// This re-port duplicates the HOME-first + nameddirtab-scan logic
1229/// (finddir_scan at c:1106) so it can take the (name, dir_prefix)
1230/// split BEFORE pre-joining, then apply quotestring to the residue
1231/// per c:1059.
1232pub fn substnamedir(s: &str) -> String {
1233 // c:1053
1234 // C `finddir` at c:1127 runs the homenode through the SAME
1235 // best-diff scan as the nameddirtab (c:1164-1165) — home COMPETES
1236 // with `hash -d` entries by `diff`, it does not preempt them.
1237 // Duplicate that competition here without the pre-format, so we
1238 // can apply quotestring on just the residue (c:1059).
1239 let home = getsparam("HOME").unwrap_or_default(); // c:1133
1240 let mut finddir_best: i32 = 0; // c:1163
1241 let mut best: Option<(String, String)> = None; // finddir_last
1242 let home_diff = if home.len() == 1 { 0 } else { home.len() as i32 }; // c:1140-1141
1243 if home_diff > 0 {
1244 if let Some(rest) = s.strip_prefix(&home) {
1245 if rest.is_empty() || rest.starts_with('/') {
1246 finddir_best = home_diff;
1247 // HOME is the implicit homenode (no name).
1248 best = Some((String::new(), rest.to_string()));
1249 }
1250 }
1251 }
1252 // c:1106 finddir_scan — best-diff walk over nameddirtab.
1253 if let Some((name, rest, diff)) = finddir_scan(s) {
1254 if diff > finddir_best {
1255 best = Some((name, rest));
1256 }
1257 }
1258 match best {
1259 // c:1059 — `zhtricat("~", d->node.nam, quotestring(s + strlen(d->dir), QT_BACKSLASH))`
1260 Some((name, rest)) => format!("~{}{}", name, quotestring(&rest, QT_BACKSLASH)),
1261 None => quotestring(s, QT_BACKSLASH), // c:1058
1262 }
1263}
1264
1265// ===========================================================
1266// Direct ports of utility entries from Src/utils.c.
1267// ===========================================================
1268
1269/// Cached current-user lookup.
1270/// Port of `get_username()` from Src/utils.c:1075 — `getpwuid(3)`
1271/// against the current real uid, with the result cached so a
1272/// re-call after setuid sees the new identity. The C source uses
1273/// a `cached_uid`+`cached_username` pair guarded by a uid match;
1274/// the Rust port uses an `OnceLock` keyed on uid for the same
1275/// invalidate-on-uid-change behaviour.
1276pub fn get_username() -> String {
1277 static CACHE: Mutex<Option<(u32, String)>> = Mutex::new(None);
1278
1279 let current_uid = unsafe { libc::getuid() };
1280 let mut guard = CACHE.lock().unwrap();
1281 if let Some((uid, name)) = &*guard {
1282 if *uid == current_uid {
1283 return name.clone();
1284 }
1285 }
1286 let name = unsafe {
1287 let pw = libc::getpwuid(current_uid);
1288 if pw.is_null() {
1289 String::new() // c:1088 — `ztrdup("")` fallback
1290 } else {
1291 // c:1086 — `cached_username = ztrdup_metafy(pswd->pw_name);`.
1292 // `ztrdup_metafy` calls `metafy((char *)s, -1, META_DUP)`
1293 // (utils.c:4929). The previous Rust port returned the raw
1294 // pw_name verbatim — fine for ASCII usernames, but high-bit
1295 // bytes (e.g. an `émile` username on some systems) would
1296 // surface to callers un-metafied. zsh's downstream pipeline
1297 // (param expansion, prompt rendering) assumes paramtab
1298 // entries are metafied; an un-metafied high-bit byte breaks
1299 // the Meta-escape contract.
1300 let raw = std::ffi::CStr::from_ptr((*pw).pw_name)
1301 .to_string_lossy()
1302 .into_owned();
1303 metafy(&raw) // c:1086 ztrdup_metafy
1304 }
1305 };
1306 *guard = Some((current_uid, name.clone()));
1307 name
1308}
1309
1310/// Port of `finddir_scan(HashNode hn, UNUSED(int flags))` from Src/utils.c:1103-1112 — ScanFunc the
1311/// C source registers with `scanhashtable(nameddirtab, …)`. Keeps the
1312/// entry with the LARGEST `diff` (zsh.h:2152 — `strlen(dir) -
1313/// strlen(nam)`, the abbreviation savings — NOT the raw dir length)
1314/// whose dir is a boundary-prefix of the path (`!dircmp`, c:1075-1086)
1315/// and which is not ND_NOABBREV (PWD/OLDPWD entries, c:1108).
1316/// WARNING: param names don't match C — Rust=(path) vs C=(hn, flags);
1317/// the C statics finddir_full/finddir_last/finddir_best are threaded
1318/// as the argument and the `(name, rest, diff)` return instead.
1319pub fn finddir_scan(path: &str) -> Option<(String, String, i32)> {
1320 // c:1106
1321 let table = nameddirtab().lock().ok()?;
1322 let mut best: Option<(String, String, i32)> = None;
1323 for (name, nd) in table.iter() {
1324 // c:1107-1108 — `nd->diff > finddir_best && !dircmp(nd->dir,
1325 // finddir_full) && !(nd->node.flags & ND_NOABBREV)`.
1326 if (nd.node.flags & crate::ported::zsh_h::ND_NOABBREV) != 0 {
1327 continue;
1328 }
1329 if best.as_ref().is_some_and(|b| nd.diff <= b.2) {
1330 continue;
1331 }
1332 if let Some(rest) = path.strip_prefix(nd.dir.as_str()) {
1333 if rest.is_empty() || rest.starts_with('/') {
1334 best = Some((name.clone(), rest.to_string(), nd.diff));
1335 }
1336 }
1337 }
1338 best
1339}
1340
1341/// Port of `Nameddir finddir(char *s)` from Src/utils.c:1127.
1342///
1343/// "See if a path has a named directory as its prefix." Compares
1344/// `s` against `$HOME` first (longest implicit named dir), then
1345/// scans the global `nameddirtab`, then falls back to
1346/// `subst_string_by_hook("zsh_directory_name", "d", s)`.
1347///
1348/// Rust signature returns `Option<String>` (the abbreviated path
1349/// `~name/rest`) instead of the C `Nameddir` pointer.
1350pub fn finddir(path: &str) -> Option<String> {
1351 // c:1127
1352 // c:1138 — `homenode.dir = home ? home : "";`. Reads the C global
1353 // `char *home` (params.c:91) DIRECTLY — not via paramtab. zshrs
1354 // ports the global as `params::home_lock()` accessed by
1355 // `homegetfn` (which ignores its ¶m arg per
1356 // `c:5109 UNUSED(Param pm)`). Pass a default param so the read
1357 // works even before paramtab["HOME"] has been hydrated (during
1358 // early init / unit-test environments).
1359 let _default_pm = crate::ported::zsh_h::param::default();
1360 let home = crate::ported::params::homegetfn(&_default_pm); // c:1138 home
1361 // c:1139-1141 + 1164-1165 — the home node (nam="", diff =
1362 // strlen(home), root's 1 → 0) runs through the SAME best-diff scan
1363 // as the table (`finddir_scan(&homenode.node, 0);
1364 // scanhashtable(nameddirtab, …)`): it COMPETES with `hash -d`
1365 // entries instead of preempting them, so a named dir with a larger
1366 // diff wins (e.g. ~ZPWR over ~/.zpwr). The previous early return
1367 // on the $HOME prefix hid every named dir under $HOME.
1368 let mut finddir_best: i32 = 0; // c:1163
1369 let mut best: Option<(String, String)> = None; // finddir_last
1370 let home_diff = if home.len() == 1 { 0 } else { home.len() as i32 }; // c:1140-1141
1371 if home_diff > 0 {
1372 if let Some(rest) = path.strip_prefix(&home) {
1373 if rest.is_empty() || rest.starts_with('/') {
1374 finddir_best = home_diff;
1375 best = Some((String::new(), rest.to_string()));
1376 }
1377 }
1378 }
1379 if let Some((name, rest, diff)) = finddir_scan(path) {
1380 // c:1165 — scanhashtable: only a strictly larger diff replaces
1381 // the home candidate (finddir_scan's `nd->diff > finddir_best`).
1382 if diff > finddir_best {
1383 finddir_best = diff;
1384 best = Some((name, rest));
1385 }
1386 }
1387 // c:1167-1175 — zsh_directory_name hook — returns ["name", "len"];
1388 // wins only when its match length beats finddir_best (c:1169).
1389 if let Some(reply) = subst_string_by_hook("zsh_directory_name", Some("d"), path) {
1390 if reply.len() >= 2 {
1391 // c:1170
1392 if let Ok(len) = reply[1].parse::<i32>() {
1393 if len > finddir_best && (len as usize) <= path.len() {
1394 return Some(format!("~[{}]{}", reply[0], &path[len as usize..]));
1395 }
1396 }
1397 }
1398 }
1399 best.map(|(name, rest)| format!("~{}{}", name, rest)) // c:1177
1400}
1401
1402/// Port of `void adduserdir(char *s, char *t, int flags, int always)`
1403/// from Src/utils.c:1187.
1404///
1405/// Adds (or removes when `t` is empty / non-absolute) an entry in
1406/// the global `nameddirtab`. ND_USERNAME entries from `getpwnam`
1407/// don't override explicit assignments. AUTONAMEDIRS gating, the
1408/// trailing-slash trim, and PWD/OLDPWD ND_NOABBREV stamp are all
1409/// preserved. Routes through `crate::ported::hashnameddir`.
1410pub fn adduserdir(name: &str, dir: &str, flags: i32, always: bool) {
1411 // c:1187
1412
1413 if !interact() {
1414 return;
1415 } // c:1193
1416 if let Ok(t) = nameddirtab().lock() {
1417 if (flags & ND_USERNAME) != 0 && t.contains_key(name) {
1418 // c:1199
1419 return;
1420 }
1421 if !always && !isset(AUTONAMEDIRS) && !t.contains_key(name) {
1422 // c:1207
1423 return;
1424 }
1425 }
1426 // c:1211 — `if (!t || *t != '/' || strlen(t) >= PATH_MAX)`. C
1427 // rejects paths >= PATH_MAX as too long (would overflow path-
1428 // expansion buffers downstream). PATH_MAX is platform-dependent
1429 // (4096 on Linux, 1024 on macOS); libc::PATH_MAX exposes it.
1430 if dir.is_empty() || !dir.starts_with('/') || dir.len() >= libc::PATH_MAX as usize
1431 // c:1211 strlen(t) >= PATH_MAX
1432 {
1433 let _ = removenameddirnode(name); // c:1214
1434 return;
1435 }
1436 let mut trimmed = dir.trim_end_matches('/').to_string(); // c:1224-1226
1437 if trimmed.is_empty() {
1438 trimmed = dir.to_string(); // c:1227-1233
1439 }
1440 let final_flags = if name == "PWD" || name == "OLDPWD" {
1441 // c:1237
1442 flags | ND_NOABBREV
1443 } else {
1444 flags
1445 };
1446 // c:1239 — `nd = (Nameddir) zshcalloc(sizeof *nd); nd->node.flags = …;
1447 // nd->dir = ztrdup(t); addnode(nameddirtab, ztrdup(s), nd);`
1448 let nd = nameddir {
1449 node: hashnode {
1450 next: None,
1451 nam: name.to_string(),
1452 flags: final_flags,
1453 },
1454 dir: trimmed,
1455 diff: 0,
1456 };
1457 crate::ported::hashnameddir::addnameddirnode(name, nd);
1458}
1459
1460/// Port of `char *getnameddir(char *name)` from Src/utils.c:1247.
1461///
1462/// Looks up `name` in `nameddirtab`; if absent, checks for a
1463/// scalar parameter whose value starts with `/` and registers it
1464/// via `adduserdir`; finally falls back to `getpwnam(name)` for
1465/// `~user`-style lookups when USE_GETPWNAM is enabled.
1466pub fn getnameddir(name: &str) -> Option<String> {
1467 // c:1247
1468 if let Ok(t) = nameddirtab().lock() {
1469 if let Some(nd) = t.get(name) {
1470 // c:1254
1471 return Some(nd.dir.clone());
1472 }
1473 }
1474 // c:1260 — `if ((s = getsparam(name)) && *s == '/')`. paramtab read.
1475 if let Some(s) = getsparam(name) {
1476 if s.starts_with('/') {
1477 adduserdir(name, &s, 0, true); // c:1264
1478 return Some(s);
1479 }
1480 }
1481 #[cfg(unix)]
1482 {
1483 // c:1268 — getpwnam fallback.
1484 let cn = CString::new(name).ok()?;
1485 let pw = unsafe { libc::getpwnam(cn.as_ptr()) };
1486 if !pw.is_null() {
1487 let raw_dir = unsafe {
1488 std::ffi::CStr::from_ptr((*pw).pw_dir)
1489 .to_string_lossy()
1490 .into_owned()
1491 };
1492 // c:1273-1274 — `isset(CHASELINKS) ? xsymlink(pw->pw_dir, 0)
1493 // : ztrdup(pw->pw_dir);`. Resolve symlinks when the option
1494 // is set. Previously omitted in the Rust port — silently
1495 // returned the raw passwd-db dir even when the user had
1496 // `setopt chaselinks`.
1497 let dir = if isset(CHASELINKS) {
1498 xsymlink(&raw_dir).unwrap_or(raw_dir)
1499 } else {
1500 raw_dir
1501 };
1502 // c:1276 — `adduserdir(name, dir, ND_USERNAME, 1);`
1503 // Cache the lookup so subsequent `~user` expansions hit the
1504 // nameddirtab fast-path at c:1254 instead of round-tripping
1505 // through getpwnam every time.
1506 adduserdir(name, &dir, ND_USERNAME, true); // c:1276
1507 return Some(dir);
1508 }
1509 }
1510 None
1511}
1512
1513/// Compare directory paths (from utils.c dircmp)
1514/// Port of `dircmp(char *s, char *t)` from `Src/utils.c:1296`.
1515pub fn dircmp(s: &str, t: &str) -> bool {
1516 let s = s.trim_end_matches('/');
1517 let t = t.trim_end_matches('/');
1518 s == t
1519}
1520
1521// Add a function to the list of pre-prompt functions. // c:1332
1522/// Register a callback to run before each prompt.
1523/// Port of `addprepromptfn(voidvoidfnptr_t func)` from Src/utils.c:1319.
1524pub fn addprepromptfn(func: fn()) {
1525 // c:1319
1526 PREPROMPT_FNS.lock().unwrap().push(func);
1527}
1528
1529// Remove a function from the list of pre-prompt functions. // c:1332
1530/// Remove a previously-registered pre-prompt callback.
1531/// Port of `delprepromptfn(voidvoidfnptr_t func)` from Src/utils.c:1332.
1532pub fn delprepromptfn(func: fn()) {
1533 // c:1332
1534 let mut list = PREPROMPT_FNS.lock().unwrap();
1535 if let Some(pos) = list.iter().position(|f| *f as usize == func as usize) {
1536 list.remove(pos);
1537 }
1538}
1539
1540// Add a function to the list of timed functions. // c:1367
1541/// Port of `void addtimedfn(voidvoidfnptr_t func, time_t when)` from
1542/// `Src/utils.c:1371`. Faithful walk of the timedfns LinkList:
1543/// allocate a `Timedfn`, lazy-init the list when empty, otherwise scan
1544/// from `firstnode` and insert BEFORE the first node whose `when` is
1545/// greater than ours. The standard linklist API only inserts AFTER a
1546/// node, so the C loop carries `ln` as the previous node and inserts
1547/// before `next` once `when < next->when`. Note: zsh's `time_t` is
1548/// signed and historically negative-`when` was supported, so we keep
1549/// `i64`.
1550pub fn addtimedfn(func: fn(), when: i64) {
1551 // c:1371
1552 let mut list = TIMED_FNS.lock().unwrap(); // c:1365 timedfns
1553 let tfdat: (i64, fn()) = (when, func); // c:1373-1375 Timedfn tfdat
1554 if list.is_empty() {
1555 // c:1377 !timedfns
1556 list.push(tfdat); // c:1378-1379 znewlinklist + zaddlinknode
1557 return;
1558 }
1559 if list.is_empty() {
1560 // c:1394 !ln (firstnode of empty list)
1561 list.push(tfdat); // c:1395 zaddlinknode
1562 return; // c:1396
1563 }
1564 let mut idx: usize = 0; // c:1381 LinkNode ln = firstnode(timedfns)
1565 loop {
1566 // c:1398 for(;;)
1567 let next = idx + 1; // c:1400 LinkNode next = nextnode(ln)
1568 if next >= list.len() {
1569 // c:1401 !next
1570 list.push(tfdat); // c:1402 zaddlinknode
1571 return; // c:1403
1572 }
1573 let tfdat2_when = list[next].0; // c:1405 tfdat2 = getdata(next)
1574 if when < tfdat2_when {
1575 // c:1406 when < tfdat2->when
1576 list.insert(next, tfdat); // c:1407 zinsertlinknode(timedfns, ln, tfdat)
1577 return; // c:1408
1578 }
1579 idx = next; // c:1410 ln = next
1580 }
1581}
1582
1583/// Remove a registered timed function (first occurrence only).
1584/// Port of `deltimedfn(voidvoidfnptr_t func)` from Src/utils.c:1430.
1585pub fn deltimedfn(func: fn()) {
1586 // c:1430
1587 let mut list = TIMED_FNS.lock().unwrap();
1588 if let Some(pos) = list.iter().position(|(_, f)| *f as usize == func as usize) {
1589 list.remove(pos);
1590 }
1591}
1592
1593/// Invoke a hook function by name plus any `<name>_functions` array.
1594/// Port of `callhookfunc(char *name, LinkList lnklst, int arrayp, int *retval)` from Src/utils.c:1469. Returns 0 if at
1595/// least one hook ran, 1 otherwise — the C source uses the same
1596/// stat semantics so the prompt machinery can detect "did periodic
1597/// fire". Hook dispatch goes through the executor singleton (which
1598/// owns the function table); we look up `name` and then walk the
1599/// `<name>_functions` array exactly as the C source does at
1600/// Src/utils.c:1469.
1601/// Direct port of
1602/// `int callhookfunc(char *name, LinkList lnklst, int arrayp, int *retval)`
1603/// at `Src/utils.c:1469`. C signature mapping:
1604/// - `char *name` → `name: &str`
1605/// - `LinkList lnklst` → `lnklst: Option<&[String]>` (Rust-shape
1606/// stand-in for the C-shape `LinkList` — both are an ordered list
1607/// of meta-fied strings; the LinkList port itself diverges)
1608/// - `int arrayp` → `arrayp: i32`
1609/// - `int *retval` → `retval: *mut i32` (out-param; NULL is fine
1610/// when the caller doesn't need the doshfunc return value)
1611///
1612/// Returns `stat` — 0 if at least one shfunc fired, 1 otherwise
1613/// (matches `c:1495 stat = 0` after every dispatch). When `retval` is
1614/// non-null, `*retval` is set to the most recent `doshfunc`-returned
1615/// status, mirroring the C body's `*retval = ret` semantics.
1616pub fn callhookfunc(name: &str, lnklst: Option<&[String]>, arrayp: i32, retval: *mut i32) -> i32 {
1617 let mut stat: i32 = 1; // c:1475
1618 let mut ret: i32 = 0; // c:1475
1619
1620 // Build the doshfunc arg list `[fname ($0), $1, $2, …]`. A provided
1621 // `lnklst` already carries the ORIGINAL hook name at [0] and the real
1622 // positional args at [1..] (matching C, where the caller does
1623 // `addlinknode(args, name)` before the args). So substitute `fname` for
1624 // [0] — `fname` is the hook name on the direct path (c:1487, identity)
1625 // and a `${name}_functions` member on the array path (c:1504-1508,
1626 // `arg0 = [*arrptr] + argarr[1..]`) — and KEEP [1..].
1627 //
1628 // The prior code prepended `fname` to the WHOLE list (name included),
1629 // so every hook invoked with args saw `$1` == its own name (e.g.
1630 // `preexec`'s `$1` was "preexec" instead of the command line). c:1483.
1631 let mk_args = |fname: &str| -> Vec<String> {
1632 let mut v: Vec<String> = vec![fname.to_string()];
1633 if let Some(extra) = lnklst {
1634 if extra.len() > 1 {
1635 v.extend_from_slice(&extra[1..]);
1636 }
1637 }
1638 v
1639 };
1640
1641 // c:1495 — `if ((shfunc = getshfunc(name))) { doshfunc(...); stat = 0; }`
1642 let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
1643 .read()
1644 .ok()
1645 .and_then(|t| t.get(name).cloned());
1646 if let Some(mut shf) = shf_clone {
1647 let args = mk_args(name);
1648 // c:1487 — `ret = doshfunc(shfunc, lnklst, 1);`. Direct
1649 // doshfunc invocation; body_runner routes through the host
1650 // body-only entry to avoid double-wrapping the scope.
1651 let name_for_body = name.to_string();
1652 let body_args = args.clone();
1653 let body_runner = move || -> i32 {
1654 crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
1655 };
1656 ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
1657 stat = 0; // c:1504
1658 }
1659
1660 if arrayp != 0 {
1661 // c:1507-1525 — fire every `${name}_functions` hook in order.
1662 let arr_name = format!("{}_functions", name); // c:1511
1663 let arr = crate::ported::params::paramtab()
1664 .read()
1665 .ok()
1666 .and_then(|t| t.get(&arr_name).and_then(|p| p.u_arr.clone()))
1667 .unwrap_or_default(); // c:1512 getaparam
1668 for fn_name in arr {
1669 // c:1514
1670 let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
1671 .read()
1672 .ok()
1673 .and_then(|t| t.get(&fn_name).cloned());
1674 if let Some(mut shf) = shf_clone {
1675 // c:1518
1676 let args = mk_args(&fn_name);
1677 // c:1508 — `newret = doshfunc(shfunc, arg0, 1);`.
1678 let name_for_body = fn_name.clone();
1679 let body_args = args.clone();
1680 let body_runner = move || -> i32 {
1681 crate::ported::exec::run_function_body(&name_for_body, &body_args[1..])
1682 .unwrap_or(0)
1683 };
1684 ret = crate::ported::exec::doshfunc(&mut shf, args, true, body_runner);
1685 stat = 0; // c:1520
1686 }
1687 }
1688 }
1689
1690 // c:1528 — `if (retval) *retval = ret;`
1691 if !retval.is_null() {
1692 unsafe {
1693 *retval = ret;
1694 }
1695 }
1696 let _ = ret;
1697
1698 stat // c:1530
1699}
1700
1701// do pre-prompt stuff // c:1530
1702/// Run pre-prompt machinery: precmd, periodic, prepromptfns.
1703/// Port of `preprompt()` from Src/utils.c:1530. Rust port skips
1704/// the `PROMPT_SP` heuristic + mailcheck (those need terminal +
1705/// MAIL state plumbing not yet present); fires the `precmd` hook
1706/// + `precmd_functions` array, the `periodic` hook on its
1707/// PERIOD-second cadence, and walks the prepromptfns registry.
1708pub fn preprompt() {
1709 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1710 // Stamp duration + exit status onto the SQLite history row hend()
1711 // added for the just-finished command (no-op when nothing is
1712 // pending). preprompt runs immediately after execode returns and
1713 // before the next ZLE read, so the elapsed time is command
1714 // wall-time, not prompt idle.
1715 crate::history::history_sqlite_finish(crate::ported::builtin::LASTVAL.load(Ordering::Relaxed));
1716 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1717 // Native p10k engine (src/extensions/p10k): snapshot `$?` and close
1718 // the command timer BEFORE the precmd hook runs, so precmd's own
1719 // commands can't clobber what the status / command_execution_time
1720 // segments show. Mirrors p10k's `_p9k_save_status` precmd ordering.
1721 crate::p10k::note_command_finished(
1722 crate::ported::builtin::LASTVAL.load(Ordering::Relaxed) as i64,
1723 );
1724 // c:1532 `static time_t lastperiodic;` — periodic-hook last-fire timestamp.
1725 static LAST_PERIODIC: AtomicI64 = AtomicI64::new(0);
1726 // `lastmailcheck` is module-scoped (LAST_MAILCHECK below) because C makes it
1727 // a file-static shared between checkmail() and checkmailpath() (c:1447).
1728
1729 // c:1535-1536 — `zlong period = getiparam("PERIOD"); zlong mailcheck
1730 // = getiparam("MAILCHECK");`
1731 let period = crate::ported::params::getiparam("PERIOD");
1732 let mailcheck = crate::ported::params::getiparam("MAILCHECK");
1733
1734 // c:1538-1543 — `winch_unblock(); ... winch_block();` — let any
1735 // pending SIGWINCH fire so the prompt picks up the latest column
1736 // count, then re-block to prevent the resize-handler from
1737 // interrupting prompt rendering mid-paint.
1738 crate::ported::signals_h::winch_unblock();
1739 crate::ported::signals_h::winch_block();
1740
1741 // c:1545-1567 — PROMPT_SP heuristic (move the prompt to a fresh line
1742 // when the previous command left dangling output). DEFERRED — a faithful
1743 // port must call `countprompt(str, &w, 0, -1)` (c:1560) to size the
1744 // PROMPT_EOL_MARK, but `countprompt` measures visible width by skipping
1745 // `Inpar`/`Outpar` (0x88/0x8a) spans, whereas zshrs's `promptexpand`
1746 // emits raw terminal escapes / readline `\x01`/`\x02` markers instead.
1747 // Until the expander is reworked to emit Inpar/Outpar (so `countprompt`
1748 // measures correctly), this can't be ported without reimplementing
1749 // countprompt's job inline — which it must not. Left as a no-op.
1750
1751 // Reap any children that exited while the shell sat idle. zsh's
1752 // SIGCHLD handler reaps asynchronously during the ZLE read (signals
1753 // are unqueued there), but zshrs runs the interactive event loop
1754 // inside a queued-signal window (init.rs `queue_signals()` at loop
1755 // top), so zhandler QUEUES each SIGCHLD instead of reaping it
1756 // (signals.rs:373). Disowned / background jobs (`&!`, `&`, and
1757 // `<(…)` process substitutions) therefore piled up as zombies
1758 // between prompts — every zpwr/zinit/p10k async `&!` per prompt
1759 // leaked a zombie, slowing startup and eventually exhausting the
1760 // process table. Mirror zhandler's reap+route here, right before
1761 // the scanjobs notification pass, so `waitpid(-1)` clears the
1762 // zombies and the DONE bits land for scanjobs to print/delete. No
1763 // foreground job is live at preprompt, so reaping -1 can't steal a
1764 // fg wait's status.
1765 {
1766 let reaped = crate::ported::signals::wait_for_processes();
1767 if !reaped.is_empty() {
1768 if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
1769 if let Ok(mut guard) = jt.lock() {
1770 for (pid, status) in reaped {
1771 let _ = crate::ported::jobs::update_bg_job(&mut guard, pid, status);
1772 }
1773 }
1774 }
1775 }
1776 }
1777
1778 // c:1569-1572 — `if (unset(NOTIFY)) scanjobs();` — sync job-status
1779 // print before prompt, via the canonical scanjobs port
1780 // (Src/jobs.c:1993).
1781 if !crate::ported::zsh_h::isset(crate::ported::zsh_h::NOTIFY) {
1782 if let Some(jt) = crate::ported::jobs::JOBTAB.get() {
1783 let mut guard = jt.lock().unwrap();
1784 crate::ported::jobs::scanjobs(&mut guard);
1785 }
1786 }
1787
1788 // c:1573-1574 — `if (errflag) return;` — bail if a previous error
1789 // already set the global flag.
1790 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
1791 return;
1792 }
1793
1794 // c:1576-1580 — `callhookfunc("precmd", NULL, 1, NULL);` + errflag bail.
1795 callhookfunc("precmd", None, 1, std::ptr::null_mut());
1796 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
1797 return;
1798 }
1799
1800 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1801 // Native p10k engine (src/extensions/p10k): rebuild PROMPT/RPROMPT
1802 // after the precmd hook so user precmd state is fresh. Equivalent
1803 // to the zsh theme's `_p9k_precmd` running as the last precmd
1804 // hook (p10k.zsh registers _p9k_precmd via add-zsh-hook). No-op
1805 // unless the powerlevel10k theme source was intercepted.
1806 crate::p10k::preprompt_render();
1807
1808 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
1809 // Fire the `async_precmd` hooks on the worker pool AFTER the prompt is
1810 // built/rendered, so they never block the paint. Non-blocking: submits to
1811 // the pool and returns immediately; the hooks write into the shared param
1812 // table which the NEXT prompt render reads. (Same call shape as the p10k
1813 // render above — the ShellExecutor-using logic lives in the extension.)
1814 crate::async_precmd::fire_async_precmd();
1815
1816 // c:1582-1589 — periodic-hook dispatch on PERIOD cadence.
1817 if period > 0 {
1818 let now = std::time::SystemTime::now()
1819 .duration_since(UNIX_EPOCH)
1820 .map(|d| d.as_secs() as i64)
1821 .unwrap_or(0);
1822 if now > LAST_PERIODIC.load(Ordering::Relaxed) + period
1823 && callhookfunc("periodic", None, 1, std::ptr::null_mut()) == 0
1824 {
1825 LAST_PERIODIC.store(now, Ordering::Relaxed);
1826 }
1827 }
1828 // c:1588-1589 — `if (errflag) return;` post-periodic bail.
1829 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
1830 return;
1831 }
1832
1833 // c:1591-1611 — mail check: `if (mailcheck && difftime(now,
1834 // lastmailcheck) > mailcheck)` walk MAILPATH (or scalar MAIL) via
1835 // checkmailpath. Faithful port — checkmailpath already exists at
1836 // utils.rs:1623, getaparam exists for the MAILPATH array.
1837 let currentmailcheck = std::time::SystemTime::now()
1838 .duration_since(UNIX_EPOCH)
1839 .map(|d| d.as_secs() as i64)
1840 .unwrap_or(0);
1841 if mailcheck > 0 && (currentmailcheck - LAST_MAILCHECK.load(Ordering::Relaxed)) > mailcheck {
1842 // c:1597 — `if (mailpath && *mailpath && **mailpath)`
1843 let mailpath = crate::ported::params::getaparam("MAILPATH");
1844 let has_mailpath = mailpath
1845 .as_ref()
1846 .map(|p| !p.is_empty() && p.first().map(|s| !s.is_empty()).unwrap_or(false))
1847 .unwrap_or(false);
1848 if has_mailpath {
1849 // c:1598 `checkmailpath(mailpath);` — emit each message to the
1850 // terminal (C writes directly to `shout`).
1851 for m in checkmailpath(mailpath.as_deref().unwrap()) {
1852 println!("{}", m);
1853 }
1854 } else {
1855 // c:1600-1608 — `if ((mailfile = getsparam("MAIL")) && *mailfile)
1856 // { x[0]=mailfile; x[1]=NULL; checkmailpath(x); }`
1857 crate::ported::signals::queue_signals(); // c:1600
1858 if let Some(mailfile) = crate::ported::params::getsparam("MAIL") {
1859 if !mailfile.is_empty() {
1860 let x = vec![mailfile]; // c:1604-1605
1861 for m in checkmailpath(&x) {
1862 println!("{}", m); // c:1606
1863 }
1864 }
1865 }
1866 crate::ported::signals::unqueue_signals(); // c:1608
1867 }
1868 LAST_MAILCHECK.store(currentmailcheck, Ordering::Relaxed); // c:1610
1869 }
1870
1871 // c:1613-1618 — `if (prepromptfns) for (...) ppnode->func();`
1872 let snapshot: Vec<fn()> = PREPROMPT_FNS.lock().unwrap().clone();
1873 for f in snapshot {
1874 f();
1875 }
1876}
1877
1878/// `static time_t lastmailcheck;` (Src/utils.c:1447) — the timestamp of the
1879/// previous mail check. `checkmail()` reads it (mtime/atime comparisons happen
1880/// against the PREVIOUS check) and writes it after walking the paths.
1881pub static LAST_MAILCHECK: AtomicI64 = AtomicI64::new(0);
1882
1883/// Direct port of `void checkmailpath(char **s)` from `Src/utils.c:1620`.
1884/// Walks each `PATH` / `PATH?message` MAILPATH component:
1885/// - empty component → `zerr` (c:1634-1636)
1886/// - `mailstat` failure → `zerr` unless ENOENT (c:1637-1639)
1887/// - directory → list entries (`PATH/entry[?message]`) and recurse
1888/// (c:1640-1669)
1889/// - file, interactive (`shout`) only: "You have new mail." or the
1890/// `?`-suffixed message expanded with `$_` bound to the file
1891/// (`parsestr`+`singsub`, c:1670-1699); and, under `MAILWARNING`,
1892/// "The mail in PATH has been read." (c:1700-1704)
1893///
1894/// **Signature note:** C is `void` and writes to `shout`; the zshrs caller
1895/// drives output, so this returns the messages (in C emission order) instead.
1896/// The new-mail / read-warning conditions are now ported faithfully — the
1897/// previous adhoc port used an unfaithful "mtime within 60s" heuristic and its
1898/// result was discarded by the caller, so the feature did nothing.
1899pub fn checkmailpath(s: &[String]) -> Vec<String> {
1900 // c:1620
1901 let mut messages = Vec::new();
1902 let interactive = *crate::ported::init::shout.lock().unwrap() != 0; // c:1670 `else if (shout)`
1903 let lastmailcheck = LAST_MAILCHECK.load(Ordering::Relaxed);
1904
1905 for path in s {
1906 // c:1627-1633 — split on the first '?': `file` before, `u` after (or None).
1907 let (file, u) = match path.find('?') {
1908 Some(p) => (&path[..p], Some(&path[p + 1..])),
1909 None => (path.as_str(), None),
1910 };
1911
1912 if file.is_empty() {
1913 // c:1634-1636 — `if (**s == 0) zerr("empty MAILPATH component: %s", *s);`
1914 zerr(&format!("empty MAILPATH component: {}", path));
1915 continue;
1916 }
1917
1918 let mut st: libc::stat = unsafe { std::mem::zeroed() };
1919 let real = unmeta(file); // c:1637 — mailstat(unmeta(*s), &st)
1920 if mailstat(&real, &mut st) == -1 {
1921 // c:1638-1639 — report anything other than "no such file".
1922 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1923 if e != libc::ENOENT {
1924 zerr(&format!("{}: {}", zsh_errno_msg(e), path));
1925 }
1926 } else if (st.st_mode as libc::mode_t & libc::S_IFMT) == libc::S_IFDIR {
1927 // c:1640-1669 — a directory: enumerate entries and recurse.
1928 match fs::read_dir(&real) {
1929 Err(_) => {
1930 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1931 zerr(&format!("{}: {}", zsh_errno_msg(e), path)); // c:1647
1932 }
1933 Ok(mut rd) => {
1934 let mut arr = Vec::new();
1935 // c:1653-1662 — `while ((fn = zreaddir(lock, 1)) && !errflag)`.
1936 while let Some(fname) = zreaddir(&mut rd, 1) {
1937 if errflag.load(Ordering::Relaxed) != 0 {
1938 break;
1939 }
1940 // c:1654-1657 — keep the `?message` suffix on each entry.
1941 arr.push(match u {
1942 Some(u) => format!("{}/{}?{}", path, fname, u),
1943 None => format!("{}/{}", path, fname),
1944 });
1945 }
1946 messages.extend(checkmailpath(&arr)); // c:1667 — recurse
1947 }
1948 }
1949 } else if interactive {
1950 // c:1671-1672 — new mail: non-empty, not yet read, newer than the
1951 // last check.
1952 if st.st_size != 0 && st.st_atime <= st.st_mtime && st.st_mtime >= lastmailcheck {
1953 match u {
1954 // c:1673-1675 — `fprintf(shout, "You have new mail.\n");`
1955 None => messages.push("You have new mail.".to_string()),
1956 // c:1676-1698 — custom message: bind `$_` to the file, then
1957 // parsestr + singsub the message (prompt-style expansion).
1958 Some(u) => {
1959 let usav = crate::ported::init::zunderscore.lock().unwrap().clone();
1960 crate::ported::exec::setunderscore(path); // c:1685 setunderscore(*s)
1961 if let Ok(parsed) = crate::ported::lex::parsestr(u) {
1962 // c:1688-1691 — parse ok → singsub then emit.
1963 messages.push(crate::ported::subst::singsub(&parsed));
1964 }
1965 crate::ported::exec::setunderscore(&usav); // c:1694-1697 restore $_
1966 }
1967 }
1968 }
1969 // c:1700-1704 — MAILWARNING: file read since the last check.
1970 if crate::ported::zsh_h::isset(crate::ported::zsh_h::MAILWARNING)
1971 && st.st_atime > st.st_mtime
1972 && st.st_atime > lastmailcheck
1973 && st.st_size != 0
1974 {
1975 messages.push(format!("The mail in {} has been read.", unmeta(file)));
1976 }
1977 }
1978 }
1979 messages
1980}
1981
1982/// Port of `printprompt4()` from `Src/utils.c:1718`.
1983///
1984/// Render the PS4 / PROMPT4 prefix and write it to stderr. zsh's
1985/// implementation reads `prompt4` global, suppresses XTRACE during
1986/// promptexpand (so subshells inside `%(?…)` don't recursively trace),
1987/// then fprintf's the expanded prefix to xtrerr. zshrs uses the same
1988/// suppress-XTRACE-around-expand pattern; ksh/sh emulation defaults
1989/// to `+ ` per Src/init.c:1192, zsh default to `+%N:%i> `.
1990///
1991/// The C source's caller (Src/exec.c::tracingcond etc.) follows this
1992/// with the per-line/per-arg fprintf — same shape mirrored at the two
1993/// zshrs call sites in fusevm_bridge.rs (BUILTIN_XTRACE_LINE / ARGS).
1994pub(crate) fn printprompt4() {
1995 // c:utils.c:1720 — `if (!isset(XTRACE)) return;`. C tests
1996 // `xtrerr` first then conditionally; the read-the-option early-
1997 // return path is equivalent for our purposes since we don't ship
1998 // the `xtrerr` separate-stream support.
1999 if !isset(XTRACE) {
2000 return;
2001 }
2002 // c:utils.c:1722-1724 — `if (prompt4) { ... s = dupstring(prompt4);`
2003 // C `prompt4` is a global initialized in init.c:1192 from the
2004 // emulation bits; PS4/PROMPT4 paramtab entries alias it via
2005 // IPDEF7R/IPDEF7 (params.c:381, 421). Read from paramtab to
2006 // honour user-set values, fall back to the same emulation
2007 // default C uses at init.c:1192.
2008 let posix = EMULATION(EMULATE_KSH | EMULATE_SH); // c:init.c:1192
2009 let prefix_template = getsparam("PS4")
2010 .or_else(|| getsparam("PROMPT4"))
2011 .unwrap_or_else(|| {
2012 if posix {
2013 "+ ".to_string() // c:init.c:1192
2014 } else {
2015 "+%N:%i> ".to_string() // c:init.c:1193
2016 }
2017 });
2018 // c:utils.c:1723,1726,1730 — `t = opts[XTRACE]; opts[XTRACE] = 0;
2019 // promptexpand(...); opts[XTRACE] = t;`
2020 let saved = isset(XTRACE);
2021 opt_state_set(&opt_name(XTRACE), false);
2022 let prefix = crate::prompt::expand_prompt(&prefix_template);
2023 opt_state_set(&opt_name(XTRACE), saved);
2024 // c:utils.c:1736 — `fprintf(xtrerr, "%s", s)`. Append to the xtrerr
2025 // line buffer rather than writing straight to stderr; the emitter
2026 // appends the command/args and flushes the whole line in one write,
2027 // matching C's single `fflush(xtrerr)` (so concurrent pipeline-stage
2028 // trace lines never interleave).
2029 crate::fusevm_bridge::xtrerr_fputs(&prefix);
2030}
2031
2032/// Port of `freestr(void *a)` from `Src/utils.c:1739`.
2033///
2034/// C body:
2035/// ```c
2036/// void freestr(void *a) { zsfree(a); }
2037/// ```
2038/// The C function is registered as the `freenode` callback for
2039/// hashtables holding plain string values. The Rust port consumes
2040/// `a` by value; Rust's `Drop` runs the equivalent of `zsfree` when
2041/// the `String` is moved into this fn and falls out of scope at the
2042/// closing brace — the no-op body is the correct port. Param name
2043/// `a` matches C exactly per Rule E.
2044pub fn freestr(_a: String) {}
2045
2046/// Port of `int gettempfile(const char *prefix, int use_heap, char **tempname)`
2047/// from Src/utils.c:2231. Creates a fresh tempfile with `O_RDWR|O_CREAT|O_EXCL`
2048/// and mode 0600 under umask 0177; returns `(fd, path)` on success.
2049///
2050/// C signature uses an out-param for the filename and returns the fd; Rust
2051/// returns both as a tuple. Matches the C control-flow: open with O_EXCL,
2052/// retry up to 16 times on EEXIST (the non-mkstemp branch, c:2255-2269).
2053pub fn gettempfile(prefix: Option<&str>) -> Option<(i32, String)> {
2054 // c:2231
2055 #[cfg(unix)]
2056 {
2057 queue_signals(); // c:2239
2058 let old_umask = unsafe { libc::umask(0o177) }; // c:2240
2059 let mut failures = 0; // c:2255
2060 let mut result: Option<(i32, String)> = None;
2061 loop {
2062 let fn_ = match gettempname(prefix, false) {
2063 // c:2260
2064 Some(n) => n,
2065 None => break,
2066 };
2067 let cn = match CString::new(fn_.clone()) {
2068 Ok(c) => c,
2069 Err(_) => break,
2070 };
2071 let fd = unsafe {
2072 libc::open(
2073 cn.as_ptr(),
2074 libc::O_RDWR | libc::O_CREAT | libc::O_EXCL, // c:2264
2075 0o600 as libc::c_int,
2076 )
2077 };
2078 if fd >= 0 {
2079 result = Some((fd, fn_));
2080 break;
2081 }
2082 let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2083 if err != libc::EEXIST {
2084 // c:2269
2085 break;
2086 }
2087 failures += 1;
2088 if failures >= 16 {
2089 // c:2269
2090 break;
2091 }
2092 }
2093 unsafe {
2094 libc::umask(old_umask);
2095 } // c:2273
2096 unqueue_signals(); // c:2274
2097 result
2098 }
2099 #[cfg(not(unix))]
2100 {
2101 let _ = prefix;
2102 None
2103 }
2104}
2105
2106/// Port of `void gettyinfo(struct ttyinfo *ti)` from Src/utils.c:1746.
2107///
2108/// The saved baseline terminal mode — C's `struct ttyinfo shttyinfo`
2109/// (Src/init.c). Captured ONCE while the terminal is still in its
2110/// original (cooked / canonical) mode, before ZLE ever puts it into
2111/// raw mode via `zsetterm()`. `trashzle()` restores this baseline via
2112/// `settyinfo(&shttyinfo)` so that external commands the user runs
2113/// (`cat`, `less`, …) execute with a cooked terminal — where `^D` is
2114/// EOF, `^C` is SIGINT, and line editing works. Without it the tty
2115/// stayed in ZLE raw mode across command execution, so `cat` never
2116/// saw EOF on `^D`.
2117#[cfg(unix)]
2118pub static SHTTYINFO: std::sync::Mutex<Option<libc::termios>> = std::sync::Mutex::new(None);
2119
2120/// Reads the current termios from the global `SHTTY`. Returns
2121/// `None` when SHTTY is closed or the call fails (matching C's
2122/// silent return when SHTTY == -1).
2123/// C body (single statement): `fdgettyinfo(SHTTY, ti);`
2124/// Rust returns `Option<termios>` instead of taking an out-ptr;
2125/// the SHTTY=-1 case naturally returns Err from fdgettyinfo → None.
2126#[cfg(unix)]
2127pub fn gettyinfo() -> Option<libc::termios> {
2128 // c:1746
2129 fdgettyinfo(SHTTY.load(Ordering::Relaxed)).ok() // c:1748
2130}
2131
2132/// Emit the `$PS4` xtrace prefix to stderr.
2133/// Read terminal mode from a file descriptor.
2134/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1753. C source uses
2135/// `tcgetattr(SHTTY, &ti->tio)`; we return the populated termios
2136/// or an io::Error on failure (caller equivalent to zsh's `zerr`).
2137#[cfg(unix)]
2138/// WARNING: param names don't match C — Rust=(fd) vs C=(SHTTY, ti)
2139pub fn fdgettyinfo(fd: i32) -> io::Result<libc::termios> {
2140 let mut tio: libc::termios = unsafe { std::mem::zeroed() };
2141 if unsafe { libc::tcgetattr(fd, &mut tio) } == -1 {
2142 Err(io::Error::last_os_error())
2143 } else {
2144 Ok(tio)
2145 }
2146}
2147
2148#[cfg(not(unix))]
2149/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from `Src/utils.c:1753`.
2150/// WARNING: param names don't match C — Rust=(_fd) vs C=(SHTTY, ti)
2151pub fn fdgettyinfo(_fd: i32) -> std::io::Result<()> {
2152 Err(std::io::Error::new(
2153 std::io::ErrorKind::Unsupported,
2154 "no termios",
2155 ))
2156}
2157
2158/// Port of `void settyinfo(struct ttyinfo *ti)` from Src/utils.c:1778.
2159///
2160/// Restores the termios state on the global `SHTTY` with EINTR
2161/// retry; no-op when SHTTY is closed.
2162/// C body (single statement): `fdsettyinfo(SHTTY, ti);`
2163/// Rust returns bool; SHTTY=-1 yields Err → false from fdsettyinfo.
2164#[cfg(unix)]
2165pub fn settyinfo(ti: &libc::termios) -> bool {
2166 // c:1778
2167 fdsettyinfo(SHTTY.load(Ordering::Relaxed), ti).is_ok() // c:1780
2168}
2169
2170/// Apply terminal mode to a file descriptor, with EINTR retry.
2171/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1785. C source loops
2172/// `while (tcsetattr(SHTTY, TCSADRAIN, &ti->tio) == -1 && errno
2173/// == EINTR)` — same retry shape here.
2174#[cfg(unix)]
2175pub fn fdsettyinfo(SHTTY2: i32, ti: &libc::termios) -> io::Result<()> {
2176 loop {
2177 if unsafe { libc::tcsetattr(SHTTY2, libc::TCSADRAIN, ti) } != -1 {
2178 return Ok(());
2179 }
2180 let err = io::Error::last_os_error();
2181 if err.kind() != io::ErrorKind::Interrupted {
2182 return Err(err);
2183 }
2184 }
2185}
2186
2187#[cfg(not(unix))]
2188/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from `Src/utils.c:1785`.
2189pub fn fdsettyinfo(SHTTY: i32, ti: &()) -> std::io::Result<()> {
2190 Err(std::io::Error::new(
2191 std::io::ErrorKind::Unsupported,
2192 "no termios",
2193 ))
2194}
2195
2196// window size changed // c:1831
2197/// Port of `adjustlines(int signalled)` from Src/utils.c:1831 — TIOCGWINSZ
2198/// lookup that seeds `$LINES`. The C variant updates the global
2199/// `zterm_lines` and returns whether it changed; this Rust port
2200/// returns the row count directly. Falls back to `$LINES` env var,
2201/// then 24, mirroring the C source's `tclines > 0 ? tclines : 24`
2202/// fallback at line 1844.
2203/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2204pub fn adjustlines() -> usize {
2205 // c:1831
2206 #[cfg(unix)]
2207 {
2208 unsafe {
2209 let mut ws: libc::winsize = std::mem::zeroed();
2210 if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_row > 0 {
2211 return ws.ws_row as usize;
2212 }
2213 }
2214 }
2215 // c:1844 fallback — paramtab `$LINES`, not OS env.
2216 getsparam("LINES")
2217 .and_then(|s| s.parse().ok())
2218 .unwrap_or(24)
2219}
2220
2221/// Port of `adjustcolumns(int signalled)` from Src/utils.c:1856 — TIOCGWINSZ
2222/// lookup that seeds `$COLUMNS`. The C variant updates the global
2223/// `zterm_columns` and returns whether it changed; this Rust port
2224/// returns the column count directly. Falls back to `$COLUMNS` env
2225/// var, then 80, mirroring the C source's `tccolumns > 0 ? tccolumns : 80`
2226/// fallback at line 1869.
2227/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2228pub fn adjustcolumns() -> usize {
2229 // c:1856
2230 #[cfg(unix)]
2231 {
2232 unsafe {
2233 let mut ws: libc::winsize = std::mem::zeroed();
2234 if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
2235 return ws.ws_col as usize;
2236 }
2237 }
2238 }
2239 // c:1820 fallback — `if (zterm_columns <= 0) zterm_columns =
2240 // tccolumns > 0 ? tccolumns : 80`. C consults
2241 // `getsparam("COLUMNS")` (paramtab), not OS env.
2242 getsparam("COLUMNS")
2243 .and_then(|s| s.parse().ok())
2244 .unwrap_or(80)
2245}
2246
2247// window size changed // c:1824
2248/// Port of `void adjustwinsize(int from)` from `Src/utils.c:1889`.
2249/// SIGWINCH handler + LINES/COLUMNS-update entry. Reads the tty's
2250/// current geometry via `TIOCGWINSZ` ioctl, updates the cached
2251/// `zterm_lines`/`zterm_columns`, and writes `$LINES` / `$COLUMNS`
2252/// when they're already set in the environment.
2253/// ```c
2254/// void
2255/// adjustwinsize(int from)
2256/// {
2257/// static int getwinsz = 1;
2258/// int ttyrows = shttyinfo.winsize.ws_row;
2259/// int ttycols = shttyinfo.winsize.ws_col;
2260/// int resetzle = 0;
2261/// if (getwinsz || from == 1) {
2262/// if (SHTTY == -1) return;
2263/// if (ioctl(SHTTY, TIOCGWINSZ, &shttyinfo.winsize) == 0) {
2264/// resetzle = (ttyrows != ... || ttycols != ...);
2265/// ...
2266/// } else {
2267/// shttyinfo.winsize.ws_row = zterm_lines;
2268/// shttyinfo.winsize.ws_col = zterm_columns;
2269/// }
2270/// }
2271/// switch (from) {
2272/// case 0: case 1:
2273/// getwinsz = 0;
2274/// if (adjustlines(from) && zgetenv("LINES")) setiparam("LINES", zterm_lines);
2275/// if (adjustcolumns(from) && zgetenv("COLUMNS")) setiparam("COLUMNS", zterm_columns);
2276/// getwinsz = 1;
2277/// break;
2278/// case 2: resetzle = adjustlines(0); break;
2279/// case 3: resetzle = adjustcolumns(0); break;
2280/// }
2281/// if (interact && resetzle) zleentry(ZLE_CMD_REFRESH);
2282/// }
2283/// ```
2284pub fn adjustwinsize(from: i32) -> (usize, usize) {
2285 // c:1889
2286
2287 // c:1891 — `static int getwinsz = 1;`
2288 let getwinsz = ADJUSTWINSIZE_GETWINSZ.load(Ordering::SeqCst);
2289
2290 let mut ttyrows: i32 = 0;
2291 let mut ttycols: i32 = 0;
2292
2293 // c:1898-1917 — TIOCGWINSZ probe.
2294 if getwinsz != 0 || from == 1 {
2295 // c:1898
2296 let shtty = SHTTY.load(Ordering::Relaxed);
2297 if shtty == -1 {
2298 // c:1900
2299 return (adjustcolumns(), adjustlines()); // c:1901
2300 }
2301 #[cfg(unix)]
2302 unsafe {
2303 let mut ws: libc::winsize = std::mem::zeroed();
2304 if libc::ioctl(shtty, libc::TIOCGWINSZ, &mut ws as *mut _) == 0 {
2305 // c:1902
2306 ttyrows = ws.ws_row as i32; // c:1907
2307 ttycols = ws.ws_col as i32; // c:1908
2308 }
2309 }
2310 }
2311
2312 let mut resetzle = 0i32;
2313 match from {
2314 // c:1921
2315 0 | 1 => {
2316 // c:1922-1923
2317 ADJUSTWINSIZE_GETWINSZ.store(0, Ordering::SeqCst); // c:1924
2318 // c:1931-1934 — C: `if (adjustlines(from) && zgetenv("LINES"))
2319 // setiparam("LINES", zterm_lines);` (same for COLUMNS). The
2320 // zgetenv gate only guards re-publishing to an ENV-exported
2321 // LINES — in C the param itself is ALWAYS live because its
2322 // valptr aliases the `zterm_lines` global that adjustlines
2323 // just wrote (createparamtable IPDEF5 → zlevarsetfn/intvargetfn
2324 // on &zterm_lines). zshrs params store their own u_val copy
2325 // (no valptr aliasing), so the probe result must be written
2326 // through setiparam unconditionally — gating on the env left
2327 // `$LINES`/`$COLUMNS` at 0 for every interactive shell
2328 // (LINES is not in the environment; zsh doesn't export it).
2329 // Effect: `$(( LINES / 3 ))` = 0 → division-by-zero in
2330 // history-search-multi-word pagination (_hsmw_main:81), and
2331 // every LINES/COLUMNS-derived layout was wrong. Recursion is
2332 // safe: setiparam → zlevarsetfn → adjustwinsize(2|3) never
2333 // re-enters this arm.
2334 let lines = if ttyrows > 0 {
2335 ttyrows as i64 // c:1837 — zterm_lines = shttyinfo.winsize.ws_row
2336 } else {
2337 adjustlines() as i64 // c:1844 — tclines/24 fallback chain
2338 };
2339 setiparam("LINES", lines); // c:1932
2340 let cols = if ttycols > 0 {
2341 ttycols as i64 // c:1862 — zterm_columns = ws_col
2342 } else {
2343 adjustcolumns() as i64 // c:1869 — tccolumns/80 fallback chain
2344 };
2345 setiparam("COLUMNS", cols); // c:1934
2346 ADJUSTWINSIZE_GETWINSZ.store(1, Ordering::SeqCst); // c:1935
2347 }
2348 2 => {
2349 // c:1937
2350 resetzle = adjustlines() as i32; // c:1938
2351 }
2352 3 => {
2353 // c:1940
2354 resetzle = adjustcolumns() as i32; // c:1941
2355 }
2356 _ => {}
2357 }
2358
2359 // c:1946-1958 — resetzle + zleentry(ZLE_CMD_REFRESH) when interact.
2360 if from >= 2 && resetzle != 0 {
2361 // ZLE refresh dispatch via zleentry(ZLE_CMD_REFRESH) lands here
2362 // once the C signal handler shape ports.
2363 let _ = ttyrows;
2364 let _ = ttycols;
2365 }
2366
2367 (adjustcolumns(), adjustlines())
2368}
2369
2370/// Port of `static int getwinsz` from `Src/utils.c:1891`. Local
2371/// reentry guard inside adjustwinsize — bumped to 0 around the
2372/// setiparam recursion so the recursive call short-circuits.
2373pub static ADJUSTWINSIZE_GETWINSZ: std::sync::atomic::AtomicI32 =
2374 std::sync::atomic::AtomicI32::new(1); // c:1891
2375
2376/// Port of `check_fd_table(int fd)` from `Src/utils.c:1968-1983`.
2377/// ```c
2378/// if (fd <= max_zsh_fd) return;
2379/// if (fd >= fdtable_size) {
2380/// int old_size = fdtable_size;
2381/// while (fd >= fdtable_size)
2382/// fdtable = zrealloc(fdtable, (fdtable_size *= 2) * sizeof(*fdtable));
2383/// memset(fdtable + old_size, 0, (fdtable_size - old_size) * sizeof(*fdtable));
2384/// }
2385/// max_zsh_fd = fd;
2386/// ```
2387/// C semantics: GROW the `fdtable` array so it can index `fd`, then
2388/// update `max_zsh_fd`. Returns void.
2389///
2390/// The Rust port keeps the same signature for name-parity but the
2391/// fdtable global isn't yet modeled — so this is a no-op shim. The
2392/// previous Rust impl was an `fcntl(F_GETFD)` validity check —
2393/// COMPLETELY DIFFERENT SEMANTICS from C (which doesn't validate
2394/// the fd at all, just grows the table). Fixed to no-op + bool
2395/// return for caller compatibility (no live callers).
2396pub fn check_fd_table(fd: i32) -> bool {
2397 // c:1969
2398 // c:1971-1972 — `if (fd <= max_zsh_fd) return;`
2399 let cur_max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:1971
2400 if fd <= cur_max {
2401 // c:1971
2402 return true; // c:1972 (return; in C)
2403 }
2404 if fd < 0 {
2405 // defensive — fdtable index must be ≥0
2406 return false;
2407 }
2408 // c:1974-1981 — `if (fd >= fdtable_size) { while (fd >= fdtable_size)
2409 // fdtable_size *= 2; zrealloc; memset(new_slots, 0); }`.
2410 // Rust Vec::resize handles the realloc + zero-fill in one call (the
2411 // expansion bumps capacity geometrically too).
2412 {
2413 let mut g = fdtable_lock().lock().unwrap();
2414 if (fd as usize) >= g.len() {
2415 g.resize((fd as usize) + 1, FDT_UNUSED); // c:1975-1979 grow
2416 }
2417 }
2418 // c:1982 — `max_zsh_fd = fd;`.
2419 MAX_ZSH_FD.store(fd, Ordering::Relaxed); // c:1982
2420 true
2421}
2422
2423/// Port of `movefd(int fd)` from `Src/utils.c:1989-2012`.
2424/// ```c
2425/// if (fd != -1 && fd < 10) {
2426/// int fe = fcntl(fd, F_DUPFD, 10);
2427/// zclose(fd); // unconditional close, even when fe == -1
2428/// fd = fe;
2429/// }
2430/// if (fd != -1) {
2431/// check_fd_table(fd);
2432/// fdtable[fd] = FDT_INTERNAL;
2433/// }
2434/// return fd;
2435/// ```
2436/// Two divergences fixed this iteration:
2437/// 1. Old Rust closed the source fd ONLY on success; C closes
2438/// unconditionally per the c:1999-2004 comment "probably better
2439/// to avoid a leak."
2440/// 2. Old Rust added `FD_CLOEXEC` after the dup; C does NOT —
2441/// CLOEXEC is added later by `addmodulefd`/`addlockfd` callers
2442/// that need it. movefd itself does not.
2443pub fn movefd(fd: i32) -> i32 {
2444 #[cfg(unix)]
2445 {
2446 let mut fd = fd;
2447 if fd != -1 && fd < 10 {
2448 // c:1992
2449 let fe = unsafe { libc::fcntl(fd, libc::F_DUPFD, 10) }; // c:1994
2450 unsafe { libc::close(fd) }; // c:2004 zclose(fd) — unconditional
2451 fd = fe; // c:2005
2452 }
2453 // c:2007-2010 — `if (fd != -1) { check_fd_table(fd); fdtable[fd]
2454 // = FDT_INTERNAL; }`. The fdtable global IS now modeled (port
2455 // of utils.c:63 — `Vec<u8>` behind `fdtable_lock`). Mark the
2456 // new fd as FDT_INTERNAL so the rest of the shell knows the
2457 // shell is using it (a later `addmodulefd` / `addlockfd`
2458 // upgrade may reclassify; raw external code shouldn't touch
2459 // it). The previous port skipped this so internal-fd tracking
2460 // (which `closeallelse` and forkexec rely on) silently
2461 // never saw zshrs-internal fds.
2462 if fd != -1 {
2463 // c:2007
2464 check_fd_table(fd); // c:2008
2465 fdtable_set(fd, FDT_INTERNAL); // c:2009
2466 }
2467 fd
2468 }
2469 #[cfg(not(unix))]
2470 {
2471 fd
2472 }
2473}
2474
2475/// Port of `redup(int x, int y)` from `Src/utils.c:2021`.
2476///
2477/// C signature: `int redup(int x, int y)`.
2478/// "Move fd x to y. If x == -1, fd y is closed. Returns y for
2479/// success, -1 for failure." (c:2014-2016 docstring.)
2480///
2481/// Body mirrors c:2023-2068: when `x == -1`, close `y` (return `y`);
2482/// when `x == y`, no-op (return `y`); otherwise `dup2(x, y)` +
2483/// `close(x)`.
2484///
2485/// C body fdtable updates (c:2053-2063) that the previous Rust port
2486/// SKIPPED with a stale "fdtable global not yet ported" comment —
2487/// fdtable IS now ported and these updates are load-bearing:
2488/// * `fdtable[y] = fdtable[x]` — the new fd inherits the old fd's
2489/// ownership category (FDT_INTERNAL / FDT_MODULE / etc.).
2490/// * If the inherited type is `FDT_FLOCK` / `FDT_FLOCK_EXEC`, promote
2491/// to `FDT_INTERNAL` (the dup'd fd doesn't carry the flock).
2492/// * If `fdtable[x] == FDT_FLOCK`, decrement `fdtable_flocks` (the
2493/// original lock-holding fd is about to be closed).
2494///
2495/// Without these updates, redup'd fds had stale `FDT_UNUSED` ownership
2496/// and `closeallelse(FDT_EXTERNAL)` etc. couldn't classify them.
2497pub fn redup(x: i32, y: i32) -> i32 {
2498 // c:2021
2499 let mut ret = y; // c:2023
2500 #[cfg(unix)]
2501 {
2502 if x < 0 {
2503 // c:2047
2504 zclose(y); // c:2048
2505 } else if x != y {
2506 // c:2049
2507 // c:2053-2057 — successful dup2: copy fdtable + FLOCK promote.
2508 if unsafe { libc::dup2(x, y) } == -1 {
2509 // c:2050
2510 ret = -1; // c:2051
2511 } else {
2512 check_fd_table(y); // c:2053
2513 let kind_x = fdtable_get(x); // c:2054
2514 let kind_y = if kind_x == FDT_FLOCK || kind_x == FDT_FLOCK_EXEC {
2515 FDT_INTERNAL // c:2055-2056
2516 } else {
2517 kind_x // c:2054
2518 };
2519 fdtable_set(y, kind_y);
2520 }
2521 // c:2062-2063 — `if (fdtable[x] == FDT_FLOCK) fdtable_flocks--;`
2522 // The original lock-holding fd is about to be closed, so the
2523 // flock-count tracker must drop. Even on dup2 failure C still
2524 // closes x, so this runs in both arms.
2525 if fdtable_get(x) == FDT_FLOCK {
2526 // c:2062
2527 FDTABLE_FLOCKS.fetch_sub(1, Ordering::SeqCst); // c:2063
2528 }
2529 zclose(x); // c:2064
2530 }
2531 }
2532 #[cfg(not(unix))]
2533 {
2534 let _ = (x, y);
2535 }
2536 ret // c:2067
2537}
2538
2539/// Port of `addmodulefd(int fd, int fdt)` from `Src/utils.c:2090-2097`.
2540/// ```c
2541/// if (fd >= 0) {
2542/// check_fd_table(fd);
2543/// fdtable[fd] = fdt;
2544/// }
2545/// ```
2546/// Two divergences fixed this iteration:
2547/// 1. C accepts an `fdt` parameter (FDT_MODULE / FDT_INTERNAL /
2548/// FDT_EXTERNAL — see callers in Src/Modules/{random,socket,tcp,
2549/// db_gdbm}.c). Rust port hardcoded `FDT_MODULE` — silently
2550/// ignored caller intent. Now takes `fdt` per C.
2551/// 2. C does NOT set `FD_CLOEXEC`. Rust port was adding it
2552/// unconditionally — divergent. CLOEXEC is the caller's
2553/// responsibility based on the fdt semantics.
2554pub fn addmodulefd(fd: i32, fdt: i32) {
2555 // c:2091
2556 // c:2093 — `if (fd >= 0)`.
2557 if fd >= 0 {
2558 // c:2094 — `check_fd_table(fd)` — grow fdtable to cover fd.
2559 check_fd_table(fd); // c:2094
2560 // c:2095 — `fdtable[fd] = fdt`. Routes through the canonical
2561 // helper which mirrors C's direct `fdtable[fd] = fdt` write.
2562 fdtable_set(fd, fdt); // c:2095
2563 }
2564}
2565
2566/// Port of `addlockfd(int fd, int cloexec)` from `Src/utils.c:2111-2121`.
2567/// ```c
2568/// if (cloexec) {
2569/// if (fdtable[fd] != FDT_FLOCK)
2570/// fdtable_flocks++;
2571/// fdtable[fd] = FDT_FLOCK;
2572/// } else {
2573/// fdtable[fd] = FDT_FLOCK_EXEC;
2574/// }
2575/// ```
2576/// Critical divergence fixed: C updates `fdtable[fd]` to FDT_FLOCK
2577/// (cloexec=true) or FDT_FLOCK_EXEC (cloexec=false). Previous Rust
2578/// port did the OPPOSITE — called `fcntl(F_SETFD, FD_CLOEXEC)` when
2579/// `cloexec` was true. That's wrong on two counts:
2580/// 1. C's "cloexec" parameter selects the FDT category, not a
2581/// libc fcntl flag.
2582/// 2. FDT_FLOCK means "lock survives across exec via fd inheritance"
2583/// — the OPPOSITE of close-on-exec. The Rust port was adding
2584/// CLOEXEC for the very case where the fd should be inheritable.
2585pub fn addlockfd(fd: i32, cloexec: bool) {
2586 // c:2112
2587 if cloexec {
2588 // c:2114
2589 // c:2115-2117 — track flock count, set FDT_FLOCK.
2590 if fdtable_get(fd) != FDT_FLOCK {
2591 FDTABLE_FLOCKS.fetch_add(1, Ordering::SeqCst);
2592 }
2593 fdtable_set(fd, FDT_FLOCK);
2594 } else {
2595 // c:2118
2596 // c:2119 — FDT_FLOCK_EXEC means flock inherited by exec.
2597 fdtable_set(fd, FDT_FLOCK_EXEC);
2598 }
2599}
2600
2601// FDTABLE_FLOCKS canonical declaration lives at utils.rs:5219
2602// (the exec.c global with same name). Reuse, don't redeclare.
2603
2604/// Close the given fd, and clear it from fdtable. // c:2123
2605/// Port of `int zclose(int fd)` from `Src/utils.c:2127`.
2606pub fn zclose(fd: i32) -> i32 {
2607 // c:2127
2608 if fd >= 0 {
2609 // c:2129
2610 // c:2130-2133 — comment carry: "Careful: we allow closing of
2611 // arbitrary fd's, beyond max_zsh_fd. In that case we don't
2612 // try anything clever."
2613 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed); // c:2134
2614 if fd <= max_fd {
2615 // c:2134
2616 // !!! ZSHRS THREADING ADAPTATION — deviates from C here !!!
2617 // C closes blindly; it is single-threaded, so a shell-recorded
2618 // fd number can never be concurrently recycled by someone else.
2619 // zshrs runs 18 worker threads whose Rust-owned fds (ReadDir
2620 // dirfds, SQLite, File) recycle freed numbers immediately — a
2621 // stale zclose then closes a FOREIGN fd, and std panics in
2622 // ReadDir::drop ("unexpected error during closedir: EBADF",
2623 // observed from the compinit bg scan). An in-range fd the
2624 // fdtable does NOT own is by definition not ours to close.
2625 if fdtable_get(fd) == FDT_UNUSED {
2626 tracing::debug!(fd, "zclose: skipping unowned in-range fd (thread-safety guard)");
2627 return 0;
2628 }
2629 if fdtable_get(fd) == FDT_FLOCK {
2630 // c:2135
2631 FDTABLE_FLOCKS.fetch_sub(1, Ordering::Relaxed);
2632 // c:2136
2633 }
2634 fdtable_set(fd, FDT_UNUSED); // c:2137
2635 // c:2138-2139 — shrink max_zsh_fd past trailing UNUSED slots.
2636 let mut m = MAX_ZSH_FD.load(Ordering::Relaxed);
2637 while m > 0 && fdtable_get(m) == FDT_UNUSED {
2638 m -= 1;
2639 }
2640 MAX_ZSH_FD.store(m, Ordering::Relaxed);
2641 // c:2140-2143 — coproc fd tracking.
2642 if fd == coprocin.load(Ordering::Relaxed) {
2643 coprocin.store(-1, Ordering::Relaxed);
2644 }
2645 if fd == coprocout.load(Ordering::Relaxed) {
2646 coprocout.store(-1, Ordering::Relaxed);
2647 }
2648 }
2649 #[cfg(unix)]
2650 unsafe {
2651 return libc::close(fd);
2652 } // c:2145
2653 #[cfg(not(unix))]
2654 return 0;
2655 }
2656 -1 // c:2147
2657}
2658
2659/// Port of `int zcloselockfd(int fd)` from `Src/utils.c:2155-2164`.
2660/// ```c
2661/// if (fd > max_zsh_fd) return -1;
2662/// if (fdtable[fd] != FDT_FLOCK && fdtable[fd] != FDT_FLOCK_EXEC)
2663/// return -1;
2664/// zclose(fd);
2665/// return 0;
2666/// ```
2667/// "Close an fd returning 0 if used for locking; return -1 if it
2668/// isn't." Caller (`bin_zsystem_flock -u`) uses the -1 return to
2669/// distinguish "fd not in the flock table" from "successfully
2670/// released a lock."
2671///
2672/// Previously the Rust port skipped the FDT_FLOCK / FDT_FLOCK_EXEC
2673/// check entirely and always returned 0 — meaning `zsystem flock -u
2674/// <unlocked-fd>` reported success on any fd. Now that the
2675/// `addlockfd` port (this iteration) populates the fdtable with the
2676/// canonical FDT_FLOCK / FDT_FLOCK_EXEC slots, the check can fire
2677/// faithfully.
2678pub fn zcloselockfd(fd: i32) -> i32 {
2679 // c:2156
2680 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2681 // c:2158 — `if (fd > max_zsh_fd) return -1;`.
2682 if fd > max_fd {
2683 return -1;
2684 }
2685 // c:2160-2161 — `if (fdtable[fd] != FDT_FLOCK && != FDT_FLOCK_EXEC)
2686 // return -1;`.
2687 let slot = fdtable_get(fd);
2688 if slot != FDT_FLOCK && slot != FDT_FLOCK_EXEC {
2689 return -1;
2690 }
2691 zclose(fd); // c:2162
2692 0 // c:2163
2693}
2694
2695/// Port of `char *gettempname(const char *prefix, int use_heap)` from Src/utils.c:2178.
2696///
2697/// Returns a unique tempfile name templated like C `mktemp(3)` —
2698/// `{prefix}.XXXXXX`. Falls back to `getsparam("TMPPREFIX")` and
2699/// then `DEFAULT_TMPPREFIX` when `prefix` is None. Does NOT create
2700/// the file (matches C — only `gettempfile` creates).
2701pub fn gettempname(prefix: Option<&str>, _use_heap: bool) -> Option<String> {
2702 // c:2178
2703 let suffix = if prefix.is_some() {
2704 ".XXXXXX"
2705 } else {
2706 "XXXXXX"
2707 }; // c:2178
2708 queue_signals(); // c:2182
2709 let prefix_owned: String = match prefix {
2710 // c:2183
2711 Some(p) => p.to_string(),
2712 // c:2184 — `getsparam("TMPPREFIX")`. Read from paramtab (not OS
2713 // env); fall back to compile-time default when unset.
2714 None => getsparam("TMPPREFIX")
2715 .unwrap_or_else(|| crate::ported::config_h::DEFAULT_TMPPREFIX.to_string()),
2716 };
2717 let template = format!("{}{}", prefix_owned, suffix); // c:2186-2188
2718 // C uses mktemp(3) which mutates the X's into a unique name // c:2192/2219
2719 // without creating the file. Rust has no mktemp; emulate with
2720 // pid+timestamp. Caller is responsible for O_EXCL open.
2721 let pid = std::process::id();
2722 let nanos = std::time::SystemTime::now()
2723 .duration_since(UNIX_EPOCH)
2724 .map(|d| d.as_nanos())
2725 .unwrap_or(0);
2726 let unique = format!("{:x}{:x}", pid, nanos & 0xffffff);
2727 let name = template.replace("XXXXXX", &unique);
2728 unqueue_signals(); // c:2221
2729 Some(name)
2730}
2731
2732/// Check if metafied - port from zsh/Src/utils.c has_token()
2733// Check if a string contains a token // c:2282
2734/// Port of `has_token(const char *s)` from `Src/utils.c:2282` — used by
2735/// `ecstrcode` (parse.rs:989) to flip the token-marker bit on the
2736/// encoded string offset. Token markers live in 0x83..=0x9f (Pound,
2737/// Stringg, Hat, Star, ..., Bnull, Nularg). Earlier impl checked
2738/// only 0x83 (Meta) which missed Dash/Equals/Inbrack/Inbrace/etc.,
2739/// so any string containing those (e.g. `dart-lang/dart` →
2740/// `dart\u{9b}lang/dart`) got encoded with the no-token bit set,
2741/// breaking byte parity with C's wordcode-emitter output.
2742/// Port of `int has_token(const char *s)` from `Src/utils.c:2280-2288`.
2743/// ```c
2744/// while (*s)
2745/// if (itok(*s++)) return 1;
2746/// return 0;
2747/// ```
2748/// Routes through the canonical `itok()` typtab-driven predicate.
2749/// Previous Rust port used hardcoded `0x83..=0x9f` which was:
2750/// - Wrong on the low end: includes `0x83` (Meta, NOT a token).
2751/// - Wrong on the high end: missing `0xa0..=0xa1` (part of the
2752/// Snull..Nularg range that `itok` legitimately covers).
2753/// The full canonical ITOK range per `Src/zsh.h:152-159` is
2754/// `0x84..=0xa1` (Pound..Nularg), with possible gaps in the middle
2755/// (Snull at 0x9d follows Bang at 0x9c).
2756pub fn has_token(s: &str) -> bool {
2757 // c:2282
2758 s.bytes().any(itok) // c:2285
2759}
2760
2761// Delete a character in a string // c:2294
2762/// Remove character from string (from utils.c chuck)
2763pub fn chuck(s: &mut String, pos: usize) {
2764 // c:2294
2765 if pos < s.len() {
2766 s.remove(pos);
2767 }
2768}
2769
2770/// Convert character to lowercase
2771/// To-lowercase that respects locale.
2772/// Port of `tulower(int c)` from Src/utils.c.
2773pub fn tulower(c: char) -> char {
2774 // c:2302
2775 c.to_lowercase().next().unwrap_or(c)
2776}
2777
2778/// Convert character to uppercase
2779/// To-uppercase that respects locale.
2780/// Port of `tuupper(int c)` from Src/utils.c.
2781pub fn tuupper(c: char) -> char {
2782 // c:2310
2783 c.to_uppercase().next().unwrap_or(c)
2784}
2785
2786/// Port of `void ztrncpy(char *s, char *t, int len)` from `Src/utils.c:2320`.
2787///
2788/// C body (c:2320-2326):
2789/// ```c
2790/// while (len--)
2791/// *s++ = *t++;
2792/// *s = '\0';
2793/// ```
2794///
2795/// Copy `len` bytes from `t` into `s`, then NUL-terminate. C does no
2796/// bounds check; the caller must ensure `s` has `len + 1` bytes of
2797/// capacity. Rust port preserves the (s, t, len) parameter shape:
2798/// `s: &mut String` (output), `t: &str` (source), `len: usize` (copy
2799/// count). The C NUL terminator at c:2325 is implicit in Rust's
2800/// length-prefixed String. The Rust port clamps `len` to `t.len()` to
2801/// avoid the out-of-bounds read C's `*s++ = *t++` would perform when
2802/// `len > strlen(t)` (UB in C; explicitly bounded in Rust).
2803pub fn ztrncpy(s: &mut String, t: &str, len: usize) {
2804 // c:2320
2805 s.clear(); // C overwrites from start of *s
2806 let take = len.min(t.len()); // c:2322 while (len--)
2807 s.push_str(&t[..take]); // c:2322 *s++ = *t++
2808 // c:2325 — `*s = '\0';` (implicit in Rust String length)
2809}
2810
2811/// Port of `void strucpy(char **s, char *t)` from `Src/utils.c:2331-2337`.
2812/// ```c
2813/// char *u = *s;
2814/// while ((*u++ = *t++));
2815/// *s = u - 1; // leave *s pointing at the NUL terminator
2816/// ```
2817/// Body: `strcpy(*s, t)` + advance `*s` to point at the new
2818/// NUL terminator. The "u" in the name is a C convention for the
2819/// local pointer-walker, NOT "upper-case" — the function does NOT
2820/// change case.
2821///
2822/// Rust API: param name `s` matches C exactly per Rule E (despite Rust
2823/// convention of `dest` for output buffers). The C pointer-advance
2824/// `*s = u - 1` translates to "the new end of s is at s.len()", which
2825/// is implicit in Rust's owning-String model.
2826pub fn strucpy(s: &mut String, t: &str) {
2827 // c:2331
2828 s.push_str(t); // c:2335 `*u++ = *t++` loop
2829 // c:2336 — `*s = u - 1;` — pointer-advance is implicit in Rust
2830 // (`s.len()` is the new end-of-string position).
2831}
2832
2833/// Port of `void struncpy(char **s, char *t, int n)` from `Src/utils.c:2341-2350`.
2834/// ```c
2835/// char *u = *s;
2836/// while (n-- && (*u = *t++)) u++;
2837/// *s = u;
2838/// if (n > 0) *u = '\0';
2839/// ```
2840/// Body: copy up to `n` bytes from `t` to `*s`, NUL-terminate.
2841/// Note c:2348 — "just one null-byte will do, unlike strncpy(3)";
2842/// doesn't pad with NULs.
2843///
2844/// Param names match C exactly per Rule E.
2845pub fn struncpy(s: &mut String, t: &str, n: usize) {
2846 // c:2341
2847 // c:2345 — `while (n-- && (*u = *t++)) u++;` — copy up to n
2848 // bytes, stop at NUL (which in Rust &str is the end-of-string).
2849 let take = n.min(t.len());
2850 s.push_str(&t[..take]);
2851}
2852
2853/// Array length - port from arrlen()
2854/// Port of `arrlen(char **s)` from `Src/utils.c:2357`.
2855pub fn arrlen<T>(s: &[T]) -> usize {
2856 // c:2357
2857 s.len()
2858}
2859
2860// Return TRUE iff arrlen(s) >= lower_bound, but more efficiently. // c:2382
2861/// Check if array length >= n (from utils.c arrlen_ge)
2862pub fn arrlen_ge<T>(arr: &[T], n: usize) -> bool {
2863 // c:2369
2864 arr.len() >= n
2865}
2866
2867// Return TRUE iff arrlen(s) > lower_bound, but more efficiently. // c:2400
2868/// Check if array length > n (from utils.c arrlen_gt)
2869pub fn arrlen_gt<T>(arr: &[T], n: usize) -> bool {
2870 // c:2382
2871 arr.len() > n
2872}
2873
2874// Return TRUE iff arrlen(s) <= upper_bound, but more efficiently. // c:2409
2875/// Check if array length <= n (from utils.c arrlen_le)
2876pub fn arrlen_le<T>(arr: &[T], n: usize) -> bool {
2877 // c:2391
2878 arr.len() <= n
2879}
2880
2881// Return TRUE iff arrlen(s) < upper_bound, but more efficiently. // c:2400
2882/// Check if array length < n (from utils.c arrlen_lt)
2883pub fn arrlen_lt<T>(arr: &[T], n: usize) -> bool {
2884 // c:2400
2885 arr.len() < n
2886}
2887
2888/// Direct port of `int skipparens(char inpar, char outpar, char **s)`
2889/// from `Src/utils.c:2409`. Walks `*s` past a balanced `inpar`/`outpar`
2890/// pair, advancing the input slice in place. Returns 0 on full
2891/// balance, `-1` when `**s != inpar` (no opening paren), or positive
2892/// when the scan ran off the end of `*s` with `level > 0`
2893/// (unbalanced).
2894///
2895/// Rust signature: `s: &mut &str` mirrors C's `char **s` out-arg —
2896/// the caller's slice handle is rewritten to point past the closing
2897/// paren (or to the empty tail on unbalanced input).
2898/// WARNING: param names don't match C — Rust=(open, close, s) vs C=(inpar, outpar, s)
2899pub fn skipparens(open: char, close: char, s: &mut &str) -> i32 {
2900 // c:2409
2901 let mut chars = s.char_indices();
2902 // c:2412 — `if (**s != inpar) return -1;`
2903 match chars.next() {
2904 Some((_, c)) if c == open => {}
2905 _ => return -1,
2906 }
2907 // c:2414 — `for (level = 1; *++*s && level;)`
2908 let mut level: i32 = 1;
2909 let mut consumed = open.len_utf8();
2910 for (i, c) in chars {
2911 consumed = i + c.len_utf8();
2912 if c == open {
2913 level += 1; // c:2416-2417
2914 } else if c == close {
2915 level -= 1; // c:2418-2419
2916 }
2917 if level == 0 {
2918 break;
2919 }
2920 }
2921 *s = &s[consumed..];
2922 // c:2421 — `return level;`. 0 on balanced match, positive on
2923 // ran-off-end (unbalanced input).
2924 level
2925}
2926
2927/// zlong with base autodetection. Returns the parsed value AND a
2928/// `&str` slice pointing to the first unconsumed character —
2929/// matching C's `char **t` out-arg.
2930///
2931/// C signature: `zlong zstrtol(const char *s, char **t, int base)`.
2932/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
2933/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Otherwise `base`
2934/// must be in [2, 36]; values outside that range emit `zerr` and
2935/// return 0.
2936///
2937/// On overflow, mirrors C's "number truncated after N digits"
2938/// behaviour: emits a `zwarn` and returns the truncated value
2939/// (last in-range computation).
2940/// Port of `zstrtol(const char *s, char **t, int base)` from `Src/utils.c:2427`.
2941/// WARNING: param names don't match C — Rust=(s, base) vs C=(s, t, base)
2942pub fn zstrtol(s: &str, base: i32) -> (i64, &str) {
2943 // c:2427
2944 zstrtol_underscore(s, base, false) // c:2427
2945}
2946
2947/// Port of `zstrtol_underscore(const char *s, char **t, int base, int underscore)` from `Src/utils.c:2438`.
2948///
2949/// C signature: `zlong zstrtol_underscore(const char *s, char **t,
2950/// int base, int underscore)`.
2951///
2952/// Convert string to zlong with optional `_` digit-separator support.
2953/// `underscore != 0` allows underscores to appear inside the digit
2954/// run (skipped during accumulation). Returns the parsed value AND
2955/// the unconsumed-tail slice (matching C's `*t = (char *)s` writeback
2956/// at line 2516).
2957///
2958/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
2959/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Bases outside [2, 36]
2960/// emit `zerr` and return `(0, original_s)`. Overflow truncates and
2961/// emits a `zwarn` per c:2510-2512.
2962pub fn zstrtol_underscore(s: &str, base: i32, underscore: bool) -> (i64, &str) {
2963 // c:2438
2964 let bytes = s.as_bytes();
2965 let mut i = 0usize;
2966 // c:2444-2445 — skip leading `inblank` whitespace.
2967 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
2968 i += 1;
2969 }
2970 // c:2447-2450 — handle sign.
2971 let neg = if i < bytes.len() && (bytes[i] == b'-') {
2972 // c:2447 IS_DASH
2973 i += 1;
2974 true
2975 } else if i < bytes.len() && bytes[i] == b'+' {
2976 // c:2449
2977 i += 1;
2978 false
2979 } else {
2980 false
2981 };
2982
2983 // c:2452-2461 — base autodetect when base == 0.
2984 let mut base = base;
2985 if base == 0 {
2986 // c:2452
2987 if i >= bytes.len() || bytes[i] != b'0' {
2988 // c:2453
2989 base = 10; // c:2454
2990 } else {
2991 i += 1; // c:2455 ++s
2992 if i < bytes.len() && (bytes[i] == b'x' || bytes[i] == b'X') {
2993 // c:2455
2994 base = 16;
2995 i += 1;
2996 } else if i < bytes.len() && (bytes[i] == b'b' || bytes[i] == b'B') {
2997 // c:2457
2998 base = 2;
2999 i += 1;
3000 } else {
3001 // c:2459
3002 base = 8; // c:2460
3003 }
3004 }
3005 }
3006 let inp_idx = i; // c:2462 inp = s
3007 if base < 2 || base > 36 {
3008 // c:2463
3009 zerr(&format!(
3010 "invalid base (must be 2 to 36 inclusive): {}",
3011 base
3012 )); // c:2464
3013 return (0, s); // c:2465 return 0
3014 }
3015
3016 let mut calc: u64 = 0; // c:2441
3017 let mut trunc_idx: Option<usize> = None; // c:2440 trunc = NULL
3018 if base <= 10 {
3019 // c:2466
3020 // c:2467-2479 — digit accumulator, low bases.
3021 let max_d = b'0' + base as u8;
3022 while i < bytes.len() {
3023 let c = bytes[i];
3024 if c >= b'0' && c < max_d {
3025 if trunc_idx.is_none() {
3026 let newcalc = calc
3027 .wrapping_mul(base as u64)
3028 .wrapping_add((c - b'0') as u64);
3029 if newcalc < calc {
3030 // c:2473
3031 trunc_idx = Some(i); // c:2474
3032 } else {
3033 calc = newcalc;
3034 }
3035 }
3036 i += 1;
3037 } else if underscore && c == b'_' {
3038 // c:2467 underscore && *s == '_'
3039 i += 1;
3040 } else {
3041 break;
3042 }
3043 }
3044 } else {
3045 // c:2480
3046 // c:2481-2495 — digit accumulator, high bases (>10).
3047 while i < bytes.len() {
3048 let c = bytes[i];
3049 let digit = if c.is_ascii_digit() {
3050 // c:2481 idigit
3051 Some((c - b'0') as u32)
3052 } else if c >= b'a' && c < b'a' + base as u8 - 10 {
3053 // c:2482
3054 Some(((c & 0x1f) + 9) as u32) // c:2486 (*s & 0x1f) + 9
3055 } else if c >= b'A' && c < b'A' + base as u8 - 10 {
3056 // c:2483
3057 Some(((c & 0x1f) + 9) as u32)
3058 } else if underscore && c == b'_' {
3059 // c:2484
3060 i += 1;
3061 continue;
3062 } else {
3063 None
3064 };
3065 match digit {
3066 Some(d) => {
3067 if trunc_idx.is_none() {
3068 let newcalc = calc.wrapping_mul(base as u64).wrapping_add(d as u64);
3069 if newcalc < calc {
3070 // c:2489
3071 trunc_idx = Some(i); // c:2490
3072 } else {
3073 calc = newcalc;
3074 }
3075 }
3076 i += 1;
3077 }
3078 None => break,
3079 }
3080 }
3081 }
3082
3083 // c:2504-2511 — special-case: signed-overflow into top bit.
3084 // The lowest negative number triggers the first test but is
3085 // representable correctly; check it explicitly.
3086 if trunc_idx.is_none() && (calc as i64) < 0 {
3087 // c:2504
3088 let top_bit_only = calc & !(1u64 << 63); // c:2506
3089 if !neg || top_bit_only != 0 {
3090 // c:2505
3091 trunc_idx = Some(i.saturating_sub(1)); // c:2508
3092 calc /= base as u64; // c:2509
3093 }
3094 }
3095
3096 if let Some(t) = trunc_idx {
3097 // c:2512
3098 let digits = t.saturating_sub(inp_idx);
3099 let inp_str = &s[inp_idx..];
3100 zwarn(&format!(
3101 "number truncated after {} digits: {}",
3102 digits, inp_str
3103 )); // c:2513
3104 }
3105
3106 let result = if neg {
3107 // c:2517
3108 -(calc as i64)
3109 } else {
3110 calc as i64
3111 };
3112 (result, &s[i..])
3113}
3114
3115/// Parse unsigned integer with underscore support
3116/// Port from zsh/Src/utils.c zstrtoul_underscore() lines 2528-2575
3117/// Port of `zstrtoul_underscore(const char *s, zulong *retval)` from
3118/// `Src/utils.c:2527-2590`. C body:
3119/// ```c
3120/// if (*s == '+') s++;
3121/// if (*s != '0') base = 10;
3122/// else if (*++s == 'x' || *s == 'X') base = 16, s++;
3123/// else if (*s == 'b' || *s == 'B') base = 2, s++;
3124/// else base = isset(OCTALZEROES) ? 8 : 10;
3125/// ```
3126/// The leading-zero case is option-gated on OCTALZEROES — without
3127/// the option (default), `0777` parses as decimal 777, NOT octal 511.
3128/// Previously the Rust port always treated leading-zero as octal —
3129/// divergent for the default shell setup.
3130pub fn zstrtoul_underscore(s: &str) -> Option<u64> {
3131 // c:2529
3132 let s = s.trim();
3133 let s = s.strip_prefix('+').unwrap_or(s); // c:2533-2534
3134
3135 let (base, rest) = if s.starts_with("0x") || s.starts_with("0X") {
3136 // c:2538
3137 (16, &s[2..])
3138 } else if s.starts_with("0b") || s.starts_with("0B") {
3139 // c:2540
3140 (2, &s[2..])
3141 } else if s.starts_with('0') && s.len() > 1 && isset(OCTALZEROES)
3142 // c:2543
3143 {
3144 (8, &s[1..])
3145 } else {
3146 (10, s)
3147 };
3148
3149 let rest = rest.replace('_', "");
3150 u64::from_str_radix(&rest, base).ok()
3151}
3152
3153/// Port of `int setblock_fd(int turnonblocking, int fd, long *modep)`
3154/// from `Src/utils.c:2579`. Toggles O_NONBLOCK on `fd`.
3155///
3156/// **Signature matches C exactly** (param order `turnonblocking, fd`):
3157/// - `turnonblocking=1` → clear O_NONBLOCK (enable blocking).
3158/// - `turnonblocking=0` → set O_NONBLOCK (disable blocking).
3159///
3160/// Returns:
3161/// - `(true, mode)` if the fd's state was changed.
3162/// - `(false, -1)` if the fd is a regular file (C skips regular
3163/// files entirely — only operates on pipes/sockets/ttys per
3164/// `c:2599 if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`).
3165/// - `(false, mode)` if state was already correct.
3166///
3167/// Rust returns `(state_changed, modep_value)` as a tuple to mirror
3168/// C's `int` return + `long *modep` out-param.
3169pub fn setblock_fd(turnonblocking: bool, fd: i32) -> (bool, libc::c_long) {
3170 // c:2579
3171 #[cfg(unix)]
3172 unsafe {
3173 // c:2598-2600 — `if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`.
3174 let mut st: libc::stat = std::mem::zeroed();
3175 if libc::fstat(fd, &mut st) != 0 {
3176 return (false, -1);
3177 }
3178 // c:2599 — skip regular files (no nonblock concept).
3179 let mode_bits = st.st_mode as u32;
3180 if (mode_bits & libc::S_IFMT as u32) == libc::S_IFREG as u32 {
3181 return (false, -1); // c:2614 *modep = -1; return 0
3182 }
3183 // c:2601 — `*modep = fcntl(fd, F_GETFL, 0);`
3184 let modep = libc::fcntl(fd, libc::F_GETFL, 0) as libc::c_long;
3185 if modep < 0 {
3186 return (false, -1); // c:2602 if (*modep != -1)
3187 }
3188 const NONBLOCK: libc::c_long = libc::O_NONBLOCK as libc::c_long;
3189 if !turnonblocking {
3190 // c:2603-2606 — want to KNOW if blocking was off; set it on.
3191 if (modep & NONBLOCK) != 0 {
3192 return (true, modep); // already nonblock — no-op, but "off"
3193 }
3194 if libc::fcntl(fd, libc::F_SETFL, modep | NONBLOCK) == 0 {
3195 return (true, modep); // c:2606
3196 }
3197 } else {
3198 // c:2607-2611 — want to clear NONBLOCK if currently set.
3199 if (modep & NONBLOCK) != 0 && libc::fcntl(fd, libc::F_SETFL, modep & !NONBLOCK) == 0 {
3200 return (true, modep); // c:2611 state changed
3201 }
3202 }
3203 (false, modep)
3204 }
3205 #[cfg(not(unix))]
3206 {
3207 let _ = (turnonblocking, fd);
3208 (false, -1)
3209 }
3210}
3211
3212/// Port of `int setblock_stdin(void)` from `Src/utils.c:2620-2625`.
3213/// ```c
3214/// long mode;
3215/// return setblock_fd(1, 0, &mode);
3216/// ```
3217/// `turnonblocking=1, fd=0` — turn ON blocking on stdin (clear
3218/// O_NONBLOCK).
3219pub fn setblock_stdin() -> i32 {
3220 // c:2624 — `return setblock_fd(1, 0, &mode);`.
3221 let (changed, _mode) = setblock_fd(true, 0); // c:2624
3222 changed as i32
3223}
3224
3225/// Port of `int read_poll(int fd, int *readchar, int polltty, zlong microseconds)`
3226/// from `Src/utils.c:2645-2738`.
3227///
3228/// Check whether input is pending on `fd` within `timeout_us`
3229/// microseconds. Faithful port of all three C paths:
3230/// 1. The `polltty` non-canonical-tty path (VMIN/VTIME termios
3231/// setup so a raw tty can be polled — c:2665-2696).
3232/// 2. The `HAVE_SELECT` wait (c:2697-2705). Rust uses `poll(2)`
3233/// as the idiom substitution for `select(2)`; `poll` return
3234/// semantics are mapped onto C's `ret` so the `ret < 0`
3235/// error-fallback still triggers.
3236/// 3. The final `setblock_fd` + non-blocking `read` fallback that
3237/// captures a byte when `select`/`poll` errored (c:2718-2729).
3238///
3239/// `readchar` is C's `int *readchar` out-param: when a byte is
3240/// actually consumed during the poll it is written through
3241/// `readchar` (so `read -t -k` on a raw tty gets the byte). `polltty`
3242/// is C's `int polltty` truthiness. Returns C's `(ret > 0)`.
3243///
3244/// WARNING: param order differs from C — Rust=(fd, readchar,
3245/// polltty, timeout_us) vs C=(fd, readchar, polltty, microseconds);
3246/// C's `microseconds` is `timeout_us` here.
3247pub fn read_poll(fd: i32, readchar: &mut i32, polltty: bool, timeout_us: i64) -> bool {
3248 // c:2645
3249 #[cfg(unix)]
3250 {
3251 let mut ret: i32 = -1; // c:2647
3252 let mut mode: libc::c_long = -1; // c:2648
3253 // C reassigns the `polltty` param; hold it as the VMIN-derived int.
3254 let mut polltty: i32 = polltty as i32; // c:2645
3255 let mut ti: Option<libc::termios> = None; // c:2659 `struct ttyinfo ti;`
3256
3257 // c:2662-2663 — `if (fd < 0 || (polltty && !isatty(fd))) polltty = 0;`
3258 if fd < 0 || (polltty != 0 && unsafe { libc::isatty(fd) } == 0) {
3259 polltty = 0; // no tty to poll
3260 }
3261
3262 // c:2665-2696 — HAS_TIO && !__CYGWIN__: non-canonical VMIN poll setup.
3263 if polltty != 0 && fd >= 0 {
3264 // c:2686 — `gettyinfo(&ti);` (operates on global SHTTY, as in C).
3265 if let Some(mut t) = gettyinfo() {
3266 // c:2687 — `if ((polltty = ti.tio.c_cc[VMIN]))`
3267 polltty = t.c_cc[libc::VMIN] as i32;
3268 if polltty != 0 {
3269 t.c_cc[libc::VMIN] = 0; // c:2688
3270 // c:2690 — termios timeout is 10ths of a second.
3271 t.c_cc[libc::VTIME] = (timeout_us / 100_000) as libc::cc_t; // c:2690
3272 settyinfo(&t); // c:2691
3273 }
3274 ti = Some(t);
3275 } else {
3276 // gettyinfo failed (SHTTY closed) — nothing to poll via VMIN.
3277 polltty = 0;
3278 }
3279 }
3280
3281 // c:2697-2705 — HAVE_SELECT wait, using poll(2) as the select(2) idiom.
3282 let timeout_ms = (timeout_us / 1000) as i32; // c:2698-2699 (expire_tv)
3283 if fd > -1 {
3284 // c:2701-2703
3285 let mut fds = [libc::pollfd {
3286 fd: fd as RawFd,
3287 events: libc::POLLIN,
3288 revents: 0,
3289 }];
3290 let raw = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
3291 ret = if raw > 0 {
3292 // Map poll revents onto select's "fd is readable" ret>0.
3293 if (fds[0].revents & libc::POLLIN) != 0 {
3294 1
3295 } else {
3296 0
3297 }
3298 } else {
3299 raw // 0 = timeout, -1 = error (triggers fallback below)
3300 };
3301 } else {
3302 // c:2705 — `select(0, NULL, NULL, NULL, &expire_tv)`: pure sleep.
3303 ret = unsafe { libc::poll(std::ptr::null_mut(), 0, timeout_ms) };
3304 }
3305
3306 // c:2718 — `if (fd >= 0 && ret < 0 && !errflag)`
3307 if fd >= 0 && ret < 0 && errflag.load(Ordering::Relaxed) == 0 {
3308 // c:2723 — final attempt: non-blocking read of a single char.
3309 // `(polltty || setblock_fd(0, fd, &mode))`
3310 let can_read = if polltty != 0 {
3311 true // polltty short-circuits setblock_fd; mode stays -1
3312 } else {
3313 let (changed, m) = setblock_fd(false, fd); // c:2723
3314 mode = m; // C writes *modep unconditionally
3315 changed
3316 };
3317 if can_read {
3318 let mut c: u8 = 0;
3319 let n = unsafe {
3320 libc::read(fd, &mut c as *mut u8 as *mut libc::c_void, 1)
3321 };
3322 if n > 0 {
3323 *readchar = c as i32; // c:2724
3324 ret = 1; // c:2725
3325 }
3326 }
3327 // c:2727-2728 — `if (mode != -1) fcntl(fd, F_SETFL, mode);`
3328 if mode != -1 {
3329 unsafe {
3330 libc::fcntl(fd, libc::F_SETFL, mode);
3331 }
3332 }
3333 }
3334
3335 // c:2730-2736 — HAS_TIO: restore canonical VMIN=1 / VTIME=0.
3336 if polltty != 0 {
3337 if let Some(mut t) = ti {
3338 t.c_cc[libc::VMIN] = 1; // c:2732
3339 t.c_cc[libc::VTIME] = 0; // c:2733
3340 settyinfo(&t); // c:2734
3341 }
3342 }
3343
3344 ret > 0 // c:2737
3345 }
3346 #[cfg(not(unix))]
3347 {
3348 let _ = (fd, readchar, polltty, timeout_us);
3349 false
3350 }
3351}
3352
3353/// Compute time difference in microseconds (from utils.c timespec_diff_us)
3354/// Port of `timespec_diff_us(const struct timespec *t1, const struct timespec *t2)` from `Src/utils.c:2752`.
3355/// Rust idiom replacement: `Instant::duration_since().as_micros()`
3356/// replaces the C tv_sec/tv_nsec arithmetic + sign-flip dance.
3357pub fn timespec_diff_us(t1: &std::time::Instant, t2: &std::time::Instant) -> i64 {
3358 if *t2 > *t1 {
3359 t2.duration_since(*t1).as_micros() as i64
3360 } else {
3361 -(t1.duration_since(*t2).as_micros() as i64)
3362 }
3363}
3364
3365/// Port of `int zmonotime(time_t *tloc)` from Src/utils.c:2780.
3366///
3367/// "Like time(), but uses the monotonic clock." Reads CLOCK_MONOTONIC
3368/// and returns `tv_sec`; writes the same value through `tloc` if
3369/// non-NULL (Rust: `Some(&mut t)`).
3370pub fn zmonotime(tloc: Option<&mut i64>) -> i64 {
3371 // c:2780
3372 #[cfg(unix)]
3373 {
3374 // c:2782 — `struct timespec ts;`
3375 let mut ts = libc::timespec {
3376 // c:2782
3377 tv_sec: 0,
3378 tv_nsec: 0,
3379 };
3380 // c:2783 — `zgettime_monotonic_if_available(&ts);`
3381 unsafe {
3382 libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); // c:2783
3383 }
3384 // c:2784-2785 — `if (tloc) *tloc = ts.tv_sec;`
3385 if let Some(t) = tloc {
3386 // c:2784
3387 *t = ts.tv_sec as i64; // c:2785
3388 }
3389 ts.tv_sec as i64 // c:2786 return ts.tv_sec
3390 }
3391 #[cfg(not(unix))]
3392 {
3393 let _ = tloc;
3394 0
3395 }
3396}
3397
3398/// Check if string looks like a number
3399/// Check whether a string parses as a decimal integer.
3400/// Sleep for a given number of seconds (fractional)
3401/// Sleep for a fractional number of seconds.
3402/// Port of `int zsleep(long us)` from Src/utils.c:2797.
3403///
3404/// "Sleep for the given number of microseconds." Wraps
3405/// `nanosleep(2)` with EINTR retry, returning 1 on completion, 0
3406/// on permanent error.
3407pub fn zsleep(us: i64) -> i32 {
3408 // c:2797
3409 let mut sleeptime = libc::timespec {
3410 // c:2797-2803
3411 tv_sec: (us / 1_000_000) as libc::time_t,
3412 tv_nsec: ((us % 1_000_000) * 1000) as libc::c_long,
3413 };
3414 #[cfg(unix)]
3415 {
3416 loop {
3417 // c:2804
3418 let mut rem = libc::timespec {
3419 tv_sec: 0,
3420 tv_nsec: 0,
3421 };
3422 let ret = unsafe { libc::nanosleep(&sleeptime, &mut rem) }; // c:2806
3423 if ret == 0 {
3424 // c:2808
3425 return 1;
3426 }
3427 let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
3428 if err != libc::EINTR {
3429 // c:2810
3430 return 0; // c:2811
3431 }
3432 sleeptime = rem; // c:2812
3433 }
3434 }
3435 #[cfg(not(unix))]
3436 {
3437 let _ = sleeptime;
3438 std::thread::sleep(std::time::Duration::from_micros(us.max(0) as u64));
3439 1
3440 }
3441}
3442
3443/// Sleep random amount up to max microseconds (from utils.c zsleep_random)
3444/// Port of `int zsleep_random(long max_us, time_t end_time)`
3445/// from Src/utils.c:2833.
3446///
3447/// "Sleep for time (fairly) randomly up to max_us microseconds.
3448/// Don't let the time extend beyond end_time. end_time is compared
3449/// to the current *monotonic* clock time. Return 1 if that seemed
3450/// to work, else 0."
3451pub fn zsleep_random(max_us: i64, end_time: i64) -> i32 {
3452 // c:2833
3453 let now = zmonotime(None); // c:2833
3454 let r16 = unsafe { libc::rand() } & 0xFFFF; // c:2845
3455 let mut r: i64 = (max_us >> 16) * (r16 as i64); // c:2852
3456 while r != 0 && now + (r / 1_000_000) > end_time {
3457 // c:2858
3458 r >>= 1; // c:2859
3459 }
3460 if r != 0 {
3461 // c:2860
3462 zsleep(r) // c:2861
3463 } else {
3464 0 // c:2862
3465 }
3466}
3467
3468/// Port of `int checkrmall(char *s)` from `Src/utils.c:2867`.
3469///
3470/// C body (c:2867-2919): count files in directory `s` (capped at 100,
3471/// honoring GLOBDOTS), emit one of 4 prompts based on count, optionally
3472/// sleep 10s under RMSTARWAIT, then `getquery("ny", 1)` for confirm.
3473///
3474/// Previous Rust port was a FAKE — single generic prompt, no file
3475/// count, no shout/errflag checks, no RMSTARWAIT, wrong valid-chars
3476/// order ("yn" instead of "ny" so 'n' isn't the default-first-arm).
3477/// Lost 9 of the 10 C semantic branches.
3478pub fn checkrmall(s: &str) -> bool {
3479 // c:2867
3480 // c:2871-2872 — `if (!shout) return 1;` — no controlling tty for
3481 // prompt output, default to yes. Rust analogue: if stderr isn't
3482 // a tty, skip the prompt and approve.
3483 let shout_is_tty = unsafe { libc::isatty(2) != 0 }; // c:2871
3484 if !shout_is_tty {
3485 // c:2871
3486 return true; // c:2872
3487 }
3488 // c:2873-2878 — `if (*s != '/')` build absolute via pwd prefix.
3489 let s_owned: String; // c:2873
3490 let s_abs: &str = if !s.starts_with('/') {
3491 // c:2873
3492 let pwd = getsparam("PWD").unwrap_or_default(); // c:2874 pwd[1]
3493 s_owned = if pwd.len() > 1 {
3494 // c:2874 if (pwd[1])
3495 crate::ported::string::tricat(&pwd, "/", s) // c:2875 zhtricat
3496 } else {
3497 // c:2876
3498 crate::ported::string::dyncat("/", s) // c:2877
3499 };
3500 s_owned.as_str()
3501 } else {
3502 s
3503 };
3504 let max_count: i32 = 100; // c:2879
3505 let mut count: i32 = 0; // c:2870 count = 0
3506 // c:2880-2892 — opendir + readdir loop, count entries (skip
3507 // dotfiles when !GLOBDOTS), cap at max_count.
3508 let ignoredots = !isset(GLOBDOTS); // c:2881
3509 // c:2880 — `if (!(dir = opendir(unmeta(s)))) return;` then
3510 // `while ((fn = zreaddir(dir, 1))) { ... }`.
3511 if let Ok(mut dir) = fs::read_dir(s_abs) {
3512 while let Some(fname) = zreaddir(&mut dir, 1) {
3513 if ignoredots && fname.starts_with('.') {
3514 // c:2885
3515 continue; // c:2886
3516 }
3517 count += 1; // c:2887
3518 if count > max_count {
3519 // c:2888
3520 break; // c:2889
3521 }
3522 }
3523 // c:closedir(dir) auto on drop
3524 }
3525 // c:2893-2904 — four-arm prompt based on file count.
3526 let stderr_h = io::stderr();
3527 let mut stderr_w = stderr_h.lock();
3528 if count > max_count {
3529 // c:2893
3530 let _ = write!(
3531 stderr_w,
3532 "zsh: sure you want to delete more than {} files in ",
3533 max_count
3534 ); // c:2894
3535 } else if count == 1 {
3536 // c:2896
3537 let _ = write!(stderr_w, "zsh: sure you want to delete the only file in ");
3538 // c:2897
3539 } else if count > 0 {
3540 // c:2898
3541 let _ = write!(
3542 stderr_w,
3543 "zsh: sure you want to delete all {} files in ",
3544 count
3545 ); // c:2899
3546 } else {
3547 // c:2901
3548 // c:2902 — `We don't know how many files the glob will expand to`
3549 let _ = write!(stderr_w, "zsh: sure you want to delete all the files in ");
3550 // c:2903
3551 }
3552 // c:2905 — `nicezputs(s, shout)` — escape non-printables in path.
3553 let _ = nicezputs(s_abs, &mut stderr_w); // c:2905
3554 // c:2906-2912 — RMSTARWAIT: print "? (waiting ten seconds)",
3555 // flush, beep, sleep 10, then newline.
3556 if isset(RMSTARWAIT) {
3557 // c:2906
3558 let _ = write!(stderr_w, "? (waiting ten seconds)"); // c:2907
3559 let _ = stderr_w.flush(); // c:2908
3560 drop(stderr_w); // release lock around zbeep + sleep
3561 zbeep(); // c:2909
3562 std::thread::sleep(std::time::Duration::from_secs(10)); // c:2910
3563 let _ = writeln!(io::stderr()); // c:2911 fputc('\n', shout)
3564 } else {
3565 drop(stderr_w);
3566 }
3567 // c:2913-2914 — `if (errflag) return 0;`
3568 if errflag.load(Ordering::Relaxed) != 0 {
3569 // c:2913
3570 return false; // c:2914
3571 }
3572 // c:2915-2917 — emit "[yn]? ", flush, beep.
3573 let _ = io::stderr().write_all(b" [yn]? "); // c:2915 nicezputs of " [yn]? "
3574 let _ = io::stderr().flush(); // c:2916
3575 zbeep(); // c:2917
3576 // c:2918 — `return (getquery("ny", 1) == 'y');`. Default-first-
3577 // char is 'n' (no) — getquery's first valid char is the default
3578 // returned when the user just hits Enter.
3579 getquery(Some("ny"), 1) == b'y' as i32 // c:2918
3580}
3581
3582/// Port of `ssize_t read_loop(int fd, char *buf, size_t len)` from
3583/// `Src/utils.c:2923`.
3584///
3585/// C body (c:2923-2945):
3586/// ```c
3587/// while (1) {
3588/// ssize_t ret = read(fd, buf, len);
3589/// if (ret == len) break;
3590/// if (ret <= 0) {
3591/// if (ret < 0) {
3592/// if (errno == EINTR) continue; // c:2933
3593/// if (fd != SHTTY) // c:2935
3594/// zwarn("read failed: %e", errno); // c:2936
3595/// }
3596/// return ret;
3597/// }
3598/// buf += ret; len -= ret;
3599/// }
3600/// ```
3601///
3602/// The previous Rust port omitted the `zwarn("read failed: %e", errno)`
3603/// emission at c:2935-2936. That's a real diagnostic divergence:
3604/// any non-SHTTY read failure in zsh emits a stderr message; the
3605/// Rust port silently propagated the io::Error to the caller, which
3606/// in most call sites was discarded (`let _ = read_loop(...)`).
3607/// Users debugging an interrupted file read (e.g. `< /broken/fd`)
3608/// would see no error message at all.
3609/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
3610pub fn read_loop(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
3611 // c:2923
3612 #[cfg(unix)]
3613 {
3614 let mut total = 0;
3615 while total < buf.len() {
3616 let n = unsafe {
3617 libc::read(
3618 // c:2928
3619 fd,
3620 buf[total..].as_mut_ptr() as *mut libc::c_void,
3621 buf.len() - total,
3622 )
3623 };
3624 if n <= 0 {
3625 // c:2931
3626 if n < 0 {
3627 let e = io::Error::last_os_error();
3628 if e.kind() == io::ErrorKind::Interrupted {
3629 // c:2933
3630 continue; // c:2934
3631 }
3632 // c:2935-2936 — `if (fd != SHTTY) zwarn("read failed: %e", errno);`
3633 let shtty = SHTTY.load(Ordering::Relaxed);
3634 if fd != shtty {
3635 // c:2935
3636 zwarn(
3637 // c:2936
3638 &format!("read failed: {}", e),
3639 );
3640 }
3641 return Err(e);
3642 }
3643 break;
3644 }
3645 total += n as usize;
3646 }
3647 Ok(total)
3648 }
3649 #[cfg(not(unix))]
3650 {
3651 let _ = (fd, buf);
3652 Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
3653 }
3654}
3655
3656/// Port of `ssize_t write_loop(int fd, const char *buf, size_t len)` from
3657/// `Src/utils.c:2949`.
3658///
3659/// C body (c:2949-2970):
3660/// ```c
3661/// while (1) {
3662/// ssize_t ret = write(fd, buf, len);
3663/// if (ret == len) break;
3664/// if (ret < 0) {
3665/// if (errno == EINTR) continue;
3666/// if (fd != SHTTY) // c:2960
3667/// zwarn("write failed: %e", errno); // c:2961
3668/// return -1;
3669/// }
3670/// buf += ret; len -= ret;
3671/// }
3672/// ```
3673///
3674/// Same divergence as `read_loop`: previous Rust port omitted the
3675/// `zwarn("write failed: %e", errno)` emission for non-SHTTY fds.
3676/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
3677pub fn write_loop(fd: i32, buf: &[u8]) -> io::Result<usize> {
3678 // c:2949
3679 #[cfg(unix)]
3680 {
3681 let mut total = 0;
3682 while total < buf.len() {
3683 let n = unsafe {
3684 libc::write(
3685 // c:2954
3686 fd,
3687 buf[total..].as_ptr() as *const libc::c_void,
3688 buf.len() - total,
3689 )
3690 };
3691 if n <= 0 {
3692 if n < 0 {
3693 let e = io::Error::last_os_error();
3694 if e.kind() == io::ErrorKind::Interrupted {
3695 // c:2958
3696 continue; // c:2959
3697 }
3698 // c:2960-2961 — `if (fd != SHTTY) zwarn("write failed: %e", errno);`
3699 let shtty = SHTTY.load(Ordering::Relaxed);
3700 if fd != shtty {
3701 // c:2960
3702 zwarn(
3703 // c:2961
3704 &format!("write failed: {}", e),
3705 );
3706 }
3707 return Err(e);
3708 }
3709 break;
3710 }
3711 total += n as usize;
3712 }
3713 Ok(total)
3714 }
3715 #[cfg(not(unix))]
3716 {
3717 let _ = (fd, buf);
3718 Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
3719 }
3720}
3721
3722/// Read a single character (from utils.c read1char).
3723///
3724/// Port of `static int read1char(int echo)` from `Src/utils.c:2972`.
3725///
3726/// C body (c:2972-2988):
3727/// ```c
3728/// char c;
3729/// int q = queue_signal_level();
3730/// dont_queue_signals();
3731/// while (read(SHTTY, &c, 1) != 1) {
3732/// if (errno != EINTR || errflag || retflag || breaks || contflag) {
3733/// restore_queue_signals(q);
3734/// return -1;
3735/// }
3736/// }
3737/// restore_queue_signals(q);
3738/// if (echo) write_loop(SHTTY, &c, 1);
3739/// return (unsigned char) c;
3740/// ```
3741///
3742/// Returns the byte read (0..=255) on success, `-1` on error. Three
3743/// divergences in the previous Rust port:
3744/// 1. **Read from stdin** instead of SHTTY (the tty fd). zsh reads
3745/// single chars during `read -k`, `bindkey`, query prompts —
3746/// always against the controlling terminal, not the standard
3747/// input which may be a pipe.
3748/// 2. **No `echo` parameter.** When `echo=1` C writes the byte
3749/// back to SHTTY so the user sees what they typed.
3750/// 3. **No `errflag` / `retflag` / `breaks` / `contflag` early-exit.**
3751/// A SIGINT-driven errflag should abort the read; previous Rust
3752/// port had no break path.
3753/// WARNING: returns i32 not `Option<char>` to match C's signature
3754/// (`(unsigned char) c` for success, `-1` for error).
3755pub fn read1char(echo: i32) -> i32 {
3756 // c:2972
3757 #[cfg(unix)]
3758 {
3759 let shtty = SHTTY.load(Ordering::Relaxed);
3760 if shtty < 0 {
3761 return -1;
3762 }
3763 let mut c: u8 = 0;
3764 loop {
3765 let rc = unsafe { libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1) };
3766 if rc == 1 {
3767 // c:2978
3768 break;
3769 }
3770 // c:2979 — `if (errno != EINTR || errflag || retflag || breaks
3771 // || contflag) return -1;`
3772 let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
3773 if err != libc::EINTR {
3774 return -1;
3775 }
3776 // Check errflag interrupt flag (the most observable of the
3777 // four guards; retflag/breaks/contflag are control-flow
3778 // states that don't apply outside the exec engine).
3779 if errflag.load(Ordering::Relaxed) != 0 {
3780 return -1;
3781 }
3782 }
3783 // c:2985-2986 — `if (echo) write_loop(SHTTY, &c, 1);`
3784 if echo != 0 {
3785 // c:2985
3786 let _ = write_loop(shtty, &[c]); // c:2986
3787 }
3788 // c:2987 — `return (unsigned char) c;`
3789 c as i32
3790 }
3791 #[cfg(not(unix))]
3792 {
3793 let _ = echo;
3794 -1
3795 }
3796}
3797
3798/// Port of `int noquery(int purge)` from Src/utils.c:2992.
3799///
3800/// "If anything has been typed before the query, return without
3801/// asking. Optionally also purge the input queue." Returns the
3802/// number of bytes pending on `SHTTY` (via FIONREAD ioctl); when
3803/// `purge` is set, drains them before returning.
3804pub fn noquery(purge: bool) -> i32 {
3805 // c:2992
3806 let mut val: libc::c_int = 0; // c:2992
3807 #[cfg(unix)]
3808 {
3809 let shtty = SHTTY.load(Ordering::Relaxed); // c:2999
3810 if shtty == -1 {
3811 return 0;
3812 }
3813 unsafe {
3814 // c:2999
3815 libc::ioctl(shtty, libc::FIONREAD, &mut val as *mut libc::c_int);
3816 }
3817 if purge {
3818 // c:3000
3819 let mut c: u8 = 0;
3820 for _ in 0..val {
3821 // c:3001
3822 let _ = unsafe {
3823 // c:3002
3824 libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1)
3825 };
3826 }
3827 }
3828 }
3829 val // c:3009
3830}
3831
3832/// Port of `int getquery(char *valid_chars, int purge)` from
3833/// `Src/utils.c:3014`.
3834///
3835/// Read a single keystroke from the TTY in raw mode and return its
3836/// character code (as `int`, mirroring C). If `valid_chars` is `None`
3837/// (C `NULL`) any byte is accepted. If `valid_chars` is `Some(s)`, only
3838/// chars in `s` (plus `\n` which maps to `s[0]`) are accepted; other
3839/// input rings the bell and re-prompts. `Y`/`N` are pre-normalised to
3840/// `y`/`n` per c:3041-3045.
3841///
3842/// `purge != 0` triggers `noquery(purge)`'s queue-purge path — if input
3843/// is queued, returns `'n'` without reading.
3844pub fn getquery(valid_chars: Option<&str>, purge: i32) -> i32 {
3845 // c:3014
3846 // c:3016 — `int c, d, nl = 0;`
3847 let mut c: i32;
3848 let mut d: i32;
3849 let mut nl: i32 = 0;
3850 // c:3017 — `int isem = !strcmp(term, "emacs");`
3851 // Stub: `term` is the $TERM environment global, declared in
3852 // `Src/init.c` (extern in zsh.h). Local stub reads $TERM from
3853 // paramtab; absent → empty string.
3854 let term: String = getsparam("TERM").unwrap_or_default();
3855 let isem: bool = term == "emacs";
3856 // c:3018 — `struct ttyinfo ti;`
3857 // Rust: termios is the canonical TTY-state type returned by gettyinfo.
3858 let mut ti: libc::termios; // c:3018
3859
3860 attachtty(mypgrp.load(Ordering::Relaxed)); // c:3020 attachtty(mypgrp)
3861
3862 // c:3022 — `gettyinfo(&ti);`
3863 ti = match gettyinfo() {
3864 Some(t) => t,
3865 None => return -1,
3866 };
3867 // c:3023-3030 — `ti.tio.c_lflag &= ~ECHO; if (!isem) {
3868 // ti.tio.c_lflag &= ~ICANON;
3869 // ti.tio.c_cc[VMIN] = 1;
3870 // ti.tio.c_cc[VTIME] = 0; }`
3871 #[cfg(unix)]
3872 {
3873 ti.c_lflag &= !(libc::ECHO); // c:3024
3874 if !isem {
3875 // c:3025
3876 ti.c_lflag &= !(libc::ICANON); // c:3026
3877 ti.c_cc[libc::VMIN] = 1; // c:3027
3878 ti.c_cc[libc::VTIME] = 0; // c:3028
3879 }
3880 }
3881 // c:3037 — `settyinfo(&ti);`
3882 settyinfo(&ti);
3883
3884 // c:3039 — `if (noquery(purge))`
3885 if noquery(purge != 0) != 0 {
3886 // Stub: `shttyinfo` is the canonical saved-TTY-state global,
3887 // declared in `Src/init.c` (extern in zsh.h:1856). Without the
3888 // global tracked here we re-fetch current termios as a degraded
3889 // best-effort restore.
3890 if !isem {
3891 // c:3040
3892 if let Some(saved) = gettyinfo() {
3893 // c:3041 settyinfo(&shttyinfo)
3894 settyinfo(&saved);
3895 }
3896 }
3897 let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"n\n"); // c:3042
3898 return b'n' as i32; // c:3043
3899 }
3900
3901 // c:3046-3061 — `while ((c = read1char(0)) >= 0) { ... }`
3902 c = -1;
3903 loop {
3904 let cc = read1char(0); // c:3046
3905 if cc < 0 {
3906 c = cc;
3907 break;
3908 }
3909 c = cc;
3910 if c == b'Y' as i32 {
3911 // c:3047
3912 c = b'y' as i32; // c:3048
3913 } else if c == b'N' as i32 {
3914 // c:3049
3915 c = b'n' as i32; // c:3050
3916 }
3917 if valid_chars.is_none() {
3918 // c:3051
3919 break; // c:3052
3920 }
3921 if c == b'\n' as i32 {
3922 // c:3053
3923 // c:3054 — `c = *valid_chars;`
3924 c = valid_chars.unwrap().bytes().next().unwrap_or(b'\n') as i32;
3925 nl = 1; // c:3055
3926 break; // c:3056
3927 }
3928 // c:3058 — `if (strchr(valid_chars, c))`
3929 if valid_chars.unwrap().bytes().any(|b| b as i32 == c) {
3930 nl = 1; // c:3059
3931 break; // c:3060
3932 }
3933 zbeep(); // c:3062
3934 }
3935 if c >= 0 {
3936 // c:3064
3937 // c:3065-3066 — `char buf = (char)c; write_loop(SHTTY, &buf, 1);`
3938 let buf = [c as u8]; // c:3065
3939 let _ = write_loop(SHTTY.load(Ordering::Relaxed), &buf); // c:3066
3940 }
3941 if nl != 0 {
3942 // c:3068
3943 let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n"); // c:3069
3944 }
3945
3946 if isem {
3947 // c:3071
3948 if c != b'\n' as i32 {
3949 // c:3072
3950 // c:3073 — `while ((d = read1char(1)) >= 0 && d != '\n');`
3951 loop {
3952 d = read1char(1);
3953 if d < 0 || d == b'\n' as i32 {
3954 break;
3955 }
3956 }
3957 }
3958 } else if c != b'\n' as i32 && valid_chars.is_none() {
3959 // c:3075
3960 // c:3077-3094 — MULTIBYTE_SUPPORT branch: drain trailing bytes
3961 // of an incomplete multibyte sequence.
3962 if isset(MULTIBYTE) && c >= 0 {
3963 // c:3077
3964 // c:3083-3093 — `for (;;) { ret = mbrlen(&cc, 1, &mbs);
3965 // if (ret != MB_INCOMPLETE) break;
3966 // c = read1char(1); if (c < 0) break;
3967 // cc = (char)c; }`
3968 // Rust: model MB_INCOMPLETE detection by checking whether
3969 // the byte buffer so far forms a complete UTF-8 prefix.
3970 // `cc` carries the current byte under examination.
3971 let mut cc: u8 = c as u8; // c:3082
3972 let mut accum: Vec<u8> = vec![cc];
3973 loop {
3974 // mbrlen returns MB_INCOMPLETE when the partial buffer
3975 // doesn't yet form a complete UTF-8 character. Rust
3976 // equivalent: from_utf8 errors with error_len() == None
3977 // when more bytes are needed.
3978 let incomplete = match std::str::from_utf8(&accum) {
3979 Ok(_) => false,
3980 Err(e) => e.error_len().is_none(),
3981 };
3982 if !incomplete {
3983 // c:3088
3984 break;
3985 }
3986 let nc = read1char(1); // c:3089
3987 if nc < 0 {
3988 // c:3090
3989 break; // c:3091
3990 }
3991 cc = nc as u8; // c:3092
3992 accum.push(cc);
3993 }
3994 let _ = cc;
3995 }
3996 // c:3097 — `write_loop(SHTTY, "\n", 1);`
3997 let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n");
3998 }
3999
4000 // c:3101 — `settyinfo(&shttyinfo);` — restore saved TTY state.
4001 // Stub-degraded path: refetch current termios. The proper port
4002 // requires wiring an `shttyinfo` global in init.rs.
4003 if let Some(saved) = gettyinfo() {
4004 settyinfo(&saved);
4005 }
4006
4007 c // c:3102 return c
4008}
4009
4010// `spscan` (Src/utils.c:3109) — canonical port lives below the
4011// thread_local block in this file. The pre-existing 3-arg
4012// `(name, candidates[], threshold) → Option<String>` shape was a
4013// drift fake (C is `void spscan(HashNode hn, scanflags)`); deleted
4014// because it had zero call sites and conflicted with the faithful
4015// port spckword needs.
4016
4017// spellcheck a word // c:3123
4018// fix s ; if hist is nonzero, fix the history list too // c:3124
4019
4020// File-static state shared with the (inlined) spscan callback. C uses
4021// raw file-statics at utils.c:3045-3050 (`best`, `d`, `guess`, `ic`,
4022// `spckpat`, `spnamepat`); Rust port mirrors them as thread_locals
4023// (per-evaluator per PORT_PLAN.md bucket-1) so concurrent worker
4024// threads each have their own correction state.
4025thread_local! {
4026 /// Port of `static int d;` from `Src/utils.c:3045`. Best dist seen.
4027 static SPCK_D: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
4028 /// Port of `static char *best;` from `Src/utils.c:3046`. Best-match.
4029 static SPCK_BEST: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4030 /// Port of `static char *guess;` from `Src/utils.c:3047`. Word to fix.
4031 static SPCK_GUESS: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4032 /// Port of `static Patprog spckpat;` from `Src/utils.c:3049`.
4033 static SPCK_PAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4034 = const { std::cell::RefCell::new(None) };
4035 /// Port of `static Patprog spnamepat;` from `Src/utils.c:3050`.
4036 static SPCK_NAMEPAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4037 = const { std::cell::RefCell::new(None) };
4038}
4039
4040/// Port of `static void spscan(HashNode hn, UNUSED(int scanflags))` from
4041/// `Src/utils.c:3074`. Inlined per-call below because hashtable.rs's
4042/// scan helpers don't model C's `scanhashtable(table, ..., scanfn, ...)`
4043/// callback shape — Rust call sites iterate the table directly. C
4044/// body:
4045/// ```c
4046/// int nd = spdist(hn->nam, guess, (int) strlen(guess) / 4 + 1);
4047/// if (nd < d && (!spckpat || !pattry(spckpat, hn->nam))) {
4048/// best = hn->nam;
4049/// d = nd;
4050/// }
4051/// ```
4052fn spscan(name: &str) {
4053 // c:3074
4054 let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4055 if guess.is_empty() {
4056 return;
4057 }
4058 let thresh = guess.len() / 4 + 1; // c:3076 `(int) strlen(guess) / 4 + 1`
4059 let nd = spdist(name, &guess, thresh) as i32; // c:3076
4060 let d = SPCK_D.with(|c| c.get());
4061 if nd < d {
4062 // c:3077
4063 // c:3078 — `if (!spckpat || !pattry(spckpat, hn->nam))`.
4064 let allow = SPCK_PAT.with(|p| {
4065 p.borrow()
4066 .as_ref()
4067 .map(|prog| !crate::ported::pattern::pattry(prog, name))
4068 .unwrap_or(true)
4069 });
4070 if allow {
4071 SPCK_BEST.with(|b| *b.borrow_mut() = Some(name.to_string())); // c:3079
4072 SPCK_D.with(|c| c.set(nd)); // c:3080
4073 }
4074 }
4075}
4076
4077/// Port of `void spckword(char **s, int hist, int cmd, int ask)` from
4078/// `Src/utils.c:3128`.
4079///
4080/// Spell-check `*s`. If a near-match is found in the appropriate
4081/// hashtable (params for `$x`, command tables for command position,
4082/// directory entries otherwise) AND the user accepts the correction
4083/// (`ask=0` auto-accepts), replace `*s` in place with the corrected
4084/// form and (if `hist!=0`) rewrite the history entry too.
4085///
4086/// Faithful 1:1 line-by-line port. Interactive prompting (c:3273-3287)
4087/// is stubbed to auto-accept when `ask=1` since `getquery` /
4088/// `promptexpand` / `shout` / `zbeep` aren't yet wired in zshrs —
4089/// flagged with WARNING at the prompt site.
4090///
4091/// Caller updates: previous Rust signature `(word, candidates[], threshold)
4092/// → Option<String>` is gone — `lex.rs` builds candidate lists itself,
4093/// but C's spckword scans the canonical hashtables directly. The new
4094/// signature matches C exactly: takes `&mut String` for in-place fix.
4095pub fn spckword(s: &mut String, hist: i32, cmd: i32, ask: i32) {
4096 use crate::ported::hashtable::{
4097 aliastab_lock, cmdnamtab_lock, fillcmdnamtable, pathchecked, reswdtab_lock, shfunctab_lock,
4098 };
4099 use crate::ported::params::{getsparam, paramtab};
4100 use crate::ported::pattern::{patcompile, PAT_HEAPDUP};
4101 use crate::ported::zsh_h::{
4102 isset, Dash, Equals, Stringg as StringTok, Tilde, AUTOCD, HASHLISTALL,
4103 };
4104 // c:3130-3133 — locals.
4105 let _t: Option<String>;
4106 let mut ic: char = '\0'; // c:3131
4107 let mut preflen: usize = 0; // c:3132
4108 // c:3133 — `autocd = cmd && isset(AUTOCD) && strcmp(*s, ".") && strcmp(*s, "..")`.
4109 let autocd = cmd != 0 && isset(AUTOCD) && s != "." && s != "..";
4110
4111 // c:3135-3136 — `if (!(*s)[0] || !(*s)[1]) return;`
4112 if s.len() < 2 {
4113 return;
4114 }
4115 // c:3137-3140 — HISTFLAG_NOEXEC or leading %/- skip.
4116 let bytes = s.as_bytes();
4117 let first = bytes[0] as char;
4118 let histdone = crate::ported::hist::histdone.load(std::sync::atomic::Ordering::Relaxed); // c:3137
4119 if (histdone & HISTFLAG_NOEXEC) != 0
4120 || (if cmd != 0 {
4121 first == '%' // c:3139 — leading % is a job
4122 } else {
4123 first == '-' || first == Dash // c:3139 — leading hyphen is an option
4124 })
4125 {
4126 return; // c:3140
4127 }
4128 // c:3141-3142 — `if (!strcmp(*s, "in")) return;`.
4129 if s == "in" {
4130 return; // c:3142
4131 }
4132 // c:3143-3155 — `cmd` branch: skip if it's already a known
4133 // function/builtin/cmdname/alias/reswd. Optional HASHLISTALL
4134 // re-fill of cmdnamtab on miss.
4135 if cmd != 0 {
4136 // c:3143
4137 let known = shfunctab_lock() // c:3144
4138 .read()
4139 .map(|t| t.get(s).is_some())
4140 .unwrap_or(false)
4141 || BUILTINS // c:3145
4142 .iter()
4143 .any(|b| b.node.nam == *s)
4144 || cmdnamtab_lock() // c:3146
4145 .read()
4146 .map(|t| t.get(s).is_some())
4147 .unwrap_or(false)
4148 || aliastab_lock() // c:3147
4149 .read()
4150 .map(|t| t.get(s).is_some())
4151 .unwrap_or(false)
4152 || reswdtab_lock() // c:3148
4153 .read()
4154 .map(|t| t.get(s).is_some())
4155 .unwrap_or(false);
4156 if known {
4157 return; // c:3149
4158 }
4159 // c:3150-3154 — HASHLISTALL: bulk-hash $PATH then retry.
4160 if isset(HASHLISTALL) {
4161 // c:3150
4162 let path: Vec<String> =
4163 getsparam("PATH") // c:3151
4164 .map(|p| p.split(':').map(String::from).collect())
4165 .unwrap_or_default();
4166 fillcmdnamtable(&path); // c:3151
4167 if cmdnamtab_lock() // c:3152
4168 .read()
4169 .map(|t| t.get(s).is_some())
4170 .unwrap_or(false)
4171 {
4172 return; // c:3153
4173 }
4174 }
4175 }
4176 // c:3156-3165 — Tilde/Equals/String prefix skip + itok/Dash
4177 // detok + early-return on any other tokenized char.
4178 let mut start = 0usize;
4179 let bytes = s.as_bytes(); // re-bind after potential string ops
4180 if !bytes.is_empty() {
4181 let c0 = bytes[0] as char;
4182 if c0 == Tilde || c0 == Equals || c0 == StringTok {
4183 // c:3157
4184 start = 1; // c:3158 t++
4185 }
4186 }
4187 // Scan from `start` for tokenized bytes.
4188 {
4189 let mut buf = s.clone().into_bytes();
4190 let mut i = start;
4191 let mut had_dash_only = true; // accumulator
4192 while i < buf.len() {
4193 let b = buf[i];
4194 if itok(b) {
4195 // c:3160
4196 if b as char == Dash {
4197 // c:3161
4198 buf[i] = b'-'; // c:3162
4199 } else {
4200 return; // c:3164
4201 }
4202 } else {
4203 had_dash_only = had_dash_only && false;
4204 }
4205 i += 1;
4206 }
4207 let _ = had_dash_only;
4208 *s = String::from_utf8_lossy(&buf).into_owned();
4209 }
4210 // c:3166 — `best = NULL;`
4211 SPCK_BEST.with(|b| *b.borrow_mut() = None);
4212 SPCK_D.with(|c| c.set(100)); // initialised at each table-scan branch in C; mirror up-front.
4213 // c:3167-3169 — `for (t = *s; *t; t++) if (*t == '/') break;`
4214 // `t` is the position of the first slash (or end of string).
4215 let t_pos = s.find('/').unwrap_or(s.len()); // c:3167
4216 // c:3170-3171 — `if (**s == Tilde && !*t) return;`
4217 let bytes = s.as_bytes();
4218 if !bytes.is_empty() && (bytes[0] as char) == Tilde && t_pos == bytes.len() {
4219 // c:3170
4220 return; // c:3171
4221 }
4222
4223 // c:3173-3178 — compile CORRECT_IGNORE pattern if set.
4224 if let Some(ci) = getsparam("CORRECT_IGNORE") {
4225 // c:3173
4226 let prog = patcompile(
4227 &{
4228 let mut __pat_tok = (&ci).to_string();
4229 crate::ported::glob::tokenize(&mut __pat_tok);
4230 __pat_tok
4231 },
4232 PAT_HEAPDUP,
4233 None,
4234 ); // c:3176
4235 SPCK_PAT.with(|p| *p.borrow_mut() = prog);
4236 } else {
4237 SPCK_PAT.with(|p| *p.borrow_mut() = None); // c:3178
4238 }
4239 // c:3180-3185 — compile CORRECT_IGNORE_FILE pattern if set.
4240 if let Some(ci) = getsparam("CORRECT_IGNORE_FILE") {
4241 // c:3180
4242 let prog = patcompile(
4243 &{
4244 let mut __pat_tok = (&ci).to_string();
4245 crate::ported::glob::tokenize(&mut __pat_tok);
4246 __pat_tok
4247 },
4248 PAT_HEAPDUP,
4249 None,
4250 ); // c:3183
4251 SPCK_NAMEPAT.with(|p| *p.borrow_mut() = prog);
4252 } else {
4253 SPCK_NAMEPAT.with(|p| *p.borrow_mut() = None); // c:3185
4254 }
4255
4256 let bytes = s.as_bytes();
4257 let first = if bytes.is_empty() {
4258 '\0'
4259 } else {
4260 bytes[0] as char
4261 };
4262
4263 // c:3187-3193 — `**s == String && !*t`: $-prefixed name → scan paramtab.
4264 if first == StringTok && t_pos == bytes.len() {
4265 // c:3187
4266 // c:3188 — `guess = *s + 1;` strip leading $.
4267 let guess = s[1..].to_string();
4268 // c:3189-3190 — `if (itype_end(guess, INAMESPC, 1) == guess) return;`
4269 if itype_end(&guess, crate::ported::ztype_h::INAMESPC, true) == 0 {
4270 // c:3189
4271 return; // c:3190
4272 }
4273 ic = StringTok; // c:3191
4274 SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4275 SPCK_D.with(|c| c.set(100)); // c:3192
4276 if let Ok(t) = paramtab().read() {
4277 // c:3193 scanhashtable(paramtab, ..., spscan, ...)
4278 for k in t.keys() {
4279 spscan(k);
4280 }
4281 }
4282 // c:3194-3202 — `**s == Equals`: =cmd → hashcmd; then scan aliases+cmdnam.
4283 } else if first == Equals {
4284 // c:3194
4285 if t_pos != bytes.len() {
4286 // c:3195
4287 return; // c:3196
4288 }
4289 // c:3197-3198 — `if (hashcmd(guess = *s + 1, pathchecked)) return;`
4290 let guess = s[1..].to_string();
4291 let path: Vec<String> = getsparam("PATH")
4292 .map(|p| p.split(':').map(String::from).collect())
4293 .unwrap_or_default();
4294 let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4295 if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4296 return; // c:3198
4297 }
4298 SPCK_D.with(|c| c.set(100)); // c:3199
4299 ic = Equals; // c:3200
4300 SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4301 if let Ok(t) = aliastab_lock().read() {
4302 // c:3201
4303 for (k, _) in t.iter() {
4304 spscan(k);
4305 }
4306 }
4307 if let Ok(t) = cmdnamtab_lock().read() {
4308 // c:3202
4309 for (k, _) in t.iter() {
4310 spscan(k);
4311 }
4312 }
4313 // c:3203-3248 — default branch: filename / dir spell-check.
4314 } else {
4315 // c:3203
4316 let mut guess = s.clone(); // c:3204
4317 // c:3205-3218 — Tilde / String inline-expand prefix handling.
4318 if !guess.is_empty()
4319 && ((guess.as_bytes()[0] as char) == Tilde
4320 || (guess.as_bytes()[0] as char) == StringTok)
4321 {
4322 // c:3205
4323 ic = guess.as_bytes()[0] as char; // c:3207
4324 if t_pos + 1 >= s.len() {
4325 // c:3208 — `if (!*++t) return;`
4326 return; // c:3209
4327 }
4328 // c:3210-3214 — `noerrs=2; singsub(&guess); noerrs = ne;`
4329 let saved_noerrs =
4330 crate::ported::exec::noerrs.load(std::sync::atomic::Ordering::Relaxed);
4331 crate::ported::exec::noerrs.store(2, std::sync::atomic::Ordering::Relaxed); // c:3212
4332 guess = crate::ported::subst::singsub(&guess); // c:3213
4333 crate::ported::exec::noerrs.store(saved_noerrs, std::sync::atomic::Ordering::Relaxed);
4334 if guess.is_empty() {
4335 return; // c:3216 `if (!guess) return;`
4336 }
4337 // c:3217 — `preflen = strlen(guess) - strlen(t);` t = original
4338 // s[t_pos..] (the post-slash remainder).
4339 let t_len = s.len() - t_pos;
4340 preflen = guess.len().saturating_sub(t_len);
4341 }
4342 // c:3219-3220 — `if (access(unmeta(guess), F_OK) == 0) return;`
4343 let cstr = match std::ffi::CString::new(unmeta(&guess).as_str()) {
4344 Ok(c) => c,
4345 Err(_) => return,
4346 };
4347 if unsafe { libc::access(cstr.as_ptr(), libc::F_OK) } == 0 {
4348 // c:3219
4349 return; // c:3220
4350 }
4351 // c:3221 — `best = spname(guess);`
4352 // The Rust spname has a signature-drift adaptation taking
4353 // `(name, dir)`; pass the parent dir extracted from guess.
4354 let path_obj = std::path::Path::new(&guess);
4355 let parent = path_obj
4356 .parent()
4357 .and_then(|p| p.to_str())
4358 .filter(|s| !s.is_empty())
4359 .unwrap_or(".");
4360 let basename = path_obj.file_name().and_then(|n| n.to_str()).unwrap_or("");
4361 let best = spname(basename, parent);
4362 SPCK_BEST.with(|b| *b.borrow_mut() = best);
4363 SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess.clone()));
4364 // c:3222-3247 — command-position default branch: hashcmd +
4365 // scan tables + autocd cdpath scan.
4366 if t_pos == s.len() && cmd != 0 {
4367 // c:3222
4368 // c:3223-3224 — hashcmd shortcut.
4369 let path: Vec<String> = getsparam("PATH")
4370 .map(|p| p.split(':').map(String::from).collect())
4371 .unwrap_or_default();
4372 let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4373 if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4374 return; // c:3224
4375 }
4376 SPCK_D.with(|c| c.set(100)); // c:3225
4377 // c:3226-3230 — scan reswd, alias, shfunc, builtin, cmdnam.
4378 if let Ok(t) = reswdtab_lock().read() {
4379 // c:3226
4380 for (k, _) in t.iter() {
4381 spscan(k);
4382 }
4383 }
4384 if let Ok(t) = aliastab_lock().read() {
4385 // c:3227
4386 for (k, _) in t.iter() {
4387 spscan(k);
4388 }
4389 }
4390 if let Ok(t) = shfunctab_lock().read() {
4391 // c:3228
4392 for (k, _) in t.iter() {
4393 spscan(k);
4394 }
4395 }
4396 // c:3229 — builtintab scan: BUILTINS is a static array.
4397 for b in BUILTINS.iter() {
4398 spscan(&b.node.nam);
4399 }
4400 if let Ok(t) = cmdnamtab_lock().read() {
4401 // c:3230
4402 for (k, _) in t.iter() {
4403 spscan(k);
4404 }
4405 }
4406 // c:3231-3247 — autocd $cdpath scan: for each cdpath
4407 // entry, find the closest filename match via mindist.
4408 // Strict `<` (not `<=` as in spscan) so a cdpath dir
4409 // wins only when STRICTLY better than the existing best
4410 // — preferring earlier cdpath dirs on ties.
4411 if autocd {
4412 // c:3232 — `if (cd_able_vars(unmeta(guess))) return;`
4413 let unmeta_guess = crate::ported::utils::unmeta(&guess);
4414 if crate::ported::builtin::cd_able_vars(&unmeta_guess).is_some() {
4415 return; // c:3233
4416 }
4417 // c:3234 — `for (pp = cdpath; *pp; pp++)`. Read CDPATH.
4418 let cdpath: Vec<String> = crate::ported::params::paramtab()
4419 .read()
4420 .ok()
4421 .and_then(|t| t.get("cdpath").and_then(|pm| pm.u_arr.clone()))
4422 .unwrap_or_default();
4423 let mut cur_d = SPCK_D.with(|c| c.get());
4424 let mut cur_best = SPCK_BEST.with(|b| b.borrow().clone());
4425 for pp in cdpath.iter() {
4426 // c:3235-3245 — `mindist(*pp, *s, bestcd, 1)`.
4427 if let Some((bestcd, thisdist)) = mindist(pp, &s) {
4428 // c:3239 — STRICT `<` (not `<=`) per C comment
4429 // at c:3236-3239.
4430 if (thisdist as i32) < cur_d {
4431 cur_best = Some(bestcd);
4432 cur_d = thisdist as i32;
4433 }
4434 }
4435 }
4436 SPCK_BEST.with(|b| *b.borrow_mut() = cur_best);
4437 SPCK_D.with(|c| c.set(cur_d));
4438 }
4439 }
4440 }
4441 // c:3250-3251 — `if (errflag) return;`
4442 if (errflag.load(std::sync::atomic::Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
4443 return; // c:3251
4444 }
4445 // c:3252 — `if (best && strlen(best) > 1 && strcmp(best, guess))`.
4446 let best = SPCK_BEST.with(|b| b.borrow().clone());
4447 let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4448 let Some(mut best) = best else {
4449 return;
4450 };
4451 if best.len() <= 1 || best == guess {
4452 return;
4453 }
4454 // c:3253-3272 — assemble the prefixed/de-tokenized replacement.
4455 if ic != '\0' {
4456 // c:3254
4457 if preflen > 0 {
4458 // c:3256
4459 // c:3258-3259 — `if (strncmp(guess, best, preflen)) return;`
4460 if !best.starts_with(&guess[..preflen.min(guess.len())]) {
4461 return; // c:3259
4462 }
4463 // c:3261-3263 — `u = ...s[0..t-*s] + best[preflen..]`.
4464 let t_off = s.len() - (s.len() - t_pos);
4465 let mut u = String::with_capacity(t_off + best.len() - preflen + 1);
4466 u.push_str(&s[..t_off]);
4467 u.push_str(&best[preflen..]);
4468 best = u;
4469 } else {
4470 // c:3264 — `u = "\0" + best;` (prepend NUL placeholder).
4471 best = format!("\0{}", best);
4472 }
4473 // c:3269-3271 — `best = u; guess = *s; *guess = *best = ztokens[ic - Pound];`
4474 let pound = crate::ported::zsh_h::Pound as u8;
4475 let zt = crate::ported::lex::ztokens.as_bytes();
4476 let token_char = if (ic as u8) >= pound {
4477 let idx = (ic as u8 - pound) as usize;
4478 if idx < zt.len() {
4479 zt[idx] as char
4480 } else {
4481 ic
4482 }
4483 } else {
4484 ic
4485 };
4486 // Set first char of both `*s` (the original) and `best`.
4487 if !s.is_empty() {
4488 let mut sb = s.clone().into_bytes();
4489 sb[0] = token_char as u8;
4490 *s = String::from_utf8_lossy(&sb).into_owned();
4491 }
4492 if !best.is_empty() {
4493 let mut bb = best.into_bytes();
4494 bb[0] = token_char as u8;
4495 best = String::from_utf8_lossy(&bb).into_owned();
4496 }
4497 }
4498 // c:3273-3289 — interactive prompt (`ask`) or auto-accept.
4499 let x: char;
4500 if ask != 0 {
4501 // c:3273
4502 // WARNING — DIVERGENCE: `noquery()`, `shout`, `promptexpand`,
4503 // `zputs(stream)`, `zbeep`, `getquery("nyae", 0)` aren't yet
4504 // wired in zshrs (interactive ZLE prompt machinery). Default
4505 // to 'n' (decline) when ask=1 — preserves the C behavior of
4506 // declining when shout is NULL (c:3286-3287). Re-enable the
4507 // interactive flow when promptexpand/getquery land.
4508 x = 'n';
4509 } else {
4510 x = 'y'; // c:3289
4511 }
4512 // c:3290-3300 — apply chosen action.
4513 if x == 'y' {
4514 // c:3290
4515 *s = best; // c:3291 `*s = dupstring(best);`
4516 if hist != 0 {
4517 // c:3292
4518 // c:3293 — `hwrep(best);` (history rewrite). Stubbed: hist
4519 // rewrite plumbing isn't yet hooked into the lex caller.
4520 }
4521 } else if x == 'a' {
4522 // c:3294
4523 crate::ported::hist::histdone
4524 .fetch_or(HISTFLAG_NOEXEC, std::sync::atomic::Ordering::Relaxed); // c:3295
4525 } else if x == 'e' {
4526 // c:3296
4527 crate::ported::hist::histdone.fetch_or(
4528 HISTFLAG_NOEXEC | crate::ported::zsh_h::HISTFLAG_RECALL,
4529 std::sync::atomic::Ordering::Relaxed,
4530 ); // c:3297
4531 }
4532 // c:3299-3300 — `if (ic) **s = ic;` — restore prefix sigil.
4533 if ic != '\0' && !s.is_empty() {
4534 let mut sb = s.clone().into_bytes();
4535 sb[0] = ic as u8;
4536 *s = String::from_utf8_lossy(&sb).into_owned();
4537 }
4538}
4539
4540/// Port of `ztrftimebuf(int *bufsizeptr, int decr)` from `Src/utils.c:3312`.
4541///
4542/// ```c
4543/// static int ztrftimebuf(int *bufsizeptr, int decr) {
4544/// if (*bufsizeptr <= decr) return 1;
4545/// *bufsizeptr -= decr;
4546/// return 0;
4547/// }
4548/// ```
4549///
4550/// "Helper for ztrftime: try to fit decr more bytes (plus a NUL)
4551/// in the buffer, and a new string length to decrement from that.
4552/// Returns 0 if the new length fits, 1 otherwise."
4553///
4554/// Previous Rust port had wrong semantics — returned `needed.max(256)`
4555/// (a buffer-sizing helper) instead of the C "decrement-and-check"
4556/// semantics. New port matches C: takes &mut bufsize + decr,
4557/// returns i32 (0 = fit, 1 = doesn't fit).
4558pub fn ztrftimebuf(bufsizeptr: &mut i32, decr: i32) -> i32 {
4559 if *bufsizeptr <= decr {
4560 return 1;
4561 }
4562 *bufsizeptr -= decr;
4563 0
4564}
4565
4566/// Format time struct (from utils.c ztrftime)
4567/// Port of `ztrftime(char *buf, int bufsize, char *fmt, struct tm *tm, long nsec)` from `Src/utils.c:3337`.
4568/// WARNING: param names don't match C — Rust=(fmt, time) vs C=(buf, bufsize, fmt, tm, nsec)
4569// Rust idiom replacement: `SystemTime::duration_since(UNIX_EPOCH)`
4570// + libc::localtime covers the C tm-struct populate; the C source's
4571// 192-line body builds the fmt-walk by hand because C has no
4572// strftime extension support — Rust delegates to libc::strftime via
4573// the strftime crate equivalent inline.
4574/// `ztrftime` — see implementation.
4575///
4576/// `use_gmt` controls whether time fields are interpreted as UTC (true,
4577/// libc::gmtime) or local time (false, libc::localtime). C's ztrftime
4578/// takes a `struct tm *` produced by the caller via gmtime/localtime
4579/// (e.g. Src/Modules/stat.c:201 picks between them based on STF_GMT);
4580/// since the Rust signature takes `SystemTime` we hoist that choice into
4581/// this bool param.
4582pub fn ztrftime(fmt: &str, time: std::time::SystemTime, use_gmt: bool) -> String {
4583 // C takes `struct tm *` + nsec directly, so negative time_t
4584 // (pre-1970) flows through localtime untouched. SystemTime
4585 // pre-epoch surfaces as duration_since Err — recover the signed
4586 // seconds rather than collapsing to 0 (the old unwrap_or_default
4587 // pinned every pre-epoch timestamp to the epoch).
4588 let (secs, nsec): (i64, u64) = match time.duration_since(UNIX_EPOCH) {
4589 Ok(d) => (d.as_secs() as i64, d.subsec_nanos() as u64),
4590 Err(e) => {
4591 let d = e.duration();
4592 let s = -(d.as_secs() as i64);
4593 let n = d.subsec_nanos() as u64;
4594 // sub-second part rolls the seconds down one: -1.25s
4595 // before epoch is secs=-2, nsec=750ms in tm terms.
4596 if n > 0 {
4597 (s - 1, 1_000_000_000 - n)
4598 } else {
4599 (s, 0)
4600 }
4601 }
4602 };
4603
4604 #[cfg(unix)]
4605 unsafe {
4606 let tm = if use_gmt {
4607 libc::gmtime(&secs)
4608 } else {
4609 libc::localtime(&secs)
4610 };
4611 if tm.is_null() {
4612 return String::new();
4613 }
4614 let tm_ref = &*tm;
4615
4616 // c:3398-3406 — pre-pass: walk fmt and substitute zsh-specific
4617 // extensions BEFORE delegating to libc::strftime. The C source
4618 // handles these inline; Rust pre-rewrites them into literal
4619 // numbers so libc::strftime sees only standard specifiers.
4620 // %K = 24-hr clock, no leading zero (0-23)
4621 // %L = 12-hr clock, no leading zero (1-12)
4622 // %f = day of month, no leading zero (1-31)
4623 // %.N = fractional seconds, N digits (0-9)
4624 let mut preprocessed = String::with_capacity(fmt.len());
4625 let bytes = fmt.as_bytes();
4626 let mut i = 0;
4627 while i < bytes.len() {
4628 if bytes[i] == b'%' && i + 1 < bytes.len() {
4629 // c:3374-3384 — parse optional `N.` prefix (digit count
4630 // for the `%.` fractional-seconds specifier).
4631 let mut j = i + 1;
4632 while j < bytes.len() && bytes[j].is_ascii_digit() {
4633 j += 1;
4634 }
4635 let digs: u32 = if j > i + 1 {
4636 std::str::from_utf8(&bytes[i + 1..j])
4637 .unwrap_or("3")
4638 .parse()
4639 .unwrap_or(3)
4640 } else {
4641 3
4642 };
4643 // c:3408 — `switch (*fmt++)` examines the specifier
4644 // char (after the digit prefix).
4645 if j < bytes.len() && bytes[j] == b'.' {
4646 // c:3409 — fractional-seconds. Default 3 digits if
4647 // none specified (j == i+1).
4648 let digs = digs.min(9);
4649 let trunc_div: u64 = 10u64.pow(9 - digs);
4650 let val = nsec / trunc_div;
4651 preprocessed.push_str(&format!("{:0width$}", val, width = digs as usize));
4652 i = j + 1;
4653 continue;
4654 }
4655 let next = bytes[i + 1];
4656 // c:3445-3460 — %K / %L / %f extensions.
4657 match next {
4658 b'K' => {
4659 preprocessed.push_str(&format!("{}", tm_ref.tm_hour));
4660 i += 2;
4661 continue;
4662 }
4663 b'L' => {
4664 let mut h12 = tm_ref.tm_hour % 12;
4665 if h12 == 0 {
4666 h12 = 12;
4667 }
4668 preprocessed.push_str(&format!("{}", h12));
4669 i += 2;
4670 continue;
4671 }
4672 b'f' => {
4673 preprocessed.push_str(&format!("{}", tm_ref.tm_mday));
4674 i += 2;
4675 continue;
4676 }
4677 b'N' => {
4678 // c:3491-3495 — `%N`: nanoseconds, always 9 digits
4679 // (`sprintf(buf, "%09ld", nsec)`).
4680 preprocessed.push_str(&format!("{:09}", nsec));
4681 i += 2;
4682 continue;
4683 }
4684 _ => {}
4685 }
4686 }
4687 preprocessed.push(bytes[i] as char);
4688 i += 1;
4689 }
4690
4691 let mut buf = vec![0u8; 256];
4692 let c_fmt = CString::new(preprocessed).unwrap_or_default();
4693 let len = libc::strftime(
4694 buf.as_mut_ptr() as *mut libc::c_char,
4695 buf.len(),
4696 c_fmt.as_ptr(),
4697 tm,
4698 );
4699
4700 if len > 0 {
4701 buf.truncate(len);
4702 String::from_utf8_lossy(&buf).to_string()
4703 } else {
4704 String::new()
4705 }
4706 }
4707
4708 #[cfg(not(unix))]
4709 {
4710 let _ = (fmt, secs, nsec);
4711 String::new()
4712 }
4713}
4714
4715/// Join array with delimiter (from utils.c zjoin)
4716/// Port of `zjoin(char **arr, int delim, int heap)` from `Src/utils.c:3622`.
4717/// Joins `arr` with `delim` between elements. When `delim` is itself a meta
4718/// byte (`imeta(delim)` — NUL, `Meta`, `Marker`, or a token in the
4719/// Pound..Nularg range; see `inittyptab` c:4195-4201), C writes the delimiter
4720/// in *metafied* form (`Meta` byte then `delim ^ 32`, c:3634-3637) rather than
4721/// the raw byte (c:3639). The earlier port emitted the raw char always, which
4722/// was byte-wrong for meta delimiters — notably `zjoin(array, '\0', …)` (NUL is
4723/// imeta), where C emits `0x83 0x20`, not a raw `0x00`.
4724///
4725/// WARNING: param names don't match C — Rust=(arr, delim) vs C=(arr, delim, heap).
4726/// The `heap` arg is dropped (Rust `String` owns its storage). As with
4727/// [`metafy`], the `Meta` byte cannot live in valid UTF-8, so the final
4728/// `String` boundary uses the same `from_utf8`/lossy fallback — byte-exact for
4729/// every ASCII (non-meta) delimiter, lossy only on meta delimiters. Callers
4730/// needing byte-exact meta-delimited output should join at the byte level.
4731pub fn zjoin(arr: &[String], delim: char) -> String {
4732 // c:3622
4733 // c:3629-3630 — empty array → "".
4734 if arr.is_empty() {
4735 return String::new();
4736 }
4737 let db = delim as u32;
4738 // c:3634 — `imeta(delim)`. Only byte-valued delimiters can be meta.
4739 let delim_meta = db < 0x100 && imeta_byte(db as u8);
4740 let mut out: Vec<u8> = Vec::new();
4741 for (i, s) in arr.iter().enumerate() {
4742 if i != 0 {
4743 // The separator that C writes after every element then chops off
4744 // the trailing one (c:3641) is equivalent to interposing it.
4745 if delim_meta {
4746 out.push(Meta); // c:3635
4747 out.push((db as u8) ^ 32); // c:3636
4748 } else if db < 0x100 {
4749 out.push(db as u8); // c:3639 — raw single byte
4750 } else {
4751 // Non-byte char delimiter (no C analogue): emit its UTF-8.
4752 let mut buf = [0u8; 4];
4753 out.extend_from_slice(delim.encode_utf8(&mut buf).as_bytes());
4754 }
4755 }
4756 out.extend_from_slice(s.as_bytes()); // c:3633 — strucpy(&ptr, *s)
4757 }
4758 String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
4759}
4760
4761/// Port of `char **colonsplit(char *s, int uniq)` from
4762/// `Src/utils.c:3650`. Splits `s` on `:`; when `uniq` is set,
4763/// duplicate segments are dropped (linear scan against the
4764/// already-emitted prefix).
4765/// ```c
4766/// char **
4767/// colonsplit(char *s, int uniq)
4768/// {
4769/// int ct;
4770/// char *t, **ret, **ptr, **p;
4771/// for (t = s, ct = 0; *t; t++)
4772/// if (*t == ':') ct++;
4773/// ptr = ret = zalloc(sizeof(char *) * (ct + 2));
4774/// t = s;
4775/// do {
4776/// s = t;
4777/// for (; *t && *t != ':'; t++);
4778/// if (uniq)
4779/// for (p = ret; p < ptr; p++)
4780/// if (strlen(*p) == t - s && !strncmp(*p, s, t - s))
4781/// goto cont;
4782/// *ptr = zalloc((t - s) + 1);
4783/// ztrncpy(*ptr++, s, t - s);
4784/// cont: ;
4785/// } while (*t++);
4786/// *ptr = NULL;
4787/// return ret;
4788/// }
4789/// ```
4790pub fn colonsplit(s: &str, uniq: bool) -> Vec<String> {
4791 // c:3650
4792 // c:3655-3657 — count colons.
4793 let ct = s.matches(':').count(); // c:3655
4794 let mut ret: Vec<String> = Vec::with_capacity(ct + 2); // c:3658 zalloc((ct+2)*sizeof(char *))
4795
4796 // c:3661-3673 — do-while loop walking segments.
4797 let bytes = s.as_bytes();
4798 let mut t: usize = 0;
4799 loop {
4800 let seg_start = t; // c:3662 s = t
4801 // c:3664 — `for (; *t && *t != ':'; t++)`
4802 while t < bytes.len() && bytes[t] != b':' {
4803 t += 1;
4804 }
4805 let seg = &s[seg_start..t];
4806 // c:3665-3668 — uniq dedupe.
4807 if !uniq || !ret.iter().any(|p| p == seg) {
4808 // c:3665
4809 ret.push(seg.to_string()); // c:3670 zalloc + ztrncpy
4810 }
4811 // c:3673 — `while (*t++)` — break if at end, else step past colon.
4812 if t >= bytes.len() {
4813 break;
4814 } // c:3673
4815 t += 1; // c:3673 t++
4816 }
4817 // c:3674 — `*ptr = NULL;` — Rust Vec needs no sentinel.
4818 ret // c:3675
4819}
4820
4821/// Port of `skipwsep(char **s)` from `Src/utils.c:3680`.
4822///
4823/// Skip whitespace separators (`iwsep`-true bytes) at the start of
4824/// `s`. Returns `(remaining_str, count)` — `count` is the number of
4825/// bytes / chars skipped. C body:
4826///
4827/// ```c
4828/// while (*t && iwsep(*t == Meta ? t[1] ^ 32 : *t)) {
4829/// if (*t == Meta) t++;
4830/// t++;
4831/// i++;
4832/// }
4833/// ```
4834///
4835/// The C version honours Meta-encoded bytes (`Meta` followed by
4836/// `^32`-XOR'd byte). Rust port mirrors that byte-by-byte.
4837pub fn skipwsep(s: &str) -> (&str, usize) {
4838 let bytes = s.as_bytes();
4839 let mut i: usize = 0;
4840 let mut count: usize = 0;
4841 while i < bytes.len() {
4842 let b = if bytes[i] == Meta && i + 1 < bytes.len() {
4843 bytes[i + 1] ^ 32
4844 } else {
4845 bytes[i]
4846 };
4847 if !iwsep(b) {
4848 break;
4849 }
4850 if bytes[i] == Meta {
4851 i += 1;
4852 }
4853 i += 1;
4854 count += 1;
4855 }
4856 (&s[i..], count)
4857}
4858
4859/// Port of `spacesplit(char *s, int allownull, int heap, int quote)` from
4860/// `Src/utils.c:3711`. Splits `s` on `$IFS` via the `ISEP`/`IWSEP` char
4861/// classes (TYPTAB, kept in sync with `$IFS` by `inittyptab`, which
4862/// re-runs on every IFS set — params.rs:8691, c:4795), NOT on hardcoded
4863/// whitespace. zsh's word-splitting rules: runs of IFS-WHITESPACE
4864/// collapse and the leading/trailing ones yield real `""` fields (which
4865/// callers elide); each IFS-NON-WHITESPACE char delimits a field, and
4866/// consecutive ones yield `nulstring` (`Nularg`, `Src/subst.c:36`
4867/// `{Nularg,'\0'}`) empty fields, which survive and are later stripped to
4868/// `""` by remnulargs.
4869///
4870/// WARNING: param names don't match C — Rust=(s, allownull) vs C=(s,
4871/// allownull, heap, quote). The `quote` arm (backslash-escaped seps) and
4872/// `heap` C-buffer param drop: all zshrs callers pass quote=0, and Rust
4873/// owns its String storage. The previous port split only on hardcoded
4874/// `[' ','\t','\n']`, ignoring `$IFS` entirely (Bug #636).
4875pub fn spacesplit(s: &str, allownull: bool) -> Vec<String> {
4876 // c:3711
4877 use crate::ported::ztype_h::zistype;
4878 let bytes = s.as_bytes();
4879 // Meta-aware decode of the logical char value + its byte length at a
4880 // byte position (mirrors skipwsep's `*x == Meta ? x[1] ^ 32 : *x`).
4881 let char_at = |i: usize| -> (u8, usize) {
4882 // c:Src/zsh.h Meta — `\u{83}` prefix; next byte XOR 0x20.
4883 if bytes[i] == Meta && i + 1 < bytes.len() {
4884 (bytes[i + 1] ^ 32, 2)
4885 } else {
4886 (bytes[i], 1)
4887 }
4888 };
4889 // skipwsep(&s) — advance past a run of IWSEP (IFS-whitespace). c:3730
4890 let skipwsep_at = |mut i: usize| -> usize {
4891 while i < bytes.len() {
4892 let (c, l) = char_at(i);
4893 if !iwsep(c) {
4894 break;
4895 }
4896 i += l;
4897 }
4898 i
4899 };
4900 // itype_end(s, ISEP, 1) — `once` advances past exactly ONE ISEP char
4901 // if present, else leaves the index unchanged (c:4462 `if (once) break`).
4902 let isep_one = |i: usize| -> usize {
4903 if i < bytes.len() {
4904 let (c, l) = char_at(i);
4905 if zistype(c, ISEP as u32) {
4906 return i + l;
4907 }
4908 }
4909 i
4910 };
4911 // findsep(&s, NULL, 0) — advance to the next ISEP char. c:3784
4912 let findsep_at = |mut i: usize| -> usize {
4913 while i < bytes.len() {
4914 let (c, l) = char_at(i);
4915 if zistype(c, ISEP as u32) {
4916 break;
4917 }
4918 i += l;
4919 }
4920 i
4921 };
4922 let nulstring = || Nularg.to_string(); // c:subst.c:36 nulstring = {Nularg,0}
4923
4924 let mut ret: Vec<String> = Vec::new();
4925 let mut si = 0usize;
4926 let mut t = si; // c:3729 — `t = s;`
4927 si = skipwsep_at(si); // c:3730 — `skipwsep(&s);`
4928 // c:3732-3735 — leading-field handling.
4929 if si < bytes.len() && isep_one(si) != si {
4930 // c:3733 — at an IFS-non-whitespace sep after skipping whitespace.
4931 ret.push(if allownull {
4932 String::new()
4933 } else {
4934 nulstring()
4935 });
4936 } else if !allownull && t != si {
4937 // c:3734-3735 — leading IFS-whitespace was skipped → real "".
4938 ret.push(String::new());
4939 }
4940 // c:3736-3756 — main word loop.
4941 while si < bytes.len() {
4942 let iend = isep_one(si); // c:3737 itype_end(s, ISEP, 1)
4943 let consumed_sep = iend != si;
4944 if consumed_sep {
4945 // c:3738-3741 — consume the single non-ws sep + following WS.
4946 si = iend;
4947 si = skipwsep_at(si);
4948 }
4949 // !!! POSIX-FAITHFUL GATE (no C counterpart) !!!
4950 // zsh (and `emulate sh`) emits a trailing empty field when a
4951 // non-whitespace IFS separator is the last thing in the input:
4952 // `IFS=:; set -- $v` on `a:b:` → (a, b, "") = 3. dash/ksh/bash drop
4953 // that trailing empty → (a, b) = 2. A MIDDLE empty (`a::b`) keeps
4954 // `si < len` here and is unaffected. Scoped to `!allownull` (the
4955 // unquoted-split path `set -- $v` uses); the quoted `"${=v}"` /
4956 // `${(s::)}` allownull path keeps zsh semantics. `zshrs --sh/--ksh/
4957 // --dash` sets posix_faithful(); `--... --zsh` leaves it false.
4958 if consumed_sep && si >= bytes.len() && !allownull && crate::dash_mode::posix_faithful()
4959 {
4960 break;
4961 }
4962 t = si; // c:3746 — `t = s;`
4963 si = findsep_at(si); // c:3747 — `findsep(&s, NULL, quote);`
4964 if si > t || allownull {
4965 // c:3748-3751 — emit the word `t..si`.
4966 ret.push(s[t..si].to_string());
4967 } else {
4968 // c:3753 — empty field between non-ws seps → nulstring.
4969 ret.push(nulstring());
4970 }
4971 t = si; // c:3754 — `t = s;`
4972 si = skipwsep_at(si); // c:3755 — `skipwsep(&s);`
4973 }
4974 // c:3757 — trailing IFS-whitespace → real "".
4975 if !allownull && t != si {
4976 ret.push(String::new());
4977 }
4978 ret
4979}
4980
4981/// Port of `findsep(char **s, char *sep, int quote)` from `Src/utils.c:3784`.
4982///
4983/// c:3762 — "Find a separator. Return 0 if already at separator, 1 if
4984/// separator found later, else -1." `pos` is C's walking `*s`: on
4985/// return it points at the separator (or end-of-string) so the caller
4986/// reads the word as `s[entry_pos..pos]`.
4987///
4988/// `sep` (c:3772):
4989/// - `None` → split on normal `$IFS` separators (ISEP chars).
4990/// - `Some(b"")` → no real separator: advance past one character.
4991/// - `Some(seq)` → look for the (possibly multi-byte) literal `seq`.
4992///
4993/// `quote` (c:3776) — a `\` before a separator suppresses it, and `\\`
4994/// collapses to `\`. This only applies when `sep` is `None` and it
4995/// strips backslashes in place, so the buffer must be modifiable
4996/// (C demands "something modifiable"; Rust takes `&mut String`).
4997///
4998/// The standalone form has no in-tree caller yet — the hot split paths
4999/// (`splitstring`, `findword`, `wordcount`) inline this logic — but it
5000/// is kept as a faithful, callable name-parity anchor.
5001/// WARNING: param names match C semantics — `pos` is C's `*s`.
5002pub fn findsep(s: &mut String, pos: &mut usize, sep: Option<&[u8]>, quote: bool) -> i32 {
5003 // c:3784
5004 use crate::ported::zsh_h::{MB_METACHARINIT, MB_METACHARLEN, MB_METACHARLENCONV};
5005 use crate::ported::ztype_h::{isep, ISEP, WC_ZISTYPE};
5006
5007 MB_METACHARINIT(); // c:3792
5008 let is_isep = |c: Option<char>| c.map(|c| WC_ZISTYPE(c, ISEP as u32)).unwrap_or(false);
5009
5010 match sep {
5011 // c:3793-3824 — default separators (ISEP), with optional quoting.
5012 None => {
5013 let start = *pos;
5014 let mut t = *pos;
5015 while t < s.len() {
5016 let b = s.as_bytes()[t];
5017 if quote && b == b'\\' {
5018 // c:3795 — `if (quote && *t == '\\')`
5019 if t + 1 < s.len() && s.as_bytes()[t + 1] == b'\\' {
5020 // c:3796-3799 — `\\` → `\`: drop one backslash,
5021 // advance past the one we keep (ilen = 1).
5022 chuck(s, t);
5023 t += 1;
5024 continue;
5025 }
5026 // c:3801 — measure the char *after* the backslash.
5027 let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t + 1..]);
5028 if is_isep(c) {
5029 // c:3802-3804 — escaped separator: strip the
5030 // backslash, then advance over the now-bare char.
5031 chuck(s, t);
5032 t += ilen.max(1);
5033 continue;
5034 }
5035 // c:3806-3810 — backslash is a normal byte.
5036 if isep(b) {
5037 break; // (never taken: '\\' is not an ISEP)
5038 }
5039 t += 1; // ilen = 1
5040 } else {
5041 // c:3814-3818 — ordinary character.
5042 let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t..]);
5043 if is_isep(c) {
5044 break; // c:3817
5045 }
5046 t += ilen.max(1);
5047 }
5048 }
5049 let i = if t > start { 1 } else { 0 }; // c:3821 `i = (t > *s)`
5050 *pos = t; // c:3822
5051 i // c:3823
5052 }
5053 // c:3825-3834 — empty separator: advance past the first char.
5054 Some(seq) if seq.is_empty() => {
5055 if *pos < s.len() {
5056 *pos += MB_METACHARLEN(&s.as_bytes()[*pos..]); // c:3831
5057 1 // c:3832
5058 } else {
5059 -1 // c:3833
5060 }
5061 }
5062 // c:3835-3845 — explicit (possibly multi-byte) literal separator.
5063 Some(seq) => {
5064 let mut i = 0i32;
5065 while *pos < s.len() {
5066 // c:3840 — `for (t=sep, tt=*s; *t && *tt && *t==*tt; ...)`
5067 let bytes = s.as_bytes();
5068 let mut k = 0usize;
5069 while k < seq.len() && *pos + k < bytes.len() && seq[k] == bytes[*pos + k] {
5070 k += 1;
5071 }
5072 if k == seq.len() {
5073 return if i > 0 { 1 } else { 0 }; // c:3841 `return (i > 0)`
5074 }
5075 *pos += MB_METACHARLEN(&bytes[*pos..]); // c:3842
5076 i += 1;
5077 }
5078 -1 // c:3844
5079 }
5080 }
5081}
5082
5083/// Find word at position (from utils.c findword)
5084/// Port of `findword(char **s, char *sep)` from `Src/utils.c:3849`.
5085pub fn findword<'a>(s: &'a str, sep: Option<&'a str>) -> Option<(&'a str, &'a str)> {
5086 let s = match sep {
5087 Some(_) => s,
5088 None => s.trim_start(),
5089 };
5090 if s.is_empty() {
5091 return None;
5092 }
5093 match sep {
5094 Some(sep) => {
5095 if let Some(pos) = s.find(sep) {
5096 Some((&s[..pos], &s[pos + sep.len()..]))
5097 } else {
5098 Some((s, ""))
5099 }
5100 }
5101 None => {
5102 let end = s.find(|c: char| c.is_ascii_whitespace()).unwrap_or(s.len());
5103 Some((&s[..end], &s[end..]))
5104 }
5105 }
5106}
5107
5108/// Port of `wordcount(char *s, char *sep, int mul)` from `Src/utils.c:3879`.
5109///
5110/// Returns the number of words in `s` that would result from splitting
5111/// on `sep` (or `$IFS` when `sep` is None). `mul` controls how
5112/// consecutive empty fields are counted:
5113/// - `mul == 0`: don't count leading/trailing/consecutive empties.
5114/// - `mul > 0`: count consecutive empties (each `sep` boundary
5115/// produces one word, even if the surrounding text is empty).
5116/// - `mul < 0`: count empty trailing fields (final separator after
5117/// the last non-empty field counts as one extra empty word).
5118///
5119/// C body (paraphrased):
5120/// ```c
5121/// if (sep) {
5122/// r = 1;
5123/// sl = strlen(sep);
5124/// for (; (c = findsep(&s, sep, 0)) >= 0; s += sl)
5125/// if ((c || mul) && (sl || *(s + sl)))
5126/// r++;
5127/// } else {
5128/// /* IFS-based: walk skipwsep / itype_end(s, ISEP, 1) */
5129/// }
5130/// ```
5131///
5132/// This port walks the metafied byte stream directly to mirror C's
5133/// pointer arithmetic. The `sep`-based branch is exact; the IFS
5134/// branch uses [`iwsep`] for whitespace-separator detection (C's
5135/// `ISEP` char class collapsed to whitespace, which is the common
5136/// case for default `$IFS` = `" \t\n"`).
5137pub fn wordcount(s: &str, sep: Option<&str>, mul: i32) -> i32 {
5138 // c:3879
5139 let bytes = s.as_bytes();
5140 if let Some(sep) = sep {
5141 // C: r = 1; sl = strlen(sep); for (; findsep(&s,sep,0) >= 0; s+=sl)
5142 // if ((c || mul) && (sl || *(s+sl))) r++;
5143 let sep_bytes = sep.as_bytes();
5144 let sl = sep_bytes.len();
5145 let mut r: i32 = 1;
5146 let mut pos = 0;
5147 while pos <= bytes.len() {
5148 let rest = &bytes[pos..];
5149 let c_offset = match sep_bytes.is_empty() {
5150 true => Some(0usize),
5151 false => rest.windows(sl).position(|w| w == sep_bytes),
5152 };
5153 let Some(c) = c_offset else { break };
5154 // C `c` is the chars before the separator; `(sl || *(s+sl))`
5155 // means: if sl is zero (empty sep), only count when there's a
5156 // following char. Otherwise (sl > 0), the second clause is true
5157 // when sep is non-empty AND there are bytes after sep.
5158 let after_off = pos + c + sl;
5159 let following_nonempty = after_off < bytes.len();
5160 let cond_b = sl != 0 || following_nonempty;
5161 if (c != 0 || mul != 0) && cond_b {
5162 r += 1;
5163 }
5164 if sl == 0 {
5165 // Avoid infinite loop on empty sep — mirrors C's findsep
5166 // which advances by 1 byte when sep is empty.
5167 pos += 1;
5168 } else {
5169 pos += c + sl;
5170 }
5171 }
5172 r
5173 } else {
5174 // IFS branch (sep == NULL). C source uses itype_end(s, ISEP, 1)
5175 // to skip ISEP chars (default $IFS = " \t\n"). We use iwsep.
5176 let mut s_pos = 0usize;
5177 let t_orig = s_pos;
5178 let mut r: i32 = 0;
5179 // C: if (mul <= 0) skipwsep(&s);
5180 if mul <= 0 {
5181 while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5182 s_pos += 1;
5183 }
5184 }
5185 // C: if ((*s && itype_end(s,ISEP,1)!=s) || (mul<0 && t!=s)) r++;
5186 let has_word_now = s_pos < bytes.len() && !iwsep(bytes[s_pos]);
5187 if has_word_now || (mul < 0 && t_orig != s_pos) {
5188 r += 1;
5189 }
5190 // C: for (; *s; r++) { advance over word + maybe-skipwsep + findsep + maybe-skipwsep }
5191 while s_pos < bytes.len() {
5192 // Advance past the current word (non-ISEP chars).
5193 let word_start = s_pos;
5194 while s_pos < bytes.len() && !iwsep(bytes[s_pos]) {
5195 s_pos += 1;
5196 }
5197 if s_pos > word_start && mul <= 0 {
5198 while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5199 s_pos += 1;
5200 }
5201 }
5202 // C: (void)findsep(&s, NULL, 0) — advance past one sep run.
5203 // Already handled above when mul<=0; for mul>0 we still need
5204 // to consume one separator byte to make progress.
5205 if s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5206 s_pos += 1;
5207 }
5208 let t_after = s_pos;
5209 if mul <= 0 {
5210 while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5211 s_pos += 1;
5212 }
5213 }
5214 if s_pos < bytes.len() {
5215 r += 1;
5216 } else {
5217 // C: if (mul < 0 && t != s) r++;
5218 if mul < 0 && t_after != s_pos {
5219 r += 1;
5220 }
5221 break;
5222 }
5223 }
5224 r
5225 }
5226}
5227
5228/// Join array with separator - port from zsh/Src/utils.c sepjoin() lines 3926-3958
5229///
5230/// If sep is None, uses first char of IFS (defaults to space).
5231/// Join an array with separator.
5232/// Port of `sepjoin(char **s, char *sep, int heap)` from Src/utils.c:3928.
5233/// WARNING: param names don't match C — Rust=(arr, sep) vs C=(s, sep, heap)
5234// Rust idiom replacement: `slice::join` covers the C `zalloc`+`strcpy`
5235// loop with running length pre-compute; the `heap` arg drops since
5236// String owns its own allocation.
5237/// `sepjoin` — see implementation.
5238pub fn sepjoin(arr: &[String], sep: Option<&str>) -> String {
5239 // c:3928
5240 // c:3934-3935 — if (!*s) return heap ? dupstring("") : ztrdup("");
5241 if arr.is_empty() {
5242 return String::new();
5243 }
5244 // c:3936-3945 — `if (!sep)` default-sep arm:
5245 // if (ifs && *ifs != ' ') sep = dupstrpfx(ifs, MB_METACHARLEN(ifs));
5246 // else sep = " ";
5247 // IFS SET-but-EMPTY takes the first branch (`*ifs` is '\0' != ' ')
5248 // and dupstrpfx("", …) yields an EMPTY separator — `"$*"` with
5249 // IFS="" concatenates. Only IFS UNSET (ifs == NULL → getsparam
5250 // None) or starting with a space falls back to " ".
5251 let ifs_storage: String;
5252 let sep_str: &str = match sep {
5253 Some(s) => s, // c:3936
5254 None => {
5255 ifs_storage = match getsparam("IFS") {
5256 // c:3938-3940 — set, first char not space (empty → "").
5257 Some(ifs) if !ifs.starts_with(' ') => ifs
5258 .chars()
5259 .next()
5260 .map(|c| c.to_string())
5261 .unwrap_or_default(),
5262 // c:3941-3944 — unset (NULL) or leading space → " ".
5263 _ => " ".to_string(),
5264 };
5265 &ifs_storage
5266 }
5267 };
5268 // c:3947-3956 — pre-compute total length, alloc, copy elements
5269 // interleaving sep. Rust slice::join collapses all that
5270 // into the one canonical call.
5271 arr.join(sep_str)
5272}
5273
5274/// Split string by separator - port from zsh/Src/utils.c sepsplit() lines 3961-3992
5275///
5276/// If sep is None, performs IFS-style word splitting (spacesplit).
5277/// Otherwise splits on the given separator string.
5278/// allownull: if true, allows empty strings in result
5279/// Split a string on `IFS` separators.
5280/// Port of `sepsplit(char *s, char *sep, int allownull, int heap)` from Src/utils.c:3962.
5281/// WARNING: param names don't match C — Rust=(s, sep, allownull) vs C=(s, sep, allownull, heap)
5282pub fn sepsplit(s: &str, sep: Option<&str>, allownull: bool) -> Vec<String> {
5283 // c:3962
5284 // Handle Nularg at start (zsh internal marker) - line 3968
5285 let s = if s.starts_with('\x00') && s.len() > 1 {
5286 &s[1..]
5287 } else {
5288 s
5289 };
5290
5291 match sep {
5292 None => spacesplit(s, allownull),
5293 Some("") => {
5294 // Empty separator: split into characters
5295 if allownull {
5296 s.chars().map(|c| c.to_string()).collect()
5297 } else {
5298 s.chars()
5299 .map(|c| c.to_string())
5300 .filter(|c| !c.is_empty())
5301 .collect()
5302 }
5303 }
5304 Some(sep) => {
5305 let parts: Vec<String> = s.split(sep).map(|p| p.to_string()).collect();
5306 if allownull {
5307 parts
5308 } else {
5309 parts.into_iter().filter(|p| !p.is_empty()).collect()
5310 }
5311 }
5312 }
5313}
5314
5315/// Port of `getshfunc(char *nam)` from `Src/utils.c:3998`.
5316///
5317/// C body:
5318/// ```c
5319/// Shfunc getshfunc(char *nam) {
5320/// return (Shfunc) shfunctab->getnode(shfunctab, nam);
5321/// }
5322/// ```
5323///
5324/// Routes through the global `shfunctab` singleton in
5325/// hashtable.rs (hashtable::shfunctab_lock). Returns an owned
5326/// `shfunc` clone so callers can read `flags` (to detect
5327/// `PM_UNDEFINED` autoload stubs), `funcdef`, `filename`, and
5328/// `body` without holding the table lock. Owned clone vs C's
5329/// `*Shfunc` raw pointer trades one allocation for Rust
5330/// lifetime safety — `getshfunc` is a function-lookup site,
5331/// not per-statement, so the cost is irrelevant.
5332///
5333/// Returning `Option<String>` of just `body` (the old contract)
5334/// made every PM_UNDEFINED autoload stub invisible because
5335/// `body=None` collapsed via `and_then` to `None`, which callers
5336/// then read as "function doesn't exist."
5337pub fn getshfunc(nam: &str) -> Option<shfunc> {
5338 let tab = shfunctab_lock().read().expect("shfunctab poisoned");
5339 tab.get(nam).cloned()
5340}
5341
5342/// Port of `char **subst_string_by_func(Shfunc func, char *arg1, char *orig)`
5343/// from Src/utils.c:4017.
5344///
5345/// Calls the named shell function with `[func, arg1?, orig]` as
5346/// positional args under `sfcontext = SFC_SUBST` and returns the
5347/// `$reply` array on success. Routes through `callhookfunc` (the
5348/// static-linked equivalent of `doshfunc`), then reads `$reply`
5349/// from the env-var fallback because the global `paramtab` is not
5350/// yet a singleton in the Rust port (params::getaparam takes a
5351/// `&ParamTable` arg).
5352pub fn subst_string_by_func(
5353 func_name: &str,
5354 arg1: Option<&str>,
5355 orig: &str,
5356) -> Option<Vec<String>> // c:4017
5357{
5358 let osc = SFCONTEXT.load(Ordering::Relaxed); // c:4019
5359 let osm = STOPMSG.load(Ordering::Relaxed);
5360 let old_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
5361 let mut args: Vec<String> = Vec::with_capacity(3); // c:4020-4026
5362 args.push(func_name.to_string()); // c:4023
5363 if let Some(a) = arg1 {
5364 // c:4024
5365 args.push(a.to_string()); // c:4025
5366 }
5367 args.push(orig.to_string()); // c:4026
5368 SFCONTEXT // c:4027
5369 .store(SFC_SUBST, Ordering::Relaxed);
5370 INCOMPFUNC.store(0, Ordering::Relaxed); // c:4028
5371
5372 // c:4030 — `if (doshfunc(func, l, 1))`. Direct doshfunc call
5373 // mirrors C. body_runner routes through the host body-only entry
5374 // (matching the same shfunc that `callhookfunc` would resolve).
5375 let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
5376 .read()
5377 .ok()
5378 .and_then(|t| t.get(func_name).cloned());
5379 let rc = if let Some(mut shf) = shf_clone {
5380 let name_for_body = func_name.to_string();
5381 let body_args = args.clone();
5382 let body_runner = move || -> i32 {
5383 crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
5384 };
5385 crate::ported::exec::doshfunc(&mut shf, args.clone(), true, body_runner)
5386 } else {
5387 callhookfunc(func_name, Some(&args), 0, std::ptr::null_mut())
5388 };
5389 // c:4033 — `ret = getaparam("reply")` against paramtab. `reply`
5390 // is a shell-local PM_ARRAY entry, never exported to env.
5391 let ret: Option<Vec<String>> = if rc != 0 {
5392 None // c:4031
5393 } else {
5394 getaparam("reply") // c:4033
5395 };
5396
5397 SFCONTEXT.store(osc, Ordering::Relaxed); // c:4035
5398 STOPMSG.store(osm, Ordering::Relaxed); // c:4036
5399 INCOMPFUNC.store(old_incompfunc, Ordering::Relaxed); // c:4037
5400 ret // c:4038
5401}
5402
5403/// Port of `char **subst_string_by_hook(char *name, char *arg1, char *orig)`
5404/// from Src/utils.c:4049.
5405///
5406/// Looks up `name` as a shell function and calls
5407/// `subst_string_by_func` on it. If that returns no result, walks
5408/// `${name}_hook` as an array of function names, trying each in
5409/// order until one yields a `$reply`.
5410pub fn subst_string_by_hook(name: &str, arg1: Option<&str>, orig: &str) -> Option<Vec<String>> // c:4049
5411{
5412 let mut ret: Option<Vec<String>> = None;
5413 if getshfunc(name).is_some() {
5414 // c:4054
5415 ret = subst_string_by_func(name, arg1, orig); // c:4055
5416 }
5417 if ret.is_none() {
5418 // c:4058
5419 let arrnam = format!("{}_hook", name); // c:4061-4063
5420 // c:4065 — `arr = getaparam(arrnam)`. The previous Rust port
5421 // read `env::var(arrnam)` and NUL-split — wrong: the hook
5422 // array is a shell-local PM_ARRAY in paramtab, not env.
5423 if let Some(arr) = getaparam(&arrnam) {
5424 // c:4065
5425 for f in arr.iter() {
5426 // c:4068
5427 if f.is_empty() {
5428 continue;
5429 }
5430 if getshfunc(f).is_some() {
5431 // c:4069
5432 ret = subst_string_by_func(f, arg1, orig); // c:4070
5433 if ret.is_some() {
5434 // c:4071
5435 break; // c:4072
5436 }
5437 }
5438 }
5439 }
5440 }
5441 ret // c:4094
5442}
5443
5444/// Make single-element array (from utils.c mkarray)
5445/// Port of `mkarray(char *s)` from `Src/utils.c:4083`.
5446pub fn mkarray(s: Option<&str>) -> Vec<String> {
5447 match s {
5448 Some(val) => vec![val.to_string()],
5449 None => Vec::new(),
5450 }
5451}
5452
5453/// Make single-element array on heap (from utils.c hmkarray)
5454/// Port of `hmkarray(char *s)` from `Src/utils.c:4094`.
5455pub fn hmkarray(s: &str) -> Vec<String> {
5456 if s.is_empty() {
5457 Vec::new()
5458 } else {
5459 vec![s.to_string()]
5460 }
5461}
5462
5463/// Port of `void zbeep(void)` from Src/utils.c:4105.
5464///
5465/// Honours `$ZBEEP` (a key-string sequence) when set and the BEEP
5466/// option when unset; emits the BEL char (\007) to SHTTY by
5467/// default. The Rust port writes via `write_loop` to mirror C's
5468/// raw-write semantics.
5469pub fn zbeep() {
5470 // c:4105
5471 queue_signals(); // c:4105
5472 if let Ok(zbeep) = std::env::var("ZBEEP") {
5473 // c:4109
5474 let (decoded, _) = getkeystring(&zbeep); // c:4111
5475 #[cfg(unix)]
5476 {
5477 let shtty = SHTTY.load(Ordering::Relaxed);
5478 if shtty != -1 {
5479 let _ = write_loop(shtty, decoded.as_bytes()); // c:4112
5480 } else {
5481 eprint!("{}", decoded);
5482 }
5483 }
5484 #[cfg(not(unix))]
5485 eprint!("{}", decoded);
5486 } else if isset(BEEP) {
5487 // c:4113
5488 #[cfg(unix)]
5489 {
5490 let shtty = SHTTY.load(Ordering::Relaxed);
5491 if shtty != -1 {
5492 let _ = write_loop(shtty, b"\x07"); // c:4114
5493 } else {
5494 eprint!("\x07");
5495 }
5496 }
5497 #[cfg(not(unix))]
5498 eprint!("\x07");
5499 }
5500 unqueue_signals(); // c:4115
5501}
5502
5503/// Free array (no-op in Rust, provided for API compat)
5504/// Port of `freearray(char **s)` from `Src/utils.c:4120`.
5505pub fn freearray(s: Vec<String>) {
5506 // c:4124 — DPUTS(!s, "freearray() with zero argument")
5507 // Rust takes Vec<String> by value (never null), so the C !s
5508 // check maps to no condition that can fire in Rust. Document
5509 // the gap.
5510 let _ = &s; // c:4124 (no-op; Vec is never NULL in Rust)
5511 // Rust Drop handles this
5512}
5513
5514/// Split on '=' returning (name, value) (from utils.c equalsplit)
5515/// Port of `equalsplit(char *s, char **t)` from `Src/utils.c:4133`.
5516/// WARNING: param names don't match C — Rust=(s) vs C=(s, t)
5517pub fn equalsplit(s: &str) -> Option<(String, String)> {
5518 let eq = s.find('=')?;
5519 Some((s[..eq].to_string(), s[eq + 1..].to_string()))
5520}
5521
5522// initialize the ztypes table // c:4151
5523/// Port of `inittyptab()` from `Src/utils.c:4155`. Initialise the
5524/// `typtab[256]` lookup table that backs the `idigit`/`ialnum`/etc.
5525/// predicates in `ztype_h`.
5526///
5527/// C body (c:4155-4250) does:
5528/// 1. Zero the table.
5529/// 2. Mark 0..=31 and 128..=159 + 127 as ICNTRL.
5530/// 3. Mark '0'..='9' as IDIGIT|IALNUM|IWORD|IIDENT|IUSER.
5531/// 4. Mark 'a'..='z' and 'A'..='Z' as IALPHA|IALNUM|IIDENT|IUSER|IWORD.
5532/// 5. Mark '_' as IIDENT|IUSER.
5533/// 6. Mark '-', '.', Dash as IUSER.
5534/// 7. Mark ' '/'\t' as IBLANK|INBLANK; '\n' as INBLANK.
5535/// 8. Mark '\0', Meta, Marker as IMETA.
5536/// 9. Mark Pound..LAST_NORMAL_TOK as ITOK|IMETA.
5537/// 10. Mark Snull..Nularg as ITOK|IMETA|INULL.
5538/// 11. Walk $IFS adding ISEP and IWSEP for blanks.
5539///
5540/// This first-pass port covers steps 1-7. Steps 8-11 require Meta /
5541/// Marker / Pound / Snull / Nularg constants from zsh_h/zsh.h and
5542/// the `ifs` global; the remaining Meta/IFS marks are skipped until
5543/// those land. Idempotent — safe to call multiple times.
5544pub fn inittyptab() {
5545 // utils.c:4155
5546
5547 // c:4160 — `if (!(typtab_flags & ZTF_INIT))` one-off init.
5548 {
5549 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5550 if (flags & ZTF_INIT) == 0 {
5551 TYPTAB_FLAGS.store(ZTF_INIT, Ordering::Relaxed);
5552 }
5553 }
5554
5555 // Local scratch, flushed to the atomic table at fn end — C mutates
5556 // typtab[] in place unlocked; the atomic store-per-slot flush keeps
5557 // readers lock-free (see TYPTAB doc in ztype_h.rs).
5558 let mut t = [0u32; 256]; // c:4168 memset(typtab, 0, sizeof(typtab))
5559 // c:4169-4170 — control chars 0..32 and 128..160.
5560 for c in 0..32u32 {
5561 t[c as usize] = ICNTRL as u32;
5562 t[(c + 128) as usize] = ICNTRL as u32;
5563 }
5564 // c:4171 — `typtab[127] = ICNTRL;`
5565 t[127] = ICNTRL as u32;
5566 // c:4172-4173 — '0'..='9'.
5567 for c in (b'0' as usize)..=(b'9' as usize) {
5568 t[c] = (IDIGIT | IALNUM | IWORD | IIDENT | IUSER) as u32;
5569 }
5570 // c:4174-4175 — 'a'..='z' and matching 'A'..='Z'.
5571 for c in (b'a' as usize)..=(b'z' as usize) {
5572 let upper = c - (b'a' as usize) + (b'A' as usize);
5573 let bits = (IALPHA | IALNUM | IIDENT | IUSER | IWORD) as u32;
5574 t[c] = bits;
5575 t[upper] = bits;
5576 }
5577 // c:4190 — `typtab['_'] = IIDENT | IUSER;`
5578 t[b'_' as usize] = (IIDENT | IUSER) as u32;
5579 // c:4191 — `typtab['-'] = typtab['.'] = typtab[(unsigned char) Dash] = IUSER;`.
5580 t[b'-' as usize] = IUSER as u32;
5581 t[b'.' as usize] = IUSER as u32;
5582 // c:4191 — `Dash` token marker (0x9b per zsh.h:182, "Only in patterns").
5583 // Marking it IUSER lets pattern-side $-named-character paths
5584 // accept it as a user-name byte. Previously omitted in the port.
5585 t[crate::ported::zsh_h::Dash as usize] = IUSER as u32;
5586 // c:4192-4194 — blanks.
5587 t[b' ' as usize] |= (IBLANK | INBLANK) as u32;
5588 t[b'\t' as usize] |= (IBLANK | INBLANK) as u32;
5589 t[b'\n' as usize] |= INBLANK as u32;
5590
5591 // c:4195 — `typtab['\0'] |= IMETA;`. Previously omitted in the
5592 // Rust port — '\0' had only ICNTRL (set by the 0..32 loop at
5593 // c:4169) and was missing IMETA. C `imeta('\0')` is true (the
5594 // NUL byte must be Meta-encoded as `Meta + ('\0' ^ 32)`); the
5595 // Rust `imeta()` typtab predicate returned false, so any code
5596 // routing through `imeta(c)` (e.g. `input::shingetline`)
5597 // failed to Meta-encode NUL bytes from stdin, corrupting the
5598 // SHIN buffer when piped binary data hit a `\0`.
5599 t[0] |= IMETA as u32; // c:4195
5600 // c:4196-4197 — Meta + Marker marked IMETA.
5601 {
5602 t[Meta as usize] |= IMETA as u32;
5603 t[Marker as usize] |= IMETA as u32;
5604 }
5605
5606 // c:4133-4134 — `for (t0 = Pound; t0 <= LAST_NORMAL_TOK; t0++)
5607 // typtab[t0] |= ITOK | IMETA;`
5608 // Marks all char-rewrite token markers (Pound, Stringg, Hat,
5609 // Star, ...). Without this, `itok(Stringg)` returns false and
5610 // `is_valid_assignment_target("$NAME")` wrongly accepts the
5611 // leading `$` as part of an identifier prefix → `$=cmd` lexes
5612 // as ENVSTRING instead of STRING.
5613 {
5614 let lo = Pound as usize;
5615 let hi = LAST_NORMAL_TOK as usize;
5616 for t0 in lo..=hi {
5617 t[t0] |= (ITOK | IMETA) as u32;
5618 }
5619 }
5620
5621 // c:4135-4136 — `for (t0 = Snull; t0 <= Nularg; t0++)
5622 // typtab[t0] |= ITOK | IMETA | INULL;`
5623 {
5624 let lo = Snull as usize;
5625 let hi = Nularg as usize;
5626 for t0 in lo..=hi {
5627 t[t0] |= (ITOK | IMETA | INULL) as u32;
5628 }
5629 }
5630
5631 // c:4202-4231 — IFS walk. Sets ISEP on every IFS char and IWSEP
5632 // on the blank (inblank) subset. Reads the current `ifs` global
5633 // (defaulting to DEFAULT_IFS), demetafies `Meta+X` pairs, and
5634 // skips a doubled blank (`s[1]==c`) so the IWSEP bit doesn't
5635 // mark "blank repeated → no-skip" IFS chars. Mirrors C exactly.
5636 {
5637 // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; ...)`.
5638 // C reads the global `ifs` variable (the same one `ifssetfn`
5639 // writes). The Rust port walks paramtab first (so the GSU
5640 // dispatch path matches C); on miss, fall through to ifs_lock
5641 // directly so a fresh `ifssetfn` update before any paramtab
5642 // entry exists is still visible to inittyptab.
5643 // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; *s; s++)`.
5644 // The default-IFS fallback is keyed on the `ifs` POINTER being
5645 // NULL (i.e. IFS unset), NOT on it pointing at an empty string.
5646 // `IFS=''` leaves a non-NULL empty `ifs`, the loop body never
5647 // runs, and typtab ends up with ZERO ISEP/IWSEP bits — which is
5648 // exactly how an empty IFS disables all word splitting
5649 // (`IFS=''; v='a b c'; print -rl -- ${=v}` is one word). The
5650 // port used to coerce "" → DEFAULT_IFS at both the paramtab read
5651 // and the fallback, so an empty IFS still split on whitespace.
5652 let pt_ifs: Option<String> = crate::ported::params::paramtab()
5653 .read()
5654 .ok()
5655 .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)));
5656 let src: String = match pt_ifs {
5657 // IFS is SET (possibly to ""): walk its chars verbatim.
5658 Some(v) => v,
5659 // No paramtab entry yet. Fall through to the ifs_lock global so
5660 // a fresh ifssetfn update landing before the paramtab entry
5661 // exists is still visible; an empty global there means "not
5662 // initialised", which is C's NULL → CURRENT_DEFAULT_IFS.
5663 None => {
5664 let g = crate::ported::params::ifs_lock()
5665 .lock()
5666 .map(|g| g.clone())
5667 .unwrap_or_default();
5668 if g.is_empty() {
5669 DEFAULT_IFS.to_string() // c:4216 (ifs == NULL)
5670 } else {
5671 g
5672 }
5673 }
5674 };
5675 let bytes = src.as_bytes();
5676 let mut i = 0;
5677 while i < bytes.len() {
5678 // c:4217 — `int c = (unsigned char) (*s == Meta ? *++s ^ 32 : *s)`.
5679 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
5680 i += 1;
5681 bytes[i] ^ 32
5682 } else {
5683 bytes[i]
5684 };
5685 // c:4218-4223 — MULTIBYTE non-ASCII skip. Bytes >= 0x80
5686 // (after demetafy) are not classified by typtab — they
5687 // reach wcsitype via WC_ZISTYPE instead.
5688 if c >= 0x80 {
5689 i += 1;
5690 continue;
5691 }
5692 let cu = c as usize;
5693 // c:4224-4229 — `if (inblank(c))` — for default-IFS
5694 // chars space/tab/newline, mark IWSEP unless the next
5695 // byte repeats the same char.
5696 let is_inblank = (t[cu] & (INBLANK as u32)) != 0;
5697 if is_inblank {
5698 if i + 1 < bytes.len() && bytes[i + 1] == c {
5699 i += 1; // c:4226 — skip the dup
5700 } else {
5701 t[cu] |= IWSEP as u32; // c:4228
5702 }
5703 }
5704 // c:4230 — `typtab[c] |= ISEP;`
5705 t[cu] |= ISEP as u32;
5706 i += 1;
5707 }
5708 }
5709
5710 // c:4232-4252 — wordchars walk. ORs IWORD onto every byte in
5711 // `$WORDCHARS` (or DEFAULT_WORDCHARS when unset). Used by every
5712 // word-class lookup in pattern matching, `${var:#word}`, etc.
5713 // Drops to ASCII-only under MULTIBYTE_SUPPORT (the non-ASCII path
5714 // routes through wordchars_wide).
5715 {
5716 // Same fallback as IFS above: paramtab first, then wordchars_lock
5717 // so wordcharssetfn updates that bypass paramtab still reach
5718 // the typtab rebuild.
5719 let wc = crate::ported::params::paramtab()
5720 .read()
5721 .ok()
5722 .and_then(|t| {
5723 t.get("WORDCHARS")
5724 .map(|pm| crate::ported::params::wordcharsgetfn(pm))
5725 })
5726 .filter(|s| !s.is_empty())
5727 .unwrap_or_else(|| {
5728 crate::ported::params::wordchars_lock()
5729 .lock()
5730 .map(|g| g.clone())
5731 .unwrap_or_default()
5732 });
5733 let src: String = if wc.is_empty() {
5734 DEFAULT_WORDCHARS.to_string()
5735 } else {
5736 wc
5737 };
5738 let bytes = src.as_bytes();
5739 let mut i = 0;
5740 while i < bytes.len() {
5741 // c:4238 — Meta+X demetafy.
5742 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
5743 i += 1;
5744 bytes[i] ^ 32
5745 } else {
5746 bytes[i]
5747 };
5748 // c:4239-4249 — MULTIBYTE non-ASCII skip.
5749 if c < 0x80 {
5750 t[c as usize] |= IWORD as u32; // c:4251
5751 }
5752 i += 1;
5753 }
5754 }
5755
5756 // c:4253-4254 — SPECCHARS walk. ORs ISPECIAL onto every member
5757 // of the hardcoded SPECCHARS string. Drives glob-special and
5758 // quote-special detection.
5759 {
5760 for &b in SPECCHARS.as_bytes() {
5761 t[b as usize] |= ISPECIAL as u32; // c:4254
5762 }
5763 }
5764
5765 // c:4255-4256 — comma special only when ZTF_SP_COMMA was set
5766 // via `makecommaspecial(1)`. KSH_GLOB / extended-glob path.
5767 {
5768 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5769 if (flags & ZTF_SP_COMMA) != 0 {
5770 // c:4255
5771 t[b',' as usize] |= ISPECIAL as u32; // c:4256
5772 }
5773 }
5774
5775 // c:4257-4261 — bangchar special when BANGHIST + interact +
5776 // bangchar != 0. Sets ZTF_BANGCHAR flag bit then marks the
5777 // bangchar byte ISPECIAL.
5778 {
5779 let bangchar2 = bangchar.load(Ordering::SeqCst) as usize;
5780 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5781 let interact_flag = (flags & ZTF_INTERACT) != 0;
5782 let banghist = isset(BANGHIST);
5783 if banghist && bangchar2 != 0 && bangchar2 < 256 && interact_flag {
5784 // c:4257
5785 TYPTAB_FLAGS.fetch_or(ZTF_BANGCHAR, Ordering::Relaxed); // c:4258
5786 t[bangchar2] |= ISPECIAL as u32; // c:4259
5787 } else {
5788 TYPTAB_FLAGS.fetch_and(!ZTF_BANGCHAR, Ordering::Relaxed); // c:4261
5789 }
5790 }
5791
5792 // c:4262-4263 — PATCHARS walk. ORs IPATTERN onto every member.
5793 // Used by pattern compilation to detect glob metachars.
5794 {
5795 for &b in PATCHARS.as_bytes() {
5796 t[b as usize] |= IPATTERN as u32; // c:4263
5797 }
5798 }
5799
5800 // Flush the scratch to the lock-free table (see header note).
5801 for (i, v) in t.iter().enumerate() {
5802 TYPTAB[i].store(*v, Ordering::Relaxed);
5803 }
5804}
5805
5806/// Port of `void makecommaspecial(int yesno)` from Src/utils.c:4270.
5807///
5808/// Toggles `ZTF_SP_COMMA` and the `ISPECIAL` bit on `,` in the
5809/// global typtab — used by glob/extended-glob to flag `,`
5810/// (KSH_GLOB) as a metacharacter.
5811pub fn makecommaspecial(yesno: bool) {
5812 // c:4270
5813 if yesno {
5814 // c:4272
5815 TYPTAB_FLAGS.fetch_or(ZTF_SP_COMMA, Ordering::Relaxed); // c:4273
5816 TYPTAB[b',' as usize].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4274
5817 } else {
5818 TYPTAB_FLAGS.fetch_and(!ZTF_SP_COMMA, Ordering::Relaxed); // c:4276
5819 TYPTAB[b',' as usize].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4277
5820 }
5821}
5822
5823/// Port of `void makebangspecial(int yesno)` from Src/utils.c:4283.
5824///
5825/// Toggles `ISPECIAL` on the current `bangchar`. When `yesno==0`
5826/// always clears; when nonzero, sets only if `ZTF_BANGCHAR` was
5827/// stored by `inittyptab` (i.e. BANGHIST is on).
5828pub fn makebangspecial(yesno: bool) {
5829 // c:4283
5830 let bc = bangchar.load(Ordering::SeqCst) as usize;
5831 if bc == 0 || bc >= 256 {
5832 return;
5833 }
5834 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5835 if !yesno {
5836 // c:4289
5837 TYPTAB[bc].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4290
5838 } else if (flags & ZTF_BANGCHAR) != 0 {
5839 // c:4291
5840 TYPTAB[bc].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4292
5841 }
5842}
5843
5844/// Port of `wcsiblank(wint_t wc)` from `Src/utils.c:4302`.
5845///
5846/// ```c
5847/// mod_export int wcsiblank(wint_t wc) {
5848/// if (iswspace(wc) && wc != L'\n')
5849/// return 1;
5850/// return 0;
5851/// }
5852/// ```
5853///
5854/// "wide-character version of the iblank() macro" — true for any
5855/// whitespace EXCEPT newline. The previous Rust port included
5856/// newline (since `c.is_whitespace()` returns true for '\n') —
5857/// wrong for callers that use this to find token boundaries.
5858pub fn wcsiblank(wc: char) -> bool {
5859 wc.is_whitespace() && wc != '\n'
5860}
5861
5862/// Port of `int wcsitype(wchar_t c, int itype)` from Src/utils.c:4321.
5863///
5864/// "zistype macro extended to support wide characters. Works for
5865/// IIDENT, IWORD, IALNUM, ISEP."
5866///
5867/// The Rust port checks whether `c` falls in the typtab class
5868/// represented by `itype`. ASCII chars consult the global TYPTAB
5869/// (the same one C's `zistype()` macro indexes); non-ASCII chars
5870/// route through Unicode predicates that mirror the C `iswalnum`
5871/// fallback at line 4346.
5872pub fn wcsitype(c: char, itype: u32) -> bool {
5873 // c:4321
5874 if !isset(MULTIBYTE) {
5875 // c:4327
5876 if (c as u32) < 256 {
5877 return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
5878 }
5879 return false;
5880 }
5881 if (c as u32) < 128 {
5882 // c:4343
5883 return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
5884 }
5885 let cls = itype as u16;
5886 if cls == IIDENT {
5887 // c:4347
5888 if isset(POSIXIDENTIFIERS) {
5889 return false; // c:4348
5890 }
5891 return c.is_alphanumeric(); // c:4350
5892 }
5893 if cls == IWORD {
5894 // c:4352
5895 if c.is_alphanumeric() {
5896 return true;
5897 } // c:4353
5898 // C: IS_COMBINING(c) — no Rust crate-free combining-mark
5899 // predicate. zero-width chars (combining, zero-width-joiner,
5900 // etc.) are treated as word per c:4362.
5901 if unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0 {
5902 // c:4362
5903 return true;
5904 }
5905 // c:4364 — `wmemchr(wordchars_wide.chars, c, …)`. Reads from
5906 // the canonical `wordchars` global (writable by
5907 // `wordcharssetfn` at `Src/params.c:5143`). Previously routed
5908 // through `std::env::var("WORDCHARS")` which is the libc
5909 // process environment — never reflects runtime `WORDCHARS=:`
5910 // assignments inside the shell.
5911 let w = crate::ported::params::paramtab()
5912 .read()
5913 .ok()
5914 .and_then(|t| {
5915 t.get("WORDCHARS")
5916 .map(|pm| crate::ported::params::wordcharsgetfn(pm))
5917 })
5918 .unwrap_or_default();
5919 return w.chars().any(|x| x == c);
5920 }
5921 if cls == ISEP {
5922 // c:4366
5923 // c:4367 — same canonical-global pattern for IFS.
5924 let ifs = crate::ported::params::paramtab()
5925 .read()
5926 .ok()
5927 .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
5928 .unwrap_or_default();
5929 return ifs.chars().any(|x| x == c);
5930 }
5931 let _ = IALNUM;
5932 c.is_alphanumeric() // c:4370
5933}
5934
5935/// Check if a character type at end of string (from utils.c itype_end)
5936/// Port of `char *itype_end(const char *ptr, int itype, int once)` from `Src/utils.c:4395`.
5937///
5938/// Walks `ptr` byte-by-byte advancing past every char whose
5939/// type-bits include `itype`. Returns the byte offset where the
5940/// walk stops (C returns the advanced pointer; the byte offset is
5941/// the Rust equivalent — `ptr + return_value` reproduces the C
5942/// pointer).
5943///
5944/// C body branches (ported line-for-line):
5945///
5946/// 1. **INAMESPC special-case** (c:4399-4413). If itype == INAMESPC
5947/// and we're not POSIXIDENTIFIERS-strict outside ksh emulation,
5948/// recurse on `ptr+(*ptr=='.')` with IIDENT to allow dotted
5949/// ksh93 namespace names. The recursion result determines
5950/// whether to advance past the dot or fall through.
5951///
5952/// 2. **MULTIBYTE branch** (c:4416-4470, gated `isset(MULTIBYTE)
5953/// && (itype != IIDENT || !isset(POSIXIDENTIFIERS))`). Walks
5954/// multibyte chars via mb_metacharlenconv (multibyte len +
5955/// wchar conversion). Per-codepoint test:
5956/// - itok byte (raw command line): test the byte via zistype
5957/// - Meta-prefixed pair: extract real byte via XOR, test via
5958/// zistype
5959/// - ASCII (< 0x80): test via zistype
5960/// - Non-ASCII codepoint: route through wcsitype (which
5961/// already handles IWORD/ISEP via WORDCHARS/IFS paramtab
5962/// lookup matching c:4364/c:4367)
5963///
5964/// 3. **Non-multibyte branch** (c:4474-4480). Simple Meta-byte +
5965/// zistype byte loop.
5966///
5967/// `once = true` stops after the first matching char (used for
5968/// "is this a valid first char" checks per c:1576 etc).
5969pub fn itype_end(s: &str, mut itype: u32, once: bool) -> usize {
5970 use crate::ported::zsh_h::{isset, Meta, EMULATE_KSH, EMULATION, MULTIBYTE, POSIXIDENTIFIERS};
5971 use crate::ported::ztype_h::{itok, zistype, IIDENT, INAMESPC};
5972
5973 // c:4399-4413 — INAMESPC handling with ksh93 namespace dot.
5974 let mut start = 0usize;
5975 if itype == INAMESPC {
5976 itype = IIDENT as u32;
5977 if !isset(POSIXIDENTIFIERS) || EMULATION(EMULATE_KSH) {
5978 // c:4403 — `t = itype_end(ptr + (*ptr == '.'), itype, 0);`
5979 let first_is_dot = s.as_bytes().first().copied() == Some(b'.');
5980 let recurse_start = if first_is_dot { 1 } else { 0 };
5981 let t = recurse_start + itype_end(&s[recurse_start..], itype, false);
5982 // c:4404-4410 — `if (t > ptr + (*ptr == '.')) {
5983 // if (*t == '.') ptr = t + 1; /* Fall through */
5984 // else if (!once) return t;
5985 // }`
5986 if t > recurse_start {
5987 let t_byte = s.as_bytes().get(t).copied();
5988 if t_byte == Some(b'.') {
5989 start = t + 1; // c:4407
5990 } else if !once {
5991 return t; // c:4409
5992 }
5993 }
5994 }
5995 }
5996
5997 let bytes = s.as_bytes();
5998 let mb = isset(MULTIBYTE) && (itype != IIDENT as u32 || !isset(POSIXIDENTIFIERS));
5999
6000 if mb {
6001 // c:4416-4470 — multibyte walk. mb_charinit() in C is a
6002 // no-op stateless reset; not needed in Rust since str::chars
6003 // is already stateless UTF-8.
6004 let mut i = start;
6005 while i < bytes.len() {
6006 let b = bytes[i];
6007 // c:4422-4425 — itok: raw token byte, test via zistype,
6008 // advance 1.
6009 if itok(b) {
6010 if !zistype(b, itype) {
6011 break;
6012 }
6013 i += 1;
6014 } else if b == Meta {
6015 // c:4438-4440 (WEOF arm) + non-MB Meta fallback:
6016 // Meta-prefixed pair = single unescaped char via XOR.
6017 let nxt = bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20;
6018 if (nxt as u32) > 127 || !zistype(nxt, itype) {
6019 break;
6020 }
6021 i += 2;
6022 } else if b < 0x80 {
6023 // c:4441-4443 (len == 1 && isascii) — ASCII: direct
6024 // zistype on the byte.
6025 if !zistype(b, itype) {
6026 break;
6027 }
6028 i += 1;
6029 } else {
6030 // c:4446-4467 — non-ASCII codepoint. Decode the UTF-8
6031 // sequence starting at `i` and route through wcsitype
6032 // (which already handles IWORD via WORDCHARS paramtab
6033 // c:4364, ISEP via IFS paramtab c:4367, default via
6034 // iswalnum/is_alphanumeric c:4370).
6035 let tail = match std::str::from_utf8(&bytes[i..]) {
6036 Ok(t) => t,
6037 Err(_) => break, // invalid UTF-8 — stop walk
6038 };
6039 let ch = match tail.chars().next() {
6040 Some(c) => c,
6041 None => break,
6042 };
6043 if !wcsitype(ch, itype) {
6044 break;
6045 }
6046 i += ch.len_utf8();
6047 }
6048 if once {
6049 break;
6050 }
6051 }
6052 i
6053 } else {
6054 // c:4474-4480 — non-multibyte arm. Simple byte loop with
6055 // Meta-prefix collapse.
6056 let mut i = start;
6057 while i < bytes.len() {
6058 let b = bytes[i];
6059 let chr = if b == Meta {
6060 bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20
6061 } else {
6062 b
6063 };
6064 if !zistype(chr, itype) {
6065 break;
6066 }
6067 i += if b == Meta { 2 } else { 1 };
6068 if once {
6069 break;
6070 }
6071 }
6072 i
6073 }
6074}
6075
6076/// Duplicate array (from utils.c arrdup)
6077/// Port of `arrdup(char **s)` from `Src/utils.c:4493`.
6078pub fn arrdup(s: &[String]) -> Vec<String> {
6079 s.to_vec()
6080}
6081
6082/// Duplicate array with max elements (from utils.c arrdup_max)
6083/// Port of `arrdup_max(char **s, unsigned max)` from `Src/utils.c:4508`.
6084pub fn arrdup_max(s: &[String], max: usize) -> Vec<String> {
6085 s.iter().take(max).cloned().collect()
6086}
6087
6088/// Duplicate array with zsh allocation (from utils.c zarrdup)
6089/// Port of `zarrdup(char **s)` from `Src/utils.c:4532`.
6090pub fn zarrdup(s: &[String]) -> Vec<String> {
6091 s.to_vec()
6092}
6093
6094/// Duplicate array of wide strings (from utils.c wcs_zarrdup) - same as zarrdup in Rust
6095/// Port of `wcs_zarrdup(wchar_t **s)` from `Src/utils.c:4547`.
6096pub fn wcs_zarrdup(s: &[String]) -> Vec<String> {
6097 s.to_vec()
6098}
6099
6100/// Spelling correction: find closest match (from utils.c spname)
6101/// Port of `spname(char *oldname)` from `Src/utils.c:4562`.
6102/// WARNING: param names don't match C — Rust=(name, dir) vs C=(oldname)
6103pub fn spname(name: &str, dir: &str) -> Option<String> {
6104 let entries = match fs::read_dir(dir) {
6105 Ok(e) => e,
6106 Err(_) => return None,
6107 };
6108
6109 let mut best = None;
6110 let mut best_dist = 4; // threshold
6111
6112 for entry in entries.flatten() {
6113 if let Some(entry_name) = entry.file_name().to_str() {
6114 let dist = spdist(name, entry_name, best_dist);
6115 if dist < best_dist {
6116 best_dist = dist;
6117 best = Some(entry_name.to_string());
6118 }
6119 }
6120 }
6121 best
6122}
6123
6124/// Spelling correction with full path (from utils.c mindist)
6125/// Port of `mindist(char *dir, char *mindistguess, char *mindistbest, int wantdir)` from `Src/utils.c:4624`.
6126/// WARNING: param names don't match C — Rust=(dir, name) vs C=(dir, mindistguess, mindistbest, wantdir)
6127pub fn mindist(dir: &str, name: &str) -> Option<(String, usize)> {
6128 let entries = match fs::read_dir(dir) {
6129 Ok(e) => e,
6130 Err(_) => return None,
6131 };
6132
6133 let mut best = None;
6134 let mut best_dist = 4;
6135
6136 for entry in entries.flatten() {
6137 if let Some(entry_name) = entry.file_name().to_str() {
6138 let dist = spdist(name, entry_name, best_dist);
6139 if dist < best_dist {
6140 best_dist = dist;
6141 best = Some(entry_name.to_string());
6142 }
6143 }
6144 }
6145 best.map(|name| (name, best_dist))
6146}
6147
6148// spellcheck a word // c:3123
6149// fix s ; if hist is nonzero, fix the history list too // c:3124
6150/// Compute edit distance between two strings (for spelling correction)
6151/// Direct port of `int spdist(char *s, char *t, int thresh)` from
6152/// `Src/utils.c:4675-4750`. Drives the CORRECT-option typo prompt:
6153/// returns 0 for identical, 1 for case-only mistakes, 2 for one
6154/// transposition / missing letter / QWERTY-adjacent mistype, 200 for
6155/// anything farther.
6156///
6157/// **This is NOT a Levenshtein DP — that was the previous Rust impl
6158/// dressed as a port.** The C source uses a keyboard-adjacency model
6159/// (qwertykeymap / dvorakkeymap arrays + tulower equality) which is
6160/// the actual behaviour `setopt CORRECT` depends on. Restored
6161/// faithfully.
6162pub fn spdist(s: &str, t: &str, thresh: usize) -> usize {
6163 // c:4679-4690 — qwerty keymap (rows of 14 chars each: numeric row,
6164 // QWERTY top row, home row, bottom row, then shift versions).
6165 // Embedded `\n` / `\t` mark off-grid cells.
6166 const QWERTYKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6167\t1234567890-=\t\
6168\tqwertyuiop[]\t\
6169\tasdfghjkl;'\n\t\
6170\tzxcvbnm,./\t\t\t\
6171\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6172\t!@#$%^&*()_+\t\
6173\tQWERTYUIOP{}\t\
6174\tASDFGHJKL:\"\n\t\
6175\tZXCVBNM<>?\n\n\t\
6176\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
6177 // c:4691-4702 — dvorak keymap, same shape.
6178 const DVORAKKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6179\t1234567890[]\t\
6180\t',.pyfgcrl/=\t\
6181\taoeuidhtns-\n\t\
6182\t;qjkxbmwvz\t\t\t\
6183\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6184\t!@#$%^&*(){}\t\
6185\t\"<>PYFGCRL?+\t\
6186\tAOEUIDHTNS_\n\t\
6187\t:QJKXBMWVZ\n\n\t\
6188\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
6189
6190 // c:4703-4707 — `keymap = isset(DVORAK) ? dvorakkeymap : qwertykeymap;`
6191 let keymap = if isset(DVORAK) {
6192 DVORAKKEYMAP.as_bytes()
6193 } else {
6194 QWERTYKEYMAP.as_bytes()
6195 };
6196
6197 let s_b = s.as_bytes();
6198 let t_b = t.as_bytes();
6199
6200 // c:4709-4710 — `if (!strcmp(s, t)) return 0;`
6201 if s == t {
6202 return 0;
6203 }
6204
6205 // c:4712-4714 — `for (p, q; *p && tulower(*p) == tulower(*q); p++, q++);
6206 // if (!*p && !*q) return 1;` — case-only mismatch.
6207 let mut p = 0usize;
6208 let mut q = 0usize;
6209 while p < s_b.len() && q < t_b.len() && tulower(s_b[p] as char) == tulower(t_b[q] as char) {
6210 p += 1;
6211 q += 1;
6212 }
6213 if p == s_b.len() && q == t_b.len() {
6214 return 1;
6215 }
6216
6217 // c:4715-4716 — `if (!thresh) return 200;`
6218 if thresh == 0 {
6219 return 200;
6220 }
6221
6222 // c:4717-4727 — first walk: detect transposition / missing letter at
6223 // first divergence.
6224 p = 0;
6225 q = 0;
6226 while p < s_b.len() && q < t_b.len() {
6227 if s_b[p] == t_b[q] {
6228 // c:4718-4719 — match: skip (don't count `aa` as transposed).
6229 p += 1;
6230 q += 1;
6231 continue;
6232 }
6233 // c:4720-4721 — `if (p[1] == q[0] && q[1] == p[0]) return
6234 // spdist(p+2, q+2, thresh-1) + 1;` transposition.
6235 if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p + 1] == t_b[q] && t_b[q + 1] == s_b[p] {
6236 // SAFETY: bytes are ASCII for the keymap test below; from_utf8
6237 // is fine on a non-ASCII tail here because spdist takes &str.
6238 let s_tail = std::str::from_utf8(&s_b[p + 2..]).unwrap_or("");
6239 let t_tail = std::str::from_utf8(&t_b[q + 2..]).unwrap_or("");
6240 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
6241 }
6242 // c:4722-4723 — `if (p[1] == q[0]) return spdist(p+1, q, thresh-1) + 2;`
6243 if p + 1 < s_b.len() && s_b[p + 1] == t_b[q] {
6244 let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6245 let t_tail = std::str::from_utf8(&t_b[q..]).unwrap_or("");
6246 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6247 }
6248 // c:4724-4725 — `if (p[0] == q[1]) return spdist(p, q+1, thresh-1) + 2;`
6249 if q + 1 < t_b.len() && s_b[p] == t_b[q + 1] {
6250 let s_tail = std::str::from_utf8(&s_b[p..]).unwrap_or("");
6251 let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6252 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6253 }
6254 // c:4726-4727 — `if (*p != *q) break;`
6255 break;
6256 }
6257 // c:4728-4729 — `if ((!*p && strlen(q) == 1) || (!*q && strlen(p) == 1))
6258 // return 2;` — single trailing-char insertion.
6259 if (p == s_b.len() && (t_b.len() - q) == 1) || (q == t_b.len() && (s_b.len() - p) == 1) {
6260 return 2;
6261 }
6262 // c:4730-4748 — second walk: keyboard-adjacency mistype detection.
6263 p = 0;
6264 q = 0;
6265 while p < s_b.len() && q < t_b.len() {
6266 if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p] != t_b[q] && s_b[p + 1] == t_b[q + 1] {
6267 // c:4737-4738 — `if (!(z = strchr(keymap, p[0])) || *z == '\n' ||
6268 // *z == '\t') return spdist(p+1, q+1,
6269 // thresh-1) + 1;`
6270 let pos = keymap.iter().position(|&b| b == s_b[p]);
6271 let z_ok = match pos {
6272 Some(i) => keymap[i] != b'\n' && keymap[i] != b'\t',
6273 None => false,
6274 };
6275 if !z_ok {
6276 let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6277 let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6278 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
6279 }
6280 // c:4739 — `t0 = z - keymap;`
6281 let t0 = pos.unwrap() as isize;
6282 // c:4740-4744 — eight adjacency offsets (-15,-14,-13,-1,+1,
6283 // +13,+14,+15) → keyboard neighbours.
6284 let offsets: [isize; 8] = [-15, -14, -13, -1, 1, 13, 14, 15];
6285 let adjacent = offsets.iter().any(|&off| {
6286 let idx = t0 + off;
6287 if idx >= 0 && (idx as usize) < keymap.len() {
6288 keymap[idx as usize] == t_b[q]
6289 } else {
6290 false
6291 }
6292 });
6293 if adjacent {
6294 // c:4745 — `return spdist(p+1, q+1, thresh-1) + 2;`
6295 let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6296 let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6297 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6298 }
6299 // c:4746 — `return 200;`
6300 return 200;
6301 } else if p < s_b.len() && q < t_b.len() && s_b[p] != t_b[q] {
6302 // c:4747-4748 — `else if (*p != *q) break;`
6303 break;
6304 }
6305 p += 1;
6306 q += 1;
6307 }
6308 // c:4749 — `return 200;`
6309 200
6310}
6311
6312/// Set terminal to cbreak mode (from utils.c setcbreak)
6313#[cfg(unix)]
6314/// Port of `setcbreak` from `Src/utils.c:4756`.
6315pub fn setcbreak() -> bool {
6316 if let Some(mut ti) = gettyinfo() {
6317 ti.c_lflag &= !(libc::ICANON | libc::ECHO);
6318 ti.c_cc[libc::VMIN] = 1;
6319 ti.c_cc[libc::VTIME] = 0;
6320 settyinfo(&ti)
6321 } else {
6322 false
6323 }
6324}
6325
6326#[cfg(not(unix))]
6327/// Port of `setcbreak` from `Src/utils.c:4756`.
6328pub fn setcbreak() -> bool {
6329 false
6330}
6331
6332/// Port of `void attachtty(pid_t pgrp)` from Src/utils.c:4775.
6333/// Hands the controlling terminal to `pgrp`. Gated by `jobbing &&
6334/// interact`; falls back to `mypgrp` and disables MONITOR on permanent
6335/// failure (matching the C source's recursion + `opts[MONITOR]=0` path).
6336#[cfg(unix)]
6337pub fn attachtty(pgrp: i32) {
6338 // c:4775
6339
6340 if !(jobbing() && interact()) {
6341 return; // c:4779
6342 }
6343 let shtty = SHTTY.load(Ordering::Relaxed); // c:4781
6344 if shtty == -1 {
6345 return;
6346 }
6347 let ep = ATTACHTTY_EP.load(Ordering::Relaxed);
6348 let rc = unsafe { libc::tcsetpgrp(shtty, pgrp) }; // c:4781
6349 if rc == -1 && ep == 0 {
6350 // c:4781
6351 let mypgrp_val = *crate::ported::jobs::MYPGRP // c:4792
6352 .get_or_init(|| Mutex::new(0))
6353 .lock()
6354 .unwrap();
6355 if pgrp != mypgrp_val && unsafe { libc::kill(-pgrp, 0) } == -1 {
6356 attachtty(mypgrp_val); // c:4793
6357 } else {
6358 let errno_val = io::Error::last_os_error().raw_os_error().unwrap_or(0);
6359 if errno_val != libc::ENOTTY {
6360 // c:4795
6361 zwarn(&format!(
6362 "can't set tty pgrp: {}", // c:4797
6363 io::Error::from_raw_os_error(errno_val)
6364 ));
6365 let _ = io::stderr().flush(); // c:4798
6366 }
6367 opt_state_set("monitor", false); // c:4815 opts[MONITOR]=0
6368 ATTACHTTY_EP.store(1, Ordering::Relaxed); // c:4815
6369 }
6370 } else if rc != -1 {
6371 // c:4815
6372 *crate::ported::jobs::LAST_ATTACHED_PGRP // c:4815
6373 .get_or_init(|| Mutex::new(0))
6374 .lock()
6375 .unwrap() = pgrp;
6376 }
6377}
6378
6379/// Port of `pid_t gettygrp(void)` from Src/utils.c:4815.
6380#[cfg(unix)]
6381pub fn gettygrp() -> i32 {
6382 // c:4815
6383 let shtty = SHTTY.load(Ordering::Relaxed);
6384 if shtty == -1 {
6385 // c:4819
6386 return -1; // c:4820
6387 }
6388 unsafe { libc::tcgetpgrp(shtty) } // c:4823
6389}
6390
6391/// Convert raw bytes (possibly containing NUL / 0x83-0x9b) to
6392/// zsh's metafied form: each `imeta(b)` byte becomes `Meta` (0x83)
6393/// followed by `b ^ 32`.
6394///
6395/// Port of `metafy(char *buf, int len, int heap)` from Src/utils.c:4856. The C source takes a
6396/// `heap` mode controlling whether the result is `zalloc`'d /
6397/// `zhalloc`'d / written into a static buffer / appended to the
6398/// existing buffer; in Rust we always return an owned `String`
6399/// since allocation strategy is uniform. The byte-level transform
6400/// is identical: walk the input, count metafy hits, allocate
6401/// `len + meta` bytes, expand each `Meta+X` pair in reverse.
6402/// Rust idiom replacement: forward byte-walk + Vec::push covers the
6403/// C two-pass (count + alloc + reverse-expand) approach; Vec grows
6404/// on demand so the pre-count is unnecessary in Rust.
6405/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len, heap)
6406pub fn metafy(buf: &str) -> String {
6407 // c:4856
6408 let bytes = buf.as_bytes();
6409 let mut out = Vec::with_capacity(bytes.len());
6410 for &b in bytes {
6411 // C: `#define imeta(c) ((c) >= Meta)` from Src/zsh.h —
6412 // every byte >= 0x83 needs escaping. The previous
6413 // narrow-range check `(0x83..=0x9b)` was a bug: bytes
6414 // 0x9c..=0xff (e.g. UTF-8 continuation bytes, high-Latin
6415 // characters) escaped C's imeta() but not the Rust
6416 // version, which then fed un-escaped bytes downstream
6417 // and corrupted Meta-aware loops.
6418 if imeta_byte(b) {
6419 out.push(Meta);
6420 out.push(b ^ 32);
6421 } else {
6422 out.push(b);
6423 }
6424 }
6425 // metafied bytes are in [0..=0x7f]∪{0x83}∪[expanded ^ 32 range];
6426 // String::from_utf8 may fail on the high bytes — fall back to
6427 // lossy. The lossy fallback inserts U+FFFD for invalid
6428 // sequences, which is fine for String-level callers (lexer /
6429 // pattern compile / display) but breaks byte-round-trip with
6430 // `unmetafy`. Byte-round-trip callers should use `metafy_bytes`
6431 // (which preserves the metafied byte sequence verbatim).
6432 String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
6433}
6434
6435/// Port of `ztrdup_metafy(const char *s)` from `Src/utils.c:4929`.
6436///
6437/// ```c
6438/// mod_export char *
6439/// ztrdup_metafy(const char *s)
6440/// {
6441/// if (!s) return NULL;
6442/// return metafy((char *)s, -1, META_DUP);
6443/// }
6444/// ```
6445pub fn ztrdup_metafy(s: &str) -> String {
6446 metafy(s)
6447}
6448
6449/// Port of `unmetafy(char *s, int *len)` from `Src/utils.c:4954`.
6450///
6451/// Take a metafied byte buffer in `s` and convert it in place to
6452/// its literal representation. C signature:
6453///
6454/// ```c
6455/// char *unmetafy(char *s, int *len);
6456/// ```
6457///
6458/// The Rust port mutates `s` in place and returns the resulting
6459/// length (mirroring C's `*len` out-parameter). C control flow:
6460///
6461/// ```c
6462/// for (p = s; *p && *p != Meta; p++); // skip prefix with no Meta
6463/// for (t = p; (*t = *p++);) // walk the rest
6464/// if (*t++ == Meta && *p)
6465/// t[-1] = *p++ ^ 32; // un-escape: XOR with 32
6466/// ```
6467///
6468/// Same algorithm here, byte-indexed against the Vec rather than
6469/// pointer-walked.
6470/// WARNING: param names don't match C — Rust=(s) vs C=(s, len)
6471pub fn unmetafy(s: &mut Vec<u8>) -> usize {
6472 // c:4954
6473 // First loop: find the first `Meta` byte. Everything before it
6474 // stays as-is, so we don't need to copy.
6475 let mut p: usize = 0;
6476 while p < s.len() && s[p] != Meta {
6477 p += 1;
6478 }
6479 // Second loop: walk from `p` onward, copying each byte into the
6480 // `t` slot (which trails `p` by one position per Meta-escape
6481 // we collapse).
6482 let mut t: usize = p;
6483 while p < s.len() {
6484 let b = s[p];
6485 s[t] = b;
6486 p += 1;
6487 if b == Meta && p < s.len() {
6488 // C: t[-1] = *p++ ^ 32; — overwrite the just-written
6489 // Meta with the un-escaped byte.
6490 s[t] = s[p] ^ 32;
6491 p += 1;
6492 }
6493 t += 1;
6494 }
6495 s.truncate(t);
6496 t
6497}
6498
6499/// Port of `int metalen(const char *s, int len)` from `Src/utils.c:4971-4983`.
6500/// ```c
6501/// int mlen = len;
6502/// while (len--) {
6503/// if (*s++ == Meta) { mlen++; s++; }
6504/// }
6505/// return mlen;
6506/// ```
6507/// Doc: "Return the character length of a metafied substring, given
6508/// the unmetafied substring length." So **input `len` is the
6509/// UNMETAFIED char count, output is the METAFIED byte count.**
6510///
6511/// Previously the Rust port had INVERTED semantics: looped while
6512/// `i < len` byte-walk, returned char count. That's the reverse
6513/// operation. Pin the correct C contract.
6514pub fn metalen(s: &str, len: usize) -> usize {
6515 // c:4972
6516 let bytes = s.as_bytes();
6517 let mut mlen = len; // c:4974
6518 let mut remaining = len;
6519 let mut i = 0;
6520 while remaining > 0 && i < bytes.len() {
6521 // c:4976 `while (len--)`
6522 if bytes[i] == Meta {
6523 // c:4977
6524 mlen += 1; // c:4978
6525 i += 2; // c:4979 s++ (already advanced past Meta)
6526 } else {
6527 i += 1;
6528 }
6529 remaining -= 1;
6530 }
6531 mlen
6532}
6533
6534/// Port of `unmeta(const char *file_name)` from `Src/utils.c:4994`.
6535///
6536/// Convert a zsh internal (metafied) string to a system-call-safe
6537/// form (e.g. for passing to `open(2)`).
6538///
6539/// C body shape (c:4994-5010):
6540/// ```c
6541/// meta = 0;
6542/// for (t = file_name; *t; t++)
6543/// if (*t == Meta) { meta = 1; break; }
6544/// if (!meta) return (char *) file_name; // no-copy fast path
6545/// for (t = file_name, p = fn; *t; p++)
6546/// if ((*p = *t++) == Meta && *t)
6547/// *p = *t++ ^ 32;
6548/// ```
6549// Rust idiom replacement: byte-scan fast path + `unmetafy` covers
6550// the C in-place decode + alloc-on-copy dance; String owns its own
6551// allocation so no `heap` flag needed.
6552/// `unmeta` — see implementation.
6553pub fn unmeta(s: &str) -> String {
6554 // c:4994
6555 let bytes = s.as_bytes();
6556 // c:4995-4996 — Meta-byte scan; no-copy fast path.
6557 if !bytes.iter().any(|&b| b == Meta) {
6558 return s.to_string();
6559 }
6560 let mut buf = bytes.to_vec();
6561 let len = unmetafy(&mut buf); // c:4999-5001
6562 buf.truncate(len);
6563 String::from_utf8_lossy(&buf).into_owned()
6564}
6565
6566/// Port of `convchar_t unmeta_one(const char *in, int *sz)` from `Src/utils.c:5056-5086`.
6567/// Non-MULTIBYTE branch (c:5077-5083):
6568/// ```c
6569/// if (in[0] == Meta) { *sz = 2; wc = (unsigned char)(in[1] ^ 32); }
6570/// else { *sz = 1; wc = (unsigned char) in[0]; }
6571/// ```
6572/// Returns `(decoded_char, bytes_consumed)`. The c:5070 NULL/empty
6573/// guard returns `(0, 0)`. Previously used hardcoded `0x83` for the
6574/// Meta byte — now routes through the canonical `Meta` constant
6575/// (defined as `'\u{83}'` at zsh.h:144).
6576/// WARNING: param names don't match C — Rust=(s) vs C=(in, sz)
6577pub fn unmeta_one(s: &str) -> (char, usize) {
6578 // c:5058
6579 let bytes = s.as_bytes();
6580 // c:5070 — `if (!in || !*in) return 0;`
6581 if bytes.is_empty() {
6582 return ('\0', 0);
6583 }
6584 // c:5077 — `if (in[0] == Meta)`.
6585 if bytes[0] == Meta && bytes.len() > 1 {
6586 // c:5078-5079 — `*sz = 2; wc = (unsigned char)(in[1] ^ 32);`.
6587 ((bytes[1] ^ 32) as char, 2)
6588 } else {
6589 // c:5081-5082 — `*sz = 1; wc = (unsigned char) in[0];`.
6590 (bytes[0] as char, 1)
6591 }
6592}
6593
6594/// Port of `ztrcmp(char const *s1, char const *s2)` from `Src/utils.c:5106`.
6595///
6596/// Byte-walking compare with lazy Meta resolution. C body skips
6597/// matching bytes wholesale, then on the first differing byte
6598/// un-meta-fies just that byte and compares. Faster than
6599/// `unmeta(s1).cmp(unmeta(s2))` because:
6600/// 1. No allocation up front,
6601/// 2. Only the first differing position pays the un-meta cost.
6602///
6603/// ```c
6604/// while(*s1 && *s1 == *s2) { s1++; s2++; }
6605/// if (!(c1 = *s1)) c1 = -1;
6606/// else if (c1 == Meta) c1 = *++s1 ^ 32;
6607/// if (!(c2 = *s2)) c2 = -1;
6608/// else if (c2 == Meta) c2 = *++s2 ^ 32;
6609/// return c1 - c2;
6610/// ```
6611pub fn ztrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
6612 // c:5106
6613 let b1 = s1.as_bytes();
6614 let b2 = s2.as_bytes();
6615 let mut i1 = 0;
6616 let mut i2 = 0;
6617 // Skip the matching prefix.
6618 while i1 < b1.len() && i2 < b2.len() && b1[i1] == b2[i2] {
6619 i1 += 1;
6620 i2 += 1;
6621 }
6622 // Resolve c1: -1 for end-of-string, else the next byte
6623 // (un-meta-fied if it's a Meta marker).
6624 let c1: i32 = if i1 >= b1.len() {
6625 -1
6626 } else if b1[i1] == Meta && i1 + 1 < b1.len() {
6627 (b1[i1 + 1] ^ 32) as i32
6628 } else {
6629 b1[i1] as i32
6630 };
6631 let c2: i32 = if i2 >= b2.len() {
6632 -1
6633 } else if b2[i2] == Meta && i2 + 1 < b2.len() {
6634 (b2[i2 + 1] ^ 32) as i32
6635 } else {
6636 b2[i2] as i32
6637 };
6638 c1.cmp(&c2)
6639}
6640
6641// pastebuf() DELETED — was a misplaced fn. The real `pastebuf()`
6642// lives in `Src/Zle/zle_misc.c:558` (a ZLE clipboard helper that
6643// pastes a Cutbuffer into the line-edit buffer). It has nothing to
6644// do with metafication, despite this file's prior body which
6645// reimplemented half of `metafy`. The real metafy lives below at
6646// `pub fn metafy()` (port of utils.c:4856).
6647
6648/// Unmetafied string length (from utils.c ztrlen lines 5135-5152)
6649pub fn ztrlen(s: &str) -> usize {
6650 // c:5136
6651 let mut len = 0;
6652 let chars: Vec<char> = s.chars().collect();
6653 let mut i = 0;
6654 while i < chars.len() {
6655 len += 1;
6656 if chars[i] as u32 == Meta as u32 && i + 1 < chars.len() {
6657 i += 2;
6658 } else {
6659 i += 1;
6660 }
6661 }
6662 len
6663}
6664
6665/// Port of `ztrlenend(char const *s, char const *eptr)` from `Src/utils.c:5162`.
6666///
6667/// ```c
6668/// for (l = 0; s < eptr; l++) {
6669/// if (*s++ == Meta) s++; // skip past Meta-escaped pair
6670/// }
6671/// return l;
6672/// ```
6673///
6674/// Count the unmetafied character length from `s` up to `end`
6675/// bytes. Each Meta-escaped pair counts as 1 character.
6676/// Previous Rust port called `chars().count()` which counts UTF-8
6677/// codepoints, not byte-walked Meta-pairs — wrong semantics.
6678pub fn ztrlenend(s: &str, eptr: usize) -> usize {
6679 let bytes = s.as_bytes();
6680 let cap = eptr.min(bytes.len());
6681 let mut l = 0;
6682 let mut i = 0;
6683 while i < cap {
6684 if bytes[i] == Meta {
6685 // Meta sentinel + escaped byte = 1 visible char.
6686 i += 2;
6687 } else {
6688 i += 1;
6689 }
6690 l += 1;
6691 }
6692 l
6693}
6694
6695/// Port of `int ztrsub(char const *t, char const *s)` from `Src/utils.c:5185-5203`.
6696/// ```c
6697/// int l = t - s;
6698/// while (s != t) {
6699/// if (*s++ == Meta) { s++; l--; }
6700/// }
6701/// return l;
6702/// ```
6703/// "Subtract two pointers in a metafied string." `s` is the start
6704/// pointer, `t` is the end pointer; both point into the SAME buffer.
6705/// Returns the count of unmetafied chars in `[s, t)` — same as
6706/// `ztrlen` of the substring but expressed via pointer arithmetic.
6707///
6708/// Rust API: takes the full buffer and a byte-offset pair
6709/// `[start..end)` since Rust can't replicate raw-pointer subtraction
6710/// across distinct `&str` arguments without UB risk. The semantics
6711/// stay faithful: count of unmetafied chars between two offsets.
6712/// Previous Rust port took two unrelated `&str` and computed
6713/// `ztrlen(&t[..t.len()-s.len()])` — a fundamentally different
6714/// operation that worked only when `s` was the suffix of `t`.
6715/// WARNING: param names don't match C — Rust=(buf, start, end) vs C=(t, s)
6716pub fn ztrsub(buf: &str, start: usize, end: usize) -> usize {
6717 // c:5187
6718 let bytes = buf.as_bytes();
6719 let end = end.min(bytes.len());
6720 let start = start.min(end);
6721 let mut l = (end - start) as isize; // c:5189
6722 let mut i = start;
6723 while i < end {
6724 if bytes[i] == Meta {
6725 // c:5192
6726 i += 2; // c:5198
6727 l -= 1; // c:5199
6728 } else {
6729 i += 1;
6730 }
6731 }
6732 l.max(0) as usize
6733}
6734
6735/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
6736/// `Src/utils.c:5217`.
6737///
6738/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
6739/// `Src/utils.c:5217-5240`. Pulls the next entry from a DIR* the
6740/// caller owns; returns the entry's bare name (metafied String) or
6741/// `None` at EOF. The C source's `ignoredots` flag (c:5232) skips
6742/// `.` and `..` when set; callers (`spnamepat` at c:4648) that
6743/// want dot entries pass 0.
6744///
6745/// Caller pattern matches C exactly:
6746/// let mut dir = fs::read_dir(path)?;
6747/// while let Some(name) = zreaddir(&mut dir, 1) { ... }
6748/// // dir auto-closed on drop (= closedir)
6749pub fn zreaddir(dir: &mut fs::ReadDir, ignoredots: i32) -> Option<String> {
6750 // c:5217
6751 for entry in dir.by_ref() {
6752 // c:5221 readdir loop
6753 let Ok(e) = entry else { continue };
6754 let Ok(name) = e.file_name().into_string() else {
6755 continue;
6756 };
6757 // c:5232 — `if (ignoredots && de->d_name[0] == '.' &&
6758 // (!de->d_name[1] || (de->d_name[1] == '.' && !de->d_name[2])))`.
6759 if ignoredots != 0 && (name == "." || name == "..") {
6760 continue; // c:5234
6761 }
6762 return Some(name); // c:5238 return de->d_name
6763 }
6764 None // c:5240 — fell off the end
6765}
6766
6767/// Port of `int zputs(char const *s, FILE *stream)` from `Src/utils.c:5263-5282`.
6768///
6769/// ```c
6770/// while (*s) {
6771/// if (*s == Meta) c = *++s ^ 32;
6772/// else if (itok(*s)) { s++; continue; }
6773/// else c = *s;
6774/// s++;
6775/// if (fputc(c, stream) < 0) return EOF;
6776/// }
6777/// return 0;
6778/// ```
6779///
6780/// Writes `s` to `stream` with Meta+X pair decoding and ITOK-byte
6781/// skipping. Returns `0` on success, `-1` on write error (mirroring
6782/// C's `EOF` sentinel for the int return).
6783pub fn zputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
6784 // c:5265
6785 let bytes = s.as_bytes(); // c:5265 *s walk
6786 let mut i = 0;
6787 while i < bytes.len() {
6788 // c:5267 while (*s)
6789 let c: u8; // c:5268 char c
6790 if bytes[i] == Meta {
6791 // c:5269 if (*s == Meta)
6792 // c:5270 — `c = *++s ^ 32;`
6793 if i + 1 < bytes.len() {
6794 c = bytes[i + 1] ^ 32;
6795 i += 1; // c:5270 ++s
6796 } else {
6797 i += 1;
6798 continue;
6799 }
6800 } else if itok(bytes[i]) {
6801 // c:5271 else if (itok(*s))
6802 // c:5272 — `s++; continue;` (skip token byte)
6803 i += 1;
6804 continue;
6805 } else {
6806 c = bytes[i]; // c:5274 else c = *s
6807 }
6808 i += 1; // c:5276 s++
6809 if stream.write_all(&[c]).is_err() {
6810 // c:5277 fputc(c, stream)
6811 return -1; // c:5278 return EOF
6812 }
6813 }
6814 0 // c:5280 return 0
6815}
6816
6817/// Port of `nicedup(char const *s, int heap)` from `Src/utils.c:5289`
6818/// (single-byte build) and `Src/utils.c:5530` (multibyte build).
6819///
6820/// C bodies:
6821/// ```c
6822/// /* Src/utils.c:5289 — !MULTIBYTE_SUPPORT */
6823/// char *retstr;
6824/// (void)sb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
6825/// return retstr;
6826///
6827/// /* Src/utils.c:5530 — MULTIBYTE_SUPPORT */
6828/// char *retstr;
6829/// (void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
6830/// return retstr;
6831/// ```
6832///
6833/// zshrs targets MULTIBYTE_SUPPORT (matches every modern zsh
6834/// build) — route through `mb_niceformat`. Previous Rust port
6835/// went through `sb_niceformat`, which is the !MULTIBYTE path —
6836/// strips wide-char handling and corrupts non-ASCII via byte-mask
6837/// `nicechar(c & 0xff)`.
6838///
6839/// C signature faithful: `(s, heap) -> String`. `heap` selects between
6840/// arena (`NICEFLAG_HEAP`) and persistent (default `ztrdup`) allocation
6841/// in C; Rust has a single allocator so both paths produce the same
6842/// owned `String`, but the `heap` parameter is preserved per Rule S1.
6843pub fn nicedup(s: &str, heap: i32) -> String {
6844 // c:5530
6845 // c:5532 — `char *retstr;`
6846 let retstr: Option<String>;
6847 // c:5534 — `(void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);`
6848 let mut slot: Option<String> = None;
6849 let _ = mb_niceformat(
6850 s,
6851 None,
6852 Some(&mut slot),
6853 if heap != 0 { NICEFLAG_HEAP } else { 0 },
6854 );
6855 retstr = slot;
6856 // c:5536 — `return retstr;`
6857 retstr.unwrap_or_default()
6858}
6859
6860/// Nice-format and duplicate string.
6861/// Port of `nicedupstring(char const *s)` from `Src/utils.c:5301`.
6862/// C body: `return nicedup(s, 1);` — heap-arena allocation form.
6863pub fn nicedupstring(s: &str) -> String {
6864 // c:5301
6865 nicedup(s, 1) // c:5303
6866}
6867
6868/// Nicely format a string
6869/// Port of `nicezputs(char const *s, FILE *stream)` from `Src/utils.c`.
6870///
6871/// Under `MULTIBYTE_SUPPORT` (the daily-driver path), C defines
6872/// `nicezputs(str, outs)` as a macro at zsh.h:3274:
6873/// `(void)mb_niceformat((str), (outs), NULL, 0)`.
6874/// Without MULTIBYTE (c:5313): `sb_niceformat(s, stream, NULL, 0)`.
6875///
6876/// The previous Rust impl was `s.chars().map(nicechar).collect()` —
6877/// wrong on every front:
6878/// 1. No unmetafy step (Meta-byte pairs surfaced as `\M-…` mangled).
6879/// 2. `nicechar` is byte-based (`c & 0xff`), corrupting non-ASCII
6880/// multibyte codepoints into spurious `\M-X` escapes.
6881/// 3. No `itok` ZSH-token-byte handling.
6882///
6883/// Route through `mb_niceformat` to match the C macro under
6884/// MULTIBYTE — which itself unmetafies and uses `wcs_nicechar` for
6885/// proper wide-char handling.
6886pub fn nicezputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
6887 // c:5313 (non-MULTIBYTE) / zsh.h:3274 (MULTIBYTE macro:
6888 // `(void)mb_niceformat((str), (outs), NULL, 0)`).
6889 let _ = mb_niceformat(s, Some(stream), None, 0);
6890 0 // c:5316 return 0
6891}
6892
6893/// Port of `niceztrlen(char const *s)` from `Src/utils.c:5324`.
6894///
6895/// Returns the length (in bytes) of the visible representation of
6896/// the metafied string `s`. C body walks each char via `nicechar`
6897/// and accumulates `strlen(nicechar(c))`; the Rust port mirrors
6898/// this via [`sb_niceformat`] which is the same render-then-measure
6899/// path.
6900pub fn niceztrlen(s: &str) -> usize {
6901 // c:5343-5348 — under MULTIBYTE_SUPPORT (which zshrs targets) `niceztrlen`
6902 // is the multibyte version: it walks whole characters via `mb_niceformat`,
6903 // whose per-char width comes from `wcs_nicechar_sel`'s wcwidth — so a
6904 // printable multibyte char (e.g. U+2010 `‐`, 3 bytes / 1 column) counts as
6905 // 1, not 3. The port previously called `sb_niceformat`, the single-byte
6906 // (`#else`, c:5320-5341) version that counts each byte separately, so any
6907 // description/match with a multibyte glyph over-measured its display width
6908 // by (bytes−columns). In `cd_get`'s described-list truncation
6909 // (computil.rs) that over-measure consumed the remaining-width budget too
6910 // early and clipped the tail off descriptions containing such a glyph —
6911 // e.g. `xz -<TAB>`'s "…multi‐threaded decompression" lost its final "on".
6912 mb_niceformat(s, None, None, 0)
6913}
6914
6915/// Multibyte-aware nice-format of a string.
6916/// Port of `mb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from Src/utils.c:5366. Walks the
6917/// (un-metafied) string char-by-char; for each control byte or
6918/// invalid sequence emits a `^X`/`\\xNN` representation, otherwise
6919/// passes the char through. The C source threads an
6920/// `mbstate_t` through `mbrtowc()` and falls back to single-byte
6921/// `\M-` notation on `MB_INVALID`; the Rust port uses Rust's
6922/// chars iterator which already produces valid scalar values, so
6923/// invalid-byte fallback collapses to the control-char branch.
6924/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream, outstrp, flags)
6925// Rust idiom replacement: `chars()` + `wcs_nicechar` covers the C
6926// mbrtowc loop with `MB_INVALID` fallback (Rust UTF-8 guarantees
6927// valid scalars, so the invalid-byte arm collapses).
6928/// `mb_niceformat` — see implementation.
6929pub fn mb_niceformat(
6930 s: &str,
6931 mut stream: Option<&mut dyn std::io::Write>,
6932 outstrp: Option<&mut Option<String>>,
6933 flags: i32,
6934) -> usize {
6935 // c:5366
6936 let mut l: usize = 0; // c:5368 size_t l = 0
6937 let mut newl: usize; // c:5368 size_t newl
6938 // c:5369 — `int umlen, outalloc, outleft, eol = 0;`. outalloc/outleft
6939 // model C's buffer-growth math; Rust String auto-grows so the realloc
6940 // loop at c:5430-5440 collapses to push_str. umlen and eol carry.
6941 let mut umlen: usize; // c:5369
6942 let mut eol: bool = false; // c:5369 int eol = 0
6943 // c:5370 — `wchar_t c;` (carried inside loop in Rust)
6944 // c:5371 — `char *ums, *ptr, *fmt, *outstr, *outptr;`
6945 let mut ums: Vec<u8>; // c:5371 char *ums
6946 let mut ptr: usize; // c:5371 char *ptr
6947 let mut fmt: String; // c:5371 char *fmt
6948 let mut outstr: Option<String>; // c:5371 char *outstr
6949 // c:5372 — `mbstate_t mbs;` (Rust UTF-8 has no shift state; tracked
6950 // by the `valid_up_to` / `error_len` logic below).
6951
6952 // c:5374-5380 — `if (outstrp) outptr = outstr = zalloc(5*strlen(s));`
6953 if outstrp.is_some() {
6954 // c:5374
6955 outstr = Some(String::with_capacity(5 * s.len())); // c:5376
6956 } else {
6957 // c:5377
6958 outstr = None; // c:5379 outstr = NULL
6959 }
6960
6961 // c:5382 — `ums = ztrdup(s);`
6962 // c:5383-5387 — comment-only block carried verbatim:
6963 /*
6964 * is this necessary at this point? niceztrlen does this
6965 * but it's used in lots of places. however, one day this may
6966 * be, too.
6967 */ // c:5383-5387
6968 // c:5388 — `untokenize(ums);` — Rust port uses the char-based
6969 // `lex::untokenize` (only fires on codepoints in the 0x84..=0xa1
6970 // ITOK range, never on UTF-8 continuation bytes); the byte-level
6971 // shape of C's untokenize is unsafe on raw UTF-8 input because a
6972 // continuation byte like 0x97 (part of `字` = 0xE5 0xAD 0x97) sits
6973 // inside the ITOK range and would be misclassified as a token.
6974 // c:5389 — `ptr = unmetafy(ums, ¨en);` — `unmeta` is the safe
6975 // UTF-8 wrapper that runs unmetafy only when Meta bytes are
6976 // present and otherwise no-ops.
6977 //
6978 // `unmetafy_str` (not `unmeta`) is what produces the raw byte buffer C
6979 // expects here. zshrs metafies at the CHARACTER level — an undecodable byte
6980 // is stored as U+0083 followed by `char::from(byte ^ 32)` — and U+0083
6981 // itself encodes as the two UTF-8 bytes C2 83. `unmeta` scans for a 0x83
6982 // BYTE, so it finds the trailing byte of that pair and mangles the string:
6983 // `$'\M-a'` came back out as `\M-^C\M-A` instead of `\M-a`.
6984 let detok = untokenize(s);
6985 ums = unmetafy_str(&detok);
6986 umlen = ums.len(); // c:5389 *umlen
6987 ptr = 0; // c:5389 ptr starts at 0 in ums
6988
6989 // c:5391 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
6990 while umlen > 0 {
6991 // c:5392
6992 // c:5393 — `cnt = eol ? MB_INVALID : mbrtowc(&c, ptr, umlen, &mbs);`
6993 // Rust equivalent: try to decode one UTF-8 scalar from ums[ptr..],
6994 // honoring `eol` (force the invalid arm when set).
6995 let cnt: usize;
6996 let decoded_c: Option<char>;
6997 if eol {
6998 // c:5396 — MB_INVALID arm via `eol = 1`.
6999 decoded_c = None;
7000 cnt = 1;
7001 } else {
7002 let remaining = &ums[ptr..ptr + umlen];
7003 match std::str::from_utf8(remaining) {
7004 Ok(s_slice) => {
7005 // Valid UTF-8 throughout remaining; consume one char.
7006 if let Some(ch) = s_slice.chars().next() {
7007 decoded_c = Some(ch);
7008 cnt = ch.len_utf8();
7009 } else {
7010 // Empty? Shouldn't happen given umlen > 0, but
7011 // mirror MB_INVALID.
7012 decoded_c = None;
7013 cnt = 1;
7014 }
7015 }
7016 Err(e) => {
7017 let valid_up_to = e.valid_up_to();
7018 if valid_up_to > 0 {
7019 // Decode the first valid char then continue.
7020 let valid_slice =
7021 unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
7022 let ch = valid_slice.chars().next().unwrap();
7023 decoded_c = Some(ch);
7024 cnt = ch.len_utf8();
7025 } else if e.error_len().is_none() {
7026 // Incomplete sequence at end of input — MB_INCOMPLETE.
7027 // c:5394 — `case MB_INCOMPLETE: eol = 1; FALL THROUGH`
7028 eol = true;
7029 decoded_c = None; // MB_INVALID
7030 cnt = 1;
7031 } else {
7032 // Definite invalid byte — MB_INVALID.
7033 decoded_c = None;
7034 cnt = 1;
7035 }
7036 }
7037 }
7038 }
7039
7040 // c:5396-5403 / c:5404-5424 switch dispatch on cnt/decoded_c.
7041 let cnt_used: usize;
7042 match decoded_c {
7043 None => {
7044 // c:5397 — `case MB_INVALID:` (or fall-through from
7045 // MB_INCOMPLETE at c:5394).
7046 // c:5400 — `fmt = nicechar_sel(*ptr, flags & NICEFLAG_QUOTE);`
7047 fmt = nicechar_sel(ums[ptr] as char, (flags & NICEFLAG_QUOTE) != 0);
7048 newl = fmt.len(); // c:5401 newl = strlen(fmt)
7049 cnt_used = 1; // c:5402 cnt = 1
7050 // c:5403 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless)
7051 }
7052 Some(c) if c == '\0' => {
7053 // c:5406 — `case 0:` — '\0' decodes to 0; consume 1 byte
7054 // and fall through to default.
7055 cnt_used = 1; // c:5409
7056 // c:5411-5421 default arm (FALL THROUGH from case 0)
7057 if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
7058 // c:5413
7059 fmt = "\\'".to_string(); // c:5414
7060 newl = 2; // c:5415
7061 } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
7062 // c:5417
7063 fmt = "\\\\".to_string(); // c:5418
7064 newl = 2; // c:5419
7065 } else {
7066 // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
7067 // flags & NICEFLAG_QUOTE);`
7068 let mut width: usize = 0;
7069 fmt =
7070 wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
7071 newl = width;
7072 }
7073 }
7074 Some(c) => {
7075 // c:5411 — `default:` arm (cnt > 0, valid char).
7076 cnt_used = cnt;
7077 if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
7078 // c:5413
7079 fmt = "\\'".to_string(); // c:5414
7080 newl = 2; // c:5415
7081 } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
7082 // c:5417
7083 fmt = "\\\\".to_string(); // c:5418
7084 newl = 2; // c:5419
7085 } else {
7086 // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
7087 // flags & NICEFLAG_QUOTE);`
7088 let mut width: usize = 0;
7089 fmt =
7090 wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
7091 newl = width;
7092 }
7093 }
7094 }
7095
7096 umlen -= cnt_used; // c:5427 umlen -= cnt
7097 ptr += cnt_used; // c:5428 ptr += cnt
7098 l += newl; // c:5429 l += newl
7099
7100 if let Some(ref mut w) = stream {
7101 // c:5431 if (stream)
7102 // c:5432 — `zputs(fmt, stream);`
7103 let _ = w.write_all(fmt.as_bytes());
7104 }
7105 if let Some(ref mut buf) = outstr {
7106 // c:5433 if (outstr)
7107 // c:5434-5446 — append fmt to outstr, growing on demand. Rust
7108 // String auto-grows; the realloc loop collapses to push_str.
7109 buf.push_str(&fmt); // c:5446 memcpy(outptr, fmt, outlen)
7110 }
7111 let _ = fmt;
7112 }
7113
7114 // c:5451 — `free(ums);` (Rust drop at scope exit)
7115 drop(ums);
7116 if let Some(slot) = outstrp {
7117 // c:5452 if (outstrp)
7118 // c:5453 — `*outptr = '\0';` (no-op for Rust String)
7119 // c:5455-5460 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping. Rust has
7120 // a single allocator so all three paths produce identical owned
7121 // String contents; transfer ownership into caller's slot.
7122 *slot = outstr.take();
7123 }
7124
7125 l // c:5462 return l
7126}
7127
7128/// Port of `is_mb_niceformat(const char *s)` from `Src/utils.c:5474`.
7129///
7130/// Predicate: would any character in `s` need representation by
7131/// `mb_niceformat` / `nicedup`? C body:
7132/// ```c
7133/// ums = ztrdup(s);
7134/// untokenize(ums);
7135/// ptr = unmetafy(ums, ¨en);
7136/// while (umlen > 0) {
7137/// cnt = mbrtowc(&c, ptr, umlen, &mbs);
7138/// switch (cnt) {
7139/// case MB_INCOMPLETE: case MB_INVALID:
7140/// if (is_nicechar(*ptr)) { ret = 1; break; }
7141/// cnt = 1;
7142/// memset(&mbs, 0, sizeof mbs);
7143/// break;
7144/// case 0: cnt = 1; /* FALLTHROUGH */
7145/// default:
7146/// if (is_wcs_nicechar(c)) ret = 1;
7147/// break;
7148/// }
7149/// if (ret) break;
7150/// umlen -= cnt; ptr += cnt;
7151/// }
7152/// ```
7153///
7154/// Rust port: unmetafy in place via [`unmetafy`], then walk the
7155/// resulting bytes. For valid UTF-8 sequences, check
7156/// `is_wcs_nicechar(scalar)`; for invalid bytes, check
7157/// `is_nicechar(byte)`. Either path bailing positive returns true.
7158pub fn is_mb_niceformat(s: &str) -> i32 {
7159 // c:5474
7160 // c:5476 — `int umlen, ret = 0;`
7161 let umlen: usize; // c:5476
7162 let mut ret: i32 = 0; // c:5476
7163 // c:5477 — `char *ums, *ptr;` (eptr modelled by bytes.len())
7164 let mut ums: Vec<u8>; // c:5477
7165 let mut ptr: usize; // c:5477
7166
7167 // c:5481-5483 — `ums = ztrdup(s); untokenize(ums); ptr =
7168 // unmetafy(ums, ¨en);`. Use BYTE-level `unmetafy_str`,
7169 // not char-level `unmeta`: `unmeta` decodes a metafied eight-bit byte
7170 // (Meta 0x83 + 0xC1 for `$'\M-a'`) into the valid Rust char U+00E1
7171 // (`á`), which the UTF-8 scan below reads as a PRINTABLE Latin-1 letter
7172 // → is_mb_niceformat=0 → `(q+)`/quotedzputs left the raw byte unquoted
7173 // (`\341`) where zsh emits `$'\M-a'`. `unmetafy_str` preserves the raw
7174 // 0xE1 byte (C's `unmetafy`), so the MB_INVALID arm's is_nicechar(0xE1)
7175 // fires (high-bit → nice) exactly as C does.
7176 let detok = untokenize(s);
7177 ums = unmetafy_str(&detok);
7178 umlen = ums.len(); // c:5483 *umlen
7179 ptr = 0; // c:5483 ptr starts at 0
7180
7181 // c:5485 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
7182 while ret == 0 && ptr < ums.len() {
7183 // c:5486 while (umlen > 0)
7184 let remaining = &ums[ptr..];
7185 match std::str::from_utf8(remaining) {
7186 Ok(s_slice) => {
7187 // c:5503-5511 — `default: if (is_wcs_nicechar(c)) ret = 1;`
7188 for ch in s_slice.chars() {
7189 if is_wcs_nicechar(ch) {
7190 // c:5508
7191 ret = 1; // c:5509
7192 break;
7193 }
7194 }
7195 break;
7196 }
7197 Err(e) => {
7198 let valid_up_to = e.valid_up_to();
7199 if valid_up_to > 0 {
7200 let valid = unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
7201 for ch in valid.chars() {
7202 if is_wcs_nicechar(ch) {
7203 // c:5508
7204 ret = 1; // c:5509
7205 break;
7206 }
7207 }
7208 if ret != 0 {
7209 break;
7210 }
7211 ptr += valid_up_to;
7212 continue;
7213 }
7214 // c:5493-5498 — `case MB_INVALID: if (is_nicechar(*ptr))
7215 // ret = 1; break;`
7216 if is_nicechar(remaining[0] as char) {
7217 // c:5494
7218 ret = 1; // c:5495
7219 break;
7220 }
7221 ptr += 1;
7222 }
7223 }
7224 }
7225
7226 drop(ums); // c:5519 free(ums)
7227 ret // c:5523 return ret
7228}
7229
7230/// Multibyte metachar length with conversion (from utils.c mb_metacharlenconv_r)
7231/// Port of `mb_metacharlenconv_r(const char *s, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5548`.
7232/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, wcp, mbsp)
7233// Rust idiom replacement: Rust strings are UTF-8 with valid scalar
7234// values, so `chars().next()` covers the mbrtowc state machine; no
7235// mbstate_t threading required.
7236/// `mb_metacharlenconv_r` — see implementation.
7237pub fn mb_metacharlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
7238 if let Some(c) = s[pos..].chars().next() {
7239 (c.len_utf8(), Some(c))
7240 } else {
7241 (0, None)
7242 }
7243}
7244
7245/// Multibyte-aware metafied-string char advance.
7246/// Port of `mb_metacharlenconv(const char *s, wint_t *wcp)` from Src/utils.c:5611. Returns
7247/// `(bytes_consumed, scalar_char)` for the next char in `s`. C
7248/// source dispatches to `mb_metacharlenconv_r()` for true
7249/// multibyte; we use Rust's UTF-8 char iterator which already
7250/// handles multi-byte correctly. ASCII fast-path: `Meta+X` is 2
7251/// bytes consumed → `(2, X^32)`; bare ASCII is `(1, c)`.
7252/// WARNING: param names don't match C — Rust=(s) vs C=(s, wcp)
7253pub fn mb_metacharlenconv(s: &str) -> (usize, Option<char>) {
7254 let bytes = s.as_bytes();
7255 if bytes.is_empty() {
7256 return (0, None);
7257 }
7258 if bytes[0] == 0x83 && bytes.len() >= 2 {
7259 // Meta+X pair → unescape.
7260 let raw = bytes[1] as u32 ^ 32;
7261 return (2, char::from_u32(raw));
7262 }
7263 if bytes[0] <= 0x7f {
7264 return (1, Some(bytes[0] as char));
7265 }
7266 // Multi-byte UTF-8 — let Rust decode.
7267 if let Some(c) = s.chars().next() {
7268 return (c.len_utf8(), Some(c));
7269 }
7270 (1, None)
7271}
7272
7273/// Multibyte metastring length to end (from utils.c mb_metastrlenend)
7274/// Port of `mb_metastrlenend(char *ptr, int width, char *eptr)` from `Src/utils.c:5655`.
7275///
7276/// Counts characters (`width == false`) or display columns
7277/// (`width == true`) in a metafied string. C walks the metafied
7278/// byte stream, un-metafies inline (`if (*ptr == Meta) inchar =
7279/// *++ptr ^ 32`, c:5672), and feeds one byte at a time to
7280/// `mbrtowc` to assemble each multibyte character (c:5691); a
7281/// complete character counts 1 (or its `WCWIDTH`), an invalid byte
7282/// counts as a single character (c:5712-5714).
7283///
7284/// Rust-port note: zshrs stores both metafied byte-pairs (`Meta` +
7285/// `byte ^ 32`, produced by `getkeystring` for `$'\xNN'` escapes)
7286/// AND real multibyte `char`s (the literal `$'é'` path) in one
7287/// `String`, whereas C only ever holds metafied bytes. So the port
7288/// first reduces the slice to its raw byte stream via `unmetafy_str`
7289/// (metafied pair → its byte; real `char` → its UTF-8 bytes — the
7290/// same bytes C's inline demetafy would yield), then runs C's
7291/// `mbrtowc` character-assembly loop over those bytes. Rust's UTF-8
7292/// validation is the `mbrtowc` analogue: the shortest valid prefix
7293/// is one character; a byte that begins no valid sequence counts as
7294/// one (c:5712).
7295pub fn mb_metastrlenend(ptr: &str, width: bool, eptr: usize) -> usize {
7296 // c:5672 — un-metafy the (optionally end-bounded) slice to raw bytes.
7297 let bytes = unmetafy_str(&ptr[..eptr.min(ptr.len())]);
7298 let mut num: usize = 0; // c:5658 size_t ret / counter
7299 let mut i: usize = 0;
7300 while i < bytes.len() {
7301 // c:5678-5687 — 7-bit US-ASCII subset: one character, skip mbrtowc.
7302 if bytes[i] <= 0x7f {
7303 num += 1;
7304 i += 1;
7305 continue;
7306 }
7307 // c:5691 — mbrtowc: take the shortest valid UTF-8 character.
7308 let mut num_in_char: usize = 1; // c:5701 trailing-octet span
7309 let hi = (i + 4).min(bytes.len());
7310 for end in (i + 1)..=hi {
7311 if std::str::from_utf8(&bytes[i..end]).is_ok() {
7312 num_in_char = end - i;
7313 break;
7314 }
7315 }
7316 if width {
7317 // c:5717-5723 — WCWIDTH of the assembled character (≥0).
7318 let wcw = std::str::from_utf8(&bytes[i..i + num_in_char])
7319 .ok()
7320 .and_then(|s| s.chars().next())
7321 .and_then(unicode_width::UnicodeWidthChar::width)
7322 .unwrap_or(0);
7323 num += wcw;
7324 } else {
7325 // c:5727 — one character (complete, or invalid treated as one).
7326 num += 1;
7327 }
7328 i += num_in_char;
7329 }
7330 num
7331}
7332
7333/// Multibyte char length with conversion (from utils.c mb_charlenconv_r)
7334/// Port of `mb_charlenconv_r(const char *s, int slen, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5747`.
7335/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp, mbsp)
7336pub fn mb_charlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
7337 mb_metacharlenconv_r(s, pos)
7338}
7339
7340/// Multibyte char length (from utils.c mb_charlenconv)
7341/// Port of `mb_charlenconv(const char *s, int slen, wint_t *wcp)` from `Src/utils.c:5793`.
7342/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp)
7343pub fn mb_charlenconv(s: &str, pos: usize) -> usize {
7344 s[pos..].chars().next().map(|c| c.len_utf8()).unwrap_or(0)
7345}
7346
7347/// Port of `int metacharlenconv(const char *x, int *c)` from `Src/utils.c:5810-5826`.
7348/// ```c
7349/// if (*x == Meta) {
7350/// if (c) *c = x[1] ^ 32;
7351/// return 2;
7352/// }
7353/// if (c) *c = (char)*x;
7354/// return 1;
7355/// ```
7356/// Single-byte metafied char advance — Meta+X is 2 bytes (decode
7357/// via XOR 32), plain byte is 1 byte. Previously used hardcoded
7358/// `0x83`; now routes through the canonical `Meta` const for
7359/// maintainability parity.
7360/// WARNING: param names don't match C — Rust=(s) vs C=(x, c)
7361pub fn metacharlenconv(s: &str) -> (usize, Option<char>) {
7362 // c:5811
7363 let bytes = s.as_bytes();
7364 if bytes.is_empty() {
7365 return (0, None);
7366 }
7367 if bytes[0] == Meta && bytes.len() >= 2 {
7368 // c:5818
7369 let raw = bytes[1] as u32 ^ 32; // c:5820
7370 return (2, char::from_u32(raw)); // c:5821
7371 }
7372 (1, Some(bytes[0] as char)) // c:5823-5825
7373}
7374
7375/// Plain (non-metafy) char advance.
7376/// Port of `charlenconv(const char *x, int len, int *c)` from Src/utils.c:5832 — the
7377/// non-MULTIBYTE_SUPPORT branch. Single-byte read with
7378/// `len`-bound check; matches the C source's `if (!len)` early
7379/// exit.
7380/// WARNING: param names don't match C — Rust=(s, len) vs C=(x, len, c)
7381pub fn charlenconv(s: &str, len: usize) -> (usize, Option<char>) {
7382 if len == 0 {
7383 return (0, None);
7384 }
7385 let bytes = s.as_bytes();
7386 if bytes.is_empty() {
7387 return (0, None);
7388 }
7389 (1, Some(bytes[0] as char))
7390}
7391
7392/// Port of `size_t sb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from `Src/utils.c:5849-5910`.
7393/// ```c
7394/// ums = ztrdup(s); untokenize(ums); ptr = unmetafy(ums, ¨en);
7395/// while (ptr < eptr) {
7396/// int c = (unsigned char) *ptr;
7397/// if (c == '\'' && (flags & NICEFLAG_QUOTE)) fmt = "\\'";
7398/// else if (c == '\\' && (flags & NICEFLAG_QUOTE)) fmt = "\\\\";
7399/// else fmt = nicechar_sel(c, ...);
7400/// }
7401/// ```
7402/// Single-byte nice format. **Unmetafies the input first** (c:5872),
7403/// then calls `nicechar_sel` for EVERY byte (not just controls).
7404///
7405/// Param mapping (Rule S1, faithful to C):
7406/// - `s: &str` ← C `const char *s`
7407/// - `stream: Option<&mut dyn Write>` ← C `FILE *stream` (`None` ≡ `NULL`)
7408/// - `outstrp: Option<&mut Option<String>>` ← C `char **outstrp`
7409/// (outer `None` ≡ `NULL`; inner `Option<String>` is the storage slot
7410/// the caller passes as `&local`, mirroring `char *retstr;
7411/// sb_niceformat(s, NULL, &retstr, ...);`).
7412/// - returns `usize` ← C `size_t l`.
7413pub fn sb_niceformat(
7414 s: &str,
7415 mut stream: Option<&mut dyn std::io::Write>,
7416 outstrp: Option<&mut Option<String>>,
7417 flags: i32,
7418) -> usize {
7419 // c:5851
7420 let mut l: usize = 0; // c:5853 size_t l = 0
7421 let mut newl: usize; // c:5853 size_t newl
7422 // c:5854 — `int umlen, outalloc, outleft;`. outalloc/outleft model
7423 // C's buffer-growth math; Rust String auto-grows so the realloc
7424 // loop at c:5897-5906 collapses to push_str. umlen carries through.
7425 let umlen: usize; // c:5854
7426 // c:5855 — `char *ums, *ptr, *eptr, *fmt, *outstr, *outptr;`
7427 let mut ums: Vec<u8>; // c:5855 char *ums
7428 let mut ptr: usize; // c:5855 char *ptr
7429 let eptr: usize; // c:5855 char *eptr
7430 let mut fmt: String; // c:5855 char *fmt
7431 let mut outstr: Option<String>; // c:5855 char *outstr
7432
7433 // c:5857-5863 — `if (outstrp) outptr = outstr = zalloc(2*strlen(s));`
7434 if outstrp.is_some() {
7435 // c:5857
7436 outstr = Some(String::with_capacity(2 * s.len())); // c:5859
7437 } else {
7438 // c:5860
7439 outstr = None; // c:5862 outstr = NULL
7440 }
7441
7442 // c:5865 — `ums = ztrdup(s);`
7443 ums = s.as_bytes().to_vec();
7444 // c:5866-5870 — comment-only block carried verbatim:
7445 /*
7446 * is this necessary at this point? niceztrlen does this
7447 * but it's used in lots of places. however, one day this may
7448 * be, too.
7449 */ // c:5866-5870
7450 // c:5871 — `untokenize(ums);` inlined byte-level (lex::untokenize
7451 // is char-based; C does byte-walks). Mirrors `Src/exec.c:2077-2099`
7452 // exec.c untokenize().
7453 let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; // ZTOKENS — Src/lex.c:38
7454 let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
7455 for &c in &ums {
7456 // exec.c:2082
7457 if (0x84u8..=0xa1u8).contains(&c) {
7458 // exec.c:2083 itok(c)
7459 if c != 0xa1u8 {
7460 // exec.c:2086 c != Nularg
7461 let idx = (c - 0x84) as usize;
7462 if idx < ztokens_table.len() {
7463 detok.push(ztokens_table[idx]);
7464 }
7465 }
7466 } else {
7467 detok.push(c); // exec.c:2094
7468 }
7469 }
7470 ums = detok;
7471 // c:5872 — `ptr = unmetafy(ums, ¨en);`
7472 umlen = unmetafy(&mut ums);
7473 ums.truncate(umlen);
7474 eptr = umlen; // c:5873 eptr = ptr + umlen
7475 ptr = 0; // c:5872 ptr starts at 0 in ums
7476
7477 while ptr < eptr {
7478 // c:5875
7479 let c: i32 = ums[ptr] as i32; // c:5876 int c = (unsigned char) *ptr
7480 if c == b'\'' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
7481 // c:5877
7482 fmt = "\\'".to_string(); // c:5878
7483 newl = 2; // c:5879
7484 } else if c == b'\\' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
7485 // c:5881
7486 fmt = "\\\\".to_string(); // c:5882
7487 newl = 2; // c:5883
7488 } else {
7489 // c:5885
7490 fmt = nicechar_sel(c as u8 as char, (flags & NICEFLAG_QUOTE) != 0); // c:5886
7491 newl = 1; // c:5887
7492 }
7493
7494 ptr += 1; // c:5890 ++ptr
7495 l += newl; // c:5891 l += newl
7496
7497 if let Some(ref mut w) = stream {
7498 // c:5893 if (stream)
7499 // c:5894 — `zputs(fmt, stream);`
7500 let _ = w.write_all(fmt.as_bytes());
7501 }
7502 if let Some(ref mut buf) = outstr {
7503 // c:5895 if (outstr)
7504 // c:5896-5912 — append fmt to outstr, growing on demand. Rust
7505 // String auto-grows; the realloc loop at c:5897-5906 collapses
7506 // to push_str (memcpy + outptr/outleft bookkeeping unneeded).
7507 buf.push_str(&fmt); // c:5907 memcpy(outptr, fmt, outlen)
7508 }
7509 // `fmt` is consumed at next iter assignment (C reuses pointer).
7510 let _ = fmt;
7511 }
7512
7513 // c:5915 — `free(ums);` (Rust drop at scope exit)
7514 drop(ums);
7515 if let Some(slot) = outstrp {
7516 // c:5916 if (outstrp)
7517 // c:5917 — `*outptr = '\0';` (no-op for String — push_str leaves
7518 // no embedded NUL, and Rust String is length-prefixed).
7519 // c:5919-5925 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping: in C this
7520 // selects between ztrdup (perm) / dupstring (heap arena) / direct
7521 // ownership-transfer (NODUP). Rust has a single allocator so all
7522 // three paths produce identical owned String contents; transfer
7523 // ownership of `outstr` into the caller's slot.
7524 *slot = outstr.take();
7525 }
7526
7527 l // c:5928 return l
7528}
7529
7530/// Port of `int is_sb_niceformat(const char *s)` from `Src/utils.c:5937-5959`.
7531///
7532/// Predicate: would `sb_niceformat` change the input? Walks each byte
7533/// after unmetafy, returns `1` if any byte is "nice" per `is_nicechar`,
7534/// else `0` (C `int` return faithful to Rule S1).
7535pub fn is_sb_niceformat(s: &str) -> i32 {
7536 // c:5937
7537 // c:5939 — `int umlen, ret = 0;`
7538 let umlen: usize; // c:5939
7539 let mut ret: i32 = 0; // c:5939
7540 // c:5940 — `char *ums, *ptr, *eptr;`
7541 let mut ums: Vec<u8>; // c:5940 char *ums
7542 let mut ptr: usize; // c:5940 char *ptr
7543 let eptr: usize; // c:5940 char *eptr
7544
7545 ums = s.as_bytes().to_vec(); // c:5942 ums = ztrdup(s)
7546 // c:5943 — `untokenize(ums);` inlined byte-level (lex::untokenize is
7547 // char-based; C does byte-walks). Mirrors `Src/exec.c:2077-2099`.
7548 let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; // ZTOKENS — Src/lex.c:38
7549 let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
7550 for &c in &ums {
7551 // exec.c:2082
7552 if (0x84u8..=0xa1u8).contains(&c) {
7553 // exec.c:2083 itok(c)
7554 if c != 0xa1u8 {
7555 // exec.c:2086 c != Nularg
7556 let idx = (c - 0x84) as usize;
7557 if idx < ztokens_table.len() {
7558 detok.push(ztokens_table[idx]);
7559 }
7560 }
7561 } else {
7562 detok.push(c); // exec.c:2094
7563 }
7564 }
7565 ums = detok;
7566 umlen = unmetafy(&mut ums); // c:5944 ptr = unmetafy(ums, ¨en)
7567 ums.truncate(umlen);
7568 eptr = umlen; // c:5945 eptr = ptr + umlen
7569 ptr = 0; // c:5944 ptr starts at 0 in ums
7570
7571 while ptr < eptr {
7572 // c:5947
7573 if is_nicechar(ums[ptr] as char) {
7574 // c:5948 is_nicechar(*ptr)
7575 ret = 1; // c:5949
7576 break; // c:5950
7577 }
7578 ptr += 1; // c:5952 ++ptr
7579 }
7580
7581 drop(ums); // c:5955 free(ums)
7582 ret // c:5957 return ret
7583}
7584
7585/// Tab expansion — direct port of `zexpandtabs(const char *s, int len, int width, int startpos, FILE *fout, int all)` in zsh/Src/utils.c:5975.
7586/// Writes `s` into `out` with TAB characters expanded to spaces against
7587/// a tabstop of `width`. `startpos` carries the cumulative emitted
7588/// column from previous calls (used by `print -X` which preserves
7589/// alignment across args). When `all_tabs` is false, only leading TABs
7590/// (those at the start of a line) are expanded; embedded TABs are
7591/// emitted verbatim and `startpos` is advanced by one tabstop. When
7592/// `all_tabs` is true, every TAB expands. Returns the new `startpos`.
7593pub(crate) fn zexpandtabs(
7594 s: &str,
7595 width: i32,
7596 startpos: i32,
7597 all_tabs: bool,
7598 out: &mut String,
7599) -> i32 {
7600 let mut startpos = startpos;
7601 let mut at_start = true;
7602 for c in s.chars() {
7603 if c == '\t' {
7604 if all_tabs || at_start {
7605 if width <= 0 || startpos % width == 0 {
7606 out.push(' ');
7607 startpos += 1;
7608 }
7609 if width > 0 {
7610 while startpos % width != 0 {
7611 out.push(' ');
7612 startpos += 1;
7613 }
7614 }
7615 } else {
7616 let rem = startpos % width;
7617 startpos += width - rem;
7618 out.push('\t');
7619 }
7620 continue;
7621 } else if c == '\n' || c == '\r' {
7622 out.push(c);
7623 startpos = 0;
7624 at_start = true;
7625 continue;
7626 }
7627 at_start = false;
7628 out.push(c);
7629 startpos += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as i32;
7630 }
7631 startpos
7632}
7633
7634/// Port of `int hasspecial(char const *s)` from `Src/utils.c:6072-6082`.
7635/// ```c
7636/// while (*s) {
7637/// if (ispecial(*s == Meta ? *++s ^ 32 : *s)) return 1;
7638/// s++;
7639/// }
7640/// return 0;
7641/// ```
7642/// Predicate: does `s` contain any byte that needs shell-quoting?
7643/// Routes through the canonical typtab-driven `ztype_h::ispecial`
7644/// which respects the `ZTF_SP_COMMA` (set by `makecommaspecial`)
7645/// and `ZTF_BANGCHAR` (BANGHIST + interact) flags. Previously used
7646/// a hardcoded char list — diverged from C for `,` (KSH_GLOB
7647/// extended-glob) and for the dynamic bangchar (`!` by default but
7648/// user-rewritable via `$HISTCHARS`).
7649pub fn hasspecial(s: &str) -> bool {
7650 // c:6072
7651 let bytes = s.as_bytes();
7652 let mut i = 0;
7653 while i < bytes.len() {
7654 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
7655 // c:6075 — `*s == Meta ? *++s ^ 32 : *s`.
7656 let v = bytes[i + 1] ^ 32;
7657 i += 2;
7658 v
7659 } else {
7660 let v = bytes[i];
7661 i += 1;
7662 v
7663 };
7664 if crate::ported::ztype_h::ispecial(c) {
7665 // c:6075
7666 return true;
7667 }
7668 }
7669 false
7670}
7671
7672/// Port of `static char *addunprintable(char *v, const char *u, const char *uend)`
7673/// from `Src/utils.c:6082-6124`. Renders unprintable bytes using
7674/// **shell-compatible C-string escapes**:
7675/// - `\0` (NUL, with `\000` form if next byte is octal-digit)
7676/// - `\a` (0x07 BEL), `\b` (BS), `\f` (FF), `\n` (LF),
7677/// `\r` (CR), `\t` (TAB), `\v` (VT)
7678/// - `\nnn` 3-digit octal fallback for any other control byte
7679///
7680/// Previously the Rust port emitted ZLE-style caret notation (`^X`,
7681/// `\u{:04x}`) — completely different convention. C semantics target
7682/// `printf %q` / `$'...'` reuse where the output must round-trip
7683/// through `printf %b` to reconstruct the original byte. Caret
7684/// notation is what ZLE *displays* but not what `addunprintable`
7685/// emits.
7686/// WARNING: param names don't match C — Rust=(c) vs C=(v, u, uend)
7687pub fn addunprintable(c: char) -> String {
7688 // c:6082
7689 let b = c as u32 & 0xff;
7690 match b as u8 {
7691 // c:6097-6103 — `\0`. C peeks next byte and uses `\000` form
7692 // when followed by an octal digit. Rust port works on a single
7693 // char so emits just `\0`; callers needing the disambiguation
7694 // form can pass the lookahead byte separately.
7695 0x00 => "\\0".to_string(),
7696 0x07 => "\\a".to_string(), // c:6106
7697 0x08 => "\\b".to_string(), // c:6107
7698 0x0c => "\\f".to_string(), // c:6108
7699 0x0a => "\\n".to_string(), // c:6109
7700 0x0d => "\\r".to_string(), // c:6110
7701 0x09 => "\\t".to_string(), // c:6111
7702 0x0b => "\\v".to_string(), // c:6112
7703 // c:6114-6119 — `\nnn` 3-digit octal default.
7704 _ => format!("\\{:o}{:o}{:o}", (b >> 6) & 7, (b >> 3) & 7, b & 7),
7705 }
7706}
7707
7708/// One element of a metafied string, as C's `MB_METACHARLENCONV` loop sees it:
7709/// either a decoded character, or a raw byte that is not one.
7710///
7711/// A byte that cannot appear in a Rust `String` (an invalid-UTF-8 byte such as
7712/// the 0xE1 produced by `$'\M-a'`) is stored metafied — `Meta` (0x83) followed
7713/// by `byte ^ 32` — so a plain `.chars()` walk sees two innocuous characters
7714/// where the shell means one raw byte, and passes them straight through. C hits
7715/// exactly this case as `WEOF` out of `MB_METACHARLENCONV` (c:6210) and routes
7716/// it to `addunprintable()`.
7717#[derive(Clone, Copy, PartialEq)]
7718enum MetaChar {
7719 Ch(char),
7720 Raw(u8),
7721}
7722
7723/// Quote a string according to the specified type
7724/// Port from zsh/Src/utils.c quotestring() (lines 6141-6452)
7725/// Quote a string per the requested bslashquote style.
7726/// Port of `quotestring(const char *s, int instring)` from Src/utils.c — used by `print
7727/// -%q`, `${(q)var}`, completion-output escaping, history
7728/// re-emission.
7729/// WARNING: param names don't match C — Rust=(s, quote_type) vs C=(s, instring)
7730pub fn quotestring(s: &str, quote_type: i32) -> String {
7731 // c:6207-6210 — C walks the string with MB_METACHARINIT/MB_METACHARLENCONV,
7732 // which yields one decoded character at a time, or WEOF for a byte that does
7733 // not decode. These three closures are that walk, kept inline exactly as C
7734 // keeps it inline in each arm below.
7735 //
7736 // `meta_chars` is the decoder: a byte that cannot live in a Rust `String`
7737 // (an invalid-UTF-8 byte such as the 0xE1 from `$'\M-a'`) is stored metafied
7738 // — Meta (0x83) then `byte ^ 32` — so a naive `.chars()` walk sees two
7739 // innocuous characters where the shell means one raw byte and passes them
7740 // straight through. That is C's WEOF case, and it must reach addunprintable.
7741 let meta_chars = |s: &str| -> Vec<MetaChar> {
7742 let mut out = Vec::with_capacity(s.len());
7743 let mut it = s.chars().peekable();
7744 while let Some(c) = it.next() {
7745 if c as u32 == Meta as u32 {
7746 if let Some(&n) = it.peek() {
7747 let nu = n as u32;
7748 if (0x80..=0xff).contains(&nu) {
7749 it.next();
7750 out.push(MetaChar::Raw((nu as u8) ^ 32));
7751 continue;
7752 }
7753 }
7754 }
7755 out.push(MetaChar::Ch(c));
7756 }
7757 out
7758 };
7759 // c:6212 — `cc != WEOF && WC_ISPRINT(cc)`. A raw byte is never printable: it
7760 // IS the WEOF case.
7761 let mc_printable = |mc: MetaChar| -> bool {
7762 match mc {
7763 MetaChar::Ch(c) => !c.is_control(),
7764 MetaChar::Raw(_) => false,
7765 }
7766 };
7767 // c:6082-6124 addunprintable — emit a non-printable element BYTE by byte, so
7768 // a non-printable multibyte character becomes one escape per UTF-8 byte.
7769 // `\0` widens to `\000` when an octal digit follows so the escape cannot
7770 // swallow it (c:6097-6103). There is deliberately no `\e` case: C has none,
7771 // so ESC comes out as the octal `\033`.
7772 let push_unprintable = |out: &mut String, mc: MetaChar, next: Option<MetaChar>| {
7773 let mut buf = [0u8; 4];
7774 let bytes: &[u8] = match mc {
7775 MetaChar::Ch(c) => c.encode_utf8(&mut buf).as_bytes(),
7776 MetaChar::Raw(b) => {
7777 buf[0] = b;
7778 &buf[0..1]
7779 }
7780 };
7781 for i in 0..bytes.len() {
7782 let b = bytes[i];
7783 if b == 0 {
7784 out.push_str("\\0");
7785 let follows = if i + 1 < bytes.len() {
7786 Some(bytes[i + 1])
7787 } else {
7788 next.map(|n| match n {
7789 MetaChar::Ch(c) => {
7790 let mut nb = [0u8; 4];
7791 c.encode_utf8(&mut nb).as_bytes()[0]
7792 }
7793 MetaChar::Raw(rb) => rb,
7794 })
7795 };
7796 if matches!(follows, Some(b'0'..=b'7')) {
7797 out.push_str("00");
7798 }
7799 } else {
7800 out.push_str(&addunprintable(b as char));
7801 }
7802 }
7803 };
7804
7805 // c:6141
7806 if s.is_empty() {
7807 return if quote_type == QT_NONE {
7808 String::new()
7809 } else if quote_type == QT_BACKSLASH {
7810 // c:6194 `if (!*s && shownull)` — shownull is 0 for plain
7811 // QT_BACKSLASH, so an empty string produces NO quotes.
7812 String::new()
7813 } else if quote_type == QT_BACKSLASH_SHOWNULL {
7814 // c:6165 sets shownull=1, so empty → '' (c:6194 adds the pair).
7815 "''".to_string()
7816 } else if quote_type == QT_SINGLE || quote_type == QT_SINGLE_OPTIONAL {
7817 "''".to_string()
7818 } else if quote_type == QT_DOUBLE {
7819 "\"\"".to_string()
7820 } else if quote_type == QT_DOLLARS {
7821 "$''".to_string()
7822 } else {
7823 String::new()
7824 };
7825 }
7826
7827 if quote_type == QT_NONE {
7828 s.to_string()
7829 } else if quote_type == QT_BACKSLASH_PATTERN {
7830 // Only bslashquote pattern characters (lines 6242-6247)
7831 let mut result = String::with_capacity(s.len() * 2);
7832 for c in s.chars() {
7833 if matches!(
7834 c,
7835 '*' | '?' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '#' | '^' | '~'
7836 ) {
7837 result.push('\\');
7838 }
7839 result.push(c);
7840 }
7841 result
7842 } else if quote_type == QT_BACKSLASH || quote_type == QT_BACKSLASH_SHOWNULL {
7843 // c:Src/utils.c:6260-6452 QT_BACKSLASH. Three sub-cases per
7844 // input char (after the special-syntax handling above):
7845 // - `\n` → emit `$'\n'` literal 5 bytes (c:6366-6371)
7846 // - special-and-printable → emit `\<char>` (c:6385-6395)
7847 // - non-printable (ctrl, multibyte outside printable
7848 // range) → emit `$'<addunprintable>'` (c:6412-6422)
7849 // Bug #144/#149 in docs/BUGS.md: previous port mapped ALL
7850 // ispecial chars to `\<char>`, so `\n`/`\t`/etc. came out
7851 // as `\<actual-byte>` instead of `$'\n'`/`$'\t'`.
7852 //
7853 // c:6301-6306 — `=` and `~` are NOT escaped unless one of:
7854 // (B) the char is at position 0 (u == s)
7855 // (C) MAGICEQUALSUBST is set AND the previous byte is `=`
7856 // or `:` (covers `var==val`, `var:=val`)
7857 // (D) the char is `~` AND EXTENDEDGLOB is set
7858 // Without these conditions, mid-word `=`/`~` falls through
7859 // to the literal-emit path (c:6418-6434).
7860 let mut result = String::with_capacity(s.len() * 2);
7861 let mut prev: char = '\0'; // would-be u[-1]
7862 let mcs = meta_chars(s);
7863 for i in 0..mcs.len() {
7864 let mc = mcs[i];
7865 let c = match mc {
7866 MetaChar::Ch(c) => c,
7867 // A raw byte is never `\n` and never ispecial — it is the
7868 // not-printable case below. Stand in with its byte value so the
7869 // `=`/`~` lookbehind still advances correctly.
7870 MetaChar::Raw(b) => b as char,
7871 };
7872 // c:6301-6306 gate for `=`/`~` (condition A/B/C/D).
7873 let eq_tilde_gate = if c == '=' || c == '~' {
7874 i == 0 // c:6303 u == s
7875 || (isset(MAGICEQUALSUBST) && (prev == '=' || prev == ':')) // c:6304-6305
7876 || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
7877 } else {
7878 true // c:6302 *u != '=' && *u != '~'
7879 };
7880 if mc == MetaChar::Ch('\n') {
7881 // c:6366-6371 — newline gets dedicated `$'\n'` form.
7882 result.push_str("$'\\n'");
7883 } else if matches!(mc, MetaChar::Ch(_))
7884 && ispecial(c)
7885 && eq_tilde_gate
7886 && c.is_ascii()
7887 && !c.is_ascii_control()
7888 {
7889 // c:6385-6395 — printable special → `\<char>`.
7890 result.push('\\');
7891 result.push(c);
7892 } else if !mc_printable(mc) {
7893 // c:6412-6422 — anything not printable (a control char, a
7894 // non-printable multibyte char, or a raw undecodable byte) is
7895 // wrapped one element at a time as `$'<addunprintable>'`.
7896 result.push_str("$'");
7897 push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
7898 result.push('\'');
7899 } else {
7900 result.push(c);
7901 }
7902 prev = c;
7903 }
7904 result
7905 } else if quote_type == QT_SINGLE {
7906 // c:Src/utils.c:6300-6395 QT_SINGLE — `(qq)` flag.
7907 // The outer ispecial-gated branch at c:6301-6313 only
7908 // triggers special handling when the QT-specific
7909 // subcondition matches; for QT_SINGLE that's `*u == '\''`.
7910 // Newlines do NOT match, so they fall through to the
7911 // pass-through path at c:6394 and are emitted LITERALLY
7912 // inside the surrounding `'…'`. Bug #199 in docs/BUGS.md:
7913 // previous port emitted `'$'\n''` (split-quote with
7914 // `$'\n'` escape between segments) for newline, diverging
7915 // from zsh's literal-newline-in-single-quotes output.
7916 let mut result = String::with_capacity(s.len() + 4);
7917 result.push('\'');
7918 for c in s.chars() {
7919 if c == '\'' {
7920 // c:6364-6379 — apostrophe close-reopen sequence.
7921 result.push_str("'\\''");
7922 } else {
7923 // Newline + everything else: pass through literally.
7924 result.push(c);
7925 }
7926 }
7927 result.push('\'');
7928 result
7929 } else if quote_type == QT_SINGLE_OPTIONAL {
7930 // c:Src/utils.c:6314-6385 QT_SINGLE_OPTIONAL — minimum
7931 // quoting. Walks the string with two states:
7932 // quotesub=1 — not currently inside a quote span. Bare
7933 // apostrophes get `\'` (backslash form). Any OTHER
7934 // special char triggers back-filling: insert `'` at
7935 // `quotestart` (start of unquoted prefix), shifting
7936 // subsequent chars right, then push char, transition
7937 // to quotesub=2.
7938 // quotesub=2 — currently inside a `'…'` span. Bare
7939 // apostrophes break the span: push `'\\'`, transition
7940 // back to quotesub=1 with quotestart=position-after.
7941 // Other specials append in-place.
7942 // End: if quotesub=2, close with `'`.
7943 // For "hello world" this yields `'hello world'` (back-
7944 // filled at start). For "it's" it yields `it\'s` (no quote
7945 // span ever opens). The naive per-char approach without
7946 // back-filling produced `hello' 'world` — parity bug.
7947 //
7948 // c:6301-6306 — the SAME `=`/`~` gate the QT_BACKSLASH arm applies.
7949 // `ispecial` alone would open a quote span on any mid-word `=` or `~`,
7950 // so `a=b` came out as `'a=b'` where zsh leaves it bare.
7951 let chars: Vec<char> = s.chars().collect();
7952 let forces_quote = |i: usize, c: char| -> bool {
7953 if !ispecial(c) {
7954 return false; // c:6301
7955 }
7956 if c == '=' || c == '~' {
7957 i == 0 // c:6303 u == s
7958 || (isset(MAGICEQUALSUBST) && (chars[i - 1] == '=' || chars[i - 1] == ':')) // c:6304-6305
7959 || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
7960 } else {
7961 true // c:6302
7962 }
7963 };
7964 let needs_quoting = chars.iter().enumerate().any(|(i, &c)| forces_quote(i, c));
7965 if !needs_quoting {
7966 return s.to_string();
7967 }
7968 let mut result: Vec<char> = Vec::with_capacity(s.len() + 4);
7969 let mut quotestart: usize = 0; // index in `result` where the next `'` would go
7970 let mut quotesub: u8 = 1; // 1 = not quoting, 2 = inside `'…'`
7971 for (i, &c) in chars.iter().enumerate() {
7972 if c == '\'' {
7973 if quotesub == 2 {
7974 // close current quote span, then `\'`, then
7975 // mark that we may need to reopen on next special
7976 result.push('\'');
7977 result.push('\\');
7978 result.push('\'');
7979 quotesub = 1;
7980 quotestart = result.len();
7981 } else {
7982 result.push('\\');
7983 result.push('\'');
7984 quotestart = result.len();
7985 }
7986 } else if forces_quote(i, c) {
7987 if quotesub == 1 {
7988 // Back-fill: insert `'` at quotestart, shifting
7989 // everything after right by 1.
7990 result.insert(quotestart, '\'');
7991 quotesub = 2;
7992 }
7993 result.push(c);
7994 } else {
7995 result.push(c);
7996 }
7997 }
7998 if quotesub == 2 {
7999 result.push('\'');
8000 }
8001 result.into_iter().collect()
8002 } else if quote_type == QT_DOUBLE {
8003 // Double quote: "string" (lines 6272-6280, 6311-6312)
8004 let mut result = String::with_capacity(s.len() + 4);
8005 result.push('"');
8006 for c in s.chars() {
8007 if matches!(c, '$' | '`' | '"' | '\\') {
8008 result.push('\\');
8009 }
8010 result.push(c);
8011 }
8012 result.push('"');
8013 result
8014 } else if quote_type == QT_DOLLARS {
8015 // c:6203-6241 — `$'…'` quoting. Each element is either printable, in
8016 // which case it is emitted verbatim (backslashing `\`, `'`, and — under
8017 // BANGHIST — the history character), or it is NOT, in which case C hands
8018 // its raw bytes to addunprintable(): a named escape (`\t`, `\n`, `\a`, …)
8019 // or a 3-digit octal one.
8020 //
8021 // Two things this must NOT do, both of which the previous port did:
8022 // * pass an undecodable byte through raw. `$'\M-a'` is the byte 0xE1,
8023 // which is not valid UTF-8 and is stored metafied; walking `.chars()`
8024 // re-emitted it verbatim instead of `\341`, so the output could not
8025 // be re-parsed.
8026 // * emit `\e` for ESC. C's addunprintable has no `\e` case — ESC comes
8027 // out as the octal `\033`.
8028 let mut result = String::with_capacity(s.len() + 4);
8029 result.push_str("$'");
8030 let mcs = meta_chars(s);
8031 let bang = crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
8032 for i in 0..mcs.len() {
8033 let mc = mcs[i];
8034 if let (true, MetaChar::Ch(c)) = (mc_printable(mc), mc) {
8035 // c:6214-6224
8036 if c == '\\' || c == '\'' || (isset(BANGHIST) && bang != 0 && c as u32 == bang as u32)
8037 {
8038 result.push('\\');
8039 }
8040 result.push(c);
8041 } else {
8042 // c:6226-6229
8043 push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
8044 }
8045 }
8046 result.push('\'');
8047 result
8048 } else if quote_type == QT_BACKTICK {
8049 // Backtick quoting (minimal - just escape backticks)
8050 s.replace('`', "\\`")
8051 } else {
8052 // Unknown quote_type — treat as no-op to match C's `default:` arm.
8053 s.to_string()
8054 }
8055}
8056
8057// ===========================================================
8058// xtrace helpers moved from src/ported/vm_helper.
8059// printprompt4 is a direct port of utils.c:1718-1735; quotedzputs
8060// is its argument-formatter companion (zsh formats `set -x` lines
8061// via the same utils.c path).
8062// ===========================================================
8063
8064/// Port of `quotedzputs(char const *s, FILE *stream)` from `Src/utils.c:6464`.
8065///
8066/// Quote a string for re-readable output (`set -x`, `typeset -p`,
8067/// `set` listing, etc.). zsh's algorithm under MULTIBYTE_SUPPORT
8068/// (c:6464-6543):
8069/// 1. empty input → `''` (c:6470-6475)
8070/// 2. needs nice-format (controls / non-printables) → emit
8071/// `$'<mb_niceformat output>'` (c:6478-6492)
8072/// 3. no SPECCHARS member → return string unchanged (c:6511-6517)
8073/// 4. otherwise wrap in `'…'` (Bourne) or RCQUOTES form,
8074/// with embedded `'` rewritten as `'\''` / `''` (c:6533-6587)
8075///
8076/// Previous Rust port omitted step 2 (the `$'…'` branch).
8077/// Result: strings containing control bytes (`\n`, `\t`, escape
8078/// sequences) were single-quoted instead of `$'…'`-quoted,
8079/// breaking round-trip through `typeset -p` / `set` / `set -x`
8080/// because POSIX single-quotes are *strong* — embedded `\n` would
8081/// be re-fed as a literal newline rather than the C-escape.
8082///
8083/// The C signature is `char *quotedzputs(char const *s, FILE *stream)`
8084/// — when `stream` is non-NULL it writes there and returns NULL,
8085/// otherwise it returns the quoted string. Rust's variant covers
8086/// only the `stream==NULL` form (the `set -x` callers all want the
8087/// string back, not direct stdout writing). The `stream==Some` form
8088/// is fputs/fputc-direct in C; Rust callers that need writer-based
8089/// output should compose `print!`/`write!` with this fn's return.
8090/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream)
8091pub(crate) fn quotedzputs(s: &str) -> String {
8092 // c:6464
8093 // c:6469-6475 — `if (!*s)` empty string emits `''` literal.
8094 if s.is_empty() {
8095 return "''".to_string(); // c:6472
8096 }
8097 // c:6477-6508 — `is_mb_niceformat(s)` / `is_sb_niceformat(s)` arm:
8098 // if the string contains nice-formatted chars (controls,
8099 // non-printables), wrap in `$'…'` using sb/mb_niceformat with
8100 // NICEFLAG_QUOTE so embedded `'`/`\` get backslash-escaped.
8101 if is_mb_niceformat(s) != 0 {
8102 // c:6478-6492 (MULTIBYTE_SUPPORT branch): use mb_niceformat
8103 // with NICEFLAG_QUOTE so multi-byte chars round-trip through
8104 // wcs_nicechar (raw UTF-8 for printable wides, `\u`/`\U` for
8105 // large codepoints) AND `'`/`\\` get backslash-escaped.
8106 // Under !MULTIBYTE_SUPPORT (c:6494-6508) C would use
8107 // sb_niceformat instead. Static-link path: MULTIBYTE is
8108 // always available in Rust.
8109 // c:6485-6492 — `mb_niceformat(s, NULL, &substr,
8110 // NICEFLAG_QUOTE|NICEFLAG_NODUP);`
8111 let mut substr: Option<String> = None;
8112 let _ = mb_niceformat(s, None, Some(&mut substr), NICEFLAG_QUOTE | NICEFLAG_NODUP);
8113 return format!("$'{}'", substr.unwrap_or_default()); // c:6488
8114 }
8115 // c:6511-6518 — `if (!hasspecial(s)) return dupstring(s);`.
8116 if !hasspecial(s) {
8117 // c:6511
8118 return s.to_string(); // c:6516
8119 }
8120 // c:6520-6529 — outstr buffer alloc (zhalloc). Rust uses growable
8121 // String; the C `l = strlen(s) + 2 + (per-' overhead)` size hint
8122 // is just allocation tuning and doesn't affect output.
8123 let mut out = String::with_capacity(s.len() + 2);
8124 let bytes = s.as_bytes();
8125 let csh_junkie = isset(CSHJUNKIEQUOTES); // c:6554 / c:6612
8126
8127 if isset(RCQUOTES) {
8128 // c:6533 — RCQUOTES: wrap entire string in `'…'`; each
8129 // embedded `'` becomes `''` (the rc-style doubled quote).
8130 out.push('\''); // c:6539
8131 // c:6540-6547 — C walks METAFIED bytes; the Rust String is
8132 // UTF-8, so the faithful transposition walks CHARS (a byte
8133 // walk Latin-1-casts every multibyte char: em-dash E2 80 94
8134 // became "â" + a token byte that downstream passes ate).
8135 // Token chars (Dash U+009B, Meta U+0083) are codepoints here.
8136 let chars_v: Vec<char> = s.chars().collect();
8137 let mut i = 0;
8138 while i < chars_v.len() {
8139 let c = if chars_v[i] == Dash {
8140 // c:6541 — `if (*s == Dash) c = '-';`
8141 i += 1;
8142 '-'
8143 } else if chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len() {
8144 // c:6543 — `else if (*s == Meta) c = *++s ^ 32;`
8145 let n = chars_v[i + 1] as u32;
8146 i += 2;
8147 char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
8148 } else {
8149 // c:6546 — `else c = *s;`
8150 let dec = chars_v[i];
8151 i += 1;
8152 dec
8153 };
8154 if c == '\'' {
8155 // c:6548-6553 — `if (c == '\'') *ptr++ = '\'';` (the
8156 // rc-quote doubling).
8157 out.push('\''); // c:6553
8158 } else if c == '\n' && csh_junkie {
8159 // c:6554-6560 — `if (c == '\n' && isset(CSHJUNKIEQUOTES))
8160 // *ptr++ = '\\';`
8161 out.push('\\'); // c:6559
8162 }
8163 // c:6561-6570 — emit c (metafy on imeta).
8164 out.push(c); // c:6569 (non-stream branch always re-metafies;
8165 // Rust String holds decoded chars directly)
8166 }
8167 out.push('\''); // c:6576
8168 } else {
8169 // c:6578-6637 — Bourne-style quoting, "avoiding empty quoted
8170 // strings". Tracks `inquote` so that `it's` becomes
8171 // `'it'\''s'` (no empty `''` runs).
8172 let mut inquote = false; // c:6466 (initialised at top of C fn)
8173 // c:6579-6586 — char-wise decode, same transposition as the
8174 // RCQUOTES arm above (C's byte walk is over metafied bytes).
8175 let chars_v: Vec<char> = s.chars().collect();
8176 let mut i = 0;
8177 while i < chars_v.len() {
8178 let c = if chars_v[i] == Dash {
8179 i += 1;
8180 '-' // c:6581
8181 } else if chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len() {
8182 let n = chars_v[i + 1] as u32; // c:6583
8183 i += 2;
8184 char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
8185 } else {
8186 let dec = chars_v[i]; // c:6585
8187 i += 1;
8188 dec
8189 };
8190 if c == '\'' {
8191 // c:6587-6602 — `'` closes any open inquote then emits
8192 // `\'` outside the quotes.
8193 if inquote {
8194 out.push('\''); // c:6593
8195 inquote = false; // c:6594
8196 }
8197 out.push('\\'); // c:6600
8198 out.push('\''); // c:6601
8199 } else {
8200 // c:6603-6629 — other chars open a quote run if not
8201 // already open, optionally backslash-escape `\n` under
8202 // CSHJUNKIEQUOTES, then emit the byte.
8203 if !inquote {
8204 out.push('\''); // c:6609
8205 inquote = true; // c:6610
8206 }
8207 if c == '\n' && csh_junkie {
8208 out.push('\\'); // c:6617
8209 }
8210 out.push(c); // c:6627 (imeta-encoding handled by Rust
8211 // String storage in the non-stream form)
8212 }
8213 }
8214 if inquote {
8215 out.push('\''); // c:6636
8216 }
8217 }
8218 // c:6639-6640 — `if (!stream) *ptr++ = '\0';` — Rust String already
8219 // NUL-terminated implicitly; no-op.
8220 out // c:6642
8221}
8222
8223/// Port of `char *dquotedztrdup(char const *s)` from `Src/utils.c:6648-6723`.
8224/// Two arms (selected by `isset(CSHJUNKIEQUOTES)` at c:6655):
8225///
8226/// **CSHJUNKIEQUOTES path** (c:6656-6686): the csh-junk-quote style
8227/// where only the non-special sections are wrapped in `"..."` and
8228/// special chars (`"`, `$`, `` ` ``) appear OUTSIDE the quotes with
8229/// backslash escape. `\n` inside the quotes gets an extra `\` so it
8230/// round-trips through history.
8231///
8232/// **Default path** (c:6687-6719): wraps the whole string in `"..."`.
8233/// `\` is doubled to `\\`. `"`, `$`, `` ` `` get backslash-escaped.
8234/// A trailing `\` gets an extra `\` appended (the `pending` quirk).
8235///
8236/// Previously the Rust port only implemented the default arm; the
8237/// CSHJUNKIEQUOTES path is now ported faithfully.
8238pub fn dquotedztrdup(s: &str) -> String {
8239 // c:6648
8240 let mut out = String::with_capacity(s.len() * 4 + 2);
8241 let bytes = s.as_bytes();
8242 // c:6655 — `if (isset(CSHJUNKIEQUOTES))`.
8243 if isset(CSHJUNKIEQUOTES) {
8244 let mut inquote = false;
8245 let mut i = 0;
8246 while i < bytes.len() {
8247 // c:6661-6662 — Meta byte decode.
8248 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
8249 i += 2;
8250 (bytes[i - 1] ^ 32) as char
8251 } else {
8252 i += 1;
8253 bytes[i - 1] as char
8254 };
8255 match c {
8256 // c:6664-6673 — `"` / `$` / `` ` `` — close quote
8257 // (if open), then `\<c>`.
8258 '"' | '$' | '`' => {
8259 if inquote {
8260 out.push('"');
8261 inquote = false;
8262 }
8263 out.push('\\');
8264 out.push(c);
8265 }
8266 // c:6674-6682 — default arm: open quote if needed,
8267 // backslash-escape newline, emit char.
8268 _ => {
8269 if !inquote {
8270 out.push('"');
8271 inquote = true;
8272 }
8273 if c == '\n' {
8274 out.push('\\');
8275 }
8276 out.push(c);
8277 }
8278 }
8279 }
8280 // c:6685-6686 — close trailing quote.
8281 if inquote {
8282 out.push('"');
8283 }
8284 } else {
8285 // c:6687-6718 — default (non-CSH) arm.
8286 out.push('"');
8287 let mut pending = false;
8288 let mut i = 0;
8289 while i < bytes.len() {
8290 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
8291 i += 2;
8292 (bytes[i - 1] ^ 32) as char
8293 } else {
8294 i += 1;
8295 bytes[i - 1] as char
8296 };
8297 match c {
8298 '\\' => {
8299 if pending {
8300 out.push('\\');
8301 }
8302 out.push('\\');
8303 pending = true;
8304 }
8305 '"' | '$' | '`' => {
8306 if pending {
8307 out.push('\\');
8308 }
8309 out.push('\\');
8310 out.push(c);
8311 pending = false;
8312 }
8313 other => {
8314 out.push(other);
8315 pending = false;
8316 }
8317 }
8318 }
8319 if pending {
8320 out.push('\\');
8321 }
8322 out.push('"');
8323 }
8324 // c:6720 — `ret = metafy(buf, p - buf, META_DUP);` re-metafy result.
8325 metafy(&out)
8326}
8327
8328/// Port of `dquotedzputs(char const *s, FILE *stream)` from
8329/// Src/utils.c:6729. C body (4 lines):
8330/// `char *d = dquotedztrdup(s);
8331/// int ret = zputs(d, stream);
8332/// zsfree(d);
8333/// return ret;`
8334/// Rust returns the quoted string directly (callers compose via
8335/// `format!` rather than streaming through a FILE*), so the zputs
8336/// call drops.
8337pub fn dquotedzputs(s: &str) -> String {
8338 // c:6729
8339 dquotedztrdup(s) // c:6731
8340}
8341
8342/// Convert UCS-4 to UTF-8 (from utils.c ucs4toutf8)
8343/// Port of `ucs4toutf8(char *dest, unsigned int wval)` from `Src/utils.c:6743`.
8344///
8345/// C accepts 0..=0x7FFFFFFF (legacy UCS-4 / 6-byte UTF-8) — wider
8346/// than Unicode's 0..=0x10FFFF — to match `wctomb(3)` on Linux
8347/// with UTF-8 locale. The Rust `char::from_u32` path would reject
8348/// codepoints above 0x10FFFF (and surrogates), so this port mirrors
8349/// the C bit-pattern dispatch directly.
8350///
8351/// C body (c:6743-6779):
8352/// ```c
8353/// if (wval < 0x80) len = 1; // ASCII
8354/// else if (wval < 0x800) len = 2;
8355/// else if (wval < 0x10000) len = 3;
8356/// else if (wval < 0x200000) len = 4;
8357/// else if (wval < 0x4000000) len = 5;
8358/// else if (wval < 0x80000000) len = 6;
8359/// else { zerr("character not in range"); return -1; }
8360///
8361/// switch (len) { /* falls through except to the last case */
8362/// case 6: dest[5] = (wval & 0x3f) | 0x80; wval >>= 6;
8363/// case 5: dest[4] = (wval & 0x3f) | 0x80; wval >>= 6;
8364/// case 4: dest[3] = (wval & 0x3f) | 0x80; wval >>= 6;
8365/// case 3: dest[2] = (wval & 0x3f) | 0x80; wval >>= 6;
8366/// case 2: dest[1] = (wval & 0x3f) | 0x80; wval >>= 6;
8367/// *dest = wval | ((0xfc << (6 - len)) & 0xfc);
8368/// break;
8369/// case 1: *dest = wval;
8370/// }
8371/// return len;
8372/// ```
8373///
8374/// Returns the encoded bytes (1..=6 long) on success, None on
8375/// out-of-range. C's `zerr("character not in range")` (c:6763) is
8376/// not emitted here — the return signals the error.
8377/// WARNING: param names don't match C — Rust=(wval) vs C=(dest, wval)
8378pub fn ucs4toutf8(wval: u32) -> Option<String> {
8379 // c:6743
8380 let len: usize = if wval < 0x80 {
8381 1
8382 }
8383 // c:6750
8384 else if wval < 0x800 {
8385 2
8386 }
8387 // c:6752
8388 else if wval < 0x10000 {
8389 3
8390 }
8391 // c:6754
8392 else if wval < 0x200000 {
8393 4
8394 }
8395 // c:6756
8396 else if wval < 0x4000000 {
8397 5
8398 }
8399 // c:6758
8400 else if wval < 0x80000000 {
8401 6
8402 }
8403 // c:6760
8404 else {
8405 // c:6762
8406 zerr("character not in range"); // c:6763
8407 return None; // c:6764
8408 };
8409 let mut buf = [0u8; 6];
8410 let mut w = wval;
8411 // c:6767-6776 — fall-through switch building trailing bytes first.
8412 match len {
8413 1 => {
8414 buf[0] = w as u8;
8415 } // c:6775
8416 n => {
8417 // Trailing (len-1) bytes: each is `(w & 0x3f) | 0x80`,
8418 // shifting w right 6 between them (c:6768-6772).
8419 for i in (1..n).rev() {
8420 buf[i] = ((w & 0x3f) as u8) | 0x80;
8421 w >>= 6;
8422 }
8423 // Leading byte: `w | ((0xfc << (6 - len)) & 0xfc)` (c:6773).
8424 buf[0] = (w as u8) | (((0xfcu32 << (6 - n)) & 0xfc) as u8);
8425 }
8426 }
8427 Some(String::from_utf8_lossy(&buf[..len]).into_owned())
8428}
8429
8430/// Port of `ucs4tomb(unsigned int wval, char *buf)` from `Src/utils.c:6788`.
8431///
8432/// Encode a UCS-4 codepoint into the buffer `buf` using the current
8433/// locale's multibyte encoding. Returns the number of bytes written,
8434/// or -1 on conversion failure. C body uses `wctomb(3)` when
8435/// `__STDC_ISO_10646__` is defined (which it is on every modern
8436/// glibc / macOS libc), falls back to UTF-8 if the codeset is
8437/// `"UTF-8"`, and uses `iconv(3)` otherwise.
8438///
8439/// This Rust port mirrors the primary `wctomb` path via libc FFI;
8440/// the iconv fallback is unused on macOS/Linux modern builds.
8441/// On conversion failure, emits `zerr("character not in range")`
8442/// to match C source line 6794.
8443///
8444/// C body shape:
8445/// ```c
8446/// int count = wctomb(buf, (wchar_t)wval);
8447/// if (count == -1) zerr("character not in range");
8448/// return count;
8449/// ```
8450pub fn ucs4tomb(wval: u32, buf: &mut [u8]) -> i32 {
8451 // libc::wctomb requires at least MB_CUR_MAX bytes (typically 4
8452 // for UTF-8, 6 for some encodings). Use a stack buffer first,
8453 // then copy into the caller's buffer.
8454 // libc crate doesn't expose wctomb on all platforms; declare
8455 // the POSIX prototype directly. wchar_t is i32 on macOS/Linux
8456 // for our supported targets.
8457 extern "C" {
8458 fn wctomb(s: *mut libc::c_char, wc: libc::wchar_t) -> libc::c_int;
8459 }
8460 // libc::c_char is i8 on most targets but u8 on aarch64-linux. Use c_char
8461 // so the wctomb arg pointer type matches per-target without a cast.
8462 let mut local = [0 as libc::c_char; 16];
8463 let count = unsafe { wctomb(local.as_mut_ptr(), wval as libc::wchar_t) };
8464 if count < 0 {
8465 zerr("character not in range");
8466 return -1;
8467 }
8468 let n = count as usize;
8469 if n > buf.len() {
8470 zerr("character not in range");
8471 return -1;
8472 }
8473 for i in 0..n {
8474 buf[i] = local[i] as u8;
8475 }
8476 count
8477}
8478
8479/// Parse getkeystring escape sequences (from utils.c getkeystring)
8480/// Handles \n \t \r \e \a \b \f \v \\ \' \" \xNN \uNNNN \UNNNNNNNN \0NNN
8481/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from `Src/utils.c:6915`.
8482/// WARNING: param names don't match C — Rust=(s) vs C=(s, len, how, misc)
8483pub fn getkeystring(s: &str) -> (String, usize) {
8484 // c:6915
8485 let mut result = String::new();
8486 let mut chars = s.chars().peekable();
8487 let mut consumed = 0;
8488
8489 while let Some(c) = chars.next() {
8490 consumed += c.len_utf8();
8491 if c != '\\' {
8492 result.push(c);
8493 continue;
8494 }
8495 match chars.next() {
8496 Some('n') => {
8497 result.push('\n');
8498 consumed += 1;
8499 }
8500 Some('t') => {
8501 result.push('\t');
8502 consumed += 1;
8503 }
8504 Some('r') => {
8505 result.push('\r');
8506 consumed += 1;
8507 }
8508 Some('e') | Some('E') => {
8509 result.push('\x1b');
8510 consumed += 1;
8511 }
8512 Some('a') => {
8513 result.push('\x07');
8514 consumed += 1;
8515 }
8516 Some('b') => {
8517 result.push('\x08');
8518 consumed += 1;
8519 }
8520 Some('f') => {
8521 result.push('\x0c');
8522 consumed += 1;
8523 }
8524 Some('v') => {
8525 result.push('\x0b');
8526 consumed += 1;
8527 }
8528 Some('\\') => {
8529 result.push('\\');
8530 consumed += 1;
8531 }
8532 Some('\'') => {
8533 result.push('\'');
8534 consumed += 1;
8535 }
8536 Some('"') => {
8537 result.push('"');
8538 consumed += 1;
8539 }
8540 Some('x') => {
8541 consumed += 1;
8542 let mut hex = String::new();
8543 for _ in 0..2 {
8544 if let Some(&c) = chars.peek() {
8545 if c.is_ascii_hexdigit() {
8546 hex.push(chars.next().unwrap());
8547 consumed += 1;
8548 } else {
8549 break;
8550 }
8551 }
8552 }
8553 // c:Src/utils.c:7169-7170 — `\x` ALWAYS runs `*t++ =
8554 // zstrtol(s+1, &s, 16)`, even with no hex digit. zstrtol
8555 // over a non-hex byte parses zero digits and returns 0, so
8556 // a NUL byte is emitted (the offending char is processed
8557 // next). `$'\xg'` → NUL + 'g'; `$'\x'` → NUL. Previously
8558 // `from_str_radix("")` errored and nothing was pushed.
8559 let val: u8 = if hex.is_empty() {
8560 0 // c:7170 zstrtol on empty reads as 0
8561 } else {
8562 u8::from_str_radix(&hex, 16).unwrap_or(0)
8563 };
8564 // c:Src/utils.c — `\xNN` is one raw BYTE; metafied
8565 // per c:7289-7294 (vm_helper::meta_encode_byte) so
8566 // the String stays valid UTF-8. Bug #127.
8567 {
8568 // c:Src/utils.c metafy byte-encode step:
8569 // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8570 let b_ = val;
8571 if b_ < 0x80 {
8572 result.push(b_ as char);
8573 } else {
8574 result.push('\u{83}');
8575 result.push(char::from(b_ ^ 32));
8576 }
8577 }
8578 }
8579 Some('u') => {
8580 consumed += 1;
8581 let mut hex = String::new();
8582 for _ in 0..4 {
8583 if let Some(&c) = chars.peek() {
8584 if c.is_ascii_hexdigit() {
8585 hex.push(chars.next().unwrap());
8586 consumed += 1;
8587 } else {
8588 break;
8589 }
8590 }
8591 }
8592 if let Ok(val) = u32::from_str_radix(&hex, 16) {
8593 if let Some(c) = char::from_u32(val) {
8594 result.push(c);
8595 }
8596 }
8597 }
8598 Some('U') => {
8599 consumed += 1;
8600 let mut hex = String::new();
8601 for _ in 0..8 {
8602 if let Some(&c) = chars.peek() {
8603 if c.is_ascii_hexdigit() {
8604 hex.push(chars.next().unwrap());
8605 consumed += 1;
8606 } else {
8607 break;
8608 }
8609 }
8610 }
8611 if let Ok(val) = u32::from_str_radix(&hex, 16) {
8612 if let Some(c) = char::from_u32(val) {
8613 result.push(c);
8614 }
8615 }
8616 }
8617 Some(c @ '0'..='7') => {
8618 consumed += 1;
8619 let mut oct = String::new();
8620 oct.push(c);
8621 for _ in 0..2 {
8622 if let Some(&c) = chars.peek() {
8623 if ('0'..='7').contains(&c) {
8624 oct.push(chars.next().unwrap());
8625 consumed += 1;
8626 } else {
8627 break;
8628 }
8629 }
8630 }
8631 if let Ok(val) = u8::from_str_radix(&oct, 8) {
8632 // c:Src/utils.c — octal escape is one raw BYTE;
8633 // metafied per c:7289-7294. Bug #127.
8634 {
8635 // c:Src/utils.c metafy byte-encode step:
8636 // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8637 let b_ = val;
8638 if b_ < 0x80 {
8639 result.push(b_ as char);
8640 } else {
8641 result.push('\u{83}');
8642 result.push(char::from(b_ ^ 32));
8643 }
8644 }
8645 }
8646 }
8647 Some('c') => {
8648 consumed += 1;
8649 // \cX = control character
8650 if let Some(c) = chars.next() {
8651 consumed += 1;
8652 result.push((c as u8 & 0x1f) as char);
8653 }
8654 }
8655 Some(mod_letter @ ('C' | 'M')) => {
8656 // c:Src/utils.c:7029-7052 + c:7265-7275 — `\C` /
8657 // `\M` set control / meta flags; optional `-`
8658 // separator; then read the next char (possibly
8659 // through additional `\C`/`\M` chains) and apply
8660 // the mask: control → `& 0x9f` (or `\C-?` → 0x7f),
8661 // meta → `| 0x80`. Bug #113 in docs/BUGS.md: the
8662 // previous Rust port matched these in the default
8663 // arm and emitted literal `\C` / `\M`. This is the
8664 // second getkeystring (utils.rs); the in-tree
8665 // `getkeystring_dollar_quote` (lex.rs) also needs
8666 // the fix and gets it in the same commit.
8667 consumed += 1;
8668 let mut control = mod_letter == 'C';
8669 let mut meta = mod_letter == 'M';
8670 // Consume optional `-` separator + chained `\C`/`\M`.
8671 loop {
8672 if chars.peek() == Some(&'-') {
8673 chars.next();
8674 consumed += 1;
8675 continue;
8676 }
8677 // chained `\C` / `\M`?
8678 let mut iter_clone = chars.clone();
8679 if iter_clone.next() == Some('\\') {
8680 if let Some(nx) = iter_clone.next() {
8681 if nx == 'C' || nx == 'M' {
8682 chars.next(); // consume '\'
8683 chars.next(); // consume C/M
8684 consumed += 2;
8685 if nx == 'C' {
8686 control = true;
8687 } else {
8688 meta = true;
8689 }
8690 continue;
8691 }
8692 }
8693 }
8694 break;
8695 }
8696 // Read one base character (allowing nested simple
8697 // escapes for common cases).
8698 let base: Option<char> = if chars.peek() == Some(&'\\') {
8699 chars.next();
8700 consumed += 1;
8701 match chars.next() {
8702 Some('n') => {
8703 consumed += 1;
8704 Some('\n')
8705 }
8706 Some('t') => {
8707 consumed += 1;
8708 Some('\t')
8709 }
8710 Some('r') => {
8711 consumed += 1;
8712 Some('\r')
8713 }
8714 Some('a') => {
8715 consumed += 1;
8716 Some('\x07')
8717 }
8718 Some('b') => {
8719 consumed += 1;
8720 Some('\x08')
8721 }
8722 Some('e') | Some('E') => {
8723 consumed += 1;
8724 Some('\x1b')
8725 }
8726 Some('f') => {
8727 consumed += 1;
8728 Some('\x0c')
8729 }
8730 Some('v') => {
8731 consumed += 1;
8732 Some('\x0b')
8733 }
8734 Some('\\') => {
8735 consumed += 1;
8736 Some('\\')
8737 }
8738 Some('\'') => {
8739 consumed += 1;
8740 Some('\'')
8741 }
8742 Some('"') => {
8743 consumed += 1;
8744 Some('"')
8745 }
8746 Some(other) => {
8747 consumed += 1;
8748 Some(other)
8749 }
8750 None => None,
8751 }
8752 } else {
8753 chars.next().inspect(|c| {
8754 consumed += c.len_utf8();
8755 })
8756 };
8757 if let Some(ch) = base {
8758 let mut byte = ch as u32;
8759 if control {
8760 if byte == '?' as u32 {
8761 byte = 0x7f;
8762 } else {
8763 byte &= 0x9f;
8764 }
8765 }
8766 if meta {
8767 byte |= 0x80;
8768 }
8769 // c:Src/utils.c:7265-7275 — masked result is one
8770 // raw BYTE (`$'\M-i'` = 0xe9), metafied per
8771 // c:7289-7294. Multibyte base chars (> 0xff)
8772 // keep the codepoint form. Bug #127.
8773 if byte <= 0xff {
8774 {
8775 // c:Src/utils.c metafy byte-encode step:
8776 // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8777 let b_ = byte as u8;
8778 if b_ < 0x80 {
8779 result.push(b_ as char);
8780 } else {
8781 result.push('\u{83}');
8782 result.push(char::from(b_ ^ 32));
8783 }
8784 }
8785 } else if let Some(c) = char::from_u32(byte) {
8786 result.push(c);
8787 }
8788 }
8789 }
8790 Some(c) if isset(BANGHIST)
8791 && crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst) != 0
8792 && c as u32
8793 == crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst) as u32 =>
8794 {
8795 // The reverse of quotestring's BANGHIST escape (c:Src/utils.c:6230
8796 // — `\!` is emitted for the history char): unquoting `$'a\!b'`
8797 // must strip the backslash so `${(Q)${(qqqq)v}}` round-trips.
8798 // In zsh this happens because `(Q)` re-lexes the string and
8799 // history expansion consumes the `\<bangchar>`; here the decoder
8800 // consumes it directly.
8801 consumed += 1;
8802 result.push(c);
8803 }
8804 Some(c) => {
8805 consumed += 1;
8806 result.push('\\');
8807 result.push(c);
8808 }
8809 None => {
8810 result.push('\\');
8811 }
8812 }
8813 }
8814 (result, consumed)
8815}
8816
8817/// Check if s is a prefix of t (from utils.c strpfx)
8818// Return non-zero if s is a prefix of t. // c:7345
8819/// `strpfx` — see implementation.
8820pub fn strpfx(s: &str, t: &str) -> bool {
8821 t.starts_with(s)
8822}
8823
8824/// Check if s is a suffix of t (from utils.c strsfx)
8825// Return non-zero if s is a suffix of t. // c:7345
8826/// `strsfx` — see implementation.
8827pub fn strsfx(s: &str, t: &str) -> bool {
8828 t.ends_with(s)
8829}
8830
8831/// Go up n directories (from utils.c upchdir)
8832/// Port of `upchdir(int n)` from `Src/utils.c:7356`.
8833pub fn upchdir(n: usize) -> io::Result<()> {
8834 let mut path = String::new();
8835 for i in 0..n {
8836 if i > 0 {
8837 path.push('/');
8838 }
8839 path.push_str("..");
8840 }
8841 std::env::set_current_dir(&path)?;
8842 Ok(())
8843}
8844
8845/// Port of `struct dirsav` from `Src/zsh.h:1159`.
8846///
8847/// ```c
8848/// struct dirsav {
8849/// int dirfd, level;
8850/// char *dirname;
8851/// dev_t dev;
8852/// ino_t ino;
8853/// };
8854/// ```
8855///
8856/// The previous Rust port omitted `dev` and `ino` which the
8857/// `restoredir` integrity check (utils.c:7592) reads. Adding them
8858/// so callers can verify the saved-and-restored cwd matches the
8859/// captured device + inode.
8860// `struct dirsav` lives in `dirsav` per Rule C
8861// (its C definition is `Src/zsh.h:1159`, not utils.c). The previous
8862// Rust port had a `pub struct DirSav` PascalCase duplicate of the
8863// canonical lowercase struct; deleted in favour of routing through
8864// `zsh_h::dirsav` directly.
8865
8866/// Port of `init_dirsav(Dirsav d)` from `Src/utils.c:7381`. Initialize a
8867/// `dirsav` struct to its empty/default state. C body memset's the
8868/// fields to 0 (dirfd to -1).
8869///
8870/// C signature: `void init_dirsav(Dirsav d)` where
8871/// `Dirsav = struct dirsav *`. Rust port returns the initialised
8872/// struct since callers always pair-with a fresh allocation.
8873/// WARNING: param names don't match C — Rust=() vs C=(path)
8874/// C body (3 lines):
8875/// `d->ino = d->dev = 0; d->dirname = NULL; d->dirfd = d->level = -1;`
8876/// The C `dirname = NULL` becomes `dirname: None`; Rust port prefills
8877/// dirname with current_dir for legacy callers that immediately read
8878/// it (mirrors what `setpwd()` does in C right after `init_dirsav`).
8879pub fn init_dirsav() -> dirsav {
8880 // c:7381
8881 dirsav {
8882 dirfd: -1,
8883 level: 0,
8884 dev: 0,
8885 ino: 0, // c:7383-7385
8886 dirname: std::env::current_dir()
8887 .ok()
8888 .map(|p| p.to_string_lossy().to_string()),
8889 }
8890}
8891
8892/// Port of `lchdir(char const *path, struct dirsav *d, int hard)` from `Src/utils.c:7400`.
8893///
8894/// c:7388 — "Change directory, without following symlinks." With `hard`
8895/// set, descends `path` one component at a time: `lstat`s each component
8896/// (so a symlink is never followed), `chdir`s into it, then re-`lstat`s
8897/// `.` and compares dev/ino against the component it just stat'd. A
8898/// symlink swapped under us between the `lstat` and the `chdir` is caught
8899/// by the mismatch and the saved directory is restored via `restoredir`.
8900/// `d` (when non-NULL) is filled so the caller can later restore the cwd.
8901///
8902/// Returns 0 on success, -1 on a normal descent failure, -2 if the cwd
8903/// could not even be restored afterwards.
8904///
8905/// zshrs targets macOS/Linux, so the live C arms are HAVE_LSTAT +
8906/// HAVE_FCHDIR; the no-lstat / no-fchdir fallbacks (which never compile
8907/// on our platforms) are elided. C restores `errno = err` before each
8908/// non-zero return so the caller can read the break-reason; that errno
8909/// propagation is elided here (no current caller inspects errno — the
8910/// -1/-2/0 return is the contract).
8911/// WARNING: param names match C — (path, d, hard).
8912#[cfg(unix)]
8913pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
8914 // c:7400
8915 use std::ffi::CString;
8916
8917 let mut ds = init_dirsav(); // c:7405 — local fallback `struct dirsav ds`
8918 let used_local = d.is_none(); // tracks C's `d == &ds`
8919 // c:7415-7418 — `if (!d) { init_dirsav(&ds); d = &ds; }`
8920 let d: &mut dirsav = d.unwrap_or(&mut ds);
8921
8922 let bytes = path.as_bytes();
8923 let mut level: i32;
8924
8925 // c:7419-7424 (HAVE_LSTAT arm) —
8926 // `if ((*path == '/' || !hard) && (d != &ds || hard))`
8927 if (bytes.first() == Some(&b'/') || hard == 0) && (!used_local || hard != 0) {
8928 level = -1; // c:7425
8929 } else {
8930 level = 0; // c:7431
8931 // c:7432-7435 — record dev/ino of `.` if not already captured.
8932 if d.dev == 0 && d.ino == 0 {
8933 if let Ok(meta) = fs::metadata(".") {
8934 d.dev = meta.dev(); // c:7433
8935 d.ino = meta.ino(); // c:7434
8936 }
8937 }
8938 }
8939
8940 // c:7439-7451 — soft (`!hard`) path: count components, set d->level,
8941 // and delegate to zchdir.
8942 if hard == 0 {
8943 if !used_local {
8944 // c:7443-7447 — count '/'-separated components into `level`.
8945 let mut p = 0usize;
8946 while p < bytes.len() {
8947 while p < bytes.len() && bytes[p] != b'/' {
8948 p += 1;
8949 }
8950 while p < bytes.len() && bytes[p] == b'/' {
8951 p += 1;
8952 }
8953 level += 1;
8954 }
8955 d.level = level; // c:7448
8956 }
8957 return zchdir(path); // c:7450
8958 }
8959
8960 // c:7453+ — hard path (HAVE_LSTAT + HAVE_FCHDIR).
8961 let mut close_dir = false; // c:7412
8962 // c:7455-7460 — save the starting dir via an fd if we don't have one.
8963 if d.dirfd < 0 {
8964 close_dir = true; // c:7456
8965 let dot = CString::new(".").unwrap();
8966 d.dirfd = unsafe { libc::open(dot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:7457
8967 if d.dirfd < 0
8968 && zgetdir(Some(&mut *d)).is_some()
8969 && d.dirname
8970 .as_deref()
8971 .map(|s| !s.starts_with('/'))
8972 .unwrap_or(false)
8973 {
8974 // c:7458-7459 — cwd is relative; fall back to opening "..".
8975 let dotdot = CString::new("..").unwrap();
8976 d.dirfd = unsafe { libc::open(dotdot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
8977 }
8978 }
8979
8980 // c:7462-7464 — absolute path: start the descent from root.
8981 if bytes.first() == Some(&b'/') {
8982 let root = CString::new("/").unwrap();
8983 if unsafe { libc::chdir(root.as_ptr()) } < 0 {
8984 zwarn(&format!(
8985 "failed to chdir(/): {}",
8986 io::Error::last_os_error()
8987 )); // c:7464
8988 }
8989 }
8990
8991 let dot = CString::new(".").unwrap();
8992 let mut pos = 0usize; // index into `path`, replaces C `path`/`pptr`
8993 let mut err: i32 = 0; // c:7408
8994 loop {
8995 // c:7466-7467 — skip leading slashes.
8996 while pos < bytes.len() && bytes[pos] == b'/' {
8997 pos += 1;
8998 }
8999 // c:7468-7480 — end of path reached: success.
9000 if pos >= bytes.len() {
9001 if !used_local {
9002 d.level = level; // c:7472
9003 }
9004 // c:7474-7477 — close the saved-dir fd if we opened it.
9005 if d.dirfd >= 0 && close_dir {
9006 unsafe { libc::close(d.dirfd) };
9007 d.dirfd = -1;
9008 }
9009 return 0; // c:7479
9010 }
9011 // c:7481 — find the end of this path component.
9012 let start = pos;
9013 let mut end = start + 1;
9014 while end < bytes.len() && bytes[end] != b'/' {
9015 end += 1;
9016 }
9017 // c:7482-7485 — component too long.
9018 if end - start > libc::PATH_MAX as usize {
9019 err = libc::ENAMETOOLONG;
9020 break;
9021 }
9022 // c:7486-7488 — copy the component into `buf`.
9023 let comp = &bytes[start..end];
9024 pos = end;
9025 let cbuf = match CString::new(comp) {
9026 Ok(c) => c,
9027 Err(_) => {
9028 err = libc::ENOENT; // embedded NUL — lstat would fail anyway.
9029 break;
9030 }
9031 };
9032 // c:7489-7492 — lstat the component (does NOT follow a symlink).
9033 let mut st1: libc::stat = unsafe { std::mem::zeroed() };
9034 if unsafe { libc::lstat(cbuf.as_ptr(), &mut st1) } != 0 {
9035 err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9036 break;
9037 }
9038 // c:7493-7496 — must be a real directory, not a symlink/other.
9039 if st1.st_mode as u32 & libc::S_IFMT as u32 != libc::S_IFDIR as u32 {
9040 err = libc::ENOTDIR;
9041 break;
9042 }
9043 // c:7497-7500 — chdir into it.
9044 if unsafe { libc::chdir(cbuf.as_ptr()) } != 0 {
9045 err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9046 break;
9047 }
9048 if level >= 0 {
9049 level += 1; // c:7501-7502
9050 }
9051 // c:7503-7510 — re-lstat `.` and verify the dir we landed in is
9052 // the same dev/ino we lstat'd, catching a symlink swap race.
9053 let mut st2: libc::stat = unsafe { std::mem::zeroed() };
9054 if unsafe { libc::lstat(dot.as_ptr(), &mut st2) } != 0 {
9055 err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9056 break;
9057 }
9058 if st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino {
9059 err = libc::ENOTDIR; // c:7508
9060 break;
9061 }
9062 }
9063
9064 // c:7512-7548 — descent failed; restore the saved directory.
9065 if restoredir(&mut *d) != 0 {
9066 let restoreerr = io::Error::last_os_error(); // c:7513
9067 // c:7519-7532 — restore failed too; force cwd to $HOME then "/".
9068 let mut reached = false;
9069 for i in 0..2 {
9070 // c:7521-7527 — destination: $HOME on pass 0, "/" on pass 1.
9071 let cdest = if i != 0 {
9072 "/".to_string()
9073 } else {
9074 match getsparam("HOME") {
9075 Some(h) => h, // c:7526
9076 None => continue, // c:7525 `if (!home) continue;`
9077 }
9078 };
9079 setsparam("PWD", &cdest); // c:7528-7529 `zsfree(pwd); pwd = ztrdup(cdest);`
9080 if let Ok(cd) = CString::new(cdest.as_bytes()) {
9081 if unsafe { libc::chdir(cd.as_ptr()) } == 0 {
9082 // c:7530
9083 reached = true;
9084 break;
9085 }
9086 }
9087 }
9088 if !reached {
9089 // c:7533-7534 — couldn't even reach "/".
9090 zerr(&format!(
9091 "lost current directory, failed to cd to /: {}",
9092 io::Error::last_os_error()
9093 ));
9094 } else {
9095 // c:7535-7537
9096 let pwd = getsparam("PWD").unwrap_or_default();
9097 zerr(&format!(
9098 "lost current directory: {}: changed to `{}'",
9099 restoreerr, pwd
9100 ));
9101 }
9102 // c:7540-7545 — close the saved-dir fd if we opened it.
9103 if d.dirfd >= 0 && close_dir {
9104 unsafe { libc::close(d.dirfd) };
9105 d.dirfd = -1;
9106 }
9107 let _ = err; // c:7546 `errno = err;` propagation elided (see doc).
9108 return -2; // c:7547
9109 }
9110 // c:7549-7558 — descent failed but the saved directory was restored.
9111 if d.dirfd >= 0 && close_dir {
9112 unsafe { libc::close(d.dirfd) };
9113 d.dirfd = -1;
9114 }
9115 let _ = err; // c:7557 `errno = err;` propagation elided (see doc).
9116 -1 // c:7558
9117}
9118
9119/// Non-unix stub: lchdir's symlink-safe descent is built on POSIX
9120/// `lstat`/`fchdir`/`chdir`, which have no Windows equivalent here.
9121#[cfg(not(unix))]
9122pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
9123 let _ = (path, d, hard);
9124 -1
9125}
9126
9127/// Port of `restoredir(struct dirsav *d)` from `Src/utils.c:7565`.
9128///
9129/// ```c
9130/// int restoredir(struct dirsav *d) {
9131/// if (d->dirname && *d->dirname == '/')
9132/// return chdir(d->dirname);
9133/// if (d->dirfd >= 0) {
9134/// if (!fchdir(d->dirfd)) {
9135/// if (!d->dirname) return 0;
9136/// else if (chdir(d->dirname)) {
9137/// close(d->dirfd); d->dirfd = -1; err = -2;
9138/// }
9139/// } else {
9140/// close(d->dirfd); d->dirfd = err = -1;
9141/// }
9142/// } else if (d->level > 0)
9143/// err = upchdir(d->level);
9144/// else if (d->level < 0) err = -1;
9145/// // dev/ino integrity check ...
9146/// }
9147/// ```
9148///
9149/// Restore the cwd captured in `d`. Absolute `dirname` short-
9150/// circuits to `chdir`. Otherwise tries `fchdir(dirfd)` (when
9151/// supported) then falls through to `upchdir(level)` for the
9152/// nested-fn-exit case. Returns 0 on success, non-zero on failure
9153/// (matching C's int return).
9154///
9155/// Signature change: previous Rust port took `saved: &str` and
9156/// returned `bool` — different shape from C, missed the dirfd /
9157/// level / dev / ino fields entirely.
9158pub fn restoredir(d: &mut dirsav) -> i32 {
9159 // C: if (d->dirname && *d->dirname == '/') return chdir(d->dirname);
9160 if let Some(name) = d.dirname.as_ref() {
9161 if name.starts_with('/') {
9162 return match std::env::set_current_dir(name) {
9163 Ok(_) => 0,
9164 Err(_) => -1,
9165 };
9166 }
9167 }
9168 let mut err: i32 = 0;
9169 // C: HAVE_FCHDIR path — try fchdir(dirfd) first.
9170 #[cfg(unix)]
9171 if d.dirfd >= 0 {
9172 let rc = unsafe { libc::fchdir(d.dirfd) };
9173 if rc == 0 {
9174 if d.dirname.is_none() {
9175 return 0;
9176 }
9177 let name = d.dirname.as_ref().unwrap();
9178 if std::env::set_current_dir(name).is_err() {
9179 unsafe { libc::close(d.dirfd) };
9180 d.dirfd = -1;
9181 err = -2;
9182 }
9183 } else {
9184 unsafe { libc::close(d.dirfd) };
9185 d.dirfd = -1;
9186 err = -1;
9187 }
9188 } else if d.level > 0 {
9189 // C: err = upchdir(d->level);
9190 let _ = upchdir(d.level as usize);
9191 } else if d.level < 0 {
9192 err = -1;
9193 }
9194 // C: dev/ino integrity check after the chdir/fchdir.
9195 if (d.dev != 0 || d.ino != 0) && err == 0 {
9196 if let Ok(meta) = fs::metadata(".") {
9197 if meta.ino() != d.ino || meta.dev() != d.dev {
9198 err = -1;
9199 }
9200 } else {
9201 err = -1;
9202 }
9203 }
9204 err
9205}
9206
9207/// Port of `privasserted()` from `Src/utils.c:7607`.
9208///
9209/// "Check whether the shell is running with privileges in effect.
9210/// This is the case if EITHER the euid is zero, OR (if the system
9211/// supports POSIX.1e (POSIX.6) capability sets) the process'
9212/// Effective or Inheritable capability sets are non-empty."
9213///
9214/// ```c
9215/// if (!geteuid()) return 1;
9216/// #ifdef HAVE_CAP_GET_PROC
9217/// cap_t caps = cap_get_proc();
9218/// if (caps) {
9219/// cap_flag_value_t val;
9220/// for (cap_value_t cap = 0;
9221/// !cap_get_flag(caps, cap, CAP_EFFECTIVE, &val); cap++)
9222/// if (val && cap != CAP_WAKE_ALARM) {
9223/// cap_free(caps);
9224/// return 1;
9225/// }
9226/// }
9227/// cap_free(caps);
9228/// #endif
9229/// return 0;
9230/// ```
9231///
9232/// The previous Rust port checked `getuid() != geteuid()` which is
9233/// the SUID-binary detection, not the "running with privileges"
9234/// check. The capability-set inspection requires libcap (gated
9235/// behind the `libcap` feature in `crate::ported::modules::cap`);
9236/// without it, only the euid==0 path is exercised — same as the
9237/// C `#else` arm when HAVE_CAP_GET_PROC isn't defined.
9238pub fn privasserted() -> bool {
9239 #[cfg(unix)]
9240 {
9241 if unsafe { libc::geteuid() } == 0 {
9242 return true;
9243 }
9244 }
9245 // POSIX.1e capabilities check (HAVE_CAP_GET_PROC) — only
9246 // active on Linux when zshrs is built with `--features
9247 // libcap`. The cap module's `cap_get_proc` returns Ok(text)
9248 // when the process has any capability set; we treat any
9249 // non-default-empty result as "privileges asserted".
9250 #[cfg(all(target_os = "linux", feature = "libcap"))]
9251 {
9252 // Pending: walk the capability set with cap_get_flag and
9253 // skip CAP_WAKE_ALARM as the C source does. The cap.rs
9254 // port doesn't yet expose the flag-iteration FFI; until
9255 // it does, conservative-true on any non-empty cap text.
9256 if let Ok(text) = crate::ported::modules::cap::cap_get_proc() {
9257 // Empty / default-empty cap text = no privileges.
9258 if !text.is_empty() && text != "=" {
9259 return true;
9260 }
9261 }
9262 }
9263 false
9264}
9265
9266/// Port of `mode_to_octal(mode_t mode)` from `Src/utils.c:7634`.
9267///
9268/// Convert a `mode_t` into the equivalent canonical octal value
9269/// by testing each `S_I*` flag explicitly and OR-ing the matching
9270/// octal bit. This is NOT just `mode & 07777` — on systems where
9271/// the libc `S_IRUSR`/etc. constants don't match the canonical
9272/// values (e.g. when zsh runs against a non-POSIX libc),
9273/// `mode & 07777` returns the libc representation. The C version
9274/// translates to canonical bits explicitly so callers get a stable
9275/// portable result.
9276///
9277/// ```c
9278/// int mode_to_octal(mode_t mode)
9279/// {
9280/// int m = 0;
9281/// if (mode & S_ISUID) m |= 04000;
9282/// ... (12 bit-by-bit mappings)
9283/// return m;
9284/// }
9285/// ```
9286pub fn mode_to_octal(mode: u32) -> i32 {
9287 // c:7634
9288 #[cfg(not(unix))]
9289 {
9290 // No POSIX permission bits on non-Unix; fall back to canonical
9291 // octal-bit layout via the same mask values C uses on POSIX.
9292 let mut o: i32 = 0;
9293 if mode & 0o4000 != 0 {
9294 o |= 0o4000;
9295 } // c:7638-7639
9296 if mode & 0o2000 != 0 {
9297 o |= 0o2000;
9298 } // c:7640-7641
9299 if mode & 0o1000 != 0 {
9300 o |= 0o1000;
9301 } // c:7642-7643
9302 if mode & 0o0400 != 0 {
9303 o |= 0o0400;
9304 } // c:7644-7645
9305 if mode & 0o0200 != 0 {
9306 o |= 0o0200;
9307 } // c:7646-7647
9308 if mode & 0o0100 != 0 {
9309 o |= 0o0100;
9310 } // c:7648-7649
9311 if mode & 0o0040 != 0 {
9312 o |= 0o0040;
9313 } // c:7650-7651
9314 if mode & 0o0020 != 0 {
9315 o |= 0o0020;
9316 } // c:7652-7653
9317 if mode & 0o0010 != 0 {
9318 o |= 0o0010;
9319 } // c:7654-7655
9320 if mode & 0o0004 != 0 {
9321 o |= 0o0004;
9322 } // c:7656-7657
9323 if mode & 0o0002 != 0 {
9324 o |= 0o0002;
9325 } // c:7658-7659
9326 if mode & 0o0001 != 0 {
9327 o |= 0o0001;
9328 } // c:7660-7661
9329 return o; // c:7662
9330 }
9331 #[cfg(unix)]
9332 {
9333 // c:7636 — int m = 0;
9334 let mut m: i32 = 0;
9335 // c:7638-7661 — 12 bit-by-bit mappings from libc S_I* → canonical octal.
9336 if mode & S_ISUID as u32 != 0 {
9337 m |= 0o4000;
9338 } // c:7638-7639
9339 if mode & S_ISGID as u32 != 0 {
9340 m |= 0o2000;
9341 } // c:7640-7641
9342 if mode & S_ISVTX as u32 != 0 {
9343 m |= 0o1000;
9344 } // c:7642-7643
9345 if mode & S_IRUSR as u32 != 0 {
9346 m |= 0o0400;
9347 } // c:7644-7645
9348 if mode & S_IWUSR as u32 != 0 {
9349 m |= 0o0200;
9350 } // c:7646-7647
9351 if mode & S_IXUSR as u32 != 0 {
9352 m |= 0o0100;
9353 } // c:7648-7649
9354 if mode & S_IRGRP as u32 != 0 {
9355 m |= 0o0040;
9356 } // c:7650-7651
9357 if mode & S_IWGRP as u32 != 0 {
9358 m |= 0o0020;
9359 } // c:7652-7653
9360 if mode & S_IXGRP as u32 != 0 {
9361 m |= 0o0010;
9362 } // c:7654-7655
9363 if mode & S_IROTH as u32 != 0 {
9364 m |= 0o0004;
9365 } // c:7656-7657
9366 if mode & S_IWOTH as u32 != 0 {
9367 m |= 0o0002;
9368 } // c:7658-7659
9369 if mode & S_IXOTH as u32 != 0 {
9370 m |= 0o0001;
9371 } // c:7660-7661
9372 m // c:7662
9373 }
9374}
9375
9376/// Port of `mailstat(char *path, struct stat *st)` from `Src/utils.c:7685`.
9377///
9378/// C signature: `int mailstat(char *path, struct stat *st)`.
9379/// Writes maildir aggregate stats into `*st` (or the native stat for
9380/// non-directory paths) and returns the underlying `stat(2)` return
9381/// (0 on success, -1 on error — matches C's `i = stat(path, st);
9382/// return i;`).
9383///
9384/// When `path` is a maildir directory (containing `cur/`, `tmp/`,
9385/// `new/` subdirs), walks `new/` + `cur/` and aggregates into `*st`:
9386/// nlink=1, S_IFDIR→S_IFREG, size=Σ message bytes,
9387/// blocks=Σ messages, atime=newest, mtime=newest. When the path is
9388/// a plain file, leaves the native `stat(2)` result in `*st`.
9389pub fn mailstat(path: &str, st: &mut libc::stat) -> i32 {
9390 // c:7685
9391 let c_path = match CString::new(path) {
9392 Ok(c) => c,
9393 Err(_) => return -1,
9394 };
9395 // C: if ((i = stat(path, st)) != 0 || !S_ISDIR(st->st_mode)) return i;
9396 let i = unsafe { libc::stat(c_path.as_ptr(), st as *mut _) }; // c:7693
9397 if i != 0 || (st.st_mode & libc::S_IFMT) != libc::S_IFDIR {
9398 // c:7693
9399 return i; // c:7693
9400 }
9401 // C 7700-7706: nlink=1, S_IFDIR → S_IFREG, zero size/blocks.
9402 st.st_nlink = 1; // c:7701
9403 st.st_mode &= !libc::S_IFDIR; // c:7702
9404 st.st_mode |= libc::S_IFREG; // c:7703
9405 st.st_size = 0; // c:7704
9406 st.st_blocks = 0; // c:7705
9407 // C 7707-7712: stat(path/cur). If absent or not a dir, return 0
9408 // with the partial out (just the IFREG-coerced root).
9409 let cur_path = match CString::new(format!("{}/cur", path)) {
9410 Ok(c) => c,
9411 Err(_) => return 0,
9412 };
9413 let mut sub: libc::stat = unsafe { std::mem::zeroed() };
9414 if unsafe { libc::stat(cur_path.as_ptr(), &mut sub) } != 0 // c:7708
9415 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9416 // c:7708
9417 {
9418 return 0; // c:7710
9419 }
9420 st.st_atime = sub.st_atime; // c:7712
9421 // C 7715-7722: stat(path/tmp).
9422 let tmp_path = match CString::new(format!("{}/tmp", path)) {
9423 Ok(c) => c,
9424 Err(_) => return 0,
9425 };
9426 if unsafe { libc::stat(tmp_path.as_ptr(), &mut sub) } != 0 // c:7716
9427 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9428 // c:7716
9429 {
9430 return 0; // c:7718
9431 }
9432 st.st_mtime = sub.st_mtime; // c:7720
9433 // C 7724-7730: stat(path/new). C overwrites mtime with new/'s mtime.
9434 let new_path = match CString::new(format!("{}/new", path)) {
9435 Ok(c) => c,
9436 Err(_) => return 0,
9437 };
9438 if unsafe { libc::stat(new_path.as_ptr(), &mut sub) } != 0 // c:7724
9439 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9440 // c:7724
9441 {
9442 return 0; // c:7726
9443 }
9444 st.st_mtime = sub.st_mtime; // c:7728
9445 // C 7749-7778: walk new/ and cur/, sum size + blocks, track newest
9446 // atime / mtime.
9447 let mut atime: libc::time_t = 0; // c:7748
9448 let mut mtime: libc::time_t = 0; // c:7748
9449 for sub_name in ["new", "cur"] {
9450 let dir = format!("{}/{}", path, sub_name);
9451 let entries = match fs::read_dir(&dir) {
9452 Ok(e) => e,
9453 Err(_) => return 0,
9454 };
9455 for entry in entries.flatten() {
9456 let name = entry.file_name();
9457 let name_bytes = name.as_encoded_bytes();
9458 // C: if (fn->d_name[0] == '.') continue;
9459 if name_bytes.first() == Some(&b'.') {
9460 // c:7758
9461 continue;
9462 }
9463 let entry_path = match CString::new(entry.path().to_string_lossy().as_bytes()) {
9464 Ok(c) => c,
9465 Err(_) => continue,
9466 };
9467 let mut entry_st: libc::stat = unsafe { std::mem::zeroed() };
9468 if unsafe { libc::stat(entry_path.as_ptr(), &mut entry_st) } != 0 {
9469 // c:7762
9470 continue;
9471 }
9472 st.st_size += entry_st.st_size; // c:7766
9473 st.st_blocks += 1; // c:7767
9474 // C: if (atime != mtime && atime > atime_max) atime_max = atime;
9475 if entry_st.st_atime != entry_st.st_mtime && entry_st.st_atime > atime {
9476 // c:7769
9477 atime = entry_st.st_atime;
9478 }
9479 if entry_st.st_mtime > mtime {
9480 // c:7771
9481 mtime = entry_st.st_mtime;
9482 }
9483 }
9484 }
9485 // C 7783-7784: if (atime) st_ret.st_atime = atime;
9486 if atime != 0 {
9487 // c:7783
9488 st.st_atime = atime;
9489 }
9490 if mtime != 0 {
9491 // c:7785
9492 st.st_mtime = mtime;
9493 }
9494 0 // c:7787
9495}
9496
9497/// Script name for error messages
9498pub static mut SCRIPT_NAME: Option<String> = None;
9499/// Script filename
9500pub static mut SCRIPT_FILENAME: Option<String> = None;
9501
9502/// Print a fatal error to stderr.
9503/// Port of `zerr(VA_ALIST1(const char *fmt))` from Src/utils.c:172. C source sets `errflag`
9504/// after emitting `<prefix>: <msg>\n` so the running script aborts
9505/// at the next safe point. The Rust port currently just prints —
9506// =====================================================================
9507// Module-static state for the zerr/zwarn/zerrnam/zwarnnam family —
9508// port of the file-statics in Src/init.c + Src/exec.c that
9509// `zwarning()` (utils.c:142) reads to build the error prefix.
9510// =====================================================================
9511
9512// name of script being sourced // c:33
9513/// Port of `char *scriptname` from `Src/init.c`. Set when `source`
9514/// is reading a script; cleared on return. Used by `zwarning()`
9515/// (utils.c:147) as the diagnostic prefix.
9516static SCRIPTNAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9517
9518/// Port of `char *argzero` from `Src/init.c`. The shell's argv[0].
9519/// Used by `zwarning()` (utils.c:147) as the fallback diagnostic
9520/// prefix when scriptname is unset.
9521static ARGZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9522
9523/// Port of `char *scriptfilename` from `Src/init.c` (listed in
9524/// `zsh.export:356`). The FILE that the current code was PARSED
9525/// from. Distinct from `scriptname` (which doshfunc overrides on
9526/// function entry at c:5903). `scriptfilename` stays at the outer
9527/// file across function calls; PS4's `%x` reads it at
9528/// `Src/prompt.c:937`.
9529///
9530/// Set at init.c:479 (`-c` mode → `scriptname = scriptfilename
9531/// = "zsh"`), init.c:1367 (`source` enters the named file),
9532/// init.c:1558+1592+1667 (save/install/restore around `.` /
9533/// `source` bin_dot dispatch).
9534static SCRIPTFILENAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9535
9536/// Port of `char *posixzero` from `Src/params.c:76`. The original
9537/// argv[0] preserved unchanged by later mutations. Used by
9538/// `argzerogetfn` for `$0` under `isset(POSIXARGZERO)`.
9539static POSIXZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9540
9541// error flag: bits from enum errflag_bits // c:124
9542/// Port of `int errflag` from `Src/init.c`. Tracks whether an
9543/// error has been raised (`ERRFLAG_ERROR = 1`) or break/return
9544/// is in flight (`ERRFLAG_INT = 2`).
9545///
9546/// Direct global access: `errflag.load(Ordering::Relaxed)` reads
9547/// the current value, `errflag.fetch_or(ERRFLAG_ERROR, …)` matches
9548/// C's `errflag |= ERRFLAG_ERROR`, `errflag.store(0, …)` matches
9549/// C's `errflag = 0`.
9550#[allow(non_upper_case_globals)]
9551pub static errflag: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9552
9553/// Port of `int noerrs` from `Src/init.c`. Counter — when `> 0`,
9554/// suppresses error printing. `noerrs >= 2` also suppresses the
9555/// `errflag` set inside `zerr`/`zerrnam`.
9556static NOERRS: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
9557
9558/// Port of `int locallevel` from `Src/init.c`. Function-call depth
9559/// (0 = top-level, 1+ = inside a fn). `zwarning()` checks this in
9560/// the script-prefix path (utils.c:150).
9561// `LOCALLEVEL` removed — see `locallevel_lock` removal comment
9562// below. Canonical static lives in `LOCALLEVEL`
9563// (port of params.c:54).
9564
9565/// Port of `int lineno` from `Src/init.c`. Current line number;
9566/// `zerrmsg()` includes it in the diagnostic when locallevel > 0
9567/// or shinstdin is unset (utils.c:301).
9568static LINENO: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
9569
9570/// Port of the `isset(SHINSTDIN)` check from utils.c:150. C reads
9571/// the SHINSTDIN option directly; the Rust port caches it here so
9572/// callers don't pull in the whole option-table for every error.
9573static SHINSTDIN_OPT: std::sync::OnceLock<Mutex<bool>> = std::sync::OnceLock::new();
9574
9575/// `ERRFLAG_ERROR` from `Src/zsh.h`. Set on `zerr`/`zerrnam` to
9576/// signal a fatal error has occurred.
9577pub const ERRFLAG_ERROR: i32 = 1;
9578
9579/// Port of `mod_export int incompfunc` from `Src/utils.c:46`.
9580/// Set non-zero while a `comp*` builtin is dispatching from inside
9581/// a user-defined completion function — guards `comparguments` /
9582/// `compset` / `compadd` / `compdescribe` / `comptags` / `compvalues`
9583/// / `compfiles` / `compgroups` / `compquote` against being called
9584/// outside the `compfunc` shfunc context (each builtin checks
9585/// `INCOMPFUNC` early and emits "can only be called from completion
9586/// function" when zero).
9587pub static INCOMPFUNC: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:46
9588
9589/// Port of `mod_export int resetneeded` from `Src/utils.c:1821`.
9590/// Set when the editor needs a redraw — incremented by widgets +
9591/// signal handlers (e.g. SIGWINCH); the next prompt loop tick clears
9592/// it after running zrefresh.
9593pub static RESETNEEDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1821
9594
9595/// Port of `mod_export int winchanged` from `Src/utils.c:1827`.
9596/// Set by the SIGWINCH handler — the next refresh re-queries the
9597/// terminal size and re-renders, then clears the flag.
9598pub static WINCHANGED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1827
9599
9600/// Port of C's `GETKEY_*` flag enumeration from `Src/zsh.h:3143`.
9601/// Passed to `getkeystring_with` to alter escape interpretation:
9602/// - GETKEY_OCTAL_ESC: `\NNN` octal escapes are interpreted (1<<0)
9603/// - GETKEY_EMACS: unknown `\<char>` drops the backslash (1<<1)
9604/// - GETKEY_BACKSLASH_C: `\c` truncates the result (1<<3)
9605pub const GETKEY_OCTAL_ESC: u32 = 1 << 0; // c:zsh.h:3143
9606/// `GETKEY_EMACS` constant.
9607pub const GETKEY_EMACS: u32 = 1 << 1; // c:zsh.h:3150
9608/// `GETKEY_CTRL` constant.
9609pub const GETKEY_CTRL: u32 = 1 << 2; // c:zsh.h:3152
9610/// `GETKEY_BACKSLASH_C` constant.
9611pub const GETKEY_BACKSLASH_C: u32 = 1 << 3; // c:zsh.h:3154
9612
9613/// `GETKEYS_PRINT = GETKEY_OCTAL_ESC | GETKEY_BACKSLASH_C |
9614/// GETKEY_EMACS` per Src/zsh.h:3185 — the flag set `bin_print` uses
9615/// when interpreting escapes (with EMACS: unknown `\<c>` → `<c>`).
9616pub const GETKEYS_PRINT: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_BACKSLASH_C; // c:zsh.h:3185
9617
9618/// `GETKEYS_ECHO = GETKEY_BACKSLASH_C` per Src/zsh.h:3178 — the flag
9619/// set `echo` uses (and `print -e`). NOT GETKEY_EMACS: unknown
9620/// `\<c>` is preserved as literal `\<c>`. NOT GETKEY_OCTAL_ESC:
9621/// `\NNN` is NOT interpreted as octal. Only `\a \b \e \E \f \n \r
9622/// \t \v \\` (plus `\c` truncation) are handled.
9623///
9624/// Per builtin.c:4754-4760 — `bin_print` selects this for BIN_ECHO
9625/// or when `-e` is set; otherwise GETKEYS_PRINT.
9626pub const GETKEYS_ECHO: u32 = GETKEY_BACKSLASH_C; // c:zsh.h:3178
9627
9628/// `GETKEYS_BINDKEY = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL`
9629/// per Src/zsh.h:3187 — the flag set `print -b` uses. Like GETKEYS_PRINT
9630/// it has EMACS (so `\C-`/`\M-` backslash escapes are processed and
9631/// unknown `\<c>` collapses to `<c>`) but adds GETKEY_CTRL (enabling the
9632/// `^X` caret notation) and drops GETKEY_BACKSLASH_C (no `\c` truncation).
9633pub const GETKEYS_BINDKEY: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL; // c:zsh.h:3187
9634
9635/// Static `int ep` from Src/utils.c:4775 — sticky flag suppressing the
9636/// `can't set tty pgrp` warning after the first failure.
9637static ATTACHTTY_EP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:4775
9638
9639// =====================================================================
9640// !!! WARNING: RUST-ONLY HELPERS — NO DIRECT C COUNTERPART !!!
9641// =====================================================================
9642//
9643// These three ported (`fdtable_lock`, `fdtable_get`, `fdtable_set`) DO
9644// NOT EXIST as functions in `Src/utils.c`. The C source declares
9645// `fdtable` as a bare `unsigned char *` global (Src/utils.c:~63) and
9646// every call site reads/writes it as a direct array index:
9647//
9648// if (fdtable[fd] != FDT_UNUSED) ...
9649// fdtable[fd] = FDT_EXTERNAL;
9650//
9651// Rust can't hand out raw mutable indexes through a `Mutex<Vec<u8>>`
9652// safely without a borrow scope, so the Rust port wraps the same
9653// slot access in three `pub fn`s. Each call site to these helpers
9654// corresponds 1:1 to a `fdtable[fd] = X` or `fdtable[fd] != X`
9655// statement in the C source — they are NOT new policy, they only
9656// adapt the storage shape from `unsigned char *` to a growable
9657// `Mutex<Vec<i32>>`.
9658//
9659// Also adapts `growfdtable` (Src/utils.c:1965) by lazily growing the
9660// Vec inside `fdtable_set` instead of a separate `growfdtable`
9661// call — the C source calls `growfdtable(fd)` immediately before
9662// every `fdtable[fd] = X` write anyway.
9663//
9664// !!! Do NOT use these for any state that the C source does not
9665// already store in `fdtable[]`. Adding a new "fd kind" here is a
9666// scope expansion, not a port. !!!
9667// =====================================================================
9668
9669static FDTABLE: std::sync::OnceLock<Mutex<Vec<i32>>> = std::sync::OnceLock::new();
9670
9671/// Port of `int max_zsh_fd` global from `Src/exec.c:201`.
9672/// "The highest fd we know zsh is using" — bumped by `fdtable_set`
9673/// callers, shrunk by `zclose` when trailing slots fall to UNUSED.
9674pub static MAX_ZSH_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
9675
9676/// Port of `int fdtable_flocks` global from `Src/exec.c:204`.
9677/// Count of `FDT_FLOCK`-tagged fds; consulted by `closem()` to
9678/// decide whether to skip the flock-fd sweep.
9679pub static FDTABLE_FLOCKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9680
9681/// Setter for `scriptname`. Called from `bin_dot` / `source`
9682/// when entering a script.
9683pub fn set_scriptname(name: Option<String>) {
9684 *scriptname_lock().lock().unwrap() = name;
9685}
9686
9687/// Read `scriptname` — direct mirror of the C file-scope
9688/// `char *scriptname;` at `Src/utils.c:36`. Exposed for the
9689/// prompt expander (`%N`), `bin_dot`, and ZLE source tracking.
9690pub fn scriptname_get() -> Option<String> {
9691 scriptname_lock().lock().unwrap().clone()
9692}
9693
9694/// Setter for `scriptfilename` — direct mirror of the C
9695/// `scriptfilename` global at `Src/init.c` (zsh.export:356).
9696/// Called from `bin_dot` at init.c:1592, restored at c:1667.
9697pub fn set_scriptfilename(name: Option<String>) {
9698 *scriptfilename_lock().lock().unwrap() = name;
9699}
9700
9701/// Read `scriptfilename` — direct mirror of the C global.
9702/// Used by PS4's `%x` (`Src/prompt.c:937`) and the function-call
9703/// frame at `Src/exec.c:1613` (`fstack.filename = scriptfilename`).
9704pub fn scriptfilename_get() -> Option<String> {
9705 scriptfilename_lock().lock().unwrap().clone()
9706}
9707
9708/// Read `locallevel` — function nesting depth. Routes through the
9709/// canonical `LOCALLEVEL` static (port of
9710/// `Src/params.c:54`). The prior Rust port stored a duplicate
9711/// `Mutex<i32>` here that split state from params.rs; both copies
9712/// were never kept in sync.
9713pub fn locallevel() -> i32 {
9714 LOCALLEVEL.load(Ordering::Relaxed)
9715}
9716
9717/// Bump `locallevel` (called by `startparamscope`).
9718pub fn inc_locallevel() {
9719 LOCALLEVEL.fetch_add(1, Ordering::Relaxed);
9720}
9721
9722/// Decrement `locallevel` (called by `endparamscope`).
9723pub fn dec_locallevel() {
9724 LOCALLEVEL.fetch_sub(1, Ordering::Relaxed);
9725}
9726
9727/// Setter for `argzero`. Called once at shell init from `parseargs`.
9728pub fn set_argzero(name: Option<String>) {
9729 // c:271 (init.c) — `argv0 = argzero = posixzero = *argv++;`. At
9730 // shell init both argzero and posixzero share the same source.
9731 // posixzero is preserved unchanged after later mutations (exec -a,
9732 // function frames) — argzero is what gets rewritten. If posixzero
9733 // hasn't been independently set yet, mirror argzero so the
9734 // POSIXARGZERO branch in `argzerogetfn` returns something useful.
9735 let mut posix_lock = posixzero_lock().lock().unwrap();
9736 if posix_lock.is_none() {
9737 *posix_lock = name.clone();
9738 }
9739 drop(posix_lock);
9740 *argzero_lock().lock().unwrap() = name;
9741}
9742
9743/// Read `argzero`. Used by `argzerogetfn` for `$0`.
9744pub fn argzero() -> Option<String> {
9745 argzero_lock().lock().unwrap().clone()
9746}
9747
9748/// Port of `char *posixzero` from `Src/params.c:76`. The original
9749/// `argv[0]` preserved from shell startup, unchanged by later `exec -a`
9750/// or function-call rewrites. C's `argzerogetfn` (params.c:4958)
9751/// returns this instead of `argzero` when `isset(POSIXARGZERO)`.
9752///
9753/// Set once at init via `set_posixzero` (called from the init path
9754/// in C at init.c:271/297/321). `set_argzero` mirrors the value here
9755/// on first call so the POSIXARGZERO branch has something to read
9756/// even if the init path hasn't run yet.
9757pub fn set_posixzero(name: Option<String>) {
9758 *posixzero_lock().lock().unwrap() = name;
9759}
9760
9761/// Read `posixzero`. C: `posixzero` (params.c:76 global). Used by
9762/// `argzerogetfn` when `isset(POSIXARGZERO)`.
9763pub fn posixzero() -> Option<String> {
9764 posixzero_lock().lock().unwrap().clone()
9765}
9766
9767/// Setter for `noerrs`. Increment to suppress error output;
9768/// decrement to restore.
9769pub fn set_noerrs(v: i32) {
9770 *noerrs_lock().lock().unwrap() = v;
9771}
9772
9773/// Setter for `locallevel`. Called by function-call entry/exit.
9774pub fn set_locallevel(v: i32) {
9775 LOCALLEVEL.store(v, Ordering::Relaxed);
9776}
9777
9778/// Setter for `lineno`. Called by the parser as it advances.
9779pub fn set_lineno(v: i32) {
9780 *lineno_lock().lock().unwrap() = v;
9781}
9782
9783/// Setter for the cached SHINSTDIN flag. Called by the option
9784/// machinery whenever `setopt shinstdin` / `unsetopt shinstdin`
9785/// fires.
9786pub fn set_shinstdin(v: bool) {
9787 *shinstdin_lock().lock().unwrap() = v;
9788}
9789
9790/// Check if string is a valid identifier
9791// `isident` DELETED — fake duplicate (cited `c:params.c:1288` from
9792// utils.rs, location mismatch). Canonical port is
9793// `isident` at `params.rs:2056`, which
9794// correctly handles namespace prefix (`ns.var`) per the real C
9795// body — the utils.rs copy dropped that arm. Callers now route
9796// through `isident` directly.
9797
9798// QuoteType enum + impl deleted — the canonical quote-type values are
9799// the bare `QT_*: i32` constants at `zsh_h.rs:175` (port of anonymous
9800// `enum { QT_NONE, QT_BACKSLASH, … }` from `Src/zsh.h:253-298`).
9801// `quotestring()` takes `quote_type: i32` matching C's signature.
9802
9803/// Map a `(q)` flag count to a `QT_*` value.
9804/// Port of the q-flag dispatch in `Src/subst.c` `paramsubst()` —
9805/// `(q)`=`QT_BACKSLASH`, `(qq)`=`QT_SINGLE`, `(qqq)`=`QT_DOUBLE`,
9806/// `(qqqq+)`=`QT_DOLLARS`.
9807pub fn qflag_quotetype(count: u32) -> i32 {
9808 match count {
9809 0 => QT_NONE,
9810 1 => QT_BACKSLASH,
9811 2 => QT_SINGLE,
9812 3 => QT_DOUBLE,
9813 _ => QT_DOLLARS,
9814 }
9815}
9816
9817/// Port of `ispecial()` macro from `Src/ztype.h:59` — `zistype(X,
9818/// ISPECIAL)`. C populates the ISPECIAL typtab bit at
9819/// `Src/utils.c:4253-4262`:
9820/// - Every byte in `SPECCHARS` (`Src/zsh.h:228` =
9821/// `"#$^*()=|{}[]\`<>?~;&\\n\\t \\\\\\'\\""`)
9822/// - `,` when `typtab_flags & ZTF_SP_COMMA` (set by
9823/// `makecommaspecial(1)` per c:4271)
9824/// - `bangchar` (default `!`) when `BANGHIST` is set AND
9825/// `ZTF_INTERACT` is set (interactive shell)
9826///
9827/// The Rust port enumerates `SPECCHARS` directly because the
9828/// typtab is not lazily initialised — production `ispecial` calls
9829/// happen after `init_typtab` runs at shell startup, but
9830/// `quotestring` is unit-tested in isolation where the typtab is
9831/// all-zero. Match the C SPECCHARS set byte-for-byte; the option-
9832/// driven augments (`,` and bangchar) are still NOT wired through
9833/// (a known gap — see the typtab-routing TODO at the param level).
9834fn ispecial(c: char) -> bool {
9835 // c:59 (Src/ztype.h)
9836 // c:228 (Src/zsh.h) `SPECCHARS` — exact byte set from the C
9837 // string literal. `^` and `{`/`}` were already present; `!`
9838 // is INTENTIONALLY OMITTED here because C only ISPECIAL-tags
9839 // bangchar under BANGHIST + interactive. Including `!`
9840 // unconditionally diverges from C for non-interactive scripts
9841 // and unbang'd input.
9842 matches!(
9843 c,
9844 '#' | '$' | '^' | '*' | '(' | ')' | '=' // c:228 first half
9845 | '|' | '{' | '}' | '[' | ']' | '`' // c:228 mid
9846 | '<' | '>' | '?' | '~' | ';' | '&' // c:228 specials
9847 | '\n' | '\t' | ' ' | '\\' | '\'' | '"' // c:228 whitespace/backslash/quotes
9848 )
9849}
9850
9851/// Convert integer to string with specified base — re-export of
9852/// the canonical port at `convbase_param`.
9853///
9854/// The previous Rust port at this file was a SECOND, divergent
9855/// implementation of `convbase`:
9856/// - Used LOWERCASE digit chars (`"0123456789abc..."`) for
9857/// bases 11..36, while C uses UPPERCASE (`(dig - 10) + 'A'`
9858/// at Src/params.c:5621).
9859/// - Skipped CBASES + OCTALZEROES prefix handling at c:5598-5604
9860/// (`0x`/`0` prefixes when option-gated).
9861/// - Returned wrong format for bases not handled by the explicit
9862/// match arms.
9863///
9864/// The canonical port lives at `params.rs::convbase_ptr` (c:5588
9865/// faithful body) + `params.rs::convbase` (c:5634 wrapper). Route
9866/// through there so utils.rs / params.rs agree byte-for-byte and
9867/// no divergent duplicate stays alive.
9868pub fn convbase(val: i64, base: u32) -> String {
9869 // c:5634 (Src/params.c)
9870 convbase_param(val, base)
9871}
9872
9873// `checkglobqual` DELETED — fake bracket-depth scanner cited
9874// `c:glob.c:1158` from utils.rs. Canonical port is
9875// `crate::ported::glob::checkglobqual(str, sl, nobareglob, sp)` at
9876// `glob.rs:813`, which matches the real C signature
9877// `(char *str, int sl, int nobareglob, char **sp)`. The utils.rs
9878// fake reduced the signature to a single bool and discarded the
9879// `sp` glob-qualifier-start out-pointer that callers need. Zero
9880// callers used the utils.rs version.
9881
9882// `dupstrpfx` DELETED — fake duplicate cited `c:string.c:161` from
9883// utils.rs (wrong location). Canonical port is
9884// `dupstrpfx` at `string.rs:161`. Zero
9885// callers used the utils.rs version.
9886
9887/// Get username from UID (from utils.c getpwuid handling)
9888pub fn statuidprint(uid: u32) -> Option<String> {
9889 #[cfg(unix)]
9890 {
9891 let pwd = unsafe { libc::getpwuid(uid) };
9892 if pwd.is_null() {
9893 return None;
9894 }
9895 let name = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_name) };
9896 name.to_str().ok().map(|s| s.to_string())
9897 }
9898 #[cfg(not(unix))]
9899 {
9900 let _ = uid;
9901 None
9902 }
9903}
9904
9905// `ztrdup` / `dyncat` / `tricat` / `bicat` DELETED — these were
9906// fake duplicates of the canonical ports in `src/ported/string.rs`
9907// (`Src/string.c:62`, `:131`, `:98`, `:145`). The utils.rs copies
9908// admitted they were not in utils.c via `c:string.c:NNN`
9909// annotations; PORT.md Rule 1 disallows the duplicate location.
9910// Callers now route through `crate::ported::string::{ztrdup,
9911// dyncat, tricat, bicat}` directly.
9912
9913// `iwsep` / `iwsep_byte` DELETED — these were fake hardcoded
9914// `c==' '||c=='\t'||c=='\n'` checks claiming to port `iwsep` from
9915// "Src/zsh.h", but the real macro lives at `Src/ztype.h:61`
9916// (`#define iwsep(X) zistype(X, IWSEP)`) and consults the `typtab[]`
9917// lookup — which mutates when IFS is reassigned. The canonical port
9918// is `iwsep(u8) -> bool` at `ztype_h.rs:133`.
9919// Internal utils.rs callers (skipwsep / spacesplit / findword) now
9920// route through it directly.
9921
9922// `imeta(c: char)` DELETED — fake `char`-arg wrapper cited
9923// `c:ztype.h:60` from utils.rs (wrong location, wrong signature).
9924// Canonical port is `crate::ported::ztype_h::imeta(u8) -> bool` at
9925// `ztype_h.rs:130`. C `imeta(X)` takes a byte; wrapping with `char`
9926// invented an `> 0xff` early-out that C doesn't have. Migrated 1
9927// caller (`zle/computil.rs:6194`).
9928
9929/// Get hostname
9930pub fn gethostname() -> String {
9931 #[cfg(unix)]
9932 {
9933 let mut buf = vec![0u8; 256];
9934 unsafe {
9935 if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) == 0 {
9936 let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
9937 return String::from_utf8_lossy(&buf[..len]).to_string();
9938 }
9939 }
9940 }
9941 // gethostname(2) failure fallback. C consults `$HOST` /
9942 // `cached_hostname`. Read paramtab; fall back to "localhost".
9943 getsparam("HOST")
9944 .or_else(|| getsparam("HOSTNAME"))
9945 .unwrap_or_else(|| "localhost".to_string())
9946}
9947
9948/// Get current working directory — re-export of the canonical
9949/// port at `compat::zgetcwd` (Src/compat.c:559).
9950// `zgetcwd` DELETED — fake `Option<String>` wrapper around the
9951// canonical `compat::zgetcwd() -> String` port (`Src/compat.c:559`).
9952// C `zgetcwd` never returns NULL (falls back to `"."` per the
9953// `dupstring(".")` arm), so the `Option` was caller-API churn, not
9954// a port. Callers route through `crate::ported::compat::zgetcwd()`
9955// directly.
9956
9957// `zchdir` duplicate deleted — canonical port at compat.rs:253
9958// matches C's `int zchdir(char *dir)` signature (returns i32, not
9959// bool). Zero callers used the utils.rs bool variant.
9960
9961// `realpath` / `mkdir` / `symlink` / `readlink` / `getenv` DELETED —
9962// five Rust-only convenience wrappers around std::fs / std::env /
9963// std::os::unix::fs with ZERO callers across the tree. Each was a
9964// thin std-lib re-export with no zsh C counterpart:
9965// - realpath → std::fs::canonicalize (libc realpath(3) — not in
9966// utils.c; the zsh source uses chrealpath at hist.c:1971)
9967// - mkdir → std::fs::create_dir (libc mkdir(2) — zsh's analogue
9968// is Modules/files.c::bin_mkdir, not a utils.c helper)
9969// - symlink → std::os::unix::fs::symlink (libc symlink(2))
9970// - readlink → std::fs::read_link (libc readlink(2))
9971// - getenv → std::env::var (libc getenv(3) — zsh's analogue is
9972// zgetenv in compat.c)
9973// PORT.md Rule 0/A: names must exist in upstream zsh C source as a
9974// `<name>(` function. None of these qualify; with zero callers they
9975// were dead code by definition. Callers needing the same semantics
9976// route through the canonical port (chrealpath / bin_mkdir / etc.)
9977// or std::fs directly.
9978
9979/// Per-prompt callback registry.
9980/// Port of the static `prepromptfns` LinkList in Src/utils.c:1319.
9981/// Holds the bare-fn pointers `addprepromptfn`/`delprepromptfn`
9982/// register and `preprompt()` walks.
9983static PREPROMPT_FNS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
9984
9985/// Set environment variable
9986pub fn setenv(name: &str, value: &str) {
9987 std::env::set_var(name, value);
9988}
9989
9990/// Unset environment variable
9991pub fn unsetenv(name: &str) {
9992 std::env::remove_var(name);
9993}
9994
9995/// Time-ordered timed-function registry.
9996/// Port of the `timedfns` LinkList Src/utils.c:1371 keeps for the
9997/// `sched` builtin. Sorted ascending by `when` (epoch seconds);
9998/// `addtimedfn` does an insertion sort (matches the C source's
9999/// `for (;;)` walk at lines 1394-1411).
10000pub static TIMED_FNS: Mutex<Vec<(i64, fn())>> = Mutex::new(Vec::new()); // c:1371 timedfns (mod_export in C)
10001
10002/// Get current user ID
10003pub fn getuid() -> u32 {
10004 #[cfg(unix)]
10005 unsafe {
10006 libc::getuid()
10007 }
10008 #[cfg(not(unix))]
10009 0
10010}
10011
10012/// Get effective user ID
10013pub fn geteuid() -> u32 {
10014 #[cfg(unix)]
10015 unsafe {
10016 libc::geteuid()
10017 }
10018 #[cfg(not(unix))]
10019 0
10020}
10021
10022/// Get current group ID
10023pub fn getgid() -> u32 {
10024 #[cfg(unix)]
10025 unsafe {
10026 libc::getgid()
10027 }
10028 #[cfg(not(unix))]
10029 0
10030}
10031
10032/// Get effective group ID
10033pub fn getegid() -> u32 {
10034 #[cfg(unix)]
10035 unsafe {
10036 libc::getegid()
10037 }
10038 #[cfg(not(unix))]
10039 0
10040}
10041
10042/// Get process ID
10043pub fn getpid() -> i32 {
10044 std::process::id() as i32
10045}
10046
10047/// Get parent process ID
10048pub fn getppid() -> i32 {
10049 #[cfg(unix)]
10050 unsafe {
10051 libc::getppid()
10052 }
10053 #[cfg(not(unix))]
10054 0
10055}
10056
10057/// `getkeystring` with the C `how` parameter (Src/utils.c:6915).
10058/// The plain `getkeystring(s)` shim above defaults `how=0` for
10059/// non-EMACS callers (zbeep, dollar-quote — they keep unknown
10060/// `\<char>` as literal `\<char>`). Pass `GETKEYS_PRINT` to get
10061/// the print/echo behavior (drop backslash on unknown).
10062/// `getkeystring` with the C `how` parameter AND the
10063/// `GETKEY_UPDATE_OFFSET` cursor-offset out-param (Src/utils.c:6915).
10064///
10065/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from
10066/// `Src/utils.c:6915`, specifically its `GETKEY_UPDATE_OFFSET` path.
10067/// When `how` sets `GETKEY_UPDATE_OFFSET` and `misc` is `Some`, `*misc`
10068/// is the C `*misc` completion offset — a byte index into `s` — and is
10069/// updated as escapes positioned before the cursor collapse:
10070/// - `-1` per collapsed `\<char>` escape whose backslash precedes the
10071/// cursor, `s - sstart < *misc` (c:6987); undone if the backslash is
10072/// kept literal in the default (non-EMACS) arm (c:7181-7183);
10073/// - additional `-6` for `\u`, `-10` (`-4`+`-6`) for `\U` (c:7073-7084),
10074/// then `+N` for the N output bytes emitted (c:7123-7124).
10075/// Octal/hex escapes get only the base `-1`, matching C's own incomplete
10076/// `/* HERE: GETKEY_UPDATE_OFFSET? */` handling (c:7155).
10077///
10078/// The offset is a byte index. For ASCII `$'...'` content (one byte per
10079/// char) it matches the C metafied-byte semantics exactly; the byte-vs-
10080/// char divergence for non-ASCII content is the pre-existing shared-cursor
10081/// limitation documented on `set_comp_sep` (compcore.rs), not new here.
10082///
10083/// Callers with no offset to track pass `misc = None`.
10084pub fn getkeystring_with(
10085 s: &str,
10086 how: u32,
10087 mut misc: Option<&mut i32>,
10088) -> (String, usize) {
10089 // c:utils.c:6915
10090 let update_off = (how & crate::ported::zsh_h::GETKEY_UPDATE_OFFSET as u32) != 0;
10091 let mut result = String::new();
10092 let mut chars = s.chars().peekable();
10093 let mut consumed = 0;
10094 // c:7194 — C's `^` handler is `control = 1; continue;`, so the control bit
10095 // stays PENDING across loop iterations and a following `\M`/`\C` escape
10096 // chains onto it (`^\M-a` → c:7034 `meta = 1 + control` → meta==2 → 0x81).
10097 // This port's `^` arm consumes its base character inline, which cannot
10098 // express that, so the flag carries the state to the modifier arm instead.
10099 let mut pending_control = false;
10100 while let Some(c) = chars.next() {
10101 consumed += c.len_utf8();
10102 // c:utils.c:7194 — `^X` caret notation. A bare `^` (not a backslash
10103 // escape) followed by any char applies the control mask to it, but
10104 // ONLY under GETKEY_CTRL — i.e. `print -b` / bindkey. Plain print
10105 // (GETKEYS_PRINT, no GETKEY_CTRL) keeps `^A` literal.
10106 if c == '^' && !pending_control && (how & GETKEY_CTRL) != 0 {
10107 // A `\M`/`\C` escape after the caret must be handled by the
10108 // modifier arm with control already pending — C reaches it via
10109 // `continue` rather than consuming the base char here. Gated on
10110 // GETKEY_EMACS because that is what enables the modifier arm at
10111 // all (c:7031/7043); without it C's `case 'M'` emits a literal
10112 // backslash instead.
10113 let mut la = chars.clone();
10114 if (how & GETKEY_EMACS) != 0
10115 && la.next() == Some('\\')
10116 && matches!(la.next(), Some('M') | Some('C'))
10117 {
10118 pending_control = true;
10119 continue;
10120 }
10121 if let Some(base) = chars.next() {
10122 consumed += base.len_utf8();
10123 let mut byte = base as u32;
10124 // c:7261-7267 — `^?` → 0x7f (DEL), else clear bits 5-6.
10125 if byte == '?' as u32 {
10126 byte = 0x7f;
10127 } else if byte <= 0xff {
10128 byte &= 0x9f;
10129 }
10130 if byte <= 0xff {
10131 let b_ = byte as u8;
10132 if b_ < 0x80 {
10133 result.push(b_ as char);
10134 } else {
10135 result.push('\u{83}');
10136 result.push(char::from(b_ ^ 32));
10137 }
10138 } else if let Some(ch) = char::from_u32(byte) {
10139 result.push(ch);
10140 }
10141 continue;
10142 }
10143 // c:7194 `s[1]` guard — a trailing `^` with no char is literal.
10144 result.push(c);
10145 continue;
10146 }
10147 if c != '\\' {
10148 result.push(c);
10149 continue;
10150 }
10151 // c is a backslash; record its byte start offset for
10152 // GETKEY_UPDATE_OFFSET (`s - sstart` in C).
10153 let bs_off = consumed - c.len_utf8();
10154 // c:utils.c:6987 — base offset decrement for a collapsed `\<char>`
10155 // escape whose backslash precedes the cursor. C gates on
10156 // `*s == '\\' && s[1]`, so a following char must exist (peek).
10157 let mut miscadded = false;
10158 if update_off && chars.peek().is_some() {
10159 if let Some(m) = misc.as_deref_mut() {
10160 if (bs_off as i32) < *m {
10161 *m -= 1;
10162 miscadded = true;
10163 }
10164 }
10165 }
10166 match chars.next() {
10167 Some('n') => {
10168 result.push('\n');
10169 consumed += 1;
10170 }
10171 Some('t') => {
10172 result.push('\t');
10173 consumed += 1;
10174 }
10175 Some('r') => {
10176 result.push('\r');
10177 consumed += 1;
10178 }
10179 // c:utils.c:7018-7024 — `\E` is EMACS-gated:
10180 // case 'E':
10181 // if (!(how & GETKEY_EMACS)) {
10182 // *t++ = '\\', s--;
10183 // if (miscadded) (*misc)++;
10184 // continue;
10185 // }
10186 // /* FALL THROUGH */
10187 // case 'e':
10188 // *t++ = '\033';
10189 // Without EMACS `\E` stays a literal backslash + `E`; only `\e`
10190 // is unconditional. Folding `E` in with `e` made `${(g::)v}` on
10191 // `\E` yield ESC where zsh yields `\E`. Unlike the octal
10192 // `continue` arm (c:7159), C restores the miscadded decrement
10193 // here, since the backslash is kept.
10194 Some('E') if (how & GETKEY_EMACS) == 0 => {
10195 consumed += 1;
10196 if miscadded {
10197 if let Some(m) = misc.as_deref_mut() {
10198 *m += 1;
10199 }
10200 }
10201 result.push('\\');
10202 result.push('E');
10203 }
10204 Some('e') | Some('E') => {
10205 result.push('\x1b');
10206 consumed += 1;
10207 }
10208 Some('a') => {
10209 result.push('\x07');
10210 consumed += 1;
10211 }
10212 Some('b') => {
10213 result.push('\x08');
10214 consumed += 1;
10215 }
10216 Some('f') => {
10217 result.push('\x0c');
10218 consumed += 1;
10219 }
10220 Some('v') => {
10221 result.push('\x0b');
10222 consumed += 1;
10223 }
10224 // c:utils.c:7140-7152 — `\\` (and `\'` under
10225 // GETKEY_DOLLAR_QUOTE) explicitly emit the trailing byte
10226 // bare. Outside DOLLAR_QUOTE, `\'` FALLTHROUGHs to default.
10227 // `\\` stays special because the default arm (c:7180-7184)
10228 // skips the leading-backslash emission iff `*s == '\\'`,
10229 // so `\\` reduces to `\` regardless of GETKEY_EMACS.
10230 //
10231 // Previous Rust port also had `Some('\'')` and `Some('"')`
10232 // arms that unconditionally dropped the backslash. That
10233 // contradicted C: under GETKEYS_ECHO (no GETKEY_EMACS),
10234 // `\'` must remain `\'` because echo doesn't strip
10235 // unknown escapes. Removed those arms — `\'` and `\"`
10236 // now flow through the default arm and honour GETKEY_EMACS.
10237 //
10238 // Root cause of `echo "${(qq)s}"` for `s="a'b"` emitting
10239 // `'a'''b'` instead of zsh's `'a'\''b'`.
10240 Some('\\') => {
10241 result.push('\\');
10242 consumed += 1;
10243 }
10244 // c:utils.c:7072-7138 — `\u` (4-hex) / `\U` (8-hex)
10245 // Unicode codepoint escapes. Always interpreted; the C
10246 // source's `case 'U':` / `case 'u':` arms have no flag
10247 // gating (only the GETKEY_UPDATE_OFFSET bookkeeping). C
10248 // calls `ucs4tomb(wval, t)` which writes UTF-8 bytes.
10249 // Previously these were absent from `getkeystring_with`,
10250 // so `echo -e "\U00000041"` emitted literal `\U00000041`
10251 // instead of 'A'.
10252 Some('u') => {
10253 consumed += 1;
10254 // c:utils.c:7077-7084 — extra `-6` when the escape precedes
10255 // the cursor (checked at the 'u', i.e. offset bs_off+1).
10256 if update_off {
10257 if let Some(m) = misc.as_deref_mut() {
10258 if ((bs_off + 1) as i32) < *m {
10259 *m -= 6;
10260 }
10261 }
10262 }
10263 let mut hex = String::new();
10264 for _ in 0..4 {
10265 if let Some(&c) = chars.peek() {
10266 if c.is_ascii_hexdigit() {
10267 hex.push(chars.next().unwrap());
10268 consumed += 1;
10269 } else {
10270 break;
10271 }
10272 }
10273 }
10274 // c:utils.c:7085 `wval = 0;` BEFORE the digit loop, and
10275 // c:7087-7095 leaves it untouched when the first char is not
10276 // a hex digit (`s--; break;`). So ZERO digits is not "not an
10277 // escape" — it emits codepoint 0 via `ucs4tomb(wval, t)`
10278 // (c:7101). `\u` → one NUL byte, `\uzz` → NUL + "zz". An Err
10279 // on the empty parse pushed nothing at all.
10280 let val = u32::from_str_radix(&hex, 16).unwrap_or(0);
10281 if let Some(ch) = char::from_u32(val) {
10282 result.push(ch);
10283 // c:utils.c:7123-7124 — add one per output byte,
10284 // checked at the last consumed input byte.
10285 if update_off {
10286 if let Some(m) = misc.as_deref_mut() {
10287 if ((consumed - 1) as i32) < *m {
10288 *m += ch.len_utf8() as i32;
10289 }
10290 }
10291 }
10292 }
10293 }
10294 Some('U') => {
10295 consumed += 1;
10296 // c:utils.c:7073-7084 — `\U` extra `-4` (c:7073) then the
10297 // shared `\u` `-6` (c:7077), both gated at offset bs_off+1.
10298 if update_off {
10299 if let Some(m) = misc.as_deref_mut() {
10300 if ((bs_off + 1) as i32) < *m {
10301 *m -= 10;
10302 }
10303 }
10304 }
10305 let mut hex = String::new();
10306 for _ in 0..8 {
10307 if let Some(&c) = chars.peek() {
10308 if c.is_ascii_hexdigit() {
10309 hex.push(chars.next().unwrap());
10310 consumed += 1;
10311 } else {
10312 break;
10313 }
10314 }
10315 }
10316 // c:utils.c:7085 — same `wval = 0` default as the `\u` arm
10317 // above: `\U` with no hex digits emits codepoint 0, not
10318 // nothing.
10319 let val = u32::from_str_radix(&hex, 16).unwrap_or(0);
10320 if let Some(ch) = char::from_u32(val) {
10321 result.push(ch);
10322 // c:utils.c:7123-7124 — add one per output byte.
10323 if update_off {
10324 if let Some(m) = misc.as_deref_mut() {
10325 if ((consumed - 1) as i32) < *m {
10326 *m += ch.len_utf8() as i32;
10327 }
10328 }
10329 }
10330 }
10331 }
10332 // c:utils.c:7156-7178 — the NUMERIC-ESCAPE branch. C handles
10333 // `\x`, `\NNN` and `\0NNN` in ONE arm, and the shared tail is
10334 // what makes them agree, so this port keeps them together too:
10335 //
10336 // if ((idigit(*s) && *s < '8') || *s == 'x') {
10337 // if (!(how & GETKEY_OCTAL_ESC)) {
10338 // if (*s == '0')
10339 // s++;
10340 // else if (*s != 'x') {
10341 // *t++ = '\\', s--;
10342 // continue;
10343 // }
10344 // }
10345 // if (s[1] && s[2] && s[3]) {
10346 // svchar = s[3]; s[3] = '\0'; u = s;
10347 // }
10348 // *t++ = zstrtol(s + (*s == 'x'), &s,
10349 // (*s == 'x') ? 16 : 8);
10350 // ...
10351 // s--;
10352 // }
10353 //
10354 // Three behaviours here were each missing when these were three
10355 // separate arms:
10356 //
10357 // 1. Without GETKEY_OCTAL_ESC a NON-ZERO digit is not an escape
10358 // at all (c:7159): C emits a literal backslash and backs up so
10359 // the digit is re-read as an ordinary char. This is INSIDE the
10360 // numeric branch, so — unlike the default arm — GETKEY_EMACS
10361 // does not suppress the backslash: `${(g:e:)v}` on `\101` is
10362 // `\101`, not `101`.
10363 // 2. The value goes through `zstrtol`, which SKIPS leading blanks
10364 // and accepts a `+`/`-` sign (c:2444-2450), and whose end
10365 // pointer lands past everything it consumed — blanks included.
10366 // So `\0 1` is byte 0o1, `\x 41` is 0x04 then `1`, `\x y` is
10367 // NUL then `y`, and `\0-1` is 0xff (a NEGATIVE parse truncated
10368 // to a byte). Peeking for "is the next char a digit" stopped at
10369 // the blank and left it in the output.
10370 // 3. The base is chosen from the char AT `s` AFTER the `\0`
10371 // introducer was skipped, so `\0x41` is HEX (`A`), not NUL
10372 // followed by `x41`.
10373 //
10374 // c:7166-7168 windows the parse by NUL-ing `s[3]` when three chars
10375 // follow `s`, capping it at 3 significant chars (2 after an `x`),
10376 // then restores the byte — so `\1234` is 0o123 then `4`.
10377 //
10378 // `*t++ = zstrtol(...)` assigns a zlong to a char, truncating to
10379 // the low byte: `\777` (511) is 0xff, `\400` (256) is NUL. Parsing
10380 // straight into u8 made those an Err and emitted nothing at all.
10381 Some(d) if d.is_digit(8) || d == 'x' => {
10382 consumed += 1;
10383 // c:7157-7161 — the pre-checks that run only without
10384 // GETKEY_OCTAL_ESC. `\0` is the octal introducer (`s++`);
10385 // any other bare digit is not an escape.
10386 let zero_intro = (how & GETKEY_OCTAL_ESC) == 0 && d == '0';
10387 if (how & GETKEY_OCTAL_ESC) == 0 && d != '0' && d != 'x' {
10388 // c:7159 `*t++ = '\\', s--; continue;`
10389 result.push('\\');
10390 result.push(d);
10391 continue;
10392 }
10393 // C's `s` after the pre-checks: the char AFTER the `\0`
10394 // introducer, else the matched char itself.
10395 //
10396 // Four chars of lookahead is all C can read: the window test
10397 // reaches `s[3]`, and the parse never sees past it. Bounding
10398 // the clone keeps this O(1) per escape — collecting the whole
10399 // tail here would make decoding an escape-dense string O(n²).
10400 let rest: Vec<char> = chars.clone().take(4).collect();
10401 let (p, after): (Option<char>, &[char]) = if zero_intro {
10402 (rest.first().copied(), rest.get(1..).unwrap_or(&[]))
10403 } else {
10404 (Some(d), &rest[..])
10405 };
10406 // c:7166 `if (s[1] && s[2] && s[3])` — needs three chars after
10407 // `s`; the window then spans `s` plus two more.
10408 let win: &[char] = if after.len() >= 3 { &after[..2] } else { after };
10409 let mut vis: Vec<char> = Vec::with_capacity(3);
10410 if let Some(pc) = p {
10411 vis.push(pc);
10412 }
10413 vis.extend_from_slice(win);
10414 // c:7170 `zstrtol(s + (*s == 'x'), &s, (*s == 'x') ? 16 : 8)`.
10415 let (base, skip_p) = if p == Some('x') { (16, 1) } else { (8, 0) };
10416 let parse_src: String = vis[skip_p.min(vis.len())..].iter().collect();
10417 let (num, tail) = zstrtol(&parse_src, base);
10418 let used_chars = parse_src[..parse_src.len() - tail.len()].chars().count();
10419 // Advance past what C consumed. `vis[0]` is the already-taken
10420 // match char unless the `\0` introducer moved `s` forward.
10421 let vis_used = skip_p + used_chars;
10422 let advance = if zero_intro { vis_used } else { vis_used.saturating_sub(1) };
10423 for _ in 0..advance {
10424 chars.next();
10425 consumed += 1;
10426 }
10427 let val = (num as u64 & 0xff) as u8;
10428 // c:Src/utils.c — the escape is one raw BYTE; metafy high
10429 // bytes (c:7289-7294) so `\377` unmetafies to a single 0xff,
10430 // not the UTF-8 pair c3 bf.
10431 if val < 0x80 {
10432 result.push(val as char);
10433 } else {
10434 result.push('\u{83}');
10435 result.push(char::from(val ^ 32));
10436 }
10437 // c:7172-7173 — under GETKEY_PRINTF_PERCENT a numeric escape
10438 // producing `%` gets a second `%`.
10439 if (how & crate::ported::zsh_h::GETKEY_PRINTF_PERCENT as u32) != 0 && val == b'%' {
10440 result.push('%');
10441 }
10442 }
10443 // c:utils.c:7029-7052 + c:7255-7275 — `\C` / `\M` set the
10444 // control / meta modifiers (bindkey-style key escapes), an
10445 // optional `-` separator, then the next base char (possibly
10446 // through chained `\C`/`\M`) gets the mask applied:
10447 // control → `& 0x9f` (or `\C-?` → 0x7f), meta → `| 0x80`.
10448 // Gated on GETKEY_EMACS (c:7031/7043 `if (how & GETKEY_EMACS)`):
10449 // print / print -b / $'…' have it, echo does not (so echo keeps
10450 // `\C-a` literal via the default arm). Mirrors the same handler
10451 // already present in `getkeystring` (utils.rs) and
10452 // `getkeystring_dollar_quote` (lex.rs); print's `getkeystring_with`
10453 // path lacked it, so `print "\C-a"` emitted literal `C-a`.
10454 Some(mod_letter @ ('C' | 'M')) if (how & GETKEY_EMACS) != 0 => {
10455 consumed += 1;
10456 // c:7034 `meta = 1 + control; /* preserve the order of ^ and
10457 // meta */` — meta is a COUNT, not a flag: 2 when `\M` was seen
10458 // while control was already pending, which makes c:7261-7264
10459 // apply `|0x80` BEFORE the control mask instead of after. The
10460 // two orders differ for `?`: `\M-\C-?` is 0xff, `\C-\M-?` is
10461 // 0x9f. Booleans could not express that.
10462 // A `^` already seen (c:7194 `control = 1; continue;`) arrives
10463 // here as pending state, so `^\M-a` gives meta = 1 + 1 = 2.
10464 let mut control: u32 = if pending_control {
10465 pending_control = false;
10466 1
10467 } else {
10468 0
10469 };
10470 let mut meta: u32 = 0;
10471 if mod_letter == 'C' {
10472 control = 1; // c:7046
10473 } else {
10474 meta = 1 + control; // c:7034
10475 }
10476 // c:7032-7033 / 7044-7045 — `if (s[1] == '-') s++;` consumes at
10477 // most ONE separator per modifier. Looping over a RUN of dashes
10478 // ate the base character of `\M--` (meta applied to `-`, 0xad),
10479 // leaving an empty binding.
10480 if chars.peek() == Some(&'-') {
10481 chars.next();
10482 consumed += 1;
10483 }
10484 // Chained `\C`/`\M`, and the caret form after a modifier.
10485 loop {
10486 let mut iter_clone = chars.clone();
10487 if iter_clone.next() == Some('\\') {
10488 if let Some(nx) = iter_clone.next() {
10489 if nx == 'C' || nx == 'M' {
10490 chars.next(); // consume '\'
10491 chars.next(); // consume C/M
10492 consumed += 2;
10493 if nx == 'C' {
10494 control = 1; // c:7046
10495 } else {
10496 meta = 1 + control; // c:7034
10497 }
10498 if chars.peek() == Some(&'-') {
10499 chars.next();
10500 consumed += 1;
10501 }
10502 continue;
10503 }
10504 }
10505 }
10506 // c:7194 — `else if (*s == '^' && !control && (how &
10507 // GETKEY_CTRL) && s[1]) { control = 1; continue; }`. C's
10508 // `case 'M'` only sets `meta` and `continue`s, so the MAIN
10509 // loop reaches this `^` handler with meta still pending:
10510 // `\M-^?` is control+meta applied to `?` (0xff). This arm
10511 // consumed the base char itself, so `^` became the base and
10512 // `\M-^?` bound TWO bytes (0xde 0x3f) — breaking the very
10513 // common `bindkey '\M-^?' backward-kill-word`.
10514 if control == 0 && (how & GETKEY_CTRL) != 0 && chars.peek() == Some(&'^') {
10515 let mut it2 = chars.clone();
10516 it2.next();
10517 if it2.next().is_some() {
10518 // c:7194 `s[1]` guard
10519 chars.next();
10520 consumed += 1;
10521 control = 1;
10522 continue;
10523 }
10524 }
10525 break;
10526 }
10527 // Read one base character (allowing nested simple escapes).
10528 let base: Option<char> = if chars.peek() == Some(&'\\') {
10529 chars.next();
10530 consumed += 1;
10531 match chars.next() {
10532 Some('n') => {
10533 consumed += 1;
10534 Some('\n')
10535 }
10536 Some('t') => {
10537 consumed += 1;
10538 Some('\t')
10539 }
10540 Some('r') => {
10541 consumed += 1;
10542 Some('\r')
10543 }
10544 Some('a') => {
10545 consumed += 1;
10546 Some('\x07')
10547 }
10548 Some('b') => {
10549 consumed += 1;
10550 Some('\x08')
10551 }
10552 Some('e') | Some('E') => {
10553 consumed += 1;
10554 Some('\x1b')
10555 }
10556 Some('f') => {
10557 consumed += 1;
10558 Some('\x0c')
10559 }
10560 Some('v') => {
10561 consumed += 1;
10562 Some('\x0b')
10563 }
10564 Some('\\') => {
10565 consumed += 1;
10566 Some('\\')
10567 }
10568 Some('\'') => {
10569 consumed += 1;
10570 Some('\'')
10571 }
10572 Some('"') => {
10573 consumed += 1;
10574 Some('"')
10575 }
10576 Some(other) => {
10577 consumed += 1;
10578 Some(other)
10579 }
10580 None => None,
10581 }
10582 } else {
10583 chars.next().inspect(|c| {
10584 consumed += c.len_utf8();
10585 })
10586 };
10587 if let Some(ch) = base {
10588 let mut byte = ch as u32;
10589 // c:7261-7264 — `if (meta == 2) { t[-1] |= 0x80; meta = 0; }`
10590 // runs BEFORE the control mask: `\M` seen while control was
10591 // already pending sets the high bit first.
10592 if meta == 2 {
10593 byte |= 0x80;
10594 }
10595 // c:7265-7271 — control mask (`\C-?` → 0x7f, else & 0x9f).
10596 if control == 1 {
10597 if byte == '?' as u32 {
10598 byte = 0x7f;
10599 } else {
10600 byte &= 0x9f;
10601 }
10602 }
10603 // c:7272-7275 — `if (meta) { t[-1] |= 0x80; }` — the
10604 // meta==1 order, applied AFTER the mask.
10605 if meta == 1 {
10606 byte |= 0x80;
10607 }
10608 // c:7289-7294 — a masked byte >= 0x80 is metafied so it
10609 // unmetafies back to the single raw byte on output.
10610 if byte <= 0xff {
10611 let b_ = byte as u8;
10612 if b_ < 0x80 {
10613 result.push(b_ as char);
10614 } else {
10615 result.push('\u{83}');
10616 result.push(char::from(b_ ^ 32));
10617 }
10618 } else if let Some(c) = char::from_u32(byte) {
10619 result.push(c);
10620 }
10621 }
10622 }
10623 // c:utils.c:7180-7184 — default arm. With GETKEY_EMACS
10624 // set, drop the backslash; otherwise keep `\<char>`.
10625 Some(c) => {
10626 consumed += 1;
10627 // c:utils.c:7045 — `\c` under GETKEY_BACKSLASH_C
10628 // means TRUNCATE: drop the rest of the input,
10629 // suppress the trailing newline. Used by `echo`
10630 // (and `print` without -r). Set TLS flag so the
10631 // caller can detect + suppress the newline.
10632 if c == 'c' && (how & GETKEY_BACKSLASH_C) != 0 {
10633 GETKEY_TRUNCATED.with(|cell| cell.set(true));
10634 break;
10635 }
10636 if (how & GETKEY_EMACS) == 0 {
10637 // c:utils.c:7181-7183 — backslash kept literal (no EMACS),
10638 // so the string does not collapse here: undo the base
10639 // GETKEY_UPDATE_OFFSET decrement applied above.
10640 if miscadded {
10641 if let Some(m) = misc.as_deref_mut() {
10642 *m += 1;
10643 }
10644 }
10645 result.push('\\');
10646 }
10647 result.push(c);
10648 }
10649 None => {
10650 result.push('\\');
10651 }
10652 }
10653 }
10654 (result, consumed)
10655}
10656
10657thread_local! {
10658 /// Sticky flag set by `getkeystring_with` when a `\c` escape
10659 /// truncates the input under `GETKEY_BACKSLASH_C`. The caller
10660 /// (bin_print / bin_echo) reads + clears via `getkey_truncated_take()`
10661 /// to suppress the trailing newline and skip any subsequent args
10662 /// in the print pass.
10663 pub static GETKEY_TRUNCATED: std::cell::Cell<bool> =
10664 const { std::cell::Cell::new(false) };
10665}
10666
10667/// Read + clear the `\c`-truncated flag set by the previous
10668/// `getkeystring_with` call. Returns true once after a truncation
10669/// fires, false otherwise.
10670pub fn getkey_truncated_take() -> bool {
10671 GETKEY_TRUNCATED.with(|c| {
10672 let v = c.get();
10673 c.set(false);
10674 v
10675 })
10676}
10677
10678/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
10679/// the C expression `fdtable[fd]` (read). Returns `FDT_UNUSED` for
10680/// any fd that has not been explicitly set, matching the C source's
10681/// post-`growfdtable` zero-fill behaviour at Src/utils.c:1979.
10682pub fn fdtable_get(fd: i32) -> i32 {
10683 // c:utils.c:fdtable[fd]
10684 if fd < 0 {
10685 return FDT_UNUSED;
10686 }
10687 let g = fdtable_lock().lock().unwrap();
10688 g.get(fd as usize).copied().unwrap_or(FDT_UNUSED)
10689}
10690
10691/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
10692/// the C statement `fdtable[fd] = kind;`. Inlines the `growfdtable`
10693/// call from Src/utils.c:1965 since C always invokes it immediately
10694/// before the assignment anyway. Also updates `MAX_ZSH_FD` to track
10695/// the highest assigned slot — C does this inside `check_fd_table`
10696/// at `Src/utils.c:1982` (`max_zsh_fd = fd;`). Previously the Rust
10697/// `fdtable_set` skipped this, leaving `max_zsh_fd = 0` after any
10698/// `addlockfd` / `addmodulefd` call, which broke `zcloselockfd`'s
10699/// `if (fd > max_zsh_fd)` guard.
10700pub fn fdtable_set(fd: i32, kind: i32) {
10701 // c:utils.c:fdtable[fd]
10702 if fd < 0 {
10703 return;
10704 }
10705 let mut g = fdtable_lock().lock().unwrap();
10706 if (fd as usize) >= g.len() {
10707 g.resize((fd as usize) + 1, FDT_UNUSED);
10708 }
10709 g[fd as usize] = kind;
10710 // c:1982 — `max_zsh_fd = fd;` (with `if (fd <= max_zsh_fd) return;`
10711 // guard at c:1971). Inline equivalent: bump only when this fd
10712 // exceeds the current max.
10713 let cur = MAX_ZSH_FD.load(Ordering::Relaxed);
10714 if fd > cur {
10715 MAX_ZSH_FD.store(fd, Ordering::Relaxed);
10716 }
10717}
10718
10719/// Port of `imeta()` macro from `Src/ztype.h:60` — `zistype(X, IMETA)`.
10720///
10721/// IMETA is set in the typtab at `Src/utils.c:4195-4201` for:
10722/// - `'\0'` (c:4195)
10723/// - `Meta` (0x83) (c:4196)
10724/// - `Marker` (0xa2) (c:4197)
10725/// - `Pound..=LAST_NORMAL_TOK` = `0x84..=0x9c` (c:4198)
10726/// - `Snull..=Nularg` = `0x9d..=0xa1` (c:4200)
10727///
10728/// The CANONICAL set is `{0x00, 0x83..=0xa2}`. The previous Rust
10729/// port used `b >= 0x83` (every byte 0x83..=0xff) which was WRONG
10730/// for bytes `0xa3..=0xff`: C `imeta()` returns false for those,
10731/// so metafy passes them through unchanged. Rust's broader range
10732/// caused `metafy` to corrupt UTF-8 multibyte content (e.g.,
10733/// `é` = `0xc3 0xa9`: both bytes are NOT IMETA in C, but Rust
10734/// escaped both as `Meta + (byte ^ 32)`, mangling the encoding
10735/// and breaking every downstream consumer that expects the raw
10736/// UTF-8 bytes to round-trip).
10737///
10738/// Use the closed-range form rather than the typtab lookup so
10739/// `metafy` / `imeta_byte` work in test contexts where the typtab
10740/// isn't initialised, AND so the fast-path doesn't go through a
10741/// Mutex lock per byte (`metafy` is called per-character on every
10742/// shell input line).
10743#[inline]
10744pub fn imeta_byte(b: u8) -> bool {
10745 // c:4195-4201 — canonical IMETA range.
10746 b == 0 || (0x83..=0xa2).contains(&b)
10747}
10748
10749// `pub fn convfloat` and `pub fn convfloat_underscore` re-export
10750// wrappers DELETED per PORT.md Rule C. C defines both in
10751// `Src/params.c:5690` and `:5765` — the canonical Rust home is
10752// `src/ported/params.rs` (`convfloat` at line 7356,
10753// `convfloat_underscore` matching). Caller convenience cost is
10754// `use crate::ported::params::convfloat;` instead of `utils::`;
10755// no Rust-side re-export needed.
10756
10757// ===========================================================
10758// Methods moved verbatim from src/ported/vm_helper because their
10759// C counterpart's source file maps 1:1 to this Rust module.
10760// Phase: drift
10761// ===========================================================
10762
10763// BEGIN moved-from-exec-rs
10764// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
10765
10766// END moved-from-exec-rs
10767
10768// ===========================================================
10769// Free ported moved verbatim from src/ported/vm_helper.
10770// ===========================================================
10771// BEGIN moved-from-exec-rs (free ported)
10772pub(crate) fn base64_decode(s: &str) -> Vec<u8> {
10773 let decode_char = |c: u8| -> Option<u8> {
10774 match c {
10775 b'A'..=b'Z' => Some(c - b'A'),
10776 b'a'..=b'z' => Some(c - b'a' + 26),
10777 b'0'..=b'9' => Some(c - b'0' + 52),
10778 b'+' => Some(62),
10779 b'/' => Some(63),
10780 _ => None,
10781 }
10782 };
10783 let bytes = s.as_bytes();
10784 let mut out = Vec::with_capacity(s.len() / 4 * 3);
10785 let mut i = 0;
10786 while i + 4 <= bytes.len() {
10787 let chunk = &bytes[i..i + 4];
10788 let pad = chunk.iter().filter(|&&c| c == b'=').count();
10789 let v0 = decode_char(chunk[0]).unwrap_or(0) as u32;
10790 let v1 = decode_char(chunk[1]).unwrap_or(0) as u32;
10791 let v2 = decode_char(chunk[2]).unwrap_or(0) as u32;
10792 let v3 = decode_char(chunk[3]).unwrap_or(0) as u32;
10793 let n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
10794 out.push(((n >> 16) & 0xff) as u8);
10795 if pad < 2 {
10796 out.push(((n >> 8) & 0xff) as u8);
10797 }
10798 if pad < 1 {
10799 out.push((n & 0xff) as u8);
10800 }
10801 i += 4;
10802 }
10803 out
10804}
10805// END moved-from-exec-rs (free ported)
10806
10807// ===========================================================
10808// Utility helpers moved from src/ported/vm_helper.
10809// All correspond to Src/utils.c logic (path/string/bslashquote helpers).
10810// ===========================================================
10811
10812// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10813// ─── RUST-ONLY ACCESSORS ───
10814//
10815// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10816// RwLock<T>>` globals declared above. C zsh uses direct global
10817// access; Rust needs these wrappers because `OnceLock::get_or_init`
10818// is the only way to lazily construct shared state. These ported sit
10819// here so the body of this file reads in C source order without
10820// the accessor wrappers interleaved between real port ported.
10821// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10822
10823// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10824// ─── RUST-ONLY ACCESSORS ───
10825//
10826// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10827// RwLock<T>>` globals declared above. C zsh uses direct global
10828// access; Rust needs these wrappers because `OnceLock::get_or_init`
10829// is the only way to lazily construct shared state. These ported sit
10830// here so the body of this file reads in C source order without
10831// the accessor wrappers interleaved between real port ported.
10832// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10833
10834// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10835// ─── RUST-ONLY ACCESSORS ───
10836//
10837// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10838// RwLock<T>>` globals declared above. C zsh uses direct global
10839// access; Rust needs these wrappers because `OnceLock::get_or_init`
10840// is the only way to lazily construct shared state. These ported sit
10841// here so the body of this file reads in C source order without
10842// the accessor wrappers interleaved between real port ported.
10843// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10844
10845// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10846// ─── RUST-ONLY ACCESSORS ───
10847//
10848// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10849// RwLock<T>>` globals declared above. C zsh uses direct global
10850// access; Rust needs these wrappers because `OnceLock::get_or_init`
10851// is the only way to lazily construct shared state. These ported sit
10852// here so the body of this file reads in C source order without
10853// the accessor wrappers interleaved between real port ported.
10854// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10855
10856// WARNING: NOT IN UTILS.C — Rust-only OnceLock get-or-init
10857// helpers. C dereferences each global directly.
10858fn scriptname_lock() -> &'static Mutex<Option<String>> {
10859 SCRIPTNAME.get_or_init(|| Mutex::new(None))
10860}
10861
10862// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10863fn argzero_lock() -> &'static Mutex<Option<String>> {
10864 ARGZERO.get_or_init(|| Mutex::new(None))
10865}
10866
10867// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10868fn scriptfilename_lock() -> &'static Mutex<Option<String>> {
10869 SCRIPTFILENAME.get_or_init(|| Mutex::new(None))
10870}
10871
10872// WARNING: NOT IN UTILS.C — Rust-only OnceLock accessor for `posixzero`.
10873// C's `posixzero` lives in params.c:76; the canonical storage lives
10874// here for OnceLock initialisation parity with `argzero`.
10875fn posixzero_lock() -> &'static Mutex<Option<String>> {
10876 POSIXZERO.get_or_init(|| Mutex::new(None))
10877}
10878
10879// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10880pub fn noerrs_lock() -> &'static Mutex<i32> {
10881 NOERRS.get_or_init(|| Mutex::new(0))
10882}
10883
10884// `locallevel_lock` removed — duplicate of canonical
10885// `LOCALLEVEL` (port of params.c:54). All
10886// accessors here now route through the canonical AtomicI32.
10887// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10888fn lineno_lock() -> &'static Mutex<i32> {
10889 LINENO.get_or_init(|| Mutex::new(0))
10890}
10891
10892// WARNING: NOT IN UTILS.C — Rust-only cache for the SHINSTDIN
10893// option flag so error-emission doesn't pull in the option-table.
10894fn shinstdin_lock() -> &'static Mutex<bool> {
10895 SHINSTDIN_OPT.get_or_init(|| Mutex::new(false))
10896}
10897
10898/// !!! RUST-ONLY HELPER — see WARNING block above. C source uses bare
10899/// `unsigned char *fdtable` global from Src/utils.c:~63.
10900fn fdtable_lock() -> &'static Mutex<Vec<i32>> {
10901 FDTABLE.get_or_init(|| Mutex::new(Vec::new()))
10902}
10903
10904#[cfg(test)]
10905mod tests {
10906 use super::*;
10907
10908 /// checkmailpath port (utils.c:1620). Drives mtime/atime/size through
10909 /// `libc::utimes` so the new-mail condition is deterministic in headless CI.
10910 #[test]
10911 fn checkmailpath_new_mail_conditions() {
10912 use std::io::Write as _;
10913 let _g = crate::test_util::global_state_lock();
10914
10915 // Unique temp file with content (size != 0).
10916 let dir = std::env::temp_dir();
10917 let path = dir.join(format!("zshrs_mailtest_{}", std::process::id()));
10918 {
10919 let mut f = std::fs::File::create(&path).unwrap();
10920 f.write_all(b"new message\n").unwrap();
10921 }
10922 let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
10923 // atime (times[0]) <= mtime (times[1]); both well past 0.
10924 let set_times = |atime: i64, mtime: i64| {
10925 let tv = [
10926 libc::timeval {
10927 tv_sec: atime as libc::time_t,
10928 tv_usec: 0,
10929 },
10930 libc::timeval {
10931 tv_sec: mtime as libc::time_t,
10932 tv_usec: 0,
10933 },
10934 ];
10935 assert_eq!(unsafe { libc::utimes(cpath.as_ptr(), tv.as_ptr()) }, 0);
10936 };
10937
10938 let saved_shout = *crate::ported::init::shout.lock().unwrap();
10939 let saved_lmc = LAST_MAILCHECK.load(Ordering::Relaxed);
10940 *crate::ported::init::shout.lock().unwrap() = 1; // interactive
10941 LAST_MAILCHECK.store(0, Ordering::Relaxed);
10942
10943 let p = path.to_str().unwrap().to_string();
10944
10945 // c:1671-1675 — unread + newer than last check → "You have new mail."
10946 set_times(1_000_000, 2_000_000);
10947 assert_eq!(
10948 checkmailpath(&[p.clone()]),
10949 vec!["You have new mail.".to_string()]
10950 );
10951
10952 // c:1676-1698 — `PATH?message`: the message (here a literal) is emitted.
10953 assert_eq!(
10954 checkmailpath(&[format!("{}?custom note", p)]),
10955 vec!["custom note".to_string()]
10956 );
10957
10958 // c:1672 — mtime older than the last check → nothing.
10959 LAST_MAILCHECK.store(9_000_000, Ordering::Relaxed);
10960 assert!(checkmailpath(&[p.clone()]).is_empty());
10961
10962 // c:1670 — non-interactive (no shout) → nothing, even when fresh.
10963 LAST_MAILCHECK.store(0, Ordering::Relaxed);
10964 *crate::ported::init::shout.lock().unwrap() = 0;
10965 assert!(checkmailpath(&[p.clone()]).is_empty());
10966
10967 // c:1634-1636 — empty component is reported (stderr), yields no message.
10968 *crate::ported::init::shout.lock().unwrap() = 1;
10969 assert!(checkmailpath(&["?orphan".to_string()]).is_empty());
10970
10971 *crate::ported::init::shout.lock().unwrap() = saved_shout;
10972 LAST_MAILCHECK.store(saved_lmc, Ordering::Relaxed);
10973 let _ = std::fs::remove_file(&path);
10974 }
10975
10976 /// zsh_errno_msg / zerrmsg `%e` rendering (utils.c:348-365). EINTR is the
10977 /// literal "interrupt"; EIO is verbatim strerror (keeps its capital); every
10978 /// other errno lowercases the first letter (`tulower`). The old
10979 /// io::Error-based path kept the capital and appended " (os error N)".
10980 #[test]
10981 fn zsh_errno_msg_matches_c_percent_e() {
10982 // c:355-358 — EINTR → "interrupt".
10983 assert_eq!(zsh_errno_msg(libc::EINTR), "interrupt");
10984
10985 // c:366 — non-EIO errno lowercases the first letter and never carries
10986 // the Rust "(os error N)" suffix.
10987 let acc = zsh_errno_msg(libc::EACCES);
10988 assert!(
10989 acc.chars().next().is_some_and(|c| c.is_lowercase()),
10990 "EACCES first letter must be lowercased: {acc:?}"
10991 );
10992 assert!(
10993 !acc.contains("(os error"),
10994 "must be strerror, not io::Error: {acc:?}"
10995 );
10996
10997 // c:359-364 — EIO is emitted verbatim (its message reads wrong
10998 // lowercased), so its first letter stays capitalized.
10999 let eio = zsh_errno_msg(libc::EIO);
11000 assert!(
11001 eio.chars().next().is_some_and(|c| c.is_uppercase()),
11002 "EIO must be verbatim strerror (capitalized): {eio:?}"
11003 );
11004 }
11005
11006 /// c:351-354 — zerrmsg's `%e` arm sets errflag |= ERRFLAG_ERROR on EINTR.
11007 #[test]
11008 fn zerrmsg_eintr_sets_errflag() {
11009 let _g = crate::test_util::global_state_lock();
11010 let saved = errflag.load(Ordering::Relaxed);
11011 errflag.store(0, Ordering::Relaxed);
11012 zerrmsg("test", Some(libc::EINTR)); // writes to stderr; side-effect is the flag
11013 assert_ne!(
11014 errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
11015 0,
11016 "EINTR must set ERRFLAG_ERROR"
11017 );
11018 // A non-EINTR errno must NOT set the flag.
11019 errflag.store(0, Ordering::Relaxed);
11020 zerrmsg("test", Some(libc::EACCES));
11021 assert_eq!(errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR, 0);
11022 errflag.store(saved, Ordering::Relaxed);
11023 }
11024
11025 /// zjoin port (utils.c:3622). ASCII delimiters join byte-for-byte; a meta
11026 /// delimiter (NUL is `imeta` per inittyptab c:4195) must be written in
11027 /// metafied form (`Meta` + `delim^32`), never as a raw byte.
11028 #[test]
11029 fn zjoin_ascii_and_meta_delims() {
11030 let v = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
11031
11032 // c:3632-3641 — ordinary (non-meta) delimiter: plain interposition.
11033 assert_eq!(zjoin(&v(&["a", "b", "c"]), ' '), "a b c");
11034 assert_eq!(zjoin(&v(&["foo"]), ':'), "foo"); // single elem, no delim
11035 // c:3629-3630 — empty array → "".
11036 assert_eq!(zjoin(&v(&[]), ' '), "");
11037
11038 // c:3634-3637 — NUL is imeta, so the separator is metafied, NOT a raw
11039 // NUL byte (the prior `arr.join("\0")` bug). The Meta byte (0x83) can't
11040 // survive the UTF-8 String boundary, but the raw NUL must be gone.
11041 let joined = zjoin(&v(&["a", "b"]), '\0');
11042 assert!(
11043 !joined.as_bytes().contains(&0u8),
11044 "NUL delim must be metafied, not emitted as a raw NUL byte: {joined:?}"
11045 );
11046 assert_ne!(joined, "a\u{0}b", "must not raw-join on a meta delimiter");
11047 assert!(joined.starts_with('a') && joined.ends_with('b'));
11048 }
11049
11050 /// findsep port (utils.c:3784). Pure function — no global/cwd state.
11051 #[test]
11052 fn findsep_default_ifs_separators() {
11053 inittyptab(); // ISEP bits are set by the type table (c:4155)
11054 // c:3814-3817 — advance to the first ISEP char, return 1.
11055 let mut s = "foo bar".to_string();
11056 let mut pos = 0usize;
11057 assert_eq!(findsep(&mut s, &mut pos, None, false), 1);
11058 assert_eq!(pos, 3); // points at the space
11059 assert_eq!(&s[..pos], "foo");
11060
11061 // c:3821 — already sitting on a separator: return 0, no advance.
11062 let mut s = " foo".to_string();
11063 let mut pos = 0usize;
11064 assert_eq!(findsep(&mut s, &mut pos, None, false), 0);
11065 assert_eq!(pos, 0);
11066 }
11067
11068 #[test]
11069 fn findsep_quote_strips_escaped_separator() {
11070 inittyptab(); // ISEP bits are set by the type table (c:4155)
11071 // c:3795-3804 — `\<sep>` is not a separator; the backslash is
11072 // stripped in place and the bare char is consumed into the word.
11073 let mut s = "foo\\ bar".to_string(); // foo<bslash><space>bar
11074 let mut pos = 0usize;
11075 let r = findsep(&mut s, &mut pos, None, true);
11076 assert_eq!(r, 1);
11077 assert_eq!(s, "foo bar"); // backslash removed
11078 assert_eq!(pos, s.len()); // whole thing is one word, no real sep
11079 }
11080
11081 #[test]
11082 fn findsep_quote_collapses_double_backslash() {
11083 // c:3796-3799 — `\\` collapses to a single literal backslash.
11084 let mut s = "a\\\\b".to_string(); // a <bslash><bslash> b
11085 let mut pos = 0usize;
11086 let r = findsep(&mut s, &mut pos, None, true);
11087 assert_eq!(r, 1);
11088 assert_eq!(s, "a\\b"); // one backslash left
11089 assert_eq!(pos, s.len()); // no separator present
11090 }
11091
11092 #[test]
11093 fn findsep_literal_multichar_separator() {
11094 // c:3836-3841 — explicit multi-byte literal separator.
11095 let mut s = "a::b".to_string();
11096 let mut pos = 0usize;
11097 assert_eq!(findsep(&mut s, &mut pos, Some(b"::"), false), 1);
11098 assert_eq!(pos, 1); // points at the "::"
11099 assert_eq!(&s[..pos], "a");
11100
11101 // c:3844 — separator absent → -1.
11102 let mut s = "abc".to_string();
11103 let mut pos = 0usize;
11104 assert_eq!(findsep(&mut s, &mut pos, Some(b"x"), false), -1);
11105 }
11106
11107 #[test]
11108 fn findsep_empty_separator_advances_one_char() {
11109 // c:3825-3834 — empty sep just steps past one character.
11110 let mut s = "ab".to_string();
11111 let mut pos = 0usize;
11112 assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), 1);
11113 assert_eq!(pos, 1);
11114
11115 let mut s = String::new();
11116 let mut pos = 0usize;
11117 assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), -1); // c:3833
11118 }
11119
11120 /// c:4033 — `subst_string_by_func` returns `getaparam("reply")`
11121 /// after the hook function finishes. The previous Rust port read
11122 /// `env::var("reply")` and split on NUL — wrong because `reply`
11123 /// is a shell-local PM_ARRAY in paramtab, never exported. This
11124 /// pin sets `reply` via `setaparam` (the paramtab write path)
11125 /// and exercises `subst_string_by_hook` end-to-end isn't viable
11126 /// in unit tests without a function-name hook, so we exercise
11127 /// just the getaparam plumbing.
11128 #[test]
11129 fn getaparam_reads_reply_from_paramtab_not_env() {
11130 let _g = crate::test_util::global_state_lock();
11131 // Stash any prior `reply` value.
11132 let saved = getaparam("reply");
11133
11134 // Write a known reply array via the canonical setaparam path.
11135 let payload = vec!["abbreviated".to_string(), "11".to_string()];
11136 let _ = setaparam("reply", payload.clone());
11137
11138 // The reply array must be reachable via getaparam — not env.
11139 // (env::var would return Err because setaparam never exports
11140 // an un-flagged array to env.)
11141 assert_eq!(
11142 getaparam("reply"),
11143 Some(payload),
11144 "getaparam(\"reply\") must return paramtab array"
11145 );
11146
11147 // Restore.
11148 let _ = setaparam("reply", saved.unwrap_or_default());
11149 }
11150
11151 /// c:1133-1134 — `finddir` reads the global `home` variable (the
11152 /// canonical `$HOME` storage, not `getenv("HOME")`). The global
11153 /// is updated by `homesetfn` (Src/params.c:5118) whenever the user
11154 /// assigns `HOME=...` inside the shell.
11155 /// Regression target: a previous Rust port read `env::var("HOME")`
11156 /// so an in-shell `HOME=...` assignment would not retarget
11157 /// `~`-abbreviation until the user re-exported HOME.
11158 #[test]
11159 fn finddir_uses_paramtab_home_not_env() {
11160 let _g = crate::test_util::global_state_lock();
11161 // C's `homesetfn` (params.c:5118) UNUSED(Param pm) — the
11162 // function ignores its param-pointer argument and writes the
11163 // canonical `char *home` global directly. zshrs's port mirrors
11164 // that: `params::homesetfn(_pm, x)` ignores _pm and updates
11165 // `home_lock()`. So we pass a stack-default param; the paramtab
11166 // wiring (PM_SPECIAL dispatch) is not on the test path.
11167 let mut pm = crate::ported::zsh_h::param::default();
11168 let saved = crate::ported::params::homegetfn(&pm);
11169 let sentinel = "/tmp/zshrs-finddir-pin".to_string();
11170 homesetfn(&mut pm, sentinel.clone());
11171
11172 // `/tmp/zshrs-finddir-pin/x` must abbreviate to `~/x`.
11173 let abbrev = finddir(&format!("{}/x", sentinel));
11174 assert_eq!(
11175 abbrev.as_deref(),
11176 Some("~/x"),
11177 "finddir must consult canonical HOME (got {:?})",
11178 abbrev
11179 );
11180
11181 // Restore.
11182 homesetfn(&mut pm, saved);
11183 }
11184
11185 #[test]
11186 fn test_sepsplit() {
11187 let _g = crate::test_util::global_state_lock();
11188 assert_eq!(sepsplit("a:b:c", Some(":"), false), vec!["a", "b", "c"]);
11189 assert_eq!(sepsplit("a::b", Some(":"), false), vec!["a", "b"]);
11190 assert_eq!(sepsplit("a::b", Some(":"), true), vec!["a", "", "b"]);
11191 }
11192
11193 #[test]
11194 fn test_unmetafy_no_meta_byte_passes_through() {
11195 let _g = crate::test_util::global_state_lock();
11196 // No Meta byte → buffer unchanged, length unchanged.
11197 let mut buf = b"hello".to_vec();
11198 let n = unmetafy(&mut buf);
11199 assert_eq!(n, 5);
11200 assert_eq!(&buf, b"hello");
11201 }
11202
11203 #[test]
11204 fn test_unmetafy_collapses_meta_escapes() {
11205 let _g = crate::test_util::global_state_lock();
11206 // C: Meta byte (0x83) followed by `'a' ^ 32` (0x41 = 'A')
11207 // unmetafies to a single byte 'a' (0x61).
11208 // i.e. {0x83, 'a' ^ 32} → {'a'}.
11209 let mut buf = vec![0x83, b'a' ^ 32];
11210 let n = unmetafy(&mut buf);
11211 assert_eq!(n, 1);
11212 assert_eq!(buf, vec![b'a']);
11213 }
11214
11215 #[test]
11216 fn test_unmetafy_mixed_prefix_then_meta() {
11217 let _g = crate::test_util::global_state_lock();
11218 // Plain prefix, then Meta-escaped 0xFF (0x83, 0xFF ^ 32 = 0xDF).
11219 let mut buf = vec![b'X', b'Y', 0x83, 0xFF ^ 32, b'Z'];
11220 let n = unmetafy(&mut buf);
11221 assert_eq!(n, 4);
11222 assert_eq!(buf, vec![b'X', b'Y', 0xFF, b'Z']);
11223 }
11224
11225 #[test]
11226 fn test_unmetafy_returns_self_value() {
11227 let _g = crate::test_util::global_state_lock();
11228 // C returns `s` (the buffer) for chaining; Rust returns
11229 // the new length. Verify length matches a call that
11230 // collapses two Meta-escapes.
11231 let mut buf = vec![
11232 b'A',
11233 0x83,
11234 b'B' ^ 32, // → 'B'
11235 0x83,
11236 b'C' ^ 32, // → 'C'
11237 b'D',
11238 ];
11239 let n = unmetafy(&mut buf);
11240 assert_eq!(n, 4);
11241 assert_eq!(buf, b"ABCD".to_vec());
11242 }
11243
11244 #[test]
11245 fn test_imeta_byte_threshold() {
11246 let _g = crate::test_util::global_state_lock();
11247 // Canonical IMETA per Src/utils.c:4195-4201:
11248 // - 0x00 (c:4195)
11249 // - 0x83..=0xa2 (Meta through Marker — c:4196-4200)
11250 //
11251 // The previous version of this test asserted `imeta_byte(0xFF) == true`
11252 // based on the WRONG `b >= Meta` predicate the Rust port
11253 // originally used. C `imeta()` reads the typtab; 0xa3..=0xff
11254 // have NO typtab assignment so they're NOT IMETA, and
11255 // `metafy` must NOT escape them.
11256 assert!(imeta_byte(0x00), "c:4195 — NUL is IMETA");
11257 assert!(!imeta_byte(0x82), "0x82 is NOT IMETA (below Meta)");
11258 assert!(imeta_byte(Meta), "c:4196 — Meta (0x83) is IMETA");
11259 assert!(imeta_byte(0xa2), "c:4197 — Marker (0xa2) is IMETA");
11260 // c:4198-4200 upper bound — Nularg is the last IMETA byte.
11261 assert!(imeta_byte(0xa1), "c:4200 — Nularg (0xa1) is IMETA");
11262 // 0xa3..=0xff are NOT IMETA — UTF-8 multi-byte content
11263 // must pass through `metafy` unchanged.
11264 assert!(!imeta_byte(0xa3), "0xa3 NOT IMETA (above Marker)");
11265 assert!(!imeta_byte(0xc3), "0xc3 NOT IMETA (UTF-8 'é' lead)");
11266 assert!(!imeta_byte(0xFF), "0xFF NOT IMETA");
11267 }
11268
11269 #[test]
11270 fn test_meta_constant_value() {
11271 let _g = crate::test_util::global_state_lock();
11272 // Locked at 0x83 by Src/zsh.h. If this test fails, zsh
11273 // bumped the Meta sentinel and the encoding mapping needs
11274 // a full audit.
11275 assert_eq!(Meta, 0x83);
11276 }
11277
11278 #[test]
11279 #[cfg(unix)]
11280 fn test_mode_to_octal_canonical_bits() {
11281 let _g = crate::test_util::global_state_lock();
11282 // rwx for owner = 0o700.
11283 let mode = (S_IRUSR | S_IWUSR | S_IXUSR) as u32;
11284 assert_eq!(mode_to_octal(mode), 0o700);
11285 // rwx all = 0o777.
11286 let all = (S_IRUSR | S_IWUSR | S_IXUSR) as u32 * (1 + 8 + 64);
11287 // Use libc constants individually for portability.
11288 let m = (S_IRUSR
11289 | S_IWUSR
11290 | S_IXUSR
11291 | S_IRGRP
11292 | S_IWGRP
11293 | S_IXGRP
11294 | S_IROTH
11295 | S_IWOTH
11296 | S_IXOTH) as u32;
11297 assert_eq!(mode_to_octal(m), 0o777);
11298 let _ = all;
11299 }
11300
11301 #[test]
11302 #[cfg(unix)]
11303 fn test_mode_to_octal_setuid_setgid_sticky() {
11304 let _g = crate::test_util::global_state_lock();
11305 assert_eq!(mode_to_octal(S_ISUID as u32), 0o4000);
11306 assert_eq!(mode_to_octal(S_ISGID as u32), 0o2000);
11307 assert_eq!(mode_to_octal(S_ISVTX as u32), 0o1000);
11308 // All three: 0o7000.
11309 let all = (S_ISUID | S_ISGID | S_ISVTX) as u32;
11310 assert_eq!(mode_to_octal(all), 0o7000);
11311 }
11312
11313 #[test]
11314 #[cfg(unix)]
11315 fn test_mailstat_plain_file_returns_native_stat() {
11316 let _g = crate::test_util::global_state_lock();
11317 // Plain file path → *st fields mirror native stat,
11318 // not the maildir aggregation.
11319 let mut st: libc::stat = unsafe { std::mem::zeroed() };
11320 let rc = mailstat("/etc/hosts", &mut st);
11321 if rc == 0 {
11322 assert_eq!(
11323 st.st_nlink as u64,
11324 fs::metadata("/etc/hosts").unwrap().nlink()
11325 );
11326 }
11327 }
11328
11329 #[test]
11330 fn test_mailstat_nonexistent_returns_neg1() {
11331 let _g = crate::test_util::global_state_lock();
11332 let mut st: libc::stat = unsafe { std::mem::zeroed() };
11333 assert_eq!(mailstat("/nonexistent/path/does/not/exist", &mut st), -1);
11334 }
11335
11336 #[test]
11337 fn test_mailstat_directory_without_maildir_subdirs() {
11338 let _g = crate::test_util::global_state_lock();
11339 // /tmp is a directory but not a maildir (no cur/tmp/new) —
11340 // returns the partial aggregate (top dir's atime/mtime,
11341 // size=0 since cur/ wasn't found before we'd start summing).
11342 let mut st: libc::stat = unsafe { std::mem::zeroed() };
11343 let rc = mailstat("/tmp", &mut st);
11344 assert_eq!(rc, 0);
11345 assert_eq!(st.st_nlink, 1);
11346 assert_eq!(st.st_size, 0);
11347 assert_eq!(st.st_blocks, 0);
11348 // S_IFDIR bit should be cleared, S_IFREG set.
11349 #[cfg(unix)]
11350 {
11351 assert_eq!(st.st_mode & libc::S_IFDIR, 0);
11352 assert_ne!(st.st_mode & libc::S_IFREG, 0);
11353 }
11354 }
11355
11356 #[test]
11357 fn test_dupstrpfx_byte_counted() {
11358 let _g = crate::test_util::global_state_lock(); // c:161
11359 // 5 bytes of ASCII = 5 chars, identical. Canonical
11360 // port lives at `string.rs:161`; pin from utils.rs's test
11361 // module via the qualified path.
11362 assert_eq!(dupstrpfx("hello", 3), "hel"); // c:161
11363 assert_eq!(dupstrpfx("hi", 10), "hi"); // c:161
11364 assert_eq!(dupstrpfx("anything", 0), ""); // c:161
11365 }
11366
11367 #[test]
11368 fn test_metafy_passes_through_ascii() {
11369 let _g = crate::test_util::global_state_lock();
11370 // ASCII bytes (< 0x83) stay untouched.
11371 assert_eq!(metafy("hello"), "hello");
11372 assert_eq!(metafy(""), "");
11373 }
11374
11375 #[test]
11376 fn test_metafy_imeta_predicate_matches_c_macro() {
11377 let _g = crate::test_util::global_state_lock();
11378 // Canonical C IMETA per Src/utils.c:4195-4201:
11379 // - 0x00 (c:4195)
11380 // - 0x83 (Meta, c:4196)
11381 // - 0x84..=0x9c (Pound..LAST_NORMAL_TOK=Bang, c:4198)
11382 // - 0x9d..=0xa1 (Snull..Nularg, c:4200)
11383 // - 0xa2 (Marker, c:4197)
11384 //
11385 // The closed set is {0x00, 0x83..=0xa2}. Every other byte
11386 // (0x01..=0x82, 0xa3..=0xff) is NOT IMETA in C and so must
11387 // NOT be Meta-encoded by `metafy`. The previous Rust
11388 // hardcoded predicate `b >= 0x83` falsely marked
11389 // 0xa3..=0xff as IMETA, corrupting UTF-8 multibyte
11390 // content.
11391
11392 // NUL is IMETA (c:4195).
11393 assert!(imeta_byte(0x00), "c:4195 — '\\0' IS imeta");
11394
11395 // 0x01..=0x82 are NOT IMETA (no typtab assignment for them).
11396 for b in 0x01u8..=0x82 {
11397 assert!(!imeta_byte(b), "byte {:#x} should NOT be imeta", b);
11398 }
11399
11400 // 0x83..=0xa2 ARE IMETA (the canonical full range).
11401 for b in 0x83u8..=0xa2 {
11402 assert!(imeta_byte(b), "c:4196-4200 — byte {:#x} IS imeta", b);
11403 }
11404
11405 // 0xa3..=0xff are NOT IMETA. C `imeta()` returns false
11406 // for these so `metafy` must pass them through unchanged.
11407 // UTF-8 continuation bytes (0x80..=0xbf) and multi-byte
11408 // leads (0xc0..=0xff) live here and must round-trip.
11409 for b in 0xa3u8..=0xff {
11410 assert!(
11411 !imeta_byte(b),
11412 "byte {:#x} should NOT be imeta (no c:4195-4201 assignment)",
11413 b
11414 );
11415 }
11416 }
11417
11418 /// Pin: `metafy` passes UTF-8 multibyte bytes through unchanged
11419 /// when they fall outside the canonical IMETA range. Previously
11420 /// the broader `b >= 0x83` predicate corrupted every UTF-8
11421 /// continuation byte and lead byte that wasn't a token marker.
11422 #[test]
11423 fn metafy_preserves_utf8_high_bytes_outside_imeta_range() {
11424 let _g = crate::test_util::global_state_lock();
11425 // 'é' = U+00E9 = UTF-8 0xc3 0xa9. Both bytes are >= 0x83 BUT
11426 // both are also > 0xa2, so they're NOT IMETA per the typtab.
11427 // C `metafy` passes them through unchanged.
11428 let input = std::str::from_utf8(&[0xC3, 0xA9]).unwrap();
11429 let out = metafy(input);
11430 // Should round-trip the two bytes exactly (no Meta escape).
11431 let out_bytes = out.as_bytes();
11432 assert_eq!(
11433 out_bytes,
11434 &[0xC3, 0xA9],
11435 "c:4196-4200 — UTF-8 bytes 0xc3/0xa9 outside IMETA range must pass through"
11436 );
11437 }
11438
11439 #[test]
11440 fn test_ztrcmp_meta_aware() {
11441 let _g = crate::test_util::global_state_lock();
11442 // Two identical metafied strings → Equal.
11443 assert_eq!(ztrcmp("foo", "foo"), std::cmp::Ordering::Equal);
11444 // "foo" < "foz".
11445 assert_eq!(ztrcmp("foo", "foz"), std::cmp::Ordering::Less);
11446 // Prefix comparison: shorter < longer.
11447 assert_eq!(ztrcmp("foo", "foobar"), std::cmp::Ordering::Less);
11448 // Meta-encoded comparison: {0x83, 'a'^32} should compare as 'a'.
11449 let s_meta = unsafe { std::str::from_utf8_unchecked(&[0x83, b'a' ^ 32]) };
11450 let s_plain = "a";
11451 // Meta-encoded "a" should compare equal to plain "a".
11452 assert_eq!(ztrcmp(s_meta, s_plain), std::cmp::Ordering::Equal);
11453 }
11454
11455 #[test]
11456 fn test_skipwsep_skips_runs() {
11457 let _g = crate::test_util::global_state_lock();
11458 // 3 spaces + 'x' → returns ("x", 3).
11459 let (rest, n) = skipwsep(" x");
11460 assert_eq!(rest, "x");
11461 assert_eq!(n, 3);
11462 // No leading whitespace → 0 skipped.
11463 let (rest, n) = skipwsep("foo");
11464 assert_eq!(rest, "foo");
11465 assert_eq!(n, 0);
11466 // Mix of space/tab/newline.
11467 let (rest, n) = skipwsep(" \t\nbar");
11468 assert_eq!(rest, "bar");
11469 assert_eq!(n, 3);
11470 }
11471
11472 #[test]
11473 fn test_imeta_macro_threshold() {
11474 let _g = crate::test_util::global_state_lock(); // c:60
11475 // `Src/ztype.h:60` `imeta(X) zistype(X, IMETA)` — typtab-driven
11476 // predicate. Per `Src/utils.c:4195-4201`, IMETA is set on:
11477 // NUL, Meta=0x83, Marker=0xa2, and the Pound..Nularg ITOK range
11478 // (0x84..=0xa2). Routes through canonical
11479 // `ztype_h::imeta(u8)`; init the typtab first since the
11480 // canonical port reads through it.
11481 inittyptab(); // c:4148
11482 assert!(imeta(0x00), "c:4195 — NUL is IMETA"); // c:4195
11483 assert!(imeta(Meta), "c:4196 — Meta (0x83) is IMETA"); // c:4196
11484 assert!(imeta(0xa2), "c:4197 — Marker (0xa2) is IMETA"); // c:4197
11485 assert!(
11486 imeta(0x84),
11487 "c:4199-4201 — Pound (0x84) is IMETA via ITOK range"
11488 );
11489 assert!(
11490 imeta(0x9b),
11491 "c:4199-4201 — Dash sentinel within ITOK range is IMETA"
11492 );
11493 assert!(!imeta(0x82), "c:4170 — 0x82 is ICNTRL, not IMETA"); // c:4170
11494 assert!(!imeta(0xa3), "byte 0xa3 is past Marker — NOT IMETA");
11495 assert!(
11496 !imeta(0xff),
11497 "byte 0xff is NOT IMETA (the prior `>= Meta` over-report)"
11498 );
11499 assert!(!imeta(b' '), "space is not IMETA");
11500 assert!(!imeta(b'A'), "'A' is not IMETA");
11501 }
11502
11503 #[test]
11504 fn test_unmeta_routes_through_unmetafy() {
11505 let _g = crate::test_util::global_state_lock();
11506 // unmeta wraps the in-place unmetafy via a byte-vector
11507 // copy; the no-Meta fast path returns the source as-is.
11508 assert_eq!(unmeta("plain"), "plain");
11509 }
11510
11511 #[test]
11512 fn test_iwsep_includes_newline() {
11513 let _g = crate::test_util::global_state_lock(); // c:61
11514 // The previous port omitted '\n' which broke wordcount on
11515 // multi-line input. Routes through canonical
11516 // `ztype_h::iwsep` (`Src/ztype.h:61`).
11517 assert!(iwsep(b'\n')); // c:61
11518 assert!(iwsep(b'\t')); // c:61
11519 assert!(iwsep(b' ')); // c:61
11520 assert!(!iwsep(b'a')); // c:61
11521 }
11522
11523 #[test]
11524 fn test_mailstat_aggregates_maildir() {
11525 let _g = crate::test_util::global_state_lock();
11526 // Create a temp maildir layout with 2 messages in new/ and 1
11527 // in cur/, verify the aggregate.
11528 let tmp = std::env::temp_dir().join(format!("zshrs_mailstat_test_{}", std::process::id()));
11529 let _ = fs::remove_dir_all(&tmp);
11530 fs::create_dir_all(tmp.join("cur")).unwrap();
11531 fs::create_dir_all(tmp.join("new")).unwrap();
11532 fs::create_dir_all(tmp.join("tmp")).unwrap();
11533 let mut f = fs::File::create(tmp.join("new").join("msg1")).unwrap();
11534 f.write_all(b"hello").unwrap();
11535 let mut f = fs::File::create(tmp.join("new").join("msg2")).unwrap();
11536 f.write_all(b"world!").unwrap();
11537 let mut f = fs::File::create(tmp.join("cur").join("msg3")).unwrap();
11538 f.write_all(b"third").unwrap();
11539 let mut st: libc::stat = unsafe { std::mem::zeroed() };
11540 let rc = mailstat(tmp.to_str().unwrap(), &mut st);
11541 assert_eq!(rc, 0, "maildir should stat");
11542 assert_eq!(st.st_blocks, 3, "3 messages total across new/ + cur/");
11543 assert_eq!(st.st_size, 5 + 6 + 5, "5+6+5 bytes total");
11544 let _ = fs::remove_dir_all(&tmp);
11545 }
11546
11547 #[test]
11548 fn test_spacesplit() {
11549 let _g = crate::test_util::global_state_lock();
11550 assert_eq!(spacesplit("a b c", false), vec!["a", "b", "c"]);
11551 assert_eq!(spacesplit("a b", false), vec!["a", "b"]);
11552 }
11553
11554 #[test]
11555 fn test_sepjoin() {
11556 let _g = crate::test_util::global_state_lock();
11557 assert_eq!(
11558 sepjoin(&["a".into(), "b".into(), "c".into()], Some(":")),
11559 "a:b:c"
11560 );
11561 assert_eq!(sepjoin(&["a".into(), "b".into()], None), "a b");
11562 }
11563
11564 #[test]
11565 fn test_isident() {
11566 let _g = crate::test_util::global_state_lock(); // c:1288
11567 // Canonical port lives at `params.rs:2056` (`Src/params.c:1288`).
11568 assert!(isident("foo")); // c:1288
11569 assert!(isident("_bar")); // c:1288
11570 assert!(isident("baz123")); // c:1288
11571 assert!(!isident("123abc")); // c:1288
11572 assert!(!isident("foo-bar")); // c:1288
11573 }
11574
11575 #[test]
11576 fn test_nicechar() {
11577 let _g = crate::test_util::global_state_lock();
11578 assert_eq!(nicechar('\n'), "\\n");
11579 assert_eq!(nicechar('\t'), "\\t");
11580 assert_eq!(nicechar('a'), "a");
11581 }
11582
11583 #[test]
11584 fn test_quotedzputs_single_quote_wrap() {
11585 let _g = crate::test_util::global_state_lock();
11586 assert_eq!(quotedzputs("simple"), "simple");
11587 assert_eq!(quotedzputs("has space"), "'has space'");
11588 assert_eq!(quotedzputs("it's"), "'it'\\''s'");
11589 }
11590
11591 #[test]
11592 fn test_quotestring_backslash() {
11593 let _g = crate::test_util::global_state_lock();
11594 assert_eq!(quotestring("hello", QT_BACKSLASH), "hello");
11595 assert_eq!(quotestring("has space", QT_BACKSLASH), "has\\ space");
11596 assert_eq!(quotestring("$var", QT_BACKSLASH), "\\$var");
11597 }
11598
11599 /// Pin: `ispecial(c)` matches the canonical SPECCHARS set at
11600 /// `Src/zsh.h:228` exactly: `"#$^*()=|{}[]\`<>?~;&\n\t \\'\""`.
11601 /// Previously the Rust local `ispecial` included `!` unconditionally
11602 /// which diverged from C — C only ISPECIAL-tags bangchar (default
11603 /// `!`) under BANGHIST + interactive (per `Src/utils.c:4257-4261`).
11604 ///
11605 /// This test exercises the path indirectly via `quotestring` with
11606 /// `QT_BACKSLASH` (which prepends `\` before every ispecial char).
11607 #[test]
11608 fn quotestring_backslash_only_specchars_no_bang_in_default() {
11609 let _g = crate::test_util::global_state_lock();
11610 // `!` is NOT in canonical SPECCHARS — should NOT be backslashed
11611 // in default non-interactive mode (matches C).
11612 assert_eq!(
11613 quotestring("a!b", QT_BACKSLASH),
11614 "a!b",
11615 "c:228 — `!` is not in SPECCHARS; only bangchar+BANGHIST adds it"
11616 );
11617 // `,` is NOT in canonical SPECCHARS until makecommaspecial(1).
11618 assert_eq!(
11619 quotestring("a,b", QT_BACKSLASH),
11620 "a,b",
11621 "c:228 — `,` not in SPECCHARS until makecommaspecial(1)"
11622 );
11623 // `^` IS in canonical SPECCHARS (per c:228 `"#$^..."`).
11624 assert_eq!(
11625 quotestring("a^b", QT_BACKSLASH),
11626 "a\\^b",
11627 "c:228 — `^` is in SPECCHARS"
11628 );
11629 // Open and close braces are in canonical SPECCHARS.
11630 assert_eq!(
11631 quotestring("a{b", QT_BACKSLASH),
11632 "a\\{b",
11633 "c:228 — open-brace is in SPECCHARS"
11634 );
11635 assert_eq!(
11636 quotestring("a}b", QT_BACKSLASH),
11637 "a\\}b",
11638 "c:228 — close-brace is in SPECCHARS"
11639 );
11640 // `#` is in canonical SPECCHARS (first char of c:228).
11641 assert_eq!(
11642 quotestring("a#b", QT_BACKSLASH),
11643 "a\\#b",
11644 "c:228 — `#` is the first char of SPECCHARS"
11645 );
11646 // `\\` (literal backslash) is in canonical SPECCHARS.
11647 assert_eq!(
11648 quotestring("a\\b", QT_BACKSLASH),
11649 "a\\\\b",
11650 "c:228 — `\\\\` in SPECCHARS"
11651 );
11652 }
11653
11654 #[test]
11655 fn test_quotestring_single() {
11656 let _g = crate::test_util::global_state_lock();
11657 assert_eq!(quotestring("hello", QT_SINGLE), "'hello'");
11658 assert_eq!(quotestring("it's", QT_SINGLE), "'it'\\''s'");
11659 }
11660
11661 #[test]
11662 fn test_quotestring_double() {
11663 let _g = crate::test_util::global_state_lock();
11664 assert_eq!(quotestring("hello", QT_DOUBLE), "\"hello\"");
11665 assert_eq!(quotestring("say \"hi\"", QT_DOUBLE), "\"say \\\"hi\\\"\"");
11666 }
11667
11668 #[test]
11669 fn test_quotestring_dollars() {
11670 let _g = crate::test_util::global_state_lock();
11671 assert_eq!(quotestring("hello", QT_DOLLARS), "$'hello'");
11672 assert_eq!(quotestring("line\nbreak", QT_DOLLARS), "$'line\\nbreak'");
11673 assert_eq!(quotestring("tab\there", QT_DOLLARS), "$'tab\\there'");
11674 }
11675
11676 #[test]
11677 fn test_quotestring_pattern() {
11678 let _g = crate::test_util::global_state_lock();
11679 assert_eq!(quotestring("*.txt", QT_BACKSLASH_PATTERN), "\\*.txt");
11680 assert_eq!(quotestring("file[1]", QT_BACKSLASH_PATTERN), "file\\[1\\]");
11681 }
11682
11683 #[test]
11684 fn test_quotetype_from_q_count() {
11685 let _g = crate::test_util::global_state_lock();
11686 assert_eq!(qflag_quotetype(1), QT_BACKSLASH);
11687 assert_eq!(qflag_quotetype(2), QT_SINGLE);
11688 assert_eq!(qflag_quotetype(3), QT_DOUBLE);
11689 assert_eq!(qflag_quotetype(4), QT_DOLLARS);
11690 }
11691
11692 #[test]
11693 fn test_tulower_tuupper() {
11694 let _g = crate::test_util::global_state_lock();
11695 assert_eq!(tulower('A'), 'a');
11696 assert_eq!(tuupper('a'), 'A');
11697 assert_eq!(tulower('1'), '1');
11698 }
11699
11700 #[test]
11701 fn test_wordcount_ifs_default() {
11702 let _g = crate::test_util::global_state_lock();
11703 // C: wordcount("a b c", NULL, 0) -> 3
11704 assert_eq!(wordcount("a b c", None, 0), 3);
11705 // Leading/trailing whitespace coalesced when mul <= 0.
11706 assert_eq!(wordcount(" a b ", None, 0), 2);
11707 // Empty string with mul == 0 -> 0 words.
11708 assert_eq!(wordcount("", None, 0), 0);
11709 // Single word, no separators.
11710 assert_eq!(wordcount("foo", None, 0), 1);
11711 }
11712
11713 #[test]
11714 fn test_wordcount_with_explicit_sep() {
11715 let _g = crate::test_util::global_state_lock();
11716 // C: wordcount("a:b:c", ":", 0) -> 3 (3 fields, 2 separators)
11717 assert_eq!(wordcount("a:b:c", Some(":"), 0), 3);
11718 // Empty fields counted when mul != 0.
11719 assert_eq!(wordcount("a::b", Some(":"), 1), 3);
11720 // Without mul, consecutive empties collapse: a, b => 2... but
11721 // C's "if ((c || mul) && (sl || *(s+sl)))" — second `:` has
11722 // c=0 and mul=0 so doesn't increment. Result: a, b => 2.
11723 assert_eq!(wordcount("a::b", Some(":"), 0), 2);
11724 }
11725
11726 #[test]
11727 fn test_ucs4tomb_ascii() {
11728 let _g = crate::test_util::global_state_lock();
11729 let mut buf = [0u8; 8];
11730 // 'A' = 0x41, ASCII, single byte in any locale.
11731 let n = ucs4tomb('A' as u32, &mut buf);
11732 // wctomb may return 1 in C/POSIX locale; in UTF-8 locale also 1.
11733 assert_eq!(n, 1);
11734 assert_eq!(buf[0], b'A');
11735 }
11736
11737 #[test]
11738 fn test_is_mb_niceformat_plain_ascii() {
11739 let _g = crate::test_util::global_state_lock();
11740 // Plain printable ASCII — nothing needs nicechar escaping.
11741 assert_eq!(is_mb_niceformat("hello world"), 0);
11742 }
11743
11744 #[test]
11745 fn test_is_mb_niceformat_with_control_char() {
11746 let _g = crate::test_util::global_state_lock();
11747 // Tab is control (< 0x20) — needs nice escaping.
11748 assert_eq!(is_mb_niceformat("a\tb"), 1);
11749 // Bell character.
11750 assert_eq!(is_mb_niceformat("a\x07b"), 1);
11751 }
11752
11753 /// c:4856/4954 — `metafy` + `unmetafy` MUST round-trip for ASCII.
11754 /// A regression in the round-trip corrupts every metafied buffer
11755 /// the lexer/param-subst pipeline produces.
11756 #[test]
11757 fn metafy_unmetafy_round_trips_for_ascii() {
11758 let _g = crate::test_util::global_state_lock();
11759 let s = "hello world";
11760 let m = metafy(s);
11761 let mut buf = m.into_bytes();
11762 unmetafy(&mut buf);
11763 assert_eq!(std::str::from_utf8(&buf).unwrap(), s);
11764 }
11765
11766 /// `ztrlen` counts metafied characters (Meta-pairs as 1).
11767 /// Regression that double-counts Meta-pair bytes would break
11768 /// every fixed-width column path (printf %s).
11769 #[test]
11770 fn ztrlen_counts_ascii_one_per_byte() {
11771 let _g = crate::test_util::global_state_lock();
11772 assert_eq!(ztrlen(""), 0);
11773 assert_eq!(ztrlen("a"), 1);
11774 assert_eq!(ztrlen("hello"), 5);
11775 }
11776
11777 /// `Src/utils.c:5141-5149` — Meta-byte pair counts as 1 char.
11778 /// `*s++ == Meta` advances 2 bytes per iteration but increments
11779 /// `l` only once. Pin: a string with one Meta+X pair counts as 1.
11780 #[test]
11781 fn ztrlen_counts_meta_pair_as_one() {
11782 let _g = crate::test_util::global_state_lock();
11783 // META (0x83) + 0x20 = unmetafies to one '\0' byte (or a NUL).
11784 let meta = char::from_u32(Meta as u32).unwrap();
11785 let s: String = [meta, '\x20'].iter().collect();
11786 assert_eq!(
11787 ztrlen(&s),
11788 1,
11789 "c:5141-5148 — Meta+X pair counts as ONE char, not two"
11790 );
11791 // Mixed: "a" + META + "x" + "b" = 3 unmetafied chars.
11792 let mixed: String = ['a', meta, '\x20', 'b'].iter().collect();
11793 assert_eq!(
11794 ztrlen(&mixed),
11795 3,
11796 "c:5141 — three unmetafied chars from 'a' + Meta+X + 'b'"
11797 );
11798 }
11799
11800 /// `Src/utils.c:2579-2618` — `setblock_fd(turnonblocking, fd, modep)`.
11801 /// Signature pin: previously the Rust port had a 2-arg
11802 /// `(fd, blocking: bool) -> bool` signature, swapping the
11803 /// turnonblocking/fd argument order vs C AND collapsing the
11804 /// 3rd `*modep` out-param entirely. The fix restored canonical
11805 /// C order `(turnonblocking, fd)` with a `(bool, c_long)` tuple
11806 /// return mirroring `int return + long *modep`.
11807 ///
11808 /// Also pin the c:2599 regular-file short-circuit: C only
11809 /// operates on non-regular fds (pipes, sockets, ttys). A regular
11810 /// file returns `(false, -1)` immediately.
11811 #[cfg(unix)]
11812 #[test]
11813 fn setblock_fd_skips_regular_files_per_c_2599() {
11814 let _g = crate::test_util::global_state_lock();
11815 // Open a regular tempfile.
11816 let dir = tempfile::TempDir::new().unwrap();
11817 let f = fs::File::create(dir.path().join("regular")).unwrap();
11818 let fd = f.as_raw_fd();
11819 let (changed, mode) = setblock_fd(true, fd);
11820 assert!(
11821 !changed,
11822 "c:2599 — regular files short-circuit; setblock_fd must NOT report a change"
11823 );
11824 assert_eq!(mode, -1, "c:2614 — `*modep = -1` for regular files");
11825 }
11826
11827 /// `Src/utils.c:2606-2611` — `setblock_fd(turnonblocking=true, fd)`
11828 /// clears O_NONBLOCK on a pipe (non-regular fd). Returns
11829 /// `(true, prior_flags)` if the state was changed.
11830 #[cfg(unix)]
11831 #[test]
11832 fn setblock_fd_clears_o_nonblock_on_pipe() {
11833 let _g = crate::test_util::global_state_lock();
11834 // Create a pipe — non-regular fd.
11835 let mut pipefd: [libc::c_int; 2] = [0; 2];
11836 let r = unsafe { libc::pipe(pipefd.as_mut_ptr()) };
11837 assert_eq!(r, 0, "pipe(2) must succeed");
11838 let read_fd = pipefd[0];
11839 let write_fd = pipefd[1];
11840 // Force NONBLOCK on the read end.
11841 let cur = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11842 unsafe {
11843 libc::fcntl(read_fd, libc::F_SETFL, cur | libc::O_NONBLOCK);
11844 }
11845 // Verify NONBLOCK is set.
11846 let now = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11847 assert_ne!(
11848 now & libc::O_NONBLOCK,
11849 0,
11850 "test setup: NONBLOCK should be set"
11851 );
11852 // Call setblock_fd to ENABLE blocking (clear NONBLOCK).
11853 let (changed, _mode) = setblock_fd(true, read_fd);
11854 assert!(
11855 changed,
11856 "c:2611 — turnonblocking=true on a NONBLOCK pipe must report state change"
11857 );
11858 // Verify NONBLOCK is now cleared.
11859 let after = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11860 assert_eq!(
11861 after & libc::O_NONBLOCK,
11862 0,
11863 "c:2611 — O_NONBLOCK must be cleared after turnonblocking=true"
11864 );
11865 // Cleanup.
11866 unsafe {
11867 libc::close(read_fd);
11868 libc::close(write_fd);
11869 }
11870 }
11871
11872 /// `Src/utils.c:2620-2625` — `setblock_stdin()`. C body
11873 /// `setblock_fd(1, 0, &mode)` enables BLOCKING on fd 0 (stdin).
11874 /// Previously the Rust port called `setblock_fd(0, false)`
11875 /// which DISABLES blocking — exact opposite. Pin via real fd
11876 /// inspection.
11877 #[cfg(unix)]
11878 #[test]
11879 fn setblock_stdin_enables_blocking_on_fd_zero() {
11880 let _g = crate::test_util::global_state_lock();
11881 // Set stdin to NONBLOCKING first to verify the function
11882 // ACTUALLY switches it back to blocking. Skip if stdin is
11883 // not a normal fd (some CI configurations).
11884 let cur = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11885 if cur < 0 {
11886 return;
11887 }
11888 // Force NONBLOCK first.
11889 unsafe {
11890 libc::fcntl(0, libc::F_SETFL, cur | libc::O_NONBLOCK);
11891 }
11892 let after_set_nb = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11893 if after_set_nb & libc::O_NONBLOCK == 0 {
11894 // System rejected the change (regular file? CI tty?) — skip.
11895 unsafe {
11896 libc::fcntl(0, libc::F_SETFL, cur);
11897 }
11898 return;
11899 }
11900 // Call setblock_stdin — should CLEAR O_NONBLOCK.
11901 setblock_stdin();
11902 let after_setblock = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11903 assert_eq!(
11904 after_setblock & libc::O_NONBLOCK,
11905 0,
11906 "c:2624 — setblock_stdin must CLEAR O_NONBLOCK (enable blocking)"
11907 );
11908 // Restore original flags.
11909 unsafe {
11910 libc::fcntl(0, libc::F_SETFL, cur);
11911 }
11912 }
11913
11914 /// `Src/utils.c:2437-2519` — `zstrtol_underscore(s, base, false)`.
11915 /// Base-10 (explicit) parses canonical decimal.
11916 #[test]
11917 fn zstrtol_underscore_base_10_parses_decimal() {
11918 let _g = crate::test_util::global_state_lock();
11919 let (v, rest) = zstrtol_underscore("12345", 10, false);
11920 assert_eq!(v, 12345, "c:2471 — decimal accumulator");
11921 assert_eq!(rest, "", "rest is empty after full consumption");
11922 // With trailing non-digit.
11923 let (v, rest) = zstrtol_underscore("100abc", 10, false);
11924 assert_eq!(v, 100);
11925 assert_eq!(
11926 rest, "abc",
11927 "c:2467 — loop exits at first non-digit; rest carries on"
11928 );
11929 }
11930
11931 /// c:2452-2461 — base==0 autodetect: `0x`→16, `0b`→2, leading
11932 /// `0`→8 (always, unlike `zstrtoul_underscore` which honors
11933 /// OCTALZEROES). `zstrtol_underscore` does NOT consult
11934 /// OCTALZEROES — pure prefix detection.
11935 #[test]
11936 fn zstrtol_underscore_base_zero_autodetects_prefix() {
11937 let _g = crate::test_util::global_state_lock();
11938 // Hex.
11939 assert_eq!(
11940 zstrtol_underscore("0xff", 0, false).0,
11941 255,
11942 "c:2455 — 0x → base 16"
11943 );
11944 assert_eq!(zstrtol_underscore("0XFF", 0, false).0, 255);
11945 // Binary.
11946 assert_eq!(
11947 zstrtol_underscore("0b1010", 0, false).0,
11948 10,
11949 "c:2457 — 0b → base 2"
11950 );
11951 // Leading 0 → octal (always, no OCTALZEROES gate).
11952 assert_eq!(
11953 zstrtol_underscore("0777", 0, false).0,
11954 511,
11955 "c:2460 — leading 0 → octal (no OCTALZEROES gate for zstrtol)"
11956 );
11957 // Decimal default.
11958 assert_eq!(zstrtol_underscore("12345", 0, false).0, 12345);
11959 }
11960
11961 /// c:2447-2450 — leading `-` and `+` consumed; `-` triggers
11962 /// negation at the end (c:2497-2498).
11963 #[test]
11964 fn zstrtol_underscore_handles_sign_chars() {
11965 let _g = crate::test_util::global_state_lock();
11966 assert_eq!(
11967 zstrtol_underscore("-42", 10, false).0,
11968 -42,
11969 "c:2447 — leading `-` → negate"
11970 );
11971 assert_eq!(
11972 zstrtol_underscore("+42", 10, false).0,
11973 42,
11974 "c:2449 — leading `+` consumed, no negation"
11975 );
11976 // Mixed whitespace + sign.
11977 assert_eq!(
11978 zstrtol_underscore(" -100", 10, false).0,
11979 -100,
11980 "c:2444 — leading whitespace skipped, then sign"
11981 );
11982 }
11983
11984 /// c:2466-2492 — base 16 (hex) accumulator: digits 0-9 + letters
11985 /// a-f / A-F via `idigit(*s)` || `'a' ≤ *s < 'a'+base-10`.
11986 /// Pin both upper- and lower-case.
11987 #[test]
11988 fn zstrtol_underscore_base_16_accepts_letters() {
11989 let _g = crate::test_util::global_state_lock();
11990 assert_eq!(
11991 zstrtol_underscore("ff", 16, false).0,
11992 255,
11993 "c:2485 — base-16 'ff' → 255"
11994 );
11995 assert_eq!(
11996 zstrtol_underscore("FF", 16, false).0,
11997 255,
11998 "c:2485 — base-16 'FF' → 255 (upper case via `*s & 0x1f`)"
11999 );
12000 assert_eq!(
12001 zstrtol_underscore("DEADbeef", 16, false).0,
12002 0xDEADBEEF,
12003 "c:2485 — mixed case 32-bit hex"
12004 );
12005 }
12006
12007 /// c:2468/2482 — `underscore` flag enables digit-separator. With
12008 /// underscore=false, `_` is treated as end-of-digits. With
12009 /// underscore=true, `_` is skipped (c:2469-2470).
12010 #[test]
12011 fn zstrtol_underscore_underscore_flag() {
12012 let _g = crate::test_util::global_state_lock();
12013 // underscore=false: `_` terminates parse.
12014 let (v, rest) = zstrtol_underscore("1_000", 10, false);
12015 assert_eq!(v, 1, "c:2467 — underscore=false: `_` terminates");
12016 assert_eq!(rest, "_000");
12017 // underscore=true: `_` accepted and skipped.
12018 let (v, rest) = zstrtol_underscore("1_000_000", 10, true);
12019 assert_eq!(
12020 v, 1_000_000,
12021 "c:2469-2470 — underscore=true: `_` skipped during accumulation"
12022 );
12023 assert_eq!(rest, "", "fully consumed including `_`s");
12024 }
12025
12026 /// `Src/utils.c:2750-2774` — `timespec_diff_us(t1, t2)` returns
12027 /// the signed microsecond delta `t2 - t1`. Pin both sign
12028 /// conventions: t2 after t1 → positive; t2 before t1 → negative.
12029 #[test]
12030 fn timespec_diff_us_sign_matches_c_t2_minus_t1() {
12031 let _g = crate::test_util::global_state_lock();
12032 let t1 = std::time::Instant::now();
12033 std::thread::sleep(std::time::Duration::from_millis(2));
12034 let t2 = std::time::Instant::now();
12035 // c:2759 — `diff_sec = t2 - t1` for the t2 > t1 path. Result
12036 // positive (microseconds elapsed).
12037 let d = timespec_diff_us(&t1, &t2);
12038 assert!(d > 0, "c:2759 — t2 after t1 → positive delta");
12039 assert!(d >= 1000, "expected >= 1ms (1000us); got {}us", d);
12040 // c:2754 — reversed (t1 > t2): result negative.
12041 let r = timespec_diff_us(&t2, &t1);
12042 assert!(r < 0, "c:2770 — t1 after t2 → negative delta");
12043 assert_eq!(d, -r, "swap of args negates the result");
12044 }
12045
12046 /// c:2752 — `timespec_diff_us(t, t)` is 0 (identical Instants).
12047 #[test]
12048 fn timespec_diff_us_same_instant_returns_zero() {
12049 let _g = crate::test_util::global_state_lock();
12050 let t = std::time::Instant::now();
12051 assert_eq!(
12052 timespec_diff_us(&t, &t),
12053 0,
12054 "c:2752 — identical Instants → 0"
12055 );
12056 }
12057
12058 /// `Src/utils.c:6743-6779` — `ucs4toutf8(dest, wval)`. Encodes
12059 /// a Unicode codepoint to UTF-8. Pin canonical 1-, 2-, 3-, and
12060 /// 4-byte encodings.
12061 #[test]
12062 fn ucs4toutf8_encodes_canonical_lengths() {
12063 let _g = crate::test_util::global_state_lock();
12064 // c:6750 — 1 byte: ASCII range [0, 0x80).
12065 assert_eq!(
12066 ucs4toutf8(0x41),
12067 Some("A".to_string()),
12068 "c:6750 — 0x41 → 'A' (1 byte)"
12069 );
12070 // c:6752 — 2 bytes: [0x80, 0x800). 'é' = U+00E9.
12071 assert_eq!(
12072 ucs4toutf8(0xe9),
12073 Some("é".to_string()),
12074 "c:6752 — U+00E9 → 'é' (2 bytes)"
12075 );
12076 // c:6754 — 3 bytes: [0x800, 0x10000). '字' = U+5B57.
12077 assert_eq!(
12078 ucs4toutf8(0x5B57),
12079 Some("字".to_string()),
12080 "c:6754 — U+5B57 → '字' (3 bytes)"
12081 );
12082 // c:6756 — 4 bytes: [0x10000, 0x200000). '𝄞' = U+1D11E.
12083 assert_eq!(
12084 ucs4toutf8(0x1D11E),
12085 Some("𝄞".to_string()),
12086 "c:6756 — U+1D11E → '𝄞' (4 bytes)"
12087 );
12088 }
12089
12090 /// `Src/utils.c:6762-6764` — values `>= 0x80000000` are
12091 /// out-of-range; C emits `zerr("character not in range")` and
12092 /// returns `-1`. Surrogates (U+D800..U+DFFF) and values in
12093 /// 0x110000..0x80000000 are *encoded as raw bytes* by C
12094 /// (matches `wctomb(3)` extended UCS-4 range).
12095 #[test]
12096 fn ucs4toutf8_rejects_invalid_codepoints() {
12097 let _g = crate::test_util::global_state_lock();
12098 // c:6762-6764 — value >= 0x80000000 → None.
12099 assert_eq!(
12100 ucs4toutf8(0xFFFF_FFFE),
12101 None,
12102 "c:6763 — values >= 0x80000000 → None"
12103 );
12104 assert_eq!(
12105 ucs4toutf8(0x8000_0000),
12106 None,
12107 "c:6760-6764 — exactly 0x80000000 is out of range"
12108 );
12109 // Surrogates encode as 3-byte sequences per C bit-pattern
12110 // (c:6754 → len=3). The C source has no Unicode-validity
12111 // gate; that's a wctomb-only concern in callers.
12112 assert!(
12113 ucs4toutf8(0xD800).is_some(),
12114 "c:6754 — surrogates encode as 3-byte UTF-8 in C (no Unicode validity check)"
12115 );
12116 }
12117
12118 /// `Src/utils.c:6648-6723` — `dquotedztrdup(s)`. Default (non-
12119 /// CSHJUNKIEQUOTES) arm: wrap whole string in `"..."`. Backslash
12120 /// doubles, `"`/`$`/`` ` `` get escaped.
12121 #[test]
12122 fn dquotedztrdup_default_path_wraps_whole_string() {
12123 let _g = crate::test_util::global_state_lock();
12124 if isset(CSHJUNKIEQUOTES) {
12125 return; // Default-only test.
12126 }
12127 // c:6690 — `*p++ = '"';` then iterate.
12128 assert_eq!(
12129 dquotedztrdup("hello"),
12130 "\"hello\"",
12131 "c:6690+6711+6718 — wrap plain text in double quotes"
12132 );
12133 // c:6703-6708 — `$` gets `\$`.
12134 assert_eq!(
12135 dquotedztrdup("$var"),
12136 "\"\\$var\"",
12137 "c:6703-6708 — `$` → `\\$`"
12138 );
12139 // c:6703-6708 — `"` gets `\"`.
12140 assert_eq!(
12141 dquotedztrdup("a\"b"),
12142 "\"a\\\"b\"",
12143 "c:6703-6708 — `\"` → `\\\"`"
12144 );
12145 }
12146
12147 /// c:6697-6701 — backslash handling with `pending` state.
12148 /// Single `\` followed by an ordinary char emits just `\` (no
12149 /// doubling). The "pending" extra `\` fires only when the next
12150 /// char is itself a backslash or one of the escaped specials
12151 /// (`"`/`$`/`` ` ``), OR at end-of-string before the closing `"`.
12152 #[test]
12153 fn dquotedztrdup_pending_backslash_only_doubles_when_needed() {
12154 let _g = crate::test_util::global_state_lock();
12155 if isset(CSHJUNKIEQUOTES) {
12156 return;
12157 }
12158 // Mid-string `\` before ordinary char: stays as single `\`.
12159 assert_eq!(
12160 dquotedztrdup("a\\b"),
12161 "\"a\\b\"",
12162 "c:6711+6712 — `\\b` mid-string: no extra (pending=0 after b)"
12163 );
12164 // Trailing `\` triggers pending quirk: c:6716-6717 emits
12165 // an extra `\` before the closing `"`.
12166 let r = dquotedztrdup("a\\");
12167 assert!(
12168 r.ends_with("\\\\\""),
12169 "c:6716-6717 — trailing `\\` → emit extra `\\` before closing `\"`: got {:?}",
12170 r
12171 );
12172 }
12173
12174 /// `Src/utils.c:6464-6549` — `quotedzputs(s)`. Three branches:
12175 /// - empty → `''`.
12176 /// - has special chars (via `hasspecial`) → wrap in `'...'`
12177 /// with `'\''` escape for embedded apostrophes.
12178 /// - else → return unchanged.
12179 /// Pin all three.
12180 #[test]
12181 fn quotedzputs_empty_string_returns_double_quotes() {
12182 let _g = crate::test_util::global_state_lock();
12183 assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
12184 }
12185
12186 /// c:6514-6543 — needs-quote path: wrap in single quotes,
12187 /// escape embedded `'` as `'\''`.
12188 #[test]
12189 fn quotedzputs_wraps_specials_in_single_quotes() {
12190 let _g = crate::test_util::global_state_lock();
12191 inittyptab();
12192 // Space is special → wrap.
12193 assert_eq!(quotedzputs("hello world"), "'hello world'");
12194 // `=` is special → wrap.
12195 assert_eq!(quotedzputs("foo=bar"), "'foo=bar'");
12196 // Embedded apostrophe — `'\''` escape.
12197 assert_eq!(
12198 quotedzputs("it's"),
12199 "'it'\\''s'",
12200 "c:6517-6519 — apostrophe → '\\''"
12201 );
12202 }
12203
12204 /// c:6512 — no specials → return unchanged.
12205 #[test]
12206 fn quotedzputs_plain_alnum_returns_unchanged() {
12207 let _g = crate::test_util::global_state_lock();
12208 inittyptab();
12209 assert_eq!(
12210 quotedzputs("hello"),
12211 "hello",
12212 "c:6512 — no specials, no wrap"
12213 );
12214 assert_eq!(quotedzputs("abc123"), "abc123");
12215 }
12216
12217 /// c:6578-6637 — Bourne path "avoids empty quoted strings" by
12218 /// tracking `inquote`. Input `'x` should not produce empty `''`
12219 /// at the front; `x'` should not produce trailing `''`.
12220 #[test]
12221 fn quotedzputs_avoids_empty_quoted_runs_on_boundaries() {
12222 let _g = crate::test_util::global_state_lock();
12223 inittyptab();
12224 // Leading `'`: c:6587 with inquote=0 skips the close, emits
12225 // `\'`. Then `x`: c:6604 opens a quote, emits `x`, end emits
12226 // closing `'`. Result: `\''x'` — the visible doubled `''` is
12227 // `\'` (escaped) + `'` (opening single-quote), NOT an empty
12228 // single-quoted string.
12229 assert_eq!(
12230 quotedzputs("'x"),
12231 "\\''x'",
12232 "c:6587-6610 — leading `'` produces `\\'` + opening `'`"
12233 );
12234 // Trailing `'`: open quote at `x`, see `'` with inquote=1 → close
12235 // (`'`), emit `\'`. End-of-string: inquote=0, no trailing `'`.
12236 // Result: `'x'\\'` — NO trailing `''`.
12237 assert_eq!(
12238 quotedzputs("x'"),
12239 "'x'\\'",
12240 "c:6631-6637 — trailing `'` doesn't generate empty `''`"
12241 );
12242 }
12243
12244 /// c:6478-6492 — `is_mb_niceformat` arm of quotedzputs uses
12245 /// mb_niceformat with NICEFLAG_QUOTE so embedded `'` becomes
12246 /// `\'` inside the `$'...'` wrapper (preventing the quote from
12247 /// terminating the wrap). Previously sb_niceformat without
12248 /// NICEFLAG_QUOTE was used, which would have left `'` raw.
12249 #[test]
12250 fn quotedzputs_dollar_quote_escapes_apostrophe_inside_wrap() {
12251 let _g = crate::test_util::global_state_lock();
12252 inittyptab();
12253 // String with embedded `'` AND a control char so the
12254 // is_mb_niceformat arm is taken (controls trigger it).
12255 let r = quotedzputs("a\nb'c");
12256 // Expected: `$'a\nb\'c'` — the embedded `'` is `\'` inside
12257 // dollar-quotes, NOT terminating the wrap.
12258 assert!(
12259 r.starts_with("$'") && r.ends_with("'"),
12260 "c:6488 — must be wrapped in $'...' got {:?}",
12261 r
12262 );
12263 assert!(
12264 r.contains("\\'"),
12265 "c:5413-5414 — embedded `'` must be `\\'` not raw `'`: got {:?}",
12266 r
12267 );
12268 }
12269
12270 /// c:6533-6576 — RCQUOTES branch: wrap everything in `'…'`,
12271 /// double embedded `'` as `''`.
12272 #[test]
12273 fn quotedzputs_rcquotes_doubles_apostrophe() {
12274 let _g = crate::test_util::global_state_lock();
12275 inittyptab();
12276 let prev = crate::ported::options::opt_state_get("rcquotes").unwrap_or(false);
12277 crate::ported::options::opt_state_set("rcquotes", true);
12278 // RCQUOTES: `it's` → `'it''s'` (doubled `'`, NOT `'\''`).
12279 let got = quotedzputs("it's");
12280 crate::ported::options::opt_state_set("rcquotes", prev);
12281 assert_eq!(
12282 got, "'it''s'",
12283 "c:6548-6553 — RCQUOTES: `'` → `''` (doubled)"
12284 );
12285 }
12286
12287 /// `Src/utils.c:2331-2337` — `strucpy(s, t)`. Appends `t` to
12288 /// `*s` and leaves `*s` pointing at the NUL terminator (Rust
12289 /// equivalent: append-in-place; `.len()` gives the new end).
12290 /// Pin: empty input → no-op; non-empty input → append.
12291 #[test]
12292 fn strucpy_appends_t_to_dest() {
12293 let _g = crate::test_util::global_state_lock();
12294 let mut dest = String::from("prefix-");
12295 strucpy(&mut dest, "suffix");
12296 assert_eq!(
12297 dest, "prefix-suffix",
12298 "c:2335 — strucpy appends t (NOT case-changes — c-name 'u' is pointer-walk, not upper)"
12299 );
12300 // Empty t — no change.
12301 let mut dest = String::from("alone");
12302 strucpy(&mut dest, "");
12303 assert_eq!(dest, "alone");
12304 // Empty dest + non-empty t.
12305 let mut dest = String::new();
12306 strucpy(&mut dest, "hello");
12307 assert_eq!(dest, "hello");
12308 }
12309
12310 /// `Src/utils.c:2341-2350` — `struncpy(s, t, n)`. Appends up to
12311 /// `n` bytes of `t` to `*s`. Pin: n=0 no-op; n < len(t) clips;
12312 /// n >= len(t) appends all.
12313 #[test]
12314 fn struncpy_appends_up_to_n_bytes() {
12315 let _g = crate::test_util::global_state_lock();
12316 // n < len(t).
12317 let mut dest = String::from("X");
12318 struncpy(&mut dest, "abcdef", 3);
12319 assert_eq!(dest, "Xabc", "c:2345 — clipped to n=3 bytes of `abcdef`");
12320 // n >= len(t).
12321 let mut dest = String::from("Y");
12322 struncpy(&mut dest, "abc", 100);
12323 assert_eq!(dest, "Yabc", "c:2345 — full copy when n >= len");
12324 // n=0 → no append.
12325 let mut dest = String::from("Z");
12326 struncpy(&mut dest, "abc", 0);
12327 assert_eq!(dest, "Z", "c:2345 — n=0 stops the copy loop immediately");
12328 }
12329
12330 /// `Src/utils.c:2280-2288` — `has_token(s)`. Returns true iff
12331 /// any byte in `s` triggers `itok()`. Pin: ASCII strings →
12332 /// false; strings containing token bytes (Pound 0x84, Bang
12333 /// 0x9c, Nularg 0xa1) → true.
12334 #[test]
12335 fn has_token_detects_typtab_token_bytes() {
12336 let _g = crate::test_util::global_state_lock();
12337 inittyptab();
12338 // c:2285 — pure ASCII has no token bytes.
12339 assert!(!has_token(""), "empty: no tokens");
12340 assert!(
12341 !has_token("hello world"),
12342 "c:2285 — ASCII text has no token bytes"
12343 );
12344 // Pound (0x84) — first token byte.
12345 let s: String = std::iter::once(0x84u8 as char).collect();
12346 assert!(
12347 has_token(&s),
12348 "c:2285 — Pound (0x84) is itok → has_token=true"
12349 );
12350 // Bang (0x9c) — last_normal_tok.
12351 let s: String = std::iter::once(0x9cu8 as char).collect();
12352 assert!(has_token(&s), "c:2285 — Bang (0x9c) is itok");
12353 // Nularg (0xa1) — upper bound.
12354 let s: String = std::iter::once(0xa1u8 as char).collect();
12355 assert!(has_token(&s), "c:2285 — Nularg (0xa1) is itok");
12356 }
12357
12358 /// `Src/utils.c:2280` — Meta byte (0x83) is NOT a token. Pin
12359 /// the regression: previously the Rust port hardcoded `0x83`
12360 /// as a token byte. Now correctly excludes it.
12361 #[test]
12362 fn has_token_excludes_meta_byte() {
12363 let _g = crate::test_util::global_state_lock();
12364 inittyptab();
12365 // 0x83 is Meta, NOT itok. Should return false.
12366 let s: String = std::iter::once(0x83u8 as char).collect();
12367 assert!(
12368 !has_token(&s),
12369 "c:2285 — Meta (0x83) is NOT itok; previous hardcoded 0x83 list misfired"
12370 );
12371 }
12372
12373 /// `Src/utils.c:6082-6124` — `addunprintable(c)`. Renders bytes
12374 /// using shell-compatible C-string escapes (`\a`/`\b`/`\f`/`\n`/
12375 /// `\r`/`\t`/`\v` for the named controls + `\nnn` octal for
12376 /// others + `\0` for NUL). Previously emitted ZLE caret form —
12377 /// wrong convention.
12378 #[test]
12379 fn addunprintable_named_control_escapes() {
12380 let _g = crate::test_util::global_state_lock();
12381 // c:6106-6112 — named-escape per byte.
12382 assert_eq!(addunprintable('\x07'), "\\a", "c:6106 — BEL → \\a");
12383 assert_eq!(addunprintable('\x08'), "\\b", "c:6107 — BS → \\b");
12384 assert_eq!(addunprintable('\x0c'), "\\f", "c:6108 — FF → \\f");
12385 assert_eq!(addunprintable('\n'), "\\n", "c:6109 — LF → \\n");
12386 assert_eq!(addunprintable('\r'), "\\r", "c:6110 — CR → \\r");
12387 assert_eq!(addunprintable('\t'), "\\t", "c:6111 — TAB → \\t");
12388 assert_eq!(addunprintable('\x0b'), "\\v", "c:6112 — VT → \\v");
12389 }
12390
12391 /// c:6097-6103 — NUL renders as `\0`. (C peeks next byte for
12392 /// `\000` disambiguation; Rust port works one char at a time
12393 /// so emits the short `\0` form.)
12394 #[test]
12395 fn addunprintable_nul_renders_as_backslash_zero() {
12396 let _g = crate::test_util::global_state_lock();
12397 assert_eq!(addunprintable('\0'), "\\0", "c:6097-6098 — NUL → \\0");
12398 }
12399
12400 /// c:6114-6119 — `\nnn` 3-digit octal fallback for un-named
12401 /// control bytes. Pin the format: each digit is one octal digit
12402 /// (0-7), zero-padded to 3 positions.
12403 #[test]
12404 fn addunprintable_octal_fallback_for_unnamed_controls() {
12405 let _g = crate::test_util::global_state_lock();
12406 // 0x01 (SOH) = 001 octal.
12407 assert_eq!(addunprintable('\x01'), "\\001", "c:6116-6118 — SOH → \\001");
12408 // 0x1b (ESC) = 033 octal.
12409 assert_eq!(addunprintable('\x1b'), "\\033", "c:6116-6118 — ESC → \\033");
12410 // 0x7f (DEL) = 177 octal.
12411 assert_eq!(addunprintable('\x7f'), "\\177", "c:6116-6118 — DEL → \\177");
12412 // 0xff (high) = 377 octal.
12413 assert_eq!(
12414 addunprintable(char::from_u32(0xff).unwrap()),
12415 "\\377",
12416 "c:6116-6118 — 0xff → \\377"
12417 );
12418 }
12419
12420 /// `Src/utils.c:6072-6082` — `hasspecial(s)`. Pin canonical
12421 /// special chars from SPECCHARS (`Src/zsh.h:228`): `#$^*()=|{}[]
12422 /// \`<>?~;&\\n\\t \\\\\\'\\"`.
12423 #[test]
12424 fn hasspecial_recognises_canonical_special_chars() {
12425 let _g = crate::test_util::global_state_lock();
12426 // typtab access is read-only here (no flag mutation); concurrent
12427 // inittyptab() rebuilds are idempotent for the default flag set.
12428 inittyptab();
12429 // c:6075 — every char in SPECCHARS triggers true.
12430 for c in "#$^*()=|{}[]<>?~;&".chars() {
12431 let s = c.to_string();
12432 assert!(
12433 hasspecial(&s),
12434 "c:6075 — '{}' is in SPECCHARS, must be special",
12435 c
12436 );
12437 }
12438 // Whitespace specials.
12439 assert!(hasspecial(" "), "space in SPECCHARS");
12440 assert!(hasspecial("\t"), "tab in SPECCHARS");
12441 assert!(hasspecial("\n"), "newline in SPECCHARS");
12442 // Non-special chars → false.
12443 assert!(
12444 !hasspecial("hello"),
12445 "c:6075 — plain alphanumerics are NOT special"
12446 );
12447 assert!(!hasspecial("ABC012"));
12448 }
12449
12450 /// `Src/utils.c:6072-6075` — Meta-byte handling: `*s == Meta ?
12451 /// *++s ^ 32 : *s`. Pin Meta+X pair gets decoded before special-
12452 /// check. A Meta+'A' decodes to 'a' (not special), so a string
12453 /// containing only Meta+'A' returns false.
12454 #[test]
12455 fn hasspecial_decodes_meta_byte_before_check() {
12456 let _g = crate::test_util::global_state_lock();
12457 // Same as above — read-only after inittyptab.
12458 inittyptab();
12459 // Meta + 0x41 ('A') → 'a' (0x61) → not special.
12460 let bytes: Vec<u8> = vec![Meta, 0x41u8];
12461 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12462 assert!(
12463 !hasspecial(s),
12464 "c:6075 — Meta+0x41 decodes to 'a' which is NOT special"
12465 );
12466 }
12467
12468 /// `Src/utils.c:5849-5910` — `sb_niceformat(s)`. Pin: ASCII
12469 /// printable passes through unchanged; controls escape.
12470 /// C-equivalent call shape: `char *out; sb_niceformat(s, NULL, &out, 0);`
12471 #[test]
12472 fn sb_niceformat_passes_printable_ascii() {
12473 let _g = crate::test_util::global_state_lock();
12474 let mut out: Option<String> = None;
12475 let l = sb_niceformat("hello", None, Some(&mut out), 0);
12476 assert_eq!(
12477 out.as_deref(),
12478 Some("hello"),
12479 "c:5886 — nicechar_sel passes printable through"
12480 );
12481 assert_eq!(l, 5, "c:5928 — return length equals output bytes");
12482
12483 let mut out: Option<String> = None;
12484 sb_niceformat("", None, Some(&mut out), 0);
12485 assert_eq!(out.as_deref(), Some(""));
12486
12487 let mut out: Option<String> = None;
12488 sb_niceformat("ABC012!?@", None, Some(&mut out), 0);
12489 assert_eq!(out.as_deref(), Some("ABC012!?@"));
12490 }
12491
12492 /// c:5886 — controls get `\n`/`\t`/`^X`/`^?` escapes via nicechar_sel.
12493 #[test]
12494 fn sb_niceformat_escapes_controls() {
12495 let _g = crate::test_util::global_state_lock();
12496 let mut out: Option<String> = None;
12497 sb_niceformat("a\nb", None, Some(&mut out), 0);
12498 assert_eq!(out.as_deref(), Some("a\\nb"), "c:5886 — newline → \\n");
12499
12500 let mut out: Option<String> = None;
12501 sb_niceformat("a\tb", None, Some(&mut out), 0);
12502 assert_eq!(out.as_deref(), Some("a\\tb"));
12503
12504 let mut out: Option<String> = None;
12505 sb_niceformat("\x01", None, Some(&mut out), 0);
12506 assert_eq!(
12507 out.as_deref(),
12508 Some("^A"),
12509 "c:5886 — control char → ^X form"
12510 );
12511
12512 let mut out: Option<String> = None;
12513 sb_niceformat("\x7f", None, Some(&mut out), 0);
12514 assert_eq!(out.as_deref(), Some("^?"));
12515 }
12516
12517 /// `Src/utils.c:5872` — unmetafy step before formatting. Pin:
12518 /// metafied Meta+X pair gets unescaped first, then run through
12519 /// nicechar_sel.
12520 #[test]
12521 fn sb_niceformat_unmetafies_before_formatting() {
12522 let _g = crate::test_util::global_state_lock();
12523 // META + 0x41 ('A') → decodes to 'a' (0x61). 'a' is printable
12524 // → passes through unchanged.
12525 let bytes: Vec<u8> = vec![Meta, 0x41u8];
12526 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12527 let mut out: Option<String> = None;
12528 sb_niceformat(s, None, Some(&mut out), 0);
12529 assert_eq!(
12530 out.as_deref(),
12531 Some("a"),
12532 "c:5872 — unmetafy first: Meta+0x41 → 'a' → printable passthrough"
12533 );
12534 // META + 0x20 → decodes to NUL → "^@" form.
12535 let bytes: Vec<u8> = vec![Meta, 0x20u8];
12536 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12537 let mut out: Option<String> = None;
12538 sb_niceformat(s, None, Some(&mut out), 0);
12539 let r = out.unwrap_or_default();
12540 assert!(
12541 !r.is_empty(),
12542 "c:5872 — Meta+0x20 → NUL → must emit some escape, not empty"
12543 );
12544 }
12545
12546 /// `Src/utils.c:5937-5959` — `is_sb_niceformat(s)`. Returns 1
12547 /// if sb_niceformat would change the input, else 0.
12548 #[test]
12549 fn is_sb_niceformat_true_for_strings_with_controls() {
12550 let _g = crate::test_util::global_state_lock();
12551 assert_eq!(is_sb_niceformat("\n"), 1, "newline is nice");
12552 assert_eq!(is_sb_niceformat("a\tb"), 1);
12553 assert_eq!(is_sb_niceformat("\x01"), 1);
12554 // Non-control ASCII → 0.
12555 assert_eq!(is_sb_niceformat("hello"), 0);
12556 assert_eq!(is_sb_niceformat(""), 0);
12557 }
12558
12559 /// `Src/utils.c:5810-5826` — `metacharlenconv(x, c)`. Returns
12560 /// `(2, decoded)` for Meta+X pair, `(1, byte)` for plain byte.
12561 /// Pin both branches.
12562 #[test]
12563 fn metacharlenconv_plain_ascii_returns_one_byte() {
12564 let _g = crate::test_util::global_state_lock();
12565 // c:5823-5825 — plain byte.
12566 let (n, c) = metacharlenconv("a");
12567 assert_eq!((n, c), (1, Some('a')), "c:5823 — plain byte → (1, byte)");
12568 let (n, c) = metacharlenconv("X");
12569 assert_eq!((n, c), (1, Some('X')));
12570 }
12571
12572 /// c:5818-5821 — Meta+X pair: `*c = x[1] ^ 32; return 2`.
12573 #[test]
12574 fn metacharlenconv_meta_pair_xor_decodes() {
12575 let _g = crate::test_util::global_state_lock();
12576 // Meta + 0x41 ('A') → 'A' ^ 32 = 'a'.
12577 let bytes: Vec<u8> = vec![Meta, 0x41u8];
12578 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12579 let (n, c) = metacharlenconv(s);
12580 assert_eq!(
12581 (n, c),
12582 (2, Some('a')),
12583 "c:5820 — Meta+0x41 → 'a' (XOR 32), consumed 2 bytes"
12584 );
12585 // Empty input.
12586 let (n, c) = metacharlenconv("");
12587 assert_eq!((n, c), (0, None));
12588 }
12589
12590 /// `Src/utils.c:5832-5843` — `charlenconv(x, len, c)`. Returns
12591 /// `(0, NUL)` when len=0, `(1, byte)` otherwise.
12592 #[test]
12593 fn charlenconv_zero_len_returns_zero() {
12594 let _g = crate::test_util::global_state_lock();
12595 // c:5834 — `if (!len) { *c = '\0'; return 0; }`.
12596 let (n, c) = charlenconv("abc", 0);
12597 assert_eq!((n, c), (0, None), "c:5834-5837 — len=0 returns 0");
12598 }
12599
12600 /// c:5840-5842 — non-zero len: read one byte, return (1, byte).
12601 #[test]
12602 fn charlenconv_returns_first_byte_for_nonzero_len() {
12603 let _g = crate::test_util::global_state_lock();
12604 let (n, c) = charlenconv("abc", 3);
12605 assert_eq!((n, c), (1, Some('a')), "c:5841 — *c = *x; return 1");
12606 let (n, c) = charlenconv("xy", 2);
12607 assert_eq!((n, c), (1, Some('x')));
12608 }
12609
12610 /// `Src/utils.c:5474-5524` — `is_mb_niceformat(s)`. Predicate:
12611 /// would mb_niceformat produce a different output? Pin canonical
12612 /// branches:
12613 /// - Pure ASCII printable → false (no escape needed).
12614 /// - Contains control char → true.
12615 /// - Pure UTF-8 wide chars → false under default PRINTEIGHTBIT-off
12616 /// since is_wcs_nicechar treats them as printable.
12617 #[test]
12618 fn is_mb_niceformat_false_for_pure_printable_ascii() {
12619 let _g = crate::test_util::global_state_lock();
12620 assert_eq!(
12621 is_mb_niceformat("hello"),
12622 0,
12623 "c:5509 — printable ASCII needs no nice-format"
12624 );
12625 assert_eq!(
12626 is_mb_niceformat(""),
12627 0,
12628 "c:5486 — empty string has no chars to flag"
12629 );
12630 assert_eq!(is_mb_niceformat("ABC012!?@"), 0);
12631 }
12632
12633 /// c:5509 + `is_wcs_nicechar` — newline/tab/control chars trigger
12634 /// the predicate.
12635 #[test]
12636 fn is_mb_niceformat_true_for_strings_with_controls() {
12637 let _g = crate::test_util::global_state_lock();
12638 assert_eq!(is_mb_niceformat("a\nb"), 1, "c:5509 — newline is nice");
12639 assert_eq!(is_mb_niceformat("\t"), 1);
12640 assert_eq!(is_mb_niceformat("\x01"), 1, "c:5509 — control char is nice");
12641 assert_eq!(is_mb_niceformat("\x7f"), 1, "c:5509 — DEL is nice");
12642 }
12643
12644 /// `Src/utils.c:5366-5460` — `mb_niceformat(s)`. Multibyte-aware
12645 /// nice-formatter. Calls `wcs_nicechar` per char. Test uses
12646 /// C-equivalent call shape: `char *out; mb_niceformat(s, NULL, &out, 0);`
12647 #[test]
12648 fn mb_niceformat_preserves_printable_wide_chars() {
12649 let _g = crate::test_util::global_state_lock();
12650 let mut out: Option<String> = None;
12651 mb_niceformat("hello", None, Some(&mut out), 0);
12652 assert_eq!(
12653 out.as_deref(),
12654 Some("hello"),
12655 "c:5407 — printable ASCII passes through"
12656 );
12657
12658 let mut out: Option<String> = None;
12659 mb_niceformat("café", None, Some(&mut out), 0);
12660 assert_eq!(
12661 out.as_deref(),
12662 Some("café"),
12663 "c:5407 — Latin-1 'é' must NOT byte-mask to \\M-X"
12664 );
12665
12666 let mut out: Option<String> = None;
12667 mb_niceformat("字", None, Some(&mut out), 0);
12668 assert_eq!(
12669 out.as_deref(),
12670 Some("字"),
12671 "c:5407 — CJK printable passes through"
12672 );
12673
12674 let mut out: Option<String> = None;
12675 mb_niceformat("abcéxyz", None, Some(&mut out), 0);
12676 assert_eq!(out.as_deref(), Some("abcéxyz"));
12677 }
12678
12679 /// `Src/utils.c:5407` + `wcs_nicechar` — controls still escape.
12680 #[test]
12681 fn mb_niceformat_escapes_controls() {
12682 let _g = crate::test_util::global_state_lock();
12683 let mut out: Option<String> = None;
12684 mb_niceformat("\n", None, Some(&mut out), 0);
12685 assert_eq!(
12686 out.as_deref(),
12687 Some("\\n"),
12688 "c:5407 → wcs_nicechar c:625 — newline escapes"
12689 );
12690
12691 let mut out: Option<String> = None;
12692 mb_niceformat("\t", None, Some(&mut out), 0);
12693 assert_eq!(
12694 out.as_deref(),
12695 Some("\\t"),
12696 "c:5407 → wcs_nicechar c:628 — tab escapes"
12697 );
12698
12699 let mut out: Option<String> = None;
12700 mb_niceformat("a\nb", None, Some(&mut out), 0);
12701 assert_eq!(out.as_deref(), Some("a\\nb"));
12702 }
12703
12704 /// `Src/utils.c:4971-4983` — `metalen(s, len)`. **Input `len` is
12705 /// the UNMETAFIED char count; output is the METAFIED byte count.**
12706 /// Pin a metafied string with one Meta+X pair: 3 unmetafied chars
12707 /// → 4 metafied bytes.
12708 #[test]
12709 fn metalen_returns_metafied_byte_count() {
12710 let _g = crate::test_util::global_state_lock();
12711 // Pure ASCII: 5 chars → 5 bytes (no Meta encountered).
12712 assert_eq!(
12713 metalen("hello", 5),
12714 5,
12715 "c:4972 — ASCII: metafied bytes == unmetafied chars"
12716 );
12717 // [a, Meta, X, b] = 4 metafied bytes representing 3 chars.
12718 let bytes: Vec<u8> = vec![b'a', Meta, 0x41, b'b'];
12719 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12720 assert_eq!(
12721 metalen(s, 3),
12722 4,
12723 "c:4978 — 3 unmetafied chars + 1 Meta = 4 metafied bytes"
12724 );
12725 // Two Meta pairs: [Meta, X, Meta, Y] = 4 bytes for 2 chars.
12726 let bytes: Vec<u8> = vec![Meta, 0x41, Meta, 0x42];
12727 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12728 assert_eq!(
12729 metalen(s, 2),
12730 4,
12731 "c:4978 — 2 unmetafied chars (both Meta+X) = 4 metafied bytes"
12732 );
12733 }
12734
12735 /// c:4974 — `mlen = len;` is the initial value (no Meta found,
12736 /// returns the input length unchanged). Pin len=0 and pure-ASCII.
12737 #[test]
12738 fn metalen_returns_input_for_no_meta_chars() {
12739 let _g = crate::test_util::global_state_lock();
12740 assert_eq!(metalen("", 0), 0, "c:4974 — empty input returns 0");
12741 assert_eq!(
12742 metalen("abc", 3),
12743 3,
12744 "c:4974 — no Meta chars: output == input len"
12745 );
12746 }
12747
12748 /// `Src/utils.c:5070` — `if (!in || !*in) return 0;`.
12749 /// Empty input returns `('\0', 0)`. Pin: empty &str → 0 bytes consumed.
12750 #[test]
12751 fn unmeta_one_empty_input_returns_zero() {
12752 let _g = crate::test_util::global_state_lock();
12753 let (c, n) = unmeta_one("");
12754 assert_eq!(c, '\0', "c:5070 — empty input returns NUL char");
12755 assert_eq!(n, 0, "c:5070 — empty input consumes 0 bytes");
12756 }
12757
12758 /// `Src/utils.c:5081-5082` — Non-Meta byte: `*sz = 1; wc = byte`.
12759 #[test]
12760 fn unmeta_one_plain_ascii_consumes_one_byte() {
12761 let _g = crate::test_util::global_state_lock();
12762 for c in "aA0!~".chars() {
12763 let s = c.to_string();
12764 let (got, n) = unmeta_one(&s);
12765 assert_eq!(got, c, "c:5082 — '{}' decodes to itself", c);
12766 assert_eq!(n, 1, "c:5081 — non-Meta byte consumes 1");
12767 }
12768 }
12769
12770 /// `Src/utils.c:5077-5079` — Meta byte: `*sz = 2; wc = in[1] ^ 32`.
12771 /// Pin via constructed Meta byte sequence.
12772 #[test]
12773 fn unmeta_one_meta_pair_decodes_xor_32() {
12774 let _g = crate::test_util::global_state_lock();
12775 // META + 0x41 ('A') → 'A' ^ 32 = 0x61 ('a'). 2 bytes consumed.
12776 let bytes: Vec<u8> = vec![Meta, 0x41u8];
12777 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12778 let (got, n) = unmeta_one(s);
12779 assert_eq!(got, 'a', "c:5079 — Meta+0x41 decodes to 0x41^32 = 'a'");
12780 assert_eq!(n, 2, "c:5078 — Meta pair consumes 2 bytes");
12781 // META + 0x20 = 0x20^32 = 0x00 (NUL).
12782 let bytes: Vec<u8> = vec![Meta, 0x20u8];
12783 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12784 let (got, n) = unmeta_one(s);
12785 assert_eq!(
12786 got, '\0',
12787 "c:5079 — Meta+0x20 decodes to NUL (the canonical metafy-NUL pattern)"
12788 );
12789 assert_eq!(n, 2);
12790 }
12791
12792 /// Edge case: bare Meta byte at end with no follower. C source
12793 /// would read past the buffer (UB); Rust port's `bytes.len() > 1`
12794 /// guard handles this by falling through to the non-Meta arm.
12795 #[test]
12796 fn unmeta_one_trailing_meta_byte_falls_through() {
12797 let _g = crate::test_util::global_state_lock();
12798 let bytes: Vec<u8> = vec![Meta];
12799 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12800 let (_, n) = unmeta_one(s);
12801 // Defensive: Rust returns the Meta byte itself as char + 1.
12802 assert_eq!(n, 1, "trailing Meta — no panic, no read past buffer");
12803 }
12804
12805 /// `Src/utils.c:5185-5203` — `ztrsub(t, s)`. Count of unmetafied
12806 /// chars between two pointers in the same metafied string.
12807 /// Rust port takes (buf, start, end) byte-offsets since raw
12808 /// pointer subtraction across distinct &str isn't safe.
12809 /// Pin pure-ASCII subrange (no Meta) → returns end-start.
12810 #[test]
12811 fn ztrsub_ascii_no_meta_returns_byte_distance() {
12812 let _g = crate::test_util::global_state_lock();
12813 // "hello world" — substring [0..5) → "hello" → 5 chars.
12814 assert_eq!(
12815 ztrsub("hello world", 0, 5),
12816 5,
12817 "c:5189 — no Meta: returns t-s byte distance"
12818 );
12819 // [6..11) → "world" → 5 chars.
12820 assert_eq!(ztrsub("hello world", 6, 11), 5);
12821 // Empty range.
12822 assert_eq!(ztrsub("hello", 2, 2), 0);
12823 }
12824
12825 /// c:5191-5199 — each Meta+X pair decrements `l` by 1 (since the
12826 /// initial l = byte-distance counts both bytes but the pair is
12827 /// one logical char). Pin via constructed Meta string.
12828 #[test]
12829 fn ztrsub_subtracts_one_per_meta_pair() {
12830 let _g = crate::test_util::global_state_lock();
12831 // Build "a" + Meta + 0x20 + "b" = 4 bytes total.
12832 // Unmetafied: "a" + 1-char-from-meta + "b" = 3 chars.
12833 let meta_byte = Meta;
12834 let buf_bytes: Vec<u8> = vec![b'a', meta_byte, 0x20, b'b'];
12835 let buf = unsafe { std::str::from_utf8_unchecked(&buf_bytes) };
12836 // [0..4) byte distance = 4; Meta pair found → l-- → 3.
12837 assert_eq!(
12838 ztrsub(buf, 0, 4),
12839 3,
12840 "c:5192-5199 — Meta pair decrements distance by 1"
12841 );
12842 }
12843
12844 /// c:5189 — `int l = t - s;` start of count. Pin edge case
12845 /// where start > end (caller bug) doesn't underflow or panic;
12846 /// Rust port clamps via `start.min(end)`.
12847 #[test]
12848 fn ztrsub_clamps_inverted_range_to_zero() {
12849 let _g = crate::test_util::global_state_lock();
12850 assert_eq!(
12851 ztrsub("hello", 4, 2),
12852 0,
12853 "inverted range start>end → 0 (defensive; not in C contract)"
12854 );
12855 // Beyond-buffer end gets clamped.
12856 assert_eq!(
12857 ztrsub("hi", 0, 100),
12858 2,
12859 "end > buf.len() clamps to buffer length"
12860 );
12861 }
12862
12863 /// `Src/utils.c:5141` — trailing Meta byte with no following char
12864 /// is a malformed-string edge case. C has a DPUTS debug-only
12865 /// fprintf for "unexpected end" but otherwise the loop terminates
12866 /// because `*s` is now NUL. Rust port must not panic on this
12867 /// truncated input.
12868 #[test]
12869 fn ztrlen_handles_trailing_meta_byte_without_panic() {
12870 let _g = crate::test_util::global_state_lock();
12871 let meta = char::from_u32(Meta as u32).unwrap();
12872 let trailing: String = ['a', meta].iter().collect();
12873 // c:5141-5149 — increment l, then encounter Meta, then loop
12874 // condition `*s` is empty → terminate. Count: 'a' + Meta = 2
12875 // (Rust port counts Meta as 1 char when no follower).
12876 let r = ztrlen(&trailing);
12877 assert!(r >= 1, "trailing Meta must not panic; got {}", r);
12878 }
12879
12880 /// c:params.c:1288 — `isident` rejects digit-leading names.
12881 /// A regression accepting them lets `typeset 1foo=bar` install
12882 /// a poisoned param that no later expansion can address.
12883 #[test]
12884 fn isident_rejects_digit_leading_names() {
12885 let _g = crate::test_util::global_state_lock(); // c:1288
12886 // c:1315-1319 — C `isident`: if first char is digit, ALL
12887 // chars must be digit (all-digit names are valid positional
12888 // params like $99). So `1foo` is rejected (digit-then-alpha)
12889 // but `99` is accepted as a valid positional-param name.
12890 // The previous utils.rs fake rejected all-digit names too,
12891 // which was non-faithful — the canonical `params::isident`
12892 // at `params.rs:2056` matches the C semantics.
12893 assert!(!isident("1foo")); // c:1318
12894 assert!(
12895 isident("99"), // c:1318
12896 "c:1315-1319 — all-digit names are valid positional params"
12897 );
12898 assert!(!isident("")); // c:1290
12899 }
12900
12901 /// `isident` MUST accept underscore-leading + alpha-leading names —
12902 /// `_foo`, `Foo_BAR_42` are valid POSIX shell idents.
12903 #[test]
12904 fn isident_accepts_underscore_and_alpha_leading() {
12905 let _g = crate::test_util::global_state_lock(); // c:1288
12906 assert!(isident("foo")); // c:1288
12907 assert!(isident("_foo")); // c:1288
12908 assert!(isident("Foo_BAR_42")); // c:1288
12909 assert!(isident("a")); // c:1288
12910 }
12911
12912 /// `isident` rejects whitespace/punctuation/$. A regression
12913 /// accepting them would let assignments install names no
12914 /// subsequent lookup can address (`typeset 'foo bar'=baz`).
12915 #[test]
12916 fn isident_rejects_special_chars() {
12917 let _g = crate::test_util::global_state_lock(); // c:1288
12918 // c:1320-1322 — non-digit-leading names use itype_end with
12919 // INAMESPC bits (alnum + `_` + `.`); other chars terminate.
12920 // The previous utils.rs fake rejected `foo.` outright, which
12921 // is incorrect — C accepts trailing `.` (it's an INAMESPC
12922 // member). Whitespace, `-`, and `$` are NOT INAMESPC chars
12923 // and remain rejected.
12924 assert!(!isident("foo bar")); // c:1322
12925 assert!(!isident("foo-bar")); // c:1322
12926 assert!(!isident("a$b")); // c:1322
12927 }
12928
12929 /// `convbase(0, 10)` MUST produce `"0"`, not `""`. The literal-zero
12930 /// edge case is a real C divergence point — regression here would
12931 /// make `$(( 0 ))` print nothing.
12932 #[test]
12933 fn convbase_zero_renders_as_zero_literal() {
12934 let _g = crate::test_util::global_state_lock();
12935 assert_eq!(convbase(0, 10), "0");
12936 }
12937
12938 /// `convbase` uses zsh's canonical `BASE#NUMBER` syntax for
12939 /// non-decimal output (matches `$(( [#16] 255 ))` → `16#FF`).
12940 /// This is the format `printf "%X"` and `$(( ... ))` both produce.
12941 #[test]
12942 fn convbase_uses_base_prefix_syntax_for_non_decimal() {
12943 let _g = crate::test_util::global_state_lock();
12944 assert_eq!(convbase(255, 16), "16#FF");
12945 assert_eq!(convbase(8, 8), "8#10");
12946 assert_eq!(convbase(5, 2), "2#101");
12947 // Base 10 has no prefix.
12948 assert_eq!(convbase(42, 10), "42");
12949 }
12950
12951 /// Negative number renders with leading `-`. Regression that drops
12952 /// the sign would silently flip arithmetic semantics in `$((...))`.
12953 #[test]
12954 fn convbase_preserves_negative_sign() {
12955 let _g = crate::test_util::global_state_lock();
12956 assert_eq!(convbase(-42, 10), "-42");
12957 }
12958
12959 /// `Src/utils.c:837` — `slashsplit` keeps an EMPTY leading segment
12960 /// when the input has a leading `/`. The caller `xsymlinks`
12961 /// (c:879) reads this empty as the signal "absolute path, start
12962 /// xbuf from /". Previous Rust test asserted filtering of all
12963 /// empties (matching the previous buggy port); that contract was
12964 /// wrong relative to C. Updated to assert the C contract.
12965 #[test]
12966 fn slashsplit_keeps_leading_empty_segment() {
12967 let _g = crate::test_util::global_state_lock();
12968 // c:851 with t==s on first iter → empty prefix segment.
12969 assert_eq!(
12970 slashsplit("/usr/local/bin"),
12971 vec![
12972 "".to_string(),
12973 "usr".to_string(),
12974 "local".to_string(),
12975 "bin".to_string()
12976 ],
12977 "c:851 — leading `/` produces empty first segment"
12978 );
12979 // Single `/`: one empty segment only.
12980 assert_eq!(
12981 slashsplit("/"),
12982 vec!["".to_string()],
12983 "c:854-857 — trailing `/` after first iter returns ['']"
12984 );
12985 // Consecutive slashes collapse (c:852-853 inner while-loop).
12986 assert_eq!(
12987 slashsplit("//foo"),
12988 vec!["".to_string(), "foo".to_string()],
12989 "c:852-853 — consecutive `/` collapse, leading empty kept"
12990 );
12991 // Empty input still empty result.
12992 assert_eq!(
12993 slashsplit(""),
12994 Vec::<String>::new(),
12995 "c:842 — empty input → empty array"
12996 );
12997 }
12998
12999 /// `Src/utils.c:837` — relative paths (no leading `/`) start
13000 /// at the first non-slash; trailing `/` doesn't add a segment
13001 /// (c:854-857 returns before tail emit).
13002 #[test]
13003 fn slashsplit_relative_path_no_trailing_empty() {
13004 let _g = crate::test_util::global_state_lock();
13005 assert_eq!(
13006 slashsplit("a/b/"),
13007 vec!["a".to_string(), "b".to_string()],
13008 "c:854-857 — trailing `/` doesn't add segment"
13009 );
13010 // Mid-slash collapse in relative path.
13011 assert_eq!(
13012 slashsplit("a//b"),
13013 vec!["a".to_string(), "b".to_string()],
13014 "c:852-853 — mid `//` collapses"
13015 );
13016 }
13017
13018 /// `equalsplit("foo=bar")` returns Some(("foo","bar")). Used by
13019 /// `typeset NAME=val` parsing. Regression returning None on a
13020 /// well-formed assignment would silently break every export/typeset.
13021 #[test]
13022 fn equalsplit_returns_first_equals_split() {
13023 let _g = crate::test_util::global_state_lock();
13024 assert_eq!(
13025 equalsplit("foo=bar"),
13026 Some(("foo".to_string(), "bar".to_string()))
13027 );
13028 assert_eq!(
13029 equalsplit("a=b=c"),
13030 Some(("a".to_string(), "b=c".to_string())),
13031 "splits on FIRST `=` only"
13032 );
13033 }
13034
13035 /// `equalsplit("foo")` returns None — no `=` to split on. Catches
13036 /// a regression returning Some(("foo","")) which would let bare
13037 /// names install with empty values silently.
13038 #[test]
13039 fn equalsplit_no_equals_returns_none() {
13040 let _g = crate::test_util::global_state_lock();
13041 assert_eq!(equalsplit("foo"), None);
13042 assert_eq!(equalsplit(""), None);
13043 }
13044
13045 /// c:5106 — `ztrcmp` is the canonical zsh string compare. Same
13046 /// inputs MUST produce same Ordering (deterministic). Regression
13047 /// to a randomised hash-order would mis-sort `${(o)array}` output.
13048 #[test]
13049 fn ztrcmp_deterministic_and_lexicographic() {
13050 let _g = crate::test_util::global_state_lock();
13051 assert_eq!(ztrcmp("abc", "abc"), std::cmp::Ordering::Equal);
13052 assert_eq!(ztrcmp("abc", "abd"), std::cmp::Ordering::Less);
13053 assert_eq!(ztrcmp("abd", "abc"), std::cmp::Ordering::Greater);
13054 // Empty strings.
13055 assert_eq!(ztrcmp("", ""), std::cmp::Ordering::Equal);
13056 assert_eq!(ztrcmp("", "a"), std::cmp::Ordering::Less);
13057 }
13058
13059 /// c:5106 — shorter-as-prefix sorts before longer (like strcmp).
13060 /// Catches a regression where length-then-content would mis-sort.
13061 #[test]
13062 fn ztrcmp_shorter_prefix_is_less() {
13063 let _g = crate::test_util::global_state_lock();
13064 assert_eq!(ztrcmp("a", "ab"), std::cmp::Ordering::Less);
13065 assert_eq!(ztrcmp("foo", "foob"), std::cmp::Ordering::Less);
13066 }
13067
13068 /// `Src/utils.c:5117-5122` — Meta+X pair decodes to `X ^ 32`
13069 /// for comparison purposes. Pin: two strings differing only in
13070 /// the metafied byte's decoded value compare correctly.
13071 #[test]
13072 fn ztrcmp_decodes_meta_byte_for_comparison() {
13073 let _g = crate::test_util::global_state_lock();
13074 // META+0x21 ('!') = NUL? Let me use safer chars.
13075 // META+'A' (0x41) decodes to 'A' ^ 32 = 'a' (0x61).
13076 // So a "META a" pair represents the byte 'a' (0x61).
13077 // Compare "Ma" (M=Meta+a-byte-pair) vs "b" (>a).
13078 let meta_byte = Meta;
13079 let s1_bytes: Vec<u8> = vec![meta_byte, 0x41u8]; // decodes to 0x61 = 'a'
13080 let s2_bytes: Vec<u8> = vec![b'b'];
13081 let s1 = unsafe { std::str::from_utf8_unchecked(&s1_bytes) };
13082 let s2 = unsafe { std::str::from_utf8_unchecked(&s2_bytes) };
13083 // 'a' (0x61) < 'b' (0x62) → Less.
13084 assert_eq!(
13085 ztrcmp(s1, s2),
13086 std::cmp::Ordering::Less,
13087 "c:5117-5118 — Meta+0x41 decodes to 'a' which < 'b'"
13088 );
13089 }
13090
13091 /// `Src/utils.c:531-539` — `is_nicechar`. Returns true for chars
13092 /// needing escape-formatting:
13093 /// - c:534 — printable ASCII (0x20-0x7e) → false.
13094 /// - c:536 — high-bit byte (>= 0x80) → !PRINTEIGHTBIT.
13095 /// - c:538 — DEL/\n/\t/<0x20 → true.
13096 /// Pin the three branches.
13097 #[test]
13098 fn is_nicechar_printable_ascii_is_not_nice() {
13099 let _g = crate::test_util::global_state_lock();
13100 // c:534 — letters, digits, punctuation in 0x20-0x7e all NOT nice.
13101 for c in "abcXYZ012!?@~".chars() {
13102 assert!(
13103 !is_nicechar(c),
13104 "c:534 — '{}' (ASCII printable) must NOT be nice",
13105 c
13106 );
13107 }
13108 // c:534 — space (0x20) is printable.
13109 assert!(!is_nicechar(' '));
13110 }
13111
13112 /// c:538 — control chars (DEL, newline, tab, <0x20) ARE nice.
13113 #[test]
13114 fn is_nicechar_control_chars_are_nice() {
13115 let _g = crate::test_util::global_state_lock();
13116 assert!(is_nicechar('\n'), "c:538 — newline is nice");
13117 assert!(is_nicechar('\t'), "c:538 — tab is nice");
13118 assert!(is_nicechar('\x7f'), "c:538 — DEL is nice");
13119 assert!(is_nicechar('\x00'), "c:538 — NUL is nice (<0x20)");
13120 assert!(is_nicechar('\x07'), "c:538 — BEL is nice (<0x20)");
13121 assert!(is_nicechar('\x1b'), "c:538 — ESC is nice (<0x20)");
13122 assert!(is_nicechar('\x1f'), "c:538 — boundary 0x1f is nice");
13123 }
13124
13125 /// `Src/utils.c:2528-2570` — `zstrtoul_underscore(s)`. Pins
13126 /// the base-prefix dispatch: `0x` → 16, `0b` → 2, leading `0`
13127 /// only when OCTALZEROES is set. Tests the default state.
13128 #[test]
13129 fn zstrtoul_underscore_recognises_hex_binary_decimal() {
13130 let _g = crate::test_util::global_state_lock();
13131 // c:2538 — hex.
13132 assert_eq!(zstrtoul_underscore("0xff"), Some(255));
13133 assert_eq!(zstrtoul_underscore("0XFF"), Some(255));
13134 // c:2540 — binary.
13135 assert_eq!(zstrtoul_underscore("0b1010"), Some(10));
13136 assert_eq!(zstrtoul_underscore("0B11"), Some(3));
13137 // c:2537 — pure decimal (no leading zero).
13138 assert_eq!(zstrtoul_underscore("12345"), Some(12345));
13139 // c:2543 — leading-zero with OCTALZEROES off (default) → decimal.
13140 if !isset(OCTALZEROES) {
13141 assert_eq!(
13142 zstrtoul_underscore("0777"),
13143 Some(777),
13144 "c:2543 — OCTALZEROES off: leading-0 parses as decimal"
13145 );
13146 assert_eq!(zstrtoul_underscore("010"), Some(10));
13147 }
13148 }
13149
13150 /// `Src/utils.c:2547-2548` — underscores are stripped before
13151 /// parsing. C: `if (*s == '_') continue;`. Used to support
13152 /// human-readable big numbers like `1_000_000`.
13153 #[test]
13154 fn zstrtoul_underscore_strips_underscores() {
13155 let _g = crate::test_util::global_state_lock();
13156 assert_eq!(
13157 zstrtoul_underscore("1_000_000"),
13158 Some(1_000_000),
13159 "c:2547-2548 — `_` stripped from numeric input"
13160 );
13161 assert_eq!(zstrtoul_underscore("0xff_ff"), Some(0xffff));
13162 assert_eq!(zstrtoul_underscore("0b1010_1010"), Some(0xaa));
13163 }
13164
13165 /// `Src/utils.c:2533-2534` — leading `+` sign is consumed.
13166 /// `zulong` is unsigned so `-` is not handled here (caller
13167 /// handles negation at `zstrtol_underscore`).
13168 #[test]
13169 fn zstrtoul_underscore_consumes_leading_plus() {
13170 let _g = crate::test_util::global_state_lock();
13171 assert_eq!(zstrtoul_underscore("+42"), Some(42));
13172 assert_eq!(zstrtoul_underscore("+0xff"), Some(255));
13173 }
13174
13175 /// `Src/utils.c:467` — `if (ZISPRINT(c)) goto done;`. Printable
13176 /// ASCII passes through nicechar_sel unchanged. Pin a single-char
13177 /// printable input → single-char output.
13178 #[test]
13179 fn nicechar_sel_passes_printable_ascii_unchanged() {
13180 let _g = crate::test_util::global_state_lock();
13181 for c in "aA0!~".chars() {
13182 assert_eq!(
13183 nicechar_sel(c, false),
13184 c.to_string(),
13185 "c:467 — printable ASCII '{}' passes through",
13186 c
13187 );
13188 }
13189 }
13190
13191 /// `Src/utils.c:487-492` — `\n` becomes `\n` (backslash-n) and
13192 /// `\t` becomes `\t`. Both two-char outputs. Pin the canonical
13193 /// escapes.
13194 #[test]
13195 fn nicechar_sel_escapes_newline_and_tab() {
13196 let _g = crate::test_util::global_state_lock();
13197 assert_eq!(
13198 nicechar_sel('\n', false),
13199 "\\n",
13200 "c:487-489 — newline escape"
13201 );
13202 assert_eq!(nicechar_sel('\t', false), "\\t", "c:490-492 — tab escape");
13203 }
13204
13205 /// `Src/utils.c:493-501` — control chars (<0x20 except \n/\t)
13206 /// get `^X` prefix in non-quotable mode and `\C-X` in quotable.
13207 /// c:500 — `c += 0x40` adds 0x40 to map 0x01 → 0x41 ('A').
13208 #[test]
13209 fn nicechar_sel_control_chars_use_caret_or_c_prefix() {
13210 let _g = crate::test_util::global_state_lock();
13211 // Non-quotable: ^A
13212 assert_eq!(
13213 nicechar_sel('\x01', false),
13214 "^A",
13215 "c:499-500 — \\x01 → ^A non-quotable"
13216 );
13217 // Quotable: \C-A
13218 assert_eq!(
13219 nicechar_sel('\x01', true),
13220 "\\C-A",
13221 "c:495-500 — \\x01 → \\C-A quotable"
13222 );
13223 // ^G (BEL = 0x07 → 0x47 = 'G')
13224 assert_eq!(nicechar_sel('\x07', false), "^G");
13225 // ^[ (ESC = 0x1b → 0x5b = '[')
13226 assert_eq!(nicechar_sel('\x1b', false), "^[");
13227 }
13228
13229 /// `Src/utils.c:479-486` — DEL (0x7f) renders as `^?` or `\C-?`.
13230 #[test]
13231 fn nicechar_sel_del_renders_as_caret_question() {
13232 let _g = crate::test_util::global_state_lock();
13233 assert_eq!(nicechar_sel('\x7f', false), "^?", "c:485-486 — DEL is `^?`");
13234 assert_eq!(
13235 nicechar_sel('\x7f', true),
13236 "\\C-?",
13237 "c:481-486 — DEL is `\\C-?` quotable"
13238 );
13239 }
13240
13241 /// `Src/utils.c:469-478` — high-bit bytes (>= 0x80) under default
13242 /// PRINTEIGHTBIT-off path: write `\M-` then mask to low ASCII.
13243 /// If the masked char is printable, output is `\M-<char>`. Pin
13244 /// the canonical 0xc1 → \M-A and 0xe1 → \M-a cases.
13245 #[test]
13246 fn nicechar_sel_highbit_uses_meta_prefix_when_printeightbit_off() {
13247 let _g = crate::test_util::global_state_lock();
13248 if isset(PRINTEIGHTBIT) {
13249 return; // Test only valid when PRINTEIGHTBIT off (default).
13250 }
13251 // 0xc1 = 'A' + 0x80 → \M-A
13252 let c = char::from_u32(0xc1).unwrap();
13253 assert_eq!(
13254 nicechar_sel(c, false),
13255 "\\M-A",
13256 "c:472-477 — high-bit 0xc1 → \\M-A under PRINTEIGHTBIT-off"
13257 );
13258 // 0xe1 = 'a' + 0x80 → \M-a
13259 let c = char::from_u32(0xe1).unwrap();
13260 assert_eq!(nicechar_sel(c, false), "\\M-a");
13261 }
13262
13263 /// `Src/utils.c:593-705` — `wcs_nicechar_sel`. Wide-char variant
13264 /// of `nicechar_sel`: control chars escape, printable wides
13265 /// emit raw UTF-8, large codepoints get `\u`/`\U` hex escape.
13266 /// Pin every branch. Previously delegated to `nicechar_sel`
13267 /// which byte-masked via `c & 0xff` — every UTF-8 codepoint
13268 /// emerged mangled.
13269 #[test]
13270 fn wcs_nicechar_sel_printable_wide_emits_utf8() {
13271 let _g = crate::test_util::global_state_lock();
13272 // c:644-678 — printable wide char emits raw UTF-8.
13273 assert_eq!(
13274 wcs_nicechar_sel('a', None, None, false),
13275 "a",
13276 "ASCII printable"
13277 );
13278 assert_eq!(
13279 wcs_nicechar_sel('é', None, None, false),
13280 "é",
13281 "Latin-1 printable"
13282 );
13283 assert_eq!(
13284 wcs_nicechar_sel('字', None, None, false),
13285 "字",
13286 "CJK printable"
13287 );
13288 }
13289
13290 /// c:625-630 — `\n` and `\t` escape (same as nicechar_sel for ASCII).
13291 #[test]
13292 fn wcs_nicechar_sel_escapes_newline_and_tab() {
13293 let _g = crate::test_util::global_state_lock();
13294 assert_eq!(wcs_nicechar_sel('\n', None, None, false), "\\n");
13295 assert_eq!(wcs_nicechar_sel('\t', None, None, false), "\\t");
13296 }
13297
13298 /// c:617-623 — DEL (0x7f) renders as `^?` (non-quotable) or
13299 /// `\C-?` (quotable). Same as the byte version.
13300 #[test]
13301 fn wcs_nicechar_sel_del_uses_caret_question() {
13302 let _g = crate::test_util::global_state_lock();
13303 assert_eq!(wcs_nicechar_sel('\x7f', None, None, false), "^?");
13304 assert_eq!(wcs_nicechar_sel('\x7f', None, None, true), "\\C-?");
13305 }
13306
13307 /// c:631-638 — control chars (0x01-0x1f, except \n/\t) emit
13308 /// `^X` or `\C-X` with the +0x40 offset.
13309 #[test]
13310 fn wcs_nicechar_sel_control_chars_use_caret_prefix() {
13311 let _g = crate::test_util::global_state_lock();
13312 assert_eq!(wcs_nicechar_sel('\x01', None, None, false), "^A");
13313 assert_eq!(wcs_nicechar_sel('\x07', None, None, false), "^G");
13314 assert_eq!(wcs_nicechar_sel('\x1b', None, None, false), "^[");
13315 assert_eq!(wcs_nicechar_sel('\x01', None, None, true), "\\C-A");
13316 }
13317
13318 /// `Src/utils.c:656-663` — large non-printable codepoints get
13319 /// hex escape: `\U%.8x` (>= 0x10000), `\u%.4x` (>= 0x100).
13320 /// Both lowercase per C's `%x`. Use a non-printable wide char
13321 /// to test (most BMP chars are printable; pick a control area
13322 /// like U+0085 NEXT LINE).
13323 #[test]
13324 fn wcs_nicechar_sel_large_nonprintable_uses_hex_escape() {
13325 let _g = crate::test_util::global_state_lock();
13326 // U+0085 NEL — control in C1 range, not iswprint.
13327 let nel = char::from_u32(0x85).unwrap();
13328 // Since 0x85 < 0x100, falls into nicechar_sel fallback.
13329 // After u9_iswprint says false, cv < 0x80 is false (cv=0x85),
13330 // PRINTEIGHTBIT off → enters !is_printable && cv < 0x80 branch?
13331 // Actually 0x85 >= 0x80 → !print_eightbit true → enters non-print branch.
13332 // 0x85 not 0x7f, not \n/\t, not <0x20. Falls past all if-elses to
13333 // post-branch logic → !is_printable, cv >= 0x100 false, cv >= 0x100 false
13334 // → falls to nicechar_sel fallback.
13335 let r = wcs_nicechar_sel(nel, None, None, false);
13336 assert!(!r.is_empty(), "must emit something for U+0085");
13337 // U+200B ZERO WIDTH SPACE — non-printable, 0x100-0xffff range.
13338 let zwsp = char::from_u32(0x200B).unwrap();
13339 let r = wcs_nicechar_sel(zwsp, None, None, false);
13340 // u9_iswprint(0x200B) returns true via unicode-width 0-width.
13341 // Per the impl: u9_iswprint true → emit raw. So r should be
13342 // the raw zwsp char.
13343 assert!(!r.is_empty());
13344 }
13345
13346 /// `Src/utils.c:709-714` — `wcs_nicechar(c)` delegates to
13347 /// `wcs_nicechar_sel(c, widthp, swidep, 0)`. Pin the parity.
13348 #[test]
13349 fn wcs_nicechar_matches_wcs_nicechar_sel_with_zero() {
13350 let _g = crate::test_util::global_state_lock();
13351 for c in ['a', '\n', '\t', '\x7f', '\x01', 'é', '字'] {
13352 assert_eq!(
13353 wcs_nicechar(c, None, None),
13354 wcs_nicechar_sel(c, None, None, false),
13355 "c:711 — `wcs_nicechar(c, NULL, NULL)` must equal `wcs_nicechar_sel(c, NULL, NULL, 0)` for {:?}",
13356 c
13357 );
13358 }
13359 }
13360
13361 /// `Src/utils.c:1989-2012` — `movefd(fd)`. Three contracts:
13362 /// 1. fd == -1 → returned as-is (no syscalls).
13363 /// 2. fd >= 10 → returned unchanged (already high).
13364 /// 3. fd < 10 → dupped with F_DUPFD >= 10, original closed
13365 /// unconditionally (even on dup failure).
13366 #[test]
13367 #[cfg(unix)]
13368 fn movefd_returns_minus_one_unchanged() {
13369 let _g = crate::test_util::global_state_lock();
13370 // c:1992 — `if (fd != -1 && fd < 10)` skipped for fd=-1.
13371 assert_eq!(movefd(-1), -1, "fd=-1 must be returned unchanged");
13372 }
13373
13374 /// c:1992 — `fd >= 10` skips the dup-and-close path.
13375 #[test]
13376 #[cfg(unix)]
13377 fn movefd_returns_high_fd_unchanged() {
13378 let _g = crate::test_util::global_state_lock();
13379 // Use a known-open fd. /dev/null open with fd guaranteed to be
13380 // <10 because new fds get the lowest free slot — but we can
13381 // dup it to 10 directly to test the early-return path.
13382 let f = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13383 if f < 0 {
13384 return;
13385 } // Skip on unusual systems.
13386 let high = unsafe { libc::fcntl(f, libc::F_DUPFD, 20) };
13387 unsafe {
13388 libc::close(f);
13389 }
13390 if high < 0 {
13391 return;
13392 }
13393 // movefd(20) must return 20 unchanged (no dup, no close).
13394 let r = movefd(high);
13395 assert_eq!(r, high, "c:1992 — fd >= 10 returned unchanged");
13396 unsafe {
13397 libc::close(high);
13398 }
13399 }
13400
13401 /// `Src/utils.c:1968-1983` — `check_fd_table(fd)`. Grows the
13402 /// fdtable Vec to `fd+1` slots (filling new entries with
13403 /// FDT_UNUSED) and bumps `MAX_ZSH_FD` to `fd`. Pin: a small
13404 /// fd grows the table to `fd+1` length; MAX_ZSH_FD reflects it.
13405 /// fd ≤ cur_max early-returns without growing (c:1971-1972).
13406 #[test]
13407 fn check_fd_table_grows_to_fd_plus_one() {
13408 let _g = crate::test_util::global_state_lock();
13409 // Reset globals so the test is deterministic regardless
13410 // of prior ordering — other tests may have grown the
13411 // table or bumped MAX_ZSH_FD.
13412 MAX_ZSH_FD.store(-1, Ordering::Relaxed);
13413 {
13414 let mut g = fdtable_lock().lock().unwrap();
13415 g.clear();
13416 }
13417
13418 // c:1971-1972 — fd ≤ cur_max is the early-return path.
13419 // With cur_max=-1, fd=-1 hits `fd <= cur_max` → true.
13420 assert!(check_fd_table(-1));
13421 assert_eq!(
13422 MAX_ZSH_FD.load(Ordering::Relaxed),
13423 -1,
13424 "fd ≤ cur_max early-returns; MAX_ZSH_FD untouched"
13425 );
13426
13427 // c:1974-1981 — fd > cur_max grows fdtable to fd+1 slots.
13428 assert!(check_fd_table(7));
13429 assert_eq!(
13430 MAX_ZSH_FD.load(Ordering::Relaxed),
13431 7,
13432 "c:1982 — MAX_ZSH_FD := fd"
13433 );
13434 let len = {
13435 let g = fdtable_lock().lock().unwrap();
13436 g.len()
13437 };
13438 assert!(
13439 len >= 8,
13440 "c:1975-1979 — fdtable grew to ≥ fd+1 = 8 slots, got {}",
13441 len
13442 );
13443
13444 // Idempotence under fd ≤ cur_max.
13445 assert!(check_fd_table(3));
13446 assert_eq!(
13447 MAX_ZSH_FD.load(Ordering::Relaxed),
13448 7,
13449 "fd ≤ cur_max keeps MAX_ZSH_FD at 7"
13450 );
13451 }
13452
13453 /// `Src/utils.c:4364,4367` — `wcsitype(c, IWORD/ISEP)` reads
13454 /// from the `wordchars`/`ifs` GLOBALS (writable by
13455 /// `wordcharssetfn`/`ifssetfn`). Previously read from
13456 /// `std::env::var` — the libc process env — which never
13457 /// reflects runtime `WORDCHARS=…` shell-side assignments.
13458 /// Pin: set wordchars via `wordcharssetfn`, verify `wcsitype`
13459 /// returns true for chars in the new set.
13460 #[test]
13461 fn wcsitype_iword_reads_from_canonical_wordchars_global() {
13462 let _g = crate::test_util::global_state_lock();
13463 // Test only meaningful in MULTIBYTE mode for non-ASCII chars;
13464 // ASCII chars (< 0x80) route through TYPTAB directly.
13465 // Skip if MULTIBYTE off.
13466 if !isset(MULTIBYTE) {
13467 return;
13468 }
13469 // Save and set WORDCHARS to a single non-ASCII char.
13470 // C dispatches `pm->gsu.s->{get,set}fn(pm, val)`; mirror via
13471 // paramtab lookup.
13472 let saved = crate::ported::params::paramtab()
13473 .read()
13474 .ok()
13475 .and_then(|t| {
13476 t.get("WORDCHARS")
13477 .map(|pm| crate::ported::params::wordcharsgetfn(pm))
13478 })
13479 .unwrap_or_default();
13480 // wordcharssetfn ignores its `_pm` arg and re-enters paramtab's
13481 // read lock via inittyptab (sibling of the IFS deadlock above).
13482 // Use a stack-local dummy pm instead of going through paramtab's
13483 // write lock so the test never holds two paramtab locks at once.
13484 let mut dummy = crate::ported::zsh_h::param::default();
13485 let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
13486 wordcharssetfn(dummy, val);
13487 };
13488 do_set(&mut dummy, "é".to_string());
13489 // 'é' is alphanumeric per Unicode → IWORD returns true via
13490 // is_alphanumeric short-circuit at c:4353. So we can't pin
13491 // the WORDCHARS-specific path with 'é'. Use a non-alnum char.
13492 do_set(&mut dummy, ":".to_string());
13493 // ':' is ASCII so wcsitype routes through TYPTAB (which now
13494 // has IWORD on ':' because wordcharssetfn called inittyptab).
13495 assert!(
13496 wcsitype(':', IWORD as u32),
13497 "c:4364 — wordchars membership through canonical global"
13498 );
13499 // Restore.
13500 do_set(&mut dummy, saved);
13501 }
13502
13503 /// `Src/utils.c:2090-2097` — `addmodulefd(fd, fdt)`. Stores the
13504 /// provided fdt in the fdtable slot for the given fd. Previously
13505 /// Rust port hardcoded FDT_MODULE and added CLOEXEC unconditionally.
13506 /// Pin: pass FDT_EXTERNAL, verify slot stores FDT_EXTERNAL (not
13507 /// FDT_MODULE).
13508 #[cfg(unix)]
13509 #[test]
13510 fn addmodulefd_respects_fdt_parameter() {
13511 let _g = crate::test_util::global_state_lock();
13512 // Open a real fd to use.
13513 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13514 if fd < 0 {
13515 return;
13516 }
13517 addmodulefd(fd, FDT_EXTERNAL);
13518 assert_eq!(
13519 fdtable_get(fd),
13520 FDT_EXTERNAL,
13521 "c:2095 — fdt parameter must be stored verbatim"
13522 );
13523 // Switch to FDT_MODULE.
13524 addmodulefd(fd, FDT_MODULE);
13525 assert_eq!(
13526 fdtable_get(fd),
13527 FDT_MODULE,
13528 "c:2095 — second call overwrites with new fdt"
13529 );
13530 // Cleanup.
13531 unsafe {
13532 libc::close(fd);
13533 }
13534 }
13535
13536 /// `Src/utils.c:2093` — `if (fd >= 0)`. Negative fd is silently
13537 /// ignored (no fdtable update). Pin the guard.
13538 #[test]
13539 fn addmodulefd_ignores_negative_fd() {
13540 let _g = crate::test_util::global_state_lock();
13541 // Should not panic, no side effect.
13542 addmodulefd(-1, FDT_MODULE);
13543 // Nothing to assert on side-effect-free path; just verify no panic.
13544 }
13545
13546 /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns -1 for
13547 /// non-lock fds, 0 for FDT_FLOCK/FDT_FLOCK_EXEC. Previously the
13548 /// Rust port always returned 0 — broke `zsystem flock -u <fd>`
13549 /// distinguishing "fd never flocked" from "successfully released".
13550 #[cfg(unix)]
13551 #[test]
13552 fn zcloselockfd_returns_minus_one_for_non_lock_fd() {
13553 let _g = crate::test_util::global_state_lock();
13554 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13555 if fd < 0 {
13556 return;
13557 }
13558 // No addlockfd call → fd is NOT in the flock table.
13559 let r = zcloselockfd(fd);
13560 assert_eq!(r, -1, "c:2160-2161 — non-lock fd must return -1, NOT 0");
13561 unsafe {
13562 libc::close(fd);
13563 }
13564 }
13565
13566 /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns 0 for an
13567 /// fd that was registered via `addlockfd`. Pin the end-to-end
13568 /// round-trip: addlockfd → zcloselockfd returns 0.
13569 #[cfg(unix)]
13570 #[test]
13571 fn zcloselockfd_returns_zero_for_flock_fd_then_closes() {
13572 let _g = crate::test_util::global_state_lock();
13573 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13574 if fd < 0 {
13575 return;
13576 }
13577 addlockfd(fd, true);
13578 let r = zcloselockfd(fd);
13579 assert_eq!(r, 0, "c:2162-2163 — FDT_FLOCK fd → zclose + return 0");
13580 // After zcloselockfd, the underlying close() has fired.
13581 // close() returning -1 with EBADF would confirm that, but
13582 // that's a libc side-effect; just verify the return value.
13583 }
13584
13585 /// `Src/utils.c:1982` — `check_fd_table` sets `max_zsh_fd = fd`
13586 /// when fd is new. `fdtable_set` inlines this to keep the guard
13587 /// in `zcloselockfd` (`if (fd > max_zsh_fd) return -1`) working
13588 /// against fds populated by `addmodulefd`/`addlockfd`. Pin: after
13589 /// `fdtable_set(fd, FDT_FLOCK)`, MAX_ZSH_FD >= fd.
13590 #[cfg(unix)]
13591 #[test]
13592 fn fdtable_set_bumps_max_zsh_fd() {
13593 let _g = crate::test_util::global_state_lock();
13594 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13595 if fd < 0 {
13596 return;
13597 }
13598 fdtable_set(fd, FDT_FLOCK);
13599 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
13600 assert!(
13601 max_fd >= fd,
13602 "c:1982 — max_zsh_fd ({}) must be >= newly-set fd ({})",
13603 max_fd,
13604 fd
13605 );
13606 // Cleanup.
13607 fdtable_set(fd, FDT_UNUSED);
13608 unsafe {
13609 libc::close(fd);
13610 }
13611 }
13612
13613 /// `Src/utils.c:2111-2121` — `addlockfd(fd, cloexec)`. Selects
13614 /// between FDT_FLOCK (when cloexec=true, lock dies on exec) and
13615 /// FDT_FLOCK_EXEC (when cloexec=false, lock survives exec).
13616 /// Previously the Rust port did `fcntl(F_SETFD, CLOEXEC)` —
13617 /// totally wrong semantics. Pin the FDT slot.
13618 #[cfg(unix)]
13619 #[test]
13620 fn addlockfd_selects_flock_category_per_cloexec_flag() {
13621 let _g = crate::test_util::global_state_lock();
13622 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13623 if fd < 0 {
13624 return;
13625 }
13626 // cloexec=true → FDT_FLOCK (lock dies on exec).
13627 addlockfd(fd, true);
13628 assert_eq!(
13629 fdtable_get(fd),
13630 FDT_FLOCK,
13631 "c:2117 — cloexec=true → FDT_FLOCK"
13632 );
13633 // cloexec=false → FDT_FLOCK_EXEC (lock survives exec).
13634 addlockfd(fd, false);
13635 assert_eq!(
13636 fdtable_get(fd),
13637 FDT_FLOCK_EXEC,
13638 "c:2119 — cloexec=false → FDT_FLOCK_EXEC"
13639 );
13640 // Cleanup.
13641 unsafe {
13642 libc::close(fd);
13643 }
13644 }
13645
13646 /// `Src/utils.c:1211` — `adduserdir` rejects paths `>= PATH_MAX`
13647 /// by removing the existing entry (treating as "can't use this
13648 /// value as a directory"). Pin with a path constructed to exceed
13649 /// PATH_MAX, verify no insertion happens.
13650 #[cfg(unix)]
13651 #[test]
13652 fn adduserdir_rejects_paths_at_or_above_path_max() {
13653 let _g = crate::test_util::global_state_lock();
13654 if !interact() {
13655 // c:1193 — non-interactive shells skip the table entirely.
13656 // Test inactive in non-interactive contexts.
13657 return;
13658 }
13659 // Ensure the entry doesn't exist beforehand.
13660 let name = "ZSHRS_TEST_PATHMAX_DIR";
13661 let _ = removenameddirnode(name);
13662 // Construct an oversized path.
13663 let mut over: String = "/".to_string();
13664 over.push_str(&"a".repeat(libc::PATH_MAX as usize));
13665 // adduserdir with the oversized path + AUTONAMEDIRS-off path
13666 // via always=true (so the AUTONAMEDIRS guard doesn't reject).
13667 adduserdir(name, &over, 0, true);
13668 // c:1211 — too-long → remove path triggered, no entry added.
13669 let tab = nameddirtab().lock().unwrap();
13670 assert!(
13671 !tab.contains_key(name),
13672 "c:1211 — strlen(t) >= PATH_MAX must NOT insert entry"
13673 );
13674 }
13675
13676 /// `Src/utils.c:760-786` — `pathprog`. Finds any regular file
13677 /// (not directory, no executable-bit check) in `$PATH`.
13678 /// Previously required `mode & 0o111 != 0` — silently missed
13679 /// non-executable autoload-function plaintext scripts. Pin
13680 /// using a known-existing file in /tmp.
13681 #[test]
13682 #[cfg(unix)]
13683 fn pathprog_finds_non_executable_files() {
13684 let _g = crate::test_util::global_state_lock();
13685 // Create a non-executable file in /tmp and add /tmp to PATH.
13686 let test_name = format!("zshrs_test_pathprog_{}", unsafe { libc::getpid() });
13687 let path = PathBuf::from("/tmp").join(&test_name);
13688 // Write content, NO executable bit.
13689 if fs::write(&path, b"plain content").is_err() {
13690 return; // /tmp may be unwritable on some systems.
13691 }
13692 // Verify it's not executable.
13693 let meta = fs::metadata(&path).unwrap();
13694 assert_eq!(
13695 meta.permissions().mode() & 0o111,
13696 0,
13697 "test setup: must be non-executable"
13698 );
13699 // Set PATH to /tmp.
13700 let saved_path = getsparam("PATH");
13701 assignsparam("PATH", "/tmp", 0);
13702 // pathprog should find the file.
13703 let r = pathprog(&test_name);
13704 // Cleanup.
13705 let _ = fs::remove_file(&path);
13706 if let Some(prev) = saved_path {
13707 assignsparam("PATH", &prev, 0);
13708 }
13709 assert_eq!(
13710 r,
13711 Some(path),
13712 "c:776 — pathprog finds non-executable files (only F_OK + !S_ISDIR)"
13713 );
13714 }
13715
13716 /// `Src/utils.c:776-778` — pathprog skips directories. Pin: a
13717 /// directory in $PATH with the queried name must NOT be returned.
13718 #[test]
13719 #[cfg(unix)]
13720 fn pathprog_skips_directories() {
13721 let _g = crate::test_util::global_state_lock();
13722 let test_name = format!("zshrs_test_pathprog_dir_{}", unsafe { libc::getpid() });
13723 let path = PathBuf::from("/tmp").join(&test_name);
13724 if fs::create_dir(&path).is_err() {
13725 return;
13726 }
13727 let saved_path = getsparam("PATH");
13728 assignsparam("PATH", "/tmp", 0);
13729 let r = pathprog(&test_name);
13730 // Cleanup.
13731 let _ = fs::remove_dir(&path);
13732 if let Some(prev) = saved_path {
13733 assignsparam("PATH", &prev, 0);
13734 }
13735 assert!(
13736 r.is_none(),
13737 "c:778 — pathprog must skip directories (!S_ISDIR)"
13738 );
13739 }
13740
13741 /// `Src/utils.c:4367` — `wcsitype(c, ISEP)` reads from canonical
13742 /// `ifs` global. Pin via `ifssetfn`. ASCII path routes through
13743 /// TYPTAB which gets ISEP bit from inittyptab's IFS walk
13744 /// (added earlier this session). End-to-end pin.
13745 #[test]
13746 fn wcsitype_isep_reads_from_canonical_ifs_global() {
13747 let _g = crate::test_util::global_state_lock();
13748 if !isset(MULTIBYTE) {
13749 return;
13750 }
13751 // C: `pm->gsu.s->{get,set}fn(pm, val)`. Mirror via paramtab.
13752 let saved = crate::ported::params::paramtab()
13753 .read()
13754 .ok()
13755 .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
13756 .unwrap_or_default();
13757 // ifssetfn ignores its `_pm` arg (the C `gsu.s->setfn` receives
13758 // the param but the IFS callback only touches the global `ifs`
13759 // store + calls inittyptab). Calling through paramtab's write
13760 // lock would deadlock because inittyptab() re-takes paramtab's
13761 // read lock for the IFS walk (utils.rs:5028) — pthread rwlock
13762 // forbids same-thread write→read upgrade. Pass a stack-local
13763 // dummy pm instead so we never hold the paramtab lock during
13764 // ifssetfn.
13765 let mut dummy = crate::ported::zsh_h::param::default();
13766 let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
13767 ifssetfn(dummy, val);
13768 };
13769 do_set(&mut dummy, ":".to_string());
13770 assert!(
13771 wcsitype(':', ISEP as u32),
13772 "c:4367 — IFS membership through canonical global"
13773 );
13774 // Restore.
13775 do_set(&mut dummy, saved);
13776 }
13777
13778 /// c:536 — high-bit byte (>= 0x80) is nice when PRINTEIGHTBIT is
13779 /// OFF (the default). Pin the default case; the alternate
13780 /// behavior under PRINTEIGHTBIT-on is harder to exercise from
13781 /// a unit test due to opt-state global mutation.
13782 #[test]
13783 fn is_nicechar_highbit_is_nice_when_printeightbit_off() {
13784 let _g = crate::test_util::global_state_lock();
13785 // Default state: PRINTEIGHTBIT is off → high-bit bytes are nice.
13786 // Use char with high-bit equivalent low byte.
13787 // 0xb5 (Meta+5 territory) — masked to 0xff → still 0xb5.
13788 let c = char::from_u32(0xb5).unwrap();
13789 // ZISPRINT(0xb5) is false (>0x7e). 0xb5 & 0x80 set → check PRINTEIGHTBIT.
13790 // Default PRINTEIGHTBIT off → returns true (nice).
13791 if !isset(PRINTEIGHTBIT) {
13792 assert!(
13793 is_nicechar(c),
13794 "c:536 — high-bit byte 0x{:x} must be nice when PRINTEIGHTBIT off",
13795 0xb5_u32
13796 );
13797 }
13798 }
13799
13800 /// `Src/utils.c:1969-1983` — `check_fd_table(fd)` grows the
13801 /// fdtable so it can index `fd` and bumps `max_zsh_fd`. The
13802 /// previous Rust port was a no-op shim that always returned true;
13803 /// real behavior is "fdtable.len() must be > fd" and
13804 /// "MAX_ZSH_FD >= fd" after the call.
13805 #[test]
13806 fn check_fd_table_grows_fdtable_and_bumps_max_zsh_fd() {
13807 let _g = crate::test_util::global_state_lock();
13808 // Snapshot prior state so we don't poison other tests.
13809 let saved_max = MAX_ZSH_FD.load(Ordering::Relaxed);
13810 // Pick a target fd well above the typical 0/1/2.
13811 let target = saved_max.max(50) + 7;
13812 check_fd_table(target);
13813 let new_max = MAX_ZSH_FD.load(Ordering::Relaxed);
13814 assert!(
13815 new_max >= target,
13816 "c:1982 — max_zsh_fd must be >= target after grow (got {})",
13817 new_max
13818 );
13819 // Calling again with a lower fd MUST NOT shrink max_zsh_fd
13820 // (c:1971-1972 early return).
13821 check_fd_table(target - 3);
13822 assert_eq!(
13823 MAX_ZSH_FD.load(Ordering::Relaxed),
13824 new_max,
13825 "c:1971 — fd <= max_zsh_fd path must not change max"
13826 );
13827 // Restore approximately — best we can do without exposing
13828 // fdtable internals; subsequent fdtable_set callers cope.
13829 MAX_ZSH_FD.store(saved_max, Ordering::Relaxed);
13830 }
13831
13832 /// `Src/utils.c:1971` — `if (fd <= max_zsh_fd) return;` early
13833 /// exit. Calling with a small fd (<= current max) must be a
13834 /// no-op.
13835 #[test]
13836 fn check_fd_table_small_fd_is_noop() {
13837 let _g = crate::test_util::global_state_lock();
13838 // Ensure max_zsh_fd is non-trivial.
13839 let _ = check_fd_table(100);
13840 let max_before = MAX_ZSH_FD.load(Ordering::Relaxed);
13841 check_fd_table(5);
13842 assert_eq!(
13843 MAX_ZSH_FD.load(Ordering::Relaxed),
13844 max_before,
13845 "c:1971 — small fd path must not touch max_zsh_fd"
13846 );
13847 }
13848
13849 /// `Src/utils.c:1969-1983` — defensive: negative fd shouldn't
13850 /// panic. C-side would have indexed past the start of `fdtable`
13851 /// (UB). Rust port should fail-soft.
13852 #[test]
13853 fn check_fd_table_negative_fd_does_not_panic() {
13854 let _g = crate::test_util::global_state_lock();
13855 // Should not panic; behavior beyond "no panic" is unspecified.
13856 let _ = check_fd_table(-1);
13857 let _ = check_fd_table(-100);
13858 }
13859
13860 /// `Src/zsh.h:3274` — under MULTIBYTE_SUPPORT, the C source
13861 /// defines `nicezputs(str, outs)` as a macro:
13862 /// `(void)mb_niceformat((str), (outs), NULL, 0)`.
13863 /// So Rust `nicezputs(s)` must equal `mb_niceformat(s)` for
13864 /// every input. Pins the macro-equivalence after fixing the
13865 /// previous `chars().map(nicechar)` impl (which corrupted
13866 /// non-ASCII multibyte codepoints into `\M-X` mangle).
13867 #[test]
13868 fn nicezputs_matches_mb_niceformat_under_multibyte() {
13869 let _g = crate::test_util::global_state_lock();
13870 // C-equivalent pattern: write to a stream, compare to
13871 // mb_niceformat outstrp-form output. Inline rather than via a
13872 // Rust-only helper.
13873 for input in ["hello", "a\nb", "é", ""] {
13874 let mut nz_buf: Vec<u8> = Vec::new();
13875 let _ = nicezputs(input, &mut nz_buf);
13876 let nz = String::from_utf8(nz_buf).expect("utf8");
13877
13878 let mut mb_out: Option<String> = None;
13879 let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
13880 assert_eq!(
13881 nz,
13882 mb_out.unwrap_or_default(),
13883 "nicezputs and mb_niceformat must agree for {:?}",
13884 input
13885 );
13886 }
13887 // \n must produce literal `\n` (backslash + n).
13888 let mut nz_buf: Vec<u8> = Vec::new();
13889 let _ = nicezputs("a\nb", &mut nz_buf);
13890 let nz = String::from_utf8(nz_buf).expect("utf8");
13891 assert!(nz.contains("\\n"), "nicechar emits `\\n`, not raw 0x0a");
13892 }
13893
13894 /// `Src/utils.c:5530` — under MULTIBYTE_SUPPORT, `nicedup(s, heap)`
13895 /// body is `(void)mb_niceformat(s, NULL, &retstr, …); return retstr;`
13896 /// so the returned string MUST equal mb_niceformat output.
13897 #[test]
13898 fn nicedup_matches_mb_niceformat_under_multibyte() {
13899 let _g = crate::test_util::global_state_lock();
13900 for input in ["hello", "a\nb", "é"] {
13901 let nd = nicedup(input, 0);
13902 let mut mb_out: Option<String> = None;
13903 let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
13904 assert_eq!(
13905 nd,
13906 mb_out.unwrap_or_default(),
13907 "nicedup must equal mb_niceformat outstrp form for {:?}",
13908 input
13909 );
13910 }
13911 // nicedupstring delegates to nicedup(s, 1).
13912 assert_eq!(nicedupstring("hé\nllo"), nicedup("hé\nllo", 1));
13913 }
13914
13915 /// `Src/utils.c:734-744` — `zwcwidth(wc)` returns 1 when MULTIBYTE
13916 /// option is unset (c:738), regardless of the codepoint's actual
13917 /// display width. The previous Rust port skipped the option gate
13918 /// and always used the Unicode width table. Pin the option-gated
13919 /// behavior end-to-end: unset MULTIBYTE → width 1 for CJK; set →
13920 /// width 2 for CJK.
13921 #[test]
13922 fn zwcwidth_returns_1_when_multibyte_unset() {
13923 let _g = crate::test_util::global_state_lock();
13924 let saved = isset(MULTIBYTE);
13925 // c:738 — unset(MULTIBYTE) path returns 1 for everything.
13926 dosetopt(MULTIBYTE, 0, 1);
13927 assert_eq!(zwcwidth('a'), 1, "c:738 — ASCII width 1 under nomultibyte");
13928 assert_eq!(
13929 zwcwidth('字'),
13930 1,
13931 "c:738 — CJK collapses to 1 under nomultibyte (was 2 via Unicode-width)"
13932 );
13933 assert_eq!(
13934 zwcwidth('\u{200B}'),
13935 1,
13936 "c:738 — zero-width space → 1 under nomultibyte (was 0 via Unicode-width)"
13937 );
13938 // c:740-744 — MULTIBYTE on: Unicode-width table applies.
13939 dosetopt(MULTIBYTE, 1, 1);
13940 assert_eq!(zwcwidth('a'), 1, "ASCII width is 1");
13941 assert_eq!(zwcwidth('字'), 2, "c:740 — CJK width 2 under multibyte");
13942 // Restore prior state.
13943 dosetopt(MULTIBYTE, if saved { 1 } else { 0 }, 1);
13944 }
13945
13946 /// `Src/utils.c:6478-6492` — `quotedzputs(s, NULL)` under
13947 /// MULTIBYTE_SUPPORT detects "needs nice-format" inputs via
13948 /// `is_mb_niceformat(s)` and emits `$'<nice-formatted body>'`
13949 /// (the dollar-quoted form so embedded `\n`/`\t`/escape sequences
13950 /// round-trip through `typeset -p`/`set`/`set -x`). The previous
13951 /// Rust port skipped this branch entirely and single-quoted such
13952 /// strings — POSIX `'…'` is *strong* (no escapes interpreted),
13953 /// breaking round-trip for control bytes.
13954 #[test]
13955 fn quotedzputs_uses_dollar_quotes_for_control_chars() {
13956 let _g = crate::test_util::global_state_lock();
13957 // Ensure typtab is initialised — hasspecial depends on ISPECIAL
13958 // bits which are populated by inittyptab. Without this the
13959 // SPECCHARS arm short-circuits to "no specials".
13960 inittyptab();
13961 // Empty stays `''`.
13962 assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
13963 // Plain alphanumeric: no specials, no controls → bare.
13964 assert_eq!(
13965 quotedzputs("hello"),
13966 "hello",
13967 "c:6511-6517 — no SPECCHARS member → return unchanged"
13968 );
13969 // Control char `\n` needs `$'…'` to round-trip.
13970 let r = quotedzputs("a\nb");
13971 assert!(
13972 r.starts_with("$'") && r.ends_with('\''),
13973 "c:6488 — control char must use $'…' form (got {:?})",
13974 r
13975 );
13976 // Tab and ESC also force the niceformat arm.
13977 let r = quotedzputs("\t");
13978 assert!(
13979 r.starts_with("$'"),
13980 "c:6488 — TAB forces $'…' arm (got {:?})",
13981 r
13982 );
13983 let r = quotedzputs("\u{1b}[31m");
13984 assert!(
13985 r.starts_with("$'"),
13986 "c:6488 — ESC sequence forces $'…' arm (got {:?})",
13987 r
13988 );
13989 // Single quote forces single-quote arm via SPECCHARS membership
13990 // (Src/zsh.h:228 — SPECCHARS includes `'`). Embedded `'`
13991 // rewrites to `'\''`.
13992 let r = quotedzputs("a'b");
13993 assert!(
13994 r.contains("'\\''"),
13995 "c:6573-6587 — embedded ' → '\\'' (got {:?})",
13996 r
13997 );
13998 }
13999
14000 /// `Src/utils.c:2923-2945` — `read_loop` returns the requested
14001 /// length on full read, or the partial count on EOF. Pin: writing
14002 /// a known buffer to a pipe and reading it back returns the same
14003 /// content + correct length. Drives the no-side-effect path that
14004 /// the diagnostic-message fix doesn't touch.
14005 #[test]
14006 #[cfg(unix)]
14007 fn read_loop_round_trips_pipe_bytes() {
14008 let _g = crate::test_util::global_state_lock();
14009 // Create a pipe; write 16 bytes; read them back.
14010 let mut fds: [libc::c_int; 2] = [0; 2];
14011 unsafe {
14012 assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
14013 }
14014 let payload = b"hello-world-1234";
14015 let written = unsafe {
14016 libc::write(
14017 fds[1],
14018 payload.as_ptr() as *const libc::c_void,
14019 payload.len(),
14020 )
14021 };
14022 assert_eq!(written, payload.len() as isize, "write all 16 bytes");
14023 unsafe {
14024 libc::close(fds[1]);
14025 }
14026 let mut buf = [0u8; 16];
14027 let got = read_loop(fds[0], &mut buf).expect("read_loop ok");
14028 assert_eq!(got, payload.len(), "c:2929 — read_loop returns full length");
14029 assert_eq!(
14030 &buf[..],
14031 &payload[..],
14032 "c:2940-2941 — buffer copied verbatim"
14033 );
14034 unsafe {
14035 libc::close(fds[0]);
14036 }
14037 }
14038
14039 /// `Src/utils.c:2949-2970` — `write_loop` returns the requested
14040 /// length when the kernel accepts all bytes (the common case for
14041 /// a pipe with room). Pin the no-side-effect happy path.
14042 #[test]
14043 #[cfg(unix)]
14044 fn write_loop_writes_all_bytes_to_pipe() {
14045 let _g = crate::test_util::global_state_lock();
14046 let mut fds: [libc::c_int; 2] = [0; 2];
14047 unsafe {
14048 assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
14049 }
14050 let payload = b"abcdef";
14051 let got = write_loop(fds[1], payload).expect("write_loop ok");
14052 assert_eq!(
14053 got,
14054 payload.len(),
14055 "c:2955-2956 — write_loop returns full length on accept"
14056 );
14057 // Read back to verify.
14058 unsafe {
14059 libc::close(fds[1]);
14060 }
14061 let mut buf = [0u8; 6];
14062 let _ = unsafe { libc::read(fds[0], buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
14063 assert_eq!(&buf[..], &payload[..]);
14064 unsafe {
14065 libc::close(fds[0]);
14066 }
14067 }
14068
14069 /// `Src/utils.c:2935-2936` — `read_loop` on a closed/invalid fd
14070 /// returns an io::Error (the C path returns `ret` and emits the
14071 /// `zwarn` to stderr). Pin the error propagation; the zwarn
14072 /// emission is a stderr side-effect tested only by inspecting
14073 /// log output (out of scope here).
14074 #[test]
14075 #[cfg(unix)]
14076 fn read_loop_returns_error_on_invalid_fd() {
14077 let _g = crate::test_util::global_state_lock();
14078 let mut buf = [0u8; 4];
14079 // fd 9999 is essentially guaranteed-not-open in a test
14080 // process; the canonical "bad fd" error path.
14081 let r = read_loop(9999, &mut buf);
14082 assert!(
14083 r.is_err(),
14084 "c:2935 — invalid fd → io::Error (and zwarn to stderr)"
14085 );
14086 }
14087
14088 /// `Src/utils.c:2972-2988` — `read1char(echo)` reads from SHTTY,
14089 /// not stdin. With SHTTY uninitialised (default -1 in test
14090 /// environment), the function MUST return -1 immediately rather
14091 /// than blocking on a stdin read. The previous Rust port read
14092 /// from stdin and returned None — which in some test runners
14093 /// would block waiting for input.
14094 #[test]
14095 #[cfg(unix)]
14096 fn read1char_returns_minus_one_when_shtty_unset() {
14097 let _g = crate::test_util::global_state_lock();
14098 // Test environment: SHTTY is -1 (no controlling tty bound
14099 // by the port). C-side would `read(-1, ...)` which fails;
14100 // Rust port should fail-fast with -1.
14101 let saved = SHTTY.load(Ordering::Relaxed);
14102 SHTTY.store(-1, Ordering::Relaxed);
14103 let got = read1char(0); // echo=0
14104 assert_eq!(got, -1, "c:2978 — SHTTY=-1 → read fails → return -1");
14105 // Restore.
14106 SHTTY.store(saved, Ordering::Relaxed);
14107 }
14108
14109 /// `Src/utils.c:1989-2012` — `movefd(fd)` dups fd to >= 10 (so it
14110 /// stays out of the user fd range 0..=9), closes the original,
14111 /// AND marks the new fd as `FDT_INTERNAL` in fdtable. Previous
14112 /// Rust port omitted the fdtable_set call — internal-fd tracking
14113 /// (`closeallelse`, forkexec) silently never saw zshrs-internal
14114 /// fds. Pin: after movefd, fdtable[new_fd] == FDT_INTERNAL.
14115 #[test]
14116 #[cfg(unix)]
14117 fn movefd_marks_fdtable_internal() {
14118 let _g = crate::test_util::global_state_lock();
14119 // Open /dev/null to get a small (< 10) fd...
14120 let dev_null = CString::new("/dev/null").unwrap();
14121 let fd = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14122 assert!(fd >= 0, "open /dev/null returned -1");
14123 let new_fd = movefd(fd);
14124 assert!(
14125 new_fd >= 10,
14126 "c:1992-1994 — movefd dups to fd >= 10 (got {})",
14127 new_fd
14128 );
14129 // c:2009 — fdtable[new_fd] := FDT_INTERNAL.
14130 let entry = fdtable_get(new_fd);
14131 assert_eq!(
14132 entry, FDT_INTERNAL,
14133 "c:2009 — movefd must mark new_fd as FDT_INTERNAL (got {})",
14134 entry
14135 );
14136 unsafe {
14137 libc::close(new_fd);
14138 }
14139 }
14140
14141 /// `Src/utils.c:1989` — `movefd(-1)` is a defensive no-op: the
14142 /// `fd != -1 && fd < 10` gate at c:1992 fails, the c:2007
14143 /// `if (fd != -1)` post-check skips, and -1 is returned
14144 /// unchanged. Pin so a refactor of the early-out doesn't
14145 /// accidentally call fcntl(-1, ...).
14146 #[test]
14147 #[cfg(unix)]
14148 fn movefd_minus_one_returns_minus_one() {
14149 let _g = crate::test_util::global_state_lock();
14150 assert_eq!(
14151 movefd(-1),
14152 -1,
14153 "c:1992 — movefd(-1) bypasses both gates and returns -1"
14154 );
14155 }
14156
14157 /// `Src/utils.c:2021-2068` — `redup(x, y)` after a successful
14158 /// `dup2(x, y)` must:
14159 /// * Copy fdtable[x] to fdtable[y] (c:2054).
14160 /// * Promote FDT_FLOCK/FDT_FLOCK_EXEC to FDT_INTERNAL on the
14161 /// dup target (c:2055-2056) — the lock doesn't transfer.
14162 /// * Then close x (c:2064).
14163 ///
14164 /// Previous Rust port skipped the fdtable updates entirely.
14165 /// Pin the ownership-transfer with two fds the test allocates.
14166 #[test]
14167 #[cfg(unix)]
14168 fn redup_copies_fdtable_ownership_to_target() {
14169 let _g = crate::test_util::global_state_lock();
14170 // Open two distinct fds — both will land in the fdtable.
14171 let dev_null = CString::new("/dev/null").unwrap();
14172 let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14173 let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14174 assert!(x >= 0 && y >= 0, "open /dev/null returned -1");
14175 assert_ne!(x, y);
14176
14177 // Mark x as FDT_INTERNAL so we can observe the copy to y.
14178 check_fd_table(x);
14179 check_fd_table(y);
14180 fdtable_set(x, FDT_INTERNAL);
14181 fdtable_set(y, FDT_UNUSED);
14182
14183 let ret = redup(x, y);
14184 assert_eq!(ret, y, "c:2067 — successful redup returns y");
14185 // c:2054 — fdtable[y] inherited from fdtable[x].
14186 assert_eq!(
14187 fdtable_get(y),
14188 FDT_INTERNAL,
14189 "c:2054 — fdtable[y] = fdtable[x] (FDT_INTERNAL)"
14190 );
14191 // x is closed by c:2064.
14192 unsafe {
14193 libc::close(y);
14194 }
14195 }
14196
14197 /// `Src/utils.c:872-908` — `xsymlinks(path)` resolves `.`/`..`
14198 /// AND follows ONE LEVEL of symlinks via `readlink(2)`. Pin the
14199 /// symlink-following behavior with a temp-dir-managed symlink
14200 /// (the actual bug-fix pin — previous Rust port just normalised
14201 /// `.`/`..` without ever calling readlink).
14202 #[test]
14203 #[cfg(unix)]
14204 fn xsymlinks_follows_one_level_of_symlinks() {
14205 let _g = crate::test_util::global_state_lock();
14206 let tmp = std::env::temp_dir();
14207 let target = tmp.join(format!("zshrs_xsymlinks_target_{}", std::process::id()));
14208 let link = tmp.join(format!("zshrs_xsymlinks_link_{}", std::process::id()));
14209 // Create target as a regular dir to symlink to.
14210 let _ = fs::create_dir(&target);
14211 let _ = fs::remove_file(&link);
14212 std::os::unix::fs::symlink(&target, &link).unwrap();
14213
14214 let got = xsymlinks(link.to_str().unwrap()).unwrap();
14215 assert_eq!(
14216 got,
14217 target.to_string_lossy(),
14218 "c:908 — xsymlinks must follow the symlink to its target"
14219 );
14220
14221 // Cleanup.
14222 let _ = fs::remove_file(&link);
14223 let _ = fs::remove_dir(&target);
14224 }
14225
14226 /// `Src/utils.c:881-882` — `.` components are skipped.
14227 /// `Src/utils.c:883-896` — `..` components walk back one
14228 /// `/`-segment of xbuf (unless xbuf is empty or `/`).
14229 ///
14230 /// Use a temp directory we create ourselves — we can't pin
14231 /// `/tmp/...` literally because the macOS sandbox symlinks
14232 /// `/tmp -> /private/tmp`, which `xsymlinks` correctly follows
14233 /// (proving the bug-fix works). Test with a directory under
14234 /// the env tempdir + a non-existent sub so readlink fails on
14235 /// every component and the test exercises ONLY the c:881-896
14236 /// `.` / `..` paths.
14237 #[test]
14238 #[cfg(unix)]
14239 fn xsymlinks_normalises_dot_and_dotdot() {
14240 let _g = crate::test_util::global_state_lock();
14241 let tmp = fs::canonicalize(std::env::temp_dir()).unwrap();
14242 let base_dir = tmp.join(format!("zshrs_xs_norm_{}", std::process::id()));
14243 let _ = fs::create_dir(&base_dir);
14244 // `<base>/.` should be `<base>` (c:881 — `.` skipped).
14245 let arg = format!("{}/./.", base_dir.display());
14246 let got = xsymlinks(&arg).unwrap();
14247 assert_eq!(
14248 got,
14249 base_dir.to_string_lossy(),
14250 "c:881 — `.` segments collapse"
14251 );
14252 // `<base>/foo/..` should be `<base>` (c:891-895 — `..` walks back).
14253 let arg = format!("{}/foo/..", base_dir.display());
14254 let got = xsymlinks(&arg).unwrap();
14255 assert_eq!(
14256 got,
14257 base_dir.to_string_lossy(),
14258 "c:891-895 — `..` walks back one segment"
14259 );
14260 let _ = fs::remove_dir(&base_dir);
14261 }
14262
14263 /// `Src/utils.c:2055-2056` — when fdtable[x] is FDT_FLOCK or
14264 /// FDT_FLOCK_EXEC, the dup'd fd y gets promoted to FDT_INTERNAL
14265 /// (the dup doesn't carry the flock). Pin the promotion.
14266 ///
14267 /// Note on fdtable_flocks: C's c:2062-2063 decrements the
14268 /// flock count in `redup` BEFORE `zclose(x)`, and zclose ALSO
14269 /// decrements when it sees fdtable[fd] == FDT_FLOCK (c:2135).
14270 /// So C double-decrements for an FDT_FLOCK source fd in redup —
14271 /// the C comment at c:2058-2061 calls this "isn't expected to
14272 /// happen" (FDT_FLOCK fds aren't normally redup'd). We mirror
14273 /// C exactly: test asserts the count drops by 2 (1 → -1) to
14274 /// pin the faithful-to-C behavior. Reporting this as a C bug
14275 /// upstream is the right path; the port preserves it verbatim.
14276 #[test]
14277 #[cfg(unix)]
14278 fn redup_promotes_flock_to_internal_on_target() {
14279 let _g = crate::test_util::global_state_lock();
14280 let dev_null = CString::new("/dev/null").unwrap();
14281 let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14282 let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14283 assert!(x >= 0 && y >= 0);
14284 assert_ne!(x, y);
14285
14286 check_fd_table(x);
14287 check_fd_table(y);
14288 // Mark x as FDT_FLOCK.
14289 fdtable_set(x, FDT_FLOCK);
14290 FDTABLE_FLOCKS.store(2, Ordering::SeqCst); // start at 2 so double-decrement lands at 0
14291
14292 let _ = redup(x, y);
14293
14294 // c:2055-2056 — promoted to FDT_INTERNAL on y, NOT carried.
14295 assert_eq!(
14296 fdtable_get(y),
14297 FDT_INTERNAL,
14298 "c:2055-2056 — FDT_FLOCK on x promotes to FDT_INTERNAL on y"
14299 );
14300 // c:2062-2063 + zclose c:2135 — double decrement.
14301 assert_eq!(
14302 FDTABLE_FLOCKS.load(Ordering::SeqCst),
14303 0,
14304 "c:2062-2063 + c:2135 — flock count double-decremented (faithful to C)"
14305 );
14306 unsafe {
14307 libc::close(y);
14308 }
14309 }
14310
14311 /// `Src/utils.c:5217-5240` — `zreaddir(dir, ignoredots)` exposes
14312 /// the dot-filter as a parameter and returns one entry per call.
14313 /// Two paths:
14314 /// * `ignoredots=1` (the common case at c:590/655/1653/2884) —
14315 /// `.` and `..` are filtered out.
14316 /// * `ignoredots=0` (used at c:4648 for spelling correction) —
14317 /// `.` and `..` are RETAINED as valid candidates.
14318 #[test]
14319 #[cfg(unix)]
14320 fn zreaddir_honors_ignoredots_flag() {
14321 let _g = crate::test_util::global_state_lock();
14322 let tmp = std::env::temp_dir();
14323 let base = tmp.join(format!("zshrs_zreaddir_{}", std::process::id()));
14324 let _ = fs::create_dir(&base);
14325 // Create one real entry to make the test non-trivial.
14326 fs::write(base.join("file"), "x").unwrap();
14327
14328 // c:5232 — ignoredots=1: skip `.` and `..`, keep `file`.
14329 let mut dir = fs::read_dir(&base).unwrap();
14330 let mut with_skip: Vec<String> = Vec::new();
14331 while let Some(n) = zreaddir(&mut dir, 1) {
14332 with_skip.push(n);
14333 }
14334 assert!(
14335 with_skip.contains(&"file".to_string()),
14336 "c:5232 — real entry survives ignoredots=1"
14337 );
14338 assert!(
14339 !with_skip.contains(&".".to_string()),
14340 "c:5232 — `.` filtered with ignoredots=1"
14341 );
14342 assert!(
14343 !with_skip.contains(&"..".to_string()),
14344 "c:5232 — `..` filtered with ignoredots=1"
14345 );
14346
14347 // c:4648-equivalent — ignoredots=0: KEEP `.` and `..`.
14348 // (libstd's fs::read_dir filters them before exposing on
14349 // macOS/Linux, so the without_skip set is functionally
14350 // identical here — pin only that the API path doesn't error.)
14351 let mut dir2 = fs::read_dir(&base).unwrap();
14352 let mut without_skip: Vec<String> = Vec::new();
14353 while let Some(n) = zreaddir(&mut dir2, 0) {
14354 without_skip.push(n);
14355 }
14356 assert!(
14357 without_skip.contains(&"file".to_string()),
14358 "ignoredots=0 still yields real entries"
14359 );
14360
14361 let _ = fs::remove_file(base.join("file"));
14362 let _ = fs::remove_dir(&base);
14363 }
14364
14365 /// `Src/utils.c:1075` — `get_username()` uses `ztrdup_metafy` on
14366 /// `pswd->pw_name`. Previous Rust port returned the raw pw_name
14367 /// verbatim — fine for ASCII usernames, broken for high-bit
14368 /// bytes (downstream paramtab consumers assume metafied entries).
14369 /// Pin: ASCII usernames round-trip identically through metafy,
14370 /// AND the result is a non-empty string for the current uid.
14371 /// `Src/utils.c:3445-3460` — ztrftime zsh-specific %K/%L/%f
14372 /// extensions return values WITHOUT leading zeros (vs the
14373 /// strftime %H/%I/%d which pad).
14374 #[test]
14375 #[cfg(unix)]
14376 fn ztrftime_zsh_extensions_no_leading_zero() {
14377 let _g = crate::test_util::global_state_lock();
14378 // Pick a known time: Jan 5 2024 09:07:42.123456789 UTC.
14379 // Use SystemTime + an offset since we want deterministic values
14380 // independent of TZ; we just verify that the format substitutes
14381 // the right NUMBER OF DIGITS for the leading-zero case.
14382 use std::time::Duration;
14383 // 2024-01-05 09:07:42 UTC = 1704445662
14384 let t = UNIX_EPOCH + Duration::new(1704445662, 123_456_789);
14385 // %H gives leading zero "09"; %K should give "9" (in some TZ
14386 // hour will be different but the digit-count rule still holds).
14387 let h_padded = ztrftime("%H", t, false);
14388 let k_unpadded = ztrftime("%K", t, false);
14389 // Both represent the same hour. If h_padded starts with `0`,
14390 // k_unpadded must be 1-char and not start with `0`.
14391 if h_padded.starts_with('0') && h_padded.len() == 2 {
14392 assert!(
14393 !k_unpadded.starts_with('0'),
14394 "c:3445 — %K must strip the leading 0 from %H={}, got %K={}",
14395 h_padded,
14396 k_unpadded
14397 );
14398 assert_eq!(
14399 k_unpadded.len(),
14400 1,
14401 "c:3445 — %K should be 1 digit when hour < 10"
14402 );
14403 }
14404 // %f for day-of-month: t is Jan 5 → in any reasonable TZ
14405 // day is between 4 and 6; format should be 1 digit when day < 10.
14406 let d_padded = ztrftime("%d", t, false);
14407 let f_unpadded = ztrftime("%f", t, false);
14408 if d_padded.starts_with('0') && d_padded.len() == 2 {
14409 assert!(!f_unpadded.starts_with('0'));
14410 assert_eq!(
14411 f_unpadded.len(),
14412 1,
14413 "c:3457 — %f should be 1 digit when day < 10"
14414 );
14415 }
14416 // %3. fractional seconds: input nsec is 123456789 → first 3
14417 // digits should be 123. Note zsh's syntax is `%N.` where N is
14418 // the digit count (c:3374-3384 + c:3409).
14419 let frac = ztrftime("%3.", t, false);
14420 assert_eq!(
14421 frac, "123",
14422 "c:3409-3438 — %3. must emit first 3 digits of nsec"
14423 );
14424 // %. with no digit prefix defaults to 3 digits per c:3409.
14425 let frac_default = ztrftime("%.", t, false);
14426 assert_eq!(frac_default, "123", "c:3409 — %. defaults to 3 digits");
14427 }
14428
14429 #[test]
14430 #[cfg(unix)]
14431 fn get_username_returns_metafied_non_empty_string() {
14432 let _g = crate::test_util::global_state_lock();
14433 let name = get_username();
14434 // CI environments may have a username from `whoami(1)`. In
14435 // weird sandbox-only setups it might be empty, but on every
14436 // standard build it should be set.
14437 if name.is_empty() {
14438 return;
14439 }
14440 // ASCII round-trip via metafy is identity. If the username
14441 // had high-bit bytes (the bug case), the output would be
14442 // Meta-escaped — both forms are non-empty so this pin
14443 // doesn't reject either, just ensures the fn returns
14444 // SOMETHING usable (not an empty string).
14445 assert!(
14446 !name.is_empty(),
14447 "c:1086 — getpwuid result must yield a non-empty username"
14448 );
14449 // Sanity: metafy(name) == name for ASCII input (every byte
14450 // is below Meta range). Confirms the c:1086 step preserves
14451 // ASCII paths byte-for-byte.
14452 if name.bytes().all(|b| b < 0x80) {
14453 assert_eq!(
14454 metafy(&name),
14455 name,
14456 "c:1086 — ASCII metafy is identity (so pin holds for ASCII users)"
14457 );
14458 }
14459 }
14460
14461 // ═══════════════════════════════════════════════════════════════════
14462 // Pure-utility tests — zstrtol/zstrtoul (numeric parse) and
14463 // getkeystring (key-escape decode). Each test pinned against either
14464 // C zsh semantics or a direct zsh shell invocation where applicable.
14465 // ═══════════════════════════════════════════════════════════════════
14466
14467 // ── zstrtol: numeric string → (value, unconsumed_tail) ──────────
14468 /// `zstrtol("42", 10)` → (42, "") — full consumption.
14469 #[test]
14470 fn zstrtol_decimal_full_consumption() {
14471 let (v, t) = zstrtol("42", 10);
14472 assert_eq!(v, 42);
14473 assert_eq!(t, "");
14474 }
14475
14476 /// `zstrtol("42abc", 10)` → (42, "abc") — stops at first non-digit.
14477 #[test]
14478 fn zstrtol_decimal_stops_at_non_digit() {
14479 let (v, t) = zstrtol("42abc", 10);
14480 assert_eq!(v, 42);
14481 assert_eq!(t, "abc");
14482 }
14483
14484 /// `zstrtol("-7", 10)` → (-7, "") — sign handling.
14485 #[test]
14486 fn zstrtol_negative_decimal() {
14487 let (v, t) = zstrtol("-7", 10);
14488 assert_eq!(v, -7);
14489 assert_eq!(t, "");
14490 }
14491
14492 /// `zstrtol("+12", 10)` → (12, "") — explicit `+` sign.
14493 #[test]
14494 fn zstrtol_explicit_plus_sign_consumed() {
14495 let (v, t) = zstrtol("+12", 10);
14496 assert_eq!(v, 12);
14497 assert_eq!(t, "");
14498 }
14499
14500 /// `zstrtol("ff", 16)` → (255, "") — hex base.
14501 #[test]
14502 fn zstrtol_hex_base_16() {
14503 let (v, t) = zstrtol("ff", 16);
14504 assert_eq!(v, 255);
14505 assert_eq!(t, "");
14506 }
14507
14508 /// `zstrtol("FF", 16)` → (255, "") — case-insensitive hex.
14509 #[test]
14510 fn zstrtol_hex_uppercase() {
14511 let (v, t) = zstrtol("FF", 16);
14512 assert_eq!(v, 255);
14513 }
14514
14515 /// `zstrtol("1010", 2)` → (10, "") — binary base.
14516 #[test]
14517 fn zstrtol_binary_base_2() {
14518 let (v, t) = zstrtol("1010", 2);
14519 assert_eq!(v, 10);
14520 assert_eq!(t, "");
14521 }
14522
14523 /// `zstrtol("17", 8)` → (15, "") — octal base.
14524 #[test]
14525 fn zstrtol_octal_base_8() {
14526 let (v, t) = zstrtol("17", 8);
14527 assert_eq!(v, 15);
14528 }
14529
14530 /// `zstrtol("0x1A", 0)` → base-detect picks hex from `0x` prefix → 26.
14531 #[test]
14532 fn zstrtol_base_zero_detects_hex_prefix() {
14533 let (v, _) = zstrtol("0x1A", 0);
14534 assert_eq!(v, 26, "0x1A with base=0 → hex 26");
14535 }
14536
14537 /// `zstrtol("0b101", 0)` → base-detect picks binary → 5.
14538 #[test]
14539 fn zstrtol_base_zero_detects_binary_prefix() {
14540 let (v, _) = zstrtol("0b101", 0);
14541 assert_eq!(v, 5, "0b101 with base=0 → binary 5");
14542 }
14543
14544 /// `zstrtol("017", 0)` → base-detect: leading `0` → octal → 15.
14545 #[test]
14546 fn zstrtol_base_zero_leading_zero_means_octal() {
14547 let (v, _) = zstrtol("017", 0);
14548 assert_eq!(v, 15, "017 with base=0 → octal 15");
14549 }
14550
14551 /// `zstrtol(" 42", 10)` → (42, "") — leading whitespace skipped.
14552 #[test]
14553 fn zstrtol_leading_whitespace_skipped() {
14554 let (v, _) = zstrtol(" 42", 10);
14555 assert_eq!(v, 42);
14556 }
14557
14558 /// `zstrtol("0", 10)` → (0, "") — zero is valid input.
14559 #[test]
14560 fn zstrtol_zero_input() {
14561 let (v, t) = zstrtol("0", 10);
14562 assert_eq!(v, 0);
14563 assert_eq!(t, "");
14564 }
14565
14566 // ── zstrtol_underscore: digit-separator support ─────────────────
14567 /// `zstrtol_underscore("1_000_000", 10, true)` → (1_000_000, "")
14568 /// — underscores skipped during digit accumulation.
14569 #[test]
14570 fn zstrtol_underscore_separator_in_decimal() {
14571 let (v, _) = zstrtol_underscore("1_000_000", 10, true);
14572 assert_eq!(v, 1_000_000);
14573 }
14574
14575 /// Without underscore flag, `_` stops parsing.
14576 #[test]
14577 fn zstrtol_underscore_disabled_stops_at_underscore() {
14578 let (v, t) = zstrtol_underscore("123_456", 10, false);
14579 assert_eq!(v, 123);
14580 assert_eq!(t, "_456");
14581 }
14582
14583 // ── zstrtoul_underscore: unsigned variant ───────────────────────
14584 /// Parses a plain decimal unsigned.
14585 #[test]
14586 fn zstrtoul_underscore_basic_decimal() {
14587 let v = zstrtoul_underscore("12345");
14588 assert_eq!(v, Some(12345));
14589 }
14590
14591 /// Empty string → None (no number to parse).
14592 #[test]
14593 fn zstrtoul_underscore_empty_returns_none() {
14594 let v = zstrtoul_underscore("");
14595 assert_eq!(v, None);
14596 }
14597
14598 // ── getkeystring: shell-escape decode ───────────────────────────
14599 /// `\n` → newline, `\t` → tab, `\r` → CR.
14600 /// Anchor: `print -r -- $'\n\t\r'` produces the three bytes.
14601 #[test]
14602 fn getkeystring_decodes_common_escapes() {
14603 assert_eq!(getkeystring("\\n").0, "\n");
14604 assert_eq!(getkeystring("\\t").0, "\t");
14605 assert_eq!(getkeystring("\\r").0, "\r");
14606 }
14607
14608 /// `\\` → single backslash.
14609 #[test]
14610 fn getkeystring_double_backslash_yields_single() {
14611 assert_eq!(getkeystring("\\\\").0, "\\");
14612 }
14613
14614 /// `\xNN` hex escape → byte value.
14615 /// Anchor: `print -r -- $'\x41'` → "A" (0x41 = 65 = 'A').
14616 #[test]
14617 fn getkeystring_hex_escape_lowercase_x() {
14618 assert_eq!(getkeystring("\\x41").0, "A");
14619 assert_eq!(getkeystring("\\x7E").0, "~");
14620 }
14621
14622 /// `\u{NNNN}` Unicode escape → corresponding char.
14623 /// Anchor: `print -r -- $'é'` → "é".
14624 #[test]
14625 fn getkeystring_unicode_escape_u_four_digits() {
14626 assert_eq!(getkeystring("\\u00E9").0, "é");
14627 }
14628
14629 /// Plain text passes through unchanged.
14630 #[test]
14631 fn getkeystring_plain_text_passes_through() {
14632 assert_eq!(getkeystring("hello").0, "hello");
14633 assert_eq!(getkeystring("").0, "");
14634 }
14635
14636 /// Mixed plain + escapes → both handled.
14637 #[test]
14638 fn getkeystring_mixed_text_and_escapes() {
14639 assert_eq!(getkeystring("a\\nb").0, "a\nb");
14640 assert_eq!(
14641 getkeystring("line1\\nline2\\tindented").0,
14642 "line1\nline2\tindented"
14643 );
14644 }
14645
14646 // ── metafy / unmetafy round-trip on ASCII (identity) ────────────
14647 /// ASCII strings: metafy is identity (no meta-bytes to escape).
14648 #[test]
14649 fn metafy_then_unmetafy_ascii_roundtrips_to_input() {
14650 let s = "hello world";
14651 let m = metafy(s);
14652 assert_eq!(m, s);
14653 let mut bytes = m.into_bytes();
14654 let _len = unmetafy(&mut bytes);
14655 assert_eq!(String::from_utf8(bytes).unwrap(), "hello world");
14656 }
14657
14658 // ═══════════════════════════════════════════════════════════════════
14659 // quotestring — emit a quoted shell-safe form of the input string.
14660 // Each QT_* mode produces a different quoting style. Tests pin
14661 // each mode for both empty and non-empty input.
14662 // ═══════════════════════════════════════════════════════════════════
14663
14664 use crate::zsh_h::{
14665 QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_DOLLARS, QT_DOUBLE, QT_NONE,
14666 QT_SINGLE, QT_SINGLE_OPTIONAL,
14667 };
14668
14669 /// QT_NONE: no quoting; passes input through unchanged.
14670 #[test]
14671 fn quotestring_qt_none_passes_through_unchanged() {
14672 let _g = crate::test_util::global_state_lock();
14673 assert_eq!(quotestring("hello world", QT_NONE), "hello world");
14674 assert_eq!(quotestring("", QT_NONE), "");
14675 assert_eq!(quotestring("a*b?c", QT_NONE), "a*b?c");
14676 }
14677
14678 /// QT_NONE empty → empty.
14679 #[test]
14680 fn quotestring_qt_none_empty_is_empty() {
14681 let _g = crate::test_util::global_state_lock();
14682 assert_eq!(quotestring("", QT_NONE), "");
14683 }
14684
14685 /// QT_BACKSLASH on empty → "''" (single-quote pair).
14686 #[test]
14687 fn quotestring_qt_backslash_empty_yields_empty() {
14688 let _g = crate::test_util::global_state_lock();
14689 // c:6194 — shownull is 0 for plain QT_BACKSLASH, so empty → "".
14690 assert_eq!(quotestring("", QT_BACKSLASH), "");
14691 }
14692
14693 /// QT_BACKSLASH_SHOWNULL on empty → "''" too.
14694 #[test]
14695 fn quotestring_qt_backslash_shownull_empty_yields_empty_single_quotes() {
14696 let _g = crate::test_util::global_state_lock();
14697 assert_eq!(quotestring("", QT_BACKSLASH_SHOWNULL), "''");
14698 }
14699
14700 /// QT_SINGLE on empty → "''" (single-quote pair).
14701 #[test]
14702 fn quotestring_qt_single_empty_yields_empty_single_quotes() {
14703 let _g = crate::test_util::global_state_lock();
14704 assert_eq!(quotestring("", QT_SINGLE), "''");
14705 }
14706
14707 /// QT_SINGLE_OPTIONAL on empty → "''" too.
14708 #[test]
14709 fn quotestring_qt_single_optional_empty_yields_empty_single_quotes() {
14710 let _g = crate::test_util::global_state_lock();
14711 assert_eq!(quotestring("", QT_SINGLE_OPTIONAL), "''");
14712 }
14713
14714 /// QT_DOUBLE on empty → "" (empty double-quote pair).
14715 #[test]
14716 fn quotestring_qt_double_empty_yields_empty_double_quotes() {
14717 let _g = crate::test_util::global_state_lock();
14718 assert_eq!(quotestring("", QT_DOUBLE), "\"\"");
14719 }
14720
14721 /// QT_DOLLARS on empty → "$''".
14722 #[test]
14723 fn quotestring_qt_dollars_empty_yields_dollar_quote_pair() {
14724 let _g = crate::test_util::global_state_lock();
14725 assert_eq!(quotestring("", QT_DOLLARS), "$''");
14726 }
14727
14728 /// QT_BACKSLASH_PATTERN escapes only pattern meta-chars.
14729 /// Input "a*b?c[d]" → "a\\*b\\?c\\[d\\]".
14730 #[test]
14731 fn quotestring_qt_backslash_pattern_escapes_glob_metas() {
14732 let _g = crate::test_util::global_state_lock();
14733 let r = quotestring("a*b?c[d]", QT_BACKSLASH_PATTERN);
14734 assert!(
14735 r.contains("\\*") && r.contains("\\?") && r.contains("\\[") && r.contains("\\]"),
14736 "all globs must be escaped; got {r:?}"
14737 );
14738 }
14739
14740 /// QT_BACKSLASH_PATTERN does NOT escape plain ASCII letters/digits.
14741 #[test]
14742 fn quotestring_qt_backslash_pattern_doesnt_escape_plain_chars() {
14743 let _g = crate::test_util::global_state_lock();
14744 let r = quotestring("plain", QT_BACKSLASH_PATTERN);
14745 assert_eq!(r, "plain", "plain text passes through");
14746 }
14747
14748 /// QT_BACKSLASH_PATTERN escapes `<`, `>`, `(`, `)`, `|`, `#`, `^`, `~`.
14749 #[test]
14750 fn quotestring_qt_backslash_pattern_escapes_all_meta_set() {
14751 let _g = crate::test_util::global_state_lock();
14752 for ch in ['<', '>', '(', ')', '|', '#', '^', '~'] {
14753 let s = format!("x{ch}y");
14754 let r = quotestring(&s, QT_BACKSLASH_PATTERN);
14755 assert!(
14756 r.contains(&format!("\\{ch}")),
14757 "{ch:?} must be escaped, got {r:?}"
14758 );
14759 }
14760 }
14761
14762 /// QT_SINGLE on simple input → "'simple'" (wrapped in single quotes).
14763 #[test]
14764 fn quotestring_qt_single_wraps_simple_input() {
14765 let _g = crate::test_util::global_state_lock();
14766 assert_eq!(quotestring("simple", QT_SINGLE), "'simple'");
14767 }
14768
14769 // ═══════════════════════════════════════════════════════════════════
14770 // metafy / unmetafy edge cases — bytes that NEED meta-encoding
14771 // (NUL, Meta byte 0x83, Nularg 0xa1, etc.). Pin that metafy escapes
14772 // them via the Meta + (byte ^ 32) scheme and unmetafy reverses it.
14773 // ═══════════════════════════════════════════════════════════════════
14774
14775 /// `metafy` is identity for plain ASCII (no meta bytes).
14776 #[test]
14777 fn metafy_ascii_is_identity_byte_for_byte() {
14778 let s = "abc 123 XYZ!@#";
14779 assert_eq!(metafy(s), s);
14780 }
14781
14782 /// Empty string round-trips.
14783 #[test]
14784 fn metafy_empty_string_returns_empty() {
14785 assert_eq!(metafy(""), "");
14786 }
14787
14788 /// metafy → unmetafy is round-trip identity for ASCII input.
14789 #[test]
14790 fn metafy_unmetafy_roundtrip_with_punctuation() {
14791 let s = "Hello, World! 123 +-=";
14792 let m = metafy(s);
14793 let mut bytes = m.into_bytes();
14794 let _ = unmetafy(&mut bytes);
14795 let result = String::from_utf8(bytes).expect("valid utf-8");
14796 assert_eq!(result, s);
14797 }
14798
14799 /// Multi-byte UTF-8 chars get meta-encoded then unmetafied. The
14800 /// round-trip MUST preserve the original bytes. Inline the byte-
14801 /// level metafy here — `metafy()`'s String return goes through
14802 /// `from_utf8_lossy` for non-UTF-8 outputs (correct for String
14803 /// consumers like the lexer or pattern compile, but breaks byte-
14804 /// round-trip because U+FFFD inserts EF BF BD where the
14805 /// original Meta-escaped bytes lived). The unmetafy step
14806 /// reverses the Meta-escape to recover the source UTF-8 verbatim.
14807 #[test]
14808 fn metafy_unmetafy_roundtrip_with_utf8_multibyte_anchored() {
14809 let s = "日本語";
14810 // Inline of metafy() byte path (no String materialisation).
14811 let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
14812 for &b in s.as_bytes() {
14813 if imeta_byte(b) {
14814 bytes.push(Meta);
14815 bytes.push(b ^ 32);
14816 } else {
14817 bytes.push(b);
14818 }
14819 }
14820 let _ = unmetafy(&mut bytes);
14821 let result = String::from_utf8(bytes).expect("valid utf-8");
14822 assert_eq!(result, s, "UTF-8 round-trip must preserve content");
14823 }
14824
14825 // ─── zsh-corpus pins for quotestring per quote_type ─────────────
14826
14827 /// `quotestring(QT_NONE)` is identity on any input.
14828 #[test]
14829 fn quotestring_corpus_qt_none_is_identity() {
14830 let s = "anything `goes` $here";
14831 assert_eq!(quotestring(s, QT_NONE), s);
14832 }
14833
14834 /// `quotestring(QT_SINGLE)` on plain word wraps in single quotes.
14835 #[test]
14836 fn quotestring_corpus_qt_single_wraps_plain_word() {
14837 let out = quotestring("hello", QT_SINGLE);
14838 assert!(
14839 out.starts_with('\'') && out.ends_with('\''),
14840 "single-quoted = wraps with ', got {out:?}"
14841 );
14842 assert!(out.contains("hello"), "content preserved");
14843 }
14844
14845 /// `quotestring(QT_DOUBLE)` on plain word wraps in double quotes.
14846 #[test]
14847 fn quotestring_corpus_qt_double_wraps_plain_word() {
14848 let out = quotestring("hello", QT_DOUBLE);
14849 assert!(
14850 out.starts_with('"') && out.ends_with('"'),
14851 "double-quoted = wraps with \", got {out:?}"
14852 );
14853 }
14854
14855 /// `quotestring(QT_SINGLE)` on string with apostrophe escapes the
14856 /// apostrophe by closing+escape+reopen: `it's` → `'it'\''s'`.
14857 #[test]
14858 fn quotestring_corpus_qt_single_escapes_apostrophe() {
14859 let out = quotestring("it's", QT_SINGLE);
14860 assert!(
14861 out.contains("\\'") || out.contains("'\\''"),
14862 "apostrophe gets escaped in single-quote form, got {out:?}",
14863 );
14864 }
14865
14866 /// `quotestring(QT_BACKSLASH)` escapes shell special chars.
14867 /// Pattern chars like `*`, `?`, `$`, `(`, `)` should be backslashed.
14868 #[test]
14869 fn quotestring_corpus_qt_backslash_escapes_glob_chars() {
14870 let out = quotestring("a*b?c", QT_BACKSLASH);
14871 assert!(out.contains("\\*"), "* gets backslashed, got {out:?}");
14872 assert!(out.contains("\\?"), "? gets backslashed, got {out:?}");
14873 }
14874
14875 /// `quotestring("", QT_DOUBLE)` returns `""` literal.
14876 #[test]
14877 fn quotestring_corpus_qt_double_empty_yields_double_quotes() {
14878 let out = quotestring("", QT_DOUBLE);
14879 assert_eq!(out, "\"\"", "empty double-quoted = \"\"");
14880 }
14881
14882 /// `quotestring("", QT_SINGLE)` returns `''` literal.
14883 #[test]
14884 fn quotestring_corpus_qt_single_empty_yields_single_quotes() {
14885 let out = quotestring("", QT_SINGLE);
14886 assert_eq!(out, "''", "empty single-quoted = ''");
14887 }
14888
14889 /// `quotestring(QT_BACKSLASH)` on plain alphanumeric is identity.
14890 /// "abc123" should round-trip with no backslashes added.
14891 #[test]
14892 fn quotestring_corpus_qt_backslash_plain_word_unchanged() {
14893 let out = quotestring("abc123", QT_BACKSLASH);
14894 assert_eq!(
14895 out, "abc123",
14896 "plain alphanumeric needs no escape, got {out:?}"
14897 );
14898 }
14899
14900 /// `ztrlen("hello")` = 5 (no meta chars).
14901 #[test]
14902 fn ztrlen_corpus_plain_ascii_byte_count() {
14903 assert_eq!(ztrlen("hello"), 5);
14904 }
14905
14906 /// `ztrlen("")` = 0.
14907 #[test]
14908 fn ztrlen_corpus_empty_is_zero() {
14909 assert_eq!(ztrlen(""), 0);
14910 }
14911
14912 // ═══════════════════════════════════════════════════════════════════
14913 // itype_end C-parity tests — pin `utils.c:4395-4495` per-itype-flag
14914 // behavior. One assertion per flag/path so a regression in the
14915 // char-class walk points at the exact branch that drifted.
14916 //
14917 // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"]
14918 // so the bug stays surfaced but CI doesn't fail until the fix.
14919 // ═══════════════════════════════════════════════════════════════════
14920
14921 /// `itype_end("abc123", IIDENT, false)` walks the full identifier.
14922 /// C utils.c:4395 — IIDENT matches letters/digits/`_` via TYPTAB.
14923 /// Requires `inittyptab()` for TYPTAB population — call at entry.
14924 #[test]
14925 fn itype_end_ident_walks_full_identifier() {
14926 let _g = crate::test_util::global_state_lock();
14927 inittyptab();
14928 use crate::ported::ztype_h::IIDENT;
14929 assert_eq!(itype_end("abc123", IIDENT as u32, false), 6);
14930 }
14931
14932 /// `itype_end("abc-def", IIDENT, false)` stops at `-` (non-IIDENT).
14933 #[test]
14934 fn itype_end_ident_stops_at_dash() {
14935 let _g = crate::test_util::global_state_lock();
14936 inittyptab();
14937 use crate::ported::ztype_h::IIDENT;
14938 assert_eq!(itype_end("abc-def", IIDENT as u32, false), 3);
14939 }
14940
14941 /// `itype_end("a", IIDENT, true)` with `once=true` stops after 1.
14942 #[test]
14943 fn itype_end_once_stops_after_first_match() {
14944 let _g = crate::test_util::global_state_lock();
14945 inittyptab();
14946 use crate::ported::ztype_h::IIDENT;
14947 assert_eq!(itype_end("abc", IIDENT as u32, true), 1);
14948 }
14949
14950 /// `itype_end("", IIDENT, false)` on empty string returns 0.
14951 #[test]
14952 fn itype_end_empty_string_returns_zero() {
14953 let _g = crate::test_util::global_state_lock();
14954 inittyptab();
14955 use crate::ported::ztype_h::IIDENT;
14956 assert_eq!(itype_end("", IIDENT as u32, false), 0);
14957 }
14958
14959 /// `itype_end(":", IIDENT, false)` on non-IIDENT first char = 0.
14960 #[test]
14961 fn itype_end_non_ident_first_char_returns_zero() {
14962 let _g = crate::test_util::global_state_lock();
14963 inittyptab();
14964 use crate::ported::ztype_h::IIDENT;
14965 assert_eq!(itype_end(":foo", IIDENT as u32, false), 0);
14966 }
14967
14968 /// `itype_end("ns.foo", INAMESPC, false)` — C utils.c:4399-4413
14969 /// INAMESPC special-cases ksh93 namespace `.` separators. With
14970 /// non-POSIXIDENTIFIERS / non-KSH emulation, dotted names should
14971 /// walk through the `.` to consume `ns.foo` as a single name.
14972 /// The Rust port may stop at `.` since dotted-namespace support
14973 /// requires POSIXIDENTIFIERS check + ksh emulation gating that
14974 /// isn't fully wired.
14975 #[test]
14976 fn itype_end_inamespc_walks_through_ksh93_dot() {
14977 let _g = crate::test_util::global_state_lock();
14978 inittyptab();
14979 use crate::ported::ztype_h::INAMESPC;
14980 // Expected: full "ns.foo" = 6 bytes walked.
14981 assert_eq!(itype_end("ns.foo", INAMESPC, false), 6);
14982 }
14983
14984 /// `itype_end("a b c", ISEP, false)` from a space char.
14985 /// ISEP matches IFS chars. Default IFS includes ` \t\n`.
14986 /// Walk should consume only the leading separator(s).
14987 #[test]
14988 fn itype_end_isep_walks_separator_run() {
14989 let _g = crate::test_util::global_state_lock();
14990 inittyptab();
14991 use crate::ported::ztype_h::ISEP;
14992 // " \t " then 'a' — 3 sep bytes.
14993 assert_eq!(itype_end(" \t a", ISEP as u32, false), 3);
14994 }
14995
14996 // ═══════════════════════════════════════════════════════════════════
14997 // unmetafy C-parity tests — pin `utils.c:4954-4983`. Meta-byte
14998 // collapse (0x83 + (b^0x20) → b) must be in-place + return new len.
14999 // ═══════════════════════════════════════════════════════════════════
15000
15001 /// `unmetafy("abc")` is a no-op for non-metafied input.
15002 #[test]
15003 fn unmetafy_pure_ascii_unchanged() {
15004 let _g = crate::test_util::global_state_lock();
15005 let mut v = b"abc".to_vec();
15006 let n = unmetafy(&mut v);
15007 assert_eq!(n, 3);
15008 assert_eq!(v, b"abc");
15009 }
15010
15011 /// `unmetafy("a\x83a")` → "a\x01" because Meta(0x83) + next ^ 0x20
15012 /// = 'a' ^ 0x20 = 0x41 ^ 0x20 = 0x61... wait, 'a'=0x61, 0x61 ^ 0x20
15013 /// = 0x41 = 'A'. So unmetafy("a\x83a") → "aA" (2 bytes).
15014 #[test]
15015 fn unmetafy_meta_pair_collapses_to_xor_byte() {
15016 let _g = crate::test_util::global_state_lock();
15017 // 'a' + Meta(0x83) + 'a' → 'a' + ('a' ^ 0x20) = 'a' + 'A' = "aA"
15018 let mut v: Vec<u8> = vec![b'a', 0x83, b'a'];
15019 let n = unmetafy(&mut v);
15020 assert_eq!(n, 2);
15021 assert_eq!(v, vec![b'a', b'A']);
15022 }
15023
15024 /// `unmetafy("")` returns 0 with empty buffer.
15025 #[test]
15026 fn unmetafy_empty_returns_zero() {
15027 let _g = crate::test_util::global_state_lock();
15028 let mut v: Vec<u8> = Vec::new();
15029 let n = unmetafy(&mut v);
15030 assert_eq!(n, 0);
15031 }
15032
15033 /// `unmetafy` with trailing lone Meta byte. C `unmetafy` checks
15034 /// `p < s.len()` before reading the byte after Meta — trailing
15035 /// Meta at EOF is preserved as a single literal Meta byte.
15036 #[test]
15037 fn unmetafy_trailing_lone_meta_preserved() {
15038 let _g = crate::test_util::global_state_lock();
15039 let mut v: Vec<u8> = vec![b'a', 0x83];
15040 let n = unmetafy(&mut v);
15041 // Rust port copies the Meta byte to t, then loop exits with
15042 // p == s.len() → no second-byte read. Final = "a\x83" (2).
15043 assert_eq!(n, 2);
15044 assert_eq!(v, vec![b'a', 0x83]);
15045 }
15046
15047 // ═══════════════════════════════════════════════════════════════════
15048 // nicechar / wcs_nicechar C-parity tests — pin display-form
15049 // conversion. Required by stradd, prompt expansion, completion etc.
15050 // ═══════════════════════════════════════════════════════════════════
15051
15052 /// `nicechar('a')` returns the printable char unchanged.
15053 #[test]
15054 fn nicechar_printable_ascii_passes_through() {
15055 let _g = crate::test_util::global_state_lock();
15056 assert_eq!(nicechar('a'), "a");
15057 }
15058
15059 /// `nicechar('\n')` returns `\n` escape sequence per C `nicechar`
15060 /// (utils.c:464+ — control chars get `^M` style or `\n` escape).
15061 /// zsh emits `\n` as literal `\n` (two chars) for non-quotable
15062 /// nicechar; the `nicechar_sel(c, false)` form is non-quotable.
15063 #[test]
15064 fn nicechar_newline_emits_escape() {
15065 let _g = crate::test_util::global_state_lock();
15066 let out = nicechar('\n');
15067 assert!(
15068 out == "\\n" || out == "\n",
15069 "newline should be escaped or literal; got {out:?}"
15070 );
15071 }
15072
15073 /// `wcs_nicechar('A', None, None)` returns "A" for printable wide
15074 /// chars per utils.c:689.
15075 #[test]
15076 fn wcs_nicechar_printable_ascii_passes_through() {
15077 let _g = crate::test_util::global_state_lock();
15078 assert_eq!(wcs_nicechar('A', None, None), "A");
15079 }
15080
15081 // ═══════════════════════════════════════════════════════════════════
15082 // Additional C-parity tests for Src/utils.c nicechar dispatch.
15083 // ═══════════════════════════════════════════════════════════════════
15084
15085 /// c:462 — nicechar for printable ASCII passes through.
15086 #[test]
15087 fn nicechar_printable_ascii_passes_through_pin() {
15088 let _g = crate::test_util::global_state_lock();
15089 assert_eq!(nicechar('a'), "a");
15090 assert_eq!(nicechar('Z'), "Z");
15091 assert_eq!(nicechar('5'), "5");
15092 assert_eq!(nicechar(' '), " ");
15093 }
15094
15095 /// c:487 — nicechar('\\n') returns '\\n' (backslash + n).
15096 #[test]
15097 fn nicechar_newline_returns_backslash_n() {
15098 let _g = crate::test_util::global_state_lock();
15099 assert_eq!(nicechar('\n'), "\\n");
15100 }
15101
15102 /// c:490 — nicechar('\\t') returns '\\t' (backslash + t).
15103 #[test]
15104 fn nicechar_tab_returns_backslash_t() {
15105 let _g = crate::test_util::global_state_lock();
15106 assert_eq!(nicechar('\t'), "\\t");
15107 }
15108
15109 /// c:479 — nicechar(0x7f) returns '^?' (DEL → ^?).
15110 #[test]
15111 fn nicechar_del_returns_caret_question() {
15112 let _g = crate::test_util::global_state_lock();
15113 assert_eq!(nicechar('\x7f'), "^?");
15114 }
15115
15116 /// c:493 — nicechar(0x01) returns '^A' (Ctrl-A).
15117 #[test]
15118 fn nicechar_ctrl_a_returns_caret_A() {
15119 let _g = crate::test_util::global_state_lock();
15120 assert_eq!(nicechar('\x01'), "^A");
15121 }
15122
15123 /// c:493 — nicechar(0x1b) returns '^[' (ESC).
15124 #[test]
15125 fn nicechar_esc_returns_caret_bracket() {
15126 let _g = crate::test_util::global_state_lock();
15127 assert_eq!(nicechar('\x1b'), "^[");
15128 }
15129
15130 /// c:462 — nicechar_sel quotable=true uses '\\C-' instead of '^'
15131 /// for control chars.
15132 #[test]
15133 fn nicechar_sel_quotable_uses_backslash_C() {
15134 let _g = crate::test_util::global_state_lock();
15135 let r = nicechar_sel('\x01', true);
15136 assert!(
15137 r.contains("\\C-") || r.contains("^"),
15138 "quotable should emit \\C- form, got {:?}",
15139 r
15140 );
15141 }
15142
15143 /// c:531 — is_nicechar('a') returns FALSE: printable chars don't
15144 /// NEED nice (escape) representation, they pass through verbatim.
15145 /// Per the C comment in utils.c:531: "Return whether the char needs
15146 /// nice representation".
15147 #[test]
15148 fn is_nicechar_printable_returns_false() {
15149 let _g = crate::test_util::global_state_lock();
15150 assert!(!is_nicechar('a'), "printable doesn't NEED nice repr");
15151 assert!(!is_nicechar('Z'));
15152 assert!(!is_nicechar('5'));
15153 }
15154
15155 /// c:531 — is_nicechar for any byte 0..127 is safe (no panic).
15156 #[test]
15157 fn is_nicechar_all_ascii_safe() {
15158 let _g = crate::test_util::global_state_lock();
15159 for c in 0u8..=127 {
15160 let _ = is_nicechar(c as char); // no panic
15161 }
15162 }
15163
15164 /// c:462 — nicechar deterministic.
15165 #[test]
15166 fn nicechar_is_deterministic() {
15167 let _g = crate::test_util::global_state_lock();
15168 for c in ['a', '\n', '\t', '\x7f', '\x01', '\x1b'] {
15169 let first = nicechar(c);
15170 for _ in 0..5 {
15171 assert_eq!(nicechar(c), first, "{:?} must be pure", c);
15172 }
15173 }
15174 }
15175
15176 /// c:706 — `pathprog("")` returns None (empty path invalid).
15177 #[test]
15178 fn pathprog_empty_returns_none() {
15179 let _g = crate::test_util::global_state_lock();
15180 assert!(pathprog("").is_none());
15181 }
15182
15183 /// c:776 — `pathprog("/nonexistent/zshrs_xyz")` returns None.
15184 #[test]
15185 fn pathprog_nonexistent_returns_none() {
15186 let _g = crate::test_util::global_state_lock();
15187 let r = pathprog("/__never_exists_zshrs_pathprog_xyz");
15188 assert!(r.is_none());
15189 }
15190
15191 // ═══════════════════════════════════════════════════════════════════
15192 // Additional C-parity tests for Src/utils.c
15193 // c:99 set_widearray / c:447 nicechar_sel / c:521 is_nicechar /
15194 // c:742 zwcwidth / c:836 findpwd / c:928 slashsplit /
15195 // c:1258 get_username / c:1426 getnameddir / c:1475 dircmp /
15196 // c:1576 callhookfunc
15197 // ═══════════════════════════════════════════════════════════════════
15198
15199 /// c:99 — `set_widearray("")` empty returns empty Vec.
15200 #[test]
15201 fn set_widearray_empty_returns_empty() {
15202 let _g = crate::test_util::global_state_lock();
15203 assert!(set_widearray("").is_empty());
15204 }
15205
15206 /// c:99 — `set_widearray` returns Vec<char>.
15207 #[test]
15208 fn set_widearray_returns_vec_char_type() {
15209 let _g = crate::test_util::global_state_lock();
15210 let _: Vec<char> = set_widearray("a");
15211 }
15212
15213 /// c:447 — `nicechar_sel('a', false)` returns "a" (printable pass-through).
15214 #[test]
15215 fn nicechar_sel_ascii_letter_passthrough() {
15216 assert_eq!(nicechar_sel('a', false), "a", "printable letter as-is");
15217 }
15218
15219 /// c:447 — `nicechar_sel` is pure.
15220 #[test]
15221 fn nicechar_sel_is_pure() {
15222 for c in ['a', '\t', '\n', '\x1b', '\x7f', '日'] {
15223 let first = nicechar_sel(c, false);
15224 for _ in 0..3 {
15225 assert_eq!(
15226 nicechar_sel(c, false),
15227 first,
15228 "nicechar_sel({:?}, false) must be pure",
15229 c
15230 );
15231 }
15232 }
15233 }
15234
15235 /// c:742 — `zwcwidth` returns usize (compile-time type pin).
15236 #[test]
15237 fn zwcwidth_returns_usize_type() {
15238 let _: usize = zwcwidth('a');
15239 }
15240
15241 /// c:742 — `zwcwidth('a')` ASCII letter returns 1.
15242 #[test]
15243 fn zwcwidth_ascii_returns_one() {
15244 assert_eq!(zwcwidth('a'), 1, "ASCII letter width = 1");
15245 }
15246
15247 /// c:836 — `findpwd("")` empty returns Option<String>.
15248 #[test]
15249 fn findpwd_empty_returns_option_string_type() {
15250 let _g = crate::test_util::global_state_lock();
15251 let _: Option<String> = findpwd("");
15252 }
15253
15254 /// c:928 — `slashsplit("")` empty returns empty Vec.
15255 #[test]
15256 fn slashsplit_empty_returns_empty() {
15257 assert!(slashsplit("").is_empty());
15258 }
15259
15260 /// c:928 — `slashsplit("a/b/c")` splits to 3 elements.
15261 #[test]
15262 fn slashsplit_simple_three_components() {
15263 let r = slashsplit("a/b/c");
15264 assert_eq!(r, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
15265 }
15266
15267 /// c:1475 — `dircmp("", "")` both empty returns bool (type pin + safe).
15268 #[test]
15269 fn dircmp_both_empty_returns_bool_type() {
15270 let _g = crate::test_util::global_state_lock();
15271 let _: bool = dircmp("", "");
15272 }
15273
15274 /// c:1426 — `getnameddir("")` empty returns None.
15275 #[test]
15276 fn getnameddir_empty_returns_none() {
15277 let _g = crate::test_util::global_state_lock();
15278 assert!(getnameddir("").is_none(), "empty → None");
15279 }
15280
15281 /// c:1258 — `get_username` returns String (compile-time type pin).
15282 #[test]
15283 fn get_username_returns_string_type() {
15284 let _g = crate::test_util::global_state_lock();
15285 let _: String = get_username();
15286 }
15287
15288 /// c:1921-1935 — adjustwinsize(1) seeds `$LINES`/`$COLUMNS` from the
15289 /// TIOCGWINSZ probe of SHTTY. The port gated the setiparam calls on
15290 /// LINES/COLUMNS being in the OS environment (a misread of C's
15291 /// zgetenv guard, which only covers env re-publication — the C param
15292 /// is always live via its zterm_lines valptr alias), so every
15293 /// interactive shell ran with LINES=0 → `$(( LINES / 3 ))` division
15294 /// by zero in history-search-multi-word pagination (_hsmw_main:81).
15295 #[test]
15296 fn adjustwinsize_seeds_lines_columns_from_shtty() {
15297 let _g = crate::test_util::global_state_lock();
15298 unsafe {
15299 let mut master: libc::c_int = -1;
15300 let mut slave: libc::c_int = -1;
15301 let mut ws: libc::winsize = std::mem::zeroed();
15302 ws.ws_row = 33;
15303 ws.ws_col = 77;
15304 if libc::openpty(
15305 &mut master,
15306 &mut slave,
15307 std::ptr::null_mut(),
15308 std::ptr::null_mut(),
15309 &mut ws,
15310 ) != 0
15311 {
15312 return; // no pty available (sandboxed CI) — skip
15313 }
15314 let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
15315 crate::ported::init::SHTTY.store(slave, Ordering::Relaxed);
15316 let _ = adjustwinsize(1);
15317 crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
15318 libc::close(slave);
15319 libc::close(master);
15320 }
15321 assert_eq!(
15322 crate::ported::params::getiparam("LINES"),
15323 33,
15324 "LINES must reflect the SHTTY winsize probe"
15325 );
15326 assert_eq!(
15327 crate::ported::params::getiparam("COLUMNS"),
15328 77,
15329 "COLUMNS must reflect the SHTTY winsize probe"
15330 );
15331 }
15332}
15333
15334// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
15335// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
15336// pipeline. Extracted body of zerrmsg's `%e` arm (Src/utils.c:355-366):
15337// EINTR → "interrupt", EIO verbatim, else lowercased strerror.
15338// zerrmsg's errno branch routes through this so the prefix
15339// formatting matches C; module callers (zsh/system) embed the
15340// string in their own zwarnnam messages where C uses `%e`.
15341/// Render an errno the way zsh's `%e` warning format does —
15342/// `zerrmsg` case `'e'` at `Src/utils.c:352-368`:
15343///
15344/// - `EINTR` → the literal string `interrupt` (C also sets
15345/// `errflag |= ERRFLAG_ERROR` and truncates the rest of the
15346/// format; every zshrs call site uses `%e` as the final
15347/// conversion so the truncation is a no-op here).
15348/// - `EIO` → `strerror(EIO)` verbatim ("I/O" reads wrong
15349/// lowercased, per the c:361-364 comment).
15350/// - everything else → `strerror(num)` with the first letter
15351/// lowercased (`tulower(errmsg[0])`, c:366).
15352///
15353/// `std::io::Error::from_raw_os_error(n)` Display is NOT this
15354/// format — it appends " (os error N)" and keeps the capital.
15355/// That mismatch was the residual byte-diff in zsh/system's
15356/// `sysopen`/`zsystem flock` warnings (docs/BUGS.md #316).
15357pub fn zsh_errno_msg(num: i32) -> String {
15358 if num == libc::EINTR {
15359 return "interrupt".to_string(); // c:355-358
15360 }
15361 let msg = unsafe {
15362 let p = libc::strerror(num); // c:360
15363 if p.is_null() {
15364 return String::new();
15365 }
15366 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
15367 };
15368 if num == libc::EIO {
15369 return msg; // c:363-364
15370 }
15371 // c:366 — `fputc(tulower(errmsg[0]), file);`
15372 let mut chars = msg.chars();
15373 match chars.next() {
15374 Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
15375 None => msg,
15376 }
15377}
15378
15379// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
15380// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
15381// pipeline. Byte-exact inverse of metafy (Src/utils.c:5022 unmetafy):
15382// decodes Meta (\u{83}) + c^32 pairs back to raw bytes. C
15383// unmetafy works in place on char*; Rust strings are UTF-8 so
15384// the decode returns Vec<u8> for byte-exact writes.
15385/// Decode a char-level metafied String back to RAW bytes for a write
15386/// or exec boundary. c:Src/utils.c:4954 unmetafy — `if (*t++ == Meta
15387/// && *p) t[-1] = *p++ ^ 32;` — transposed onto the char-level
15388/// encoding `meta_encode_byte` produces: U+0083 followed by a scalar
15389/// in U+0080..=U+00FF yields the single raw byte `payload ^ 32`;
15390/// everything else emits its UTF-8 bytes verbatim. A U+0083 followed
15391/// by anything outside that range is not our encoding and passes
15392/// through untouched. Bug #127.
15393pub fn unmetafy_str(s: &str) -> Vec<u8> {
15394 let mut out = Vec::with_capacity(s.len());
15395 let mut it = s.chars().peekable();
15396 let mut buf = [0u8; 4];
15397 while let Some(c) = it.next() {
15398 if c == '\u{83}' {
15399 if let Some(&n) = it.peek() {
15400 let nu = n as u32;
15401 if (0x80..=0xff).contains(&nu) {
15402 out.push((nu as u8) ^ 32);
15403 it.next();
15404 continue;
15405 }
15406 }
15407 }
15408 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
15409 }
15410 out
15411}