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 // c:1582-1589 — periodic-hook dispatch on PERIOD cadence.
1809 if period > 0 {
1810 let now = std::time::SystemTime::now()
1811 .duration_since(UNIX_EPOCH)
1812 .map(|d| d.as_secs() as i64)
1813 .unwrap_or(0);
1814 if now > LAST_PERIODIC.load(Ordering::Relaxed) + period
1815 && callhookfunc("periodic", None, 1, std::ptr::null_mut()) == 0
1816 {
1817 LAST_PERIODIC.store(now, Ordering::Relaxed);
1818 }
1819 }
1820 // c:1588-1589 — `if (errflag) return;` post-periodic bail.
1821 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
1822 return;
1823 }
1824
1825 // c:1591-1611 — mail check: `if (mailcheck && difftime(now,
1826 // lastmailcheck) > mailcheck)` walk MAILPATH (or scalar MAIL) via
1827 // checkmailpath. Faithful port — checkmailpath already exists at
1828 // utils.rs:1623, getaparam exists for the MAILPATH array.
1829 let currentmailcheck = std::time::SystemTime::now()
1830 .duration_since(UNIX_EPOCH)
1831 .map(|d| d.as_secs() as i64)
1832 .unwrap_or(0);
1833 if mailcheck > 0 && (currentmailcheck - LAST_MAILCHECK.load(Ordering::Relaxed)) > mailcheck {
1834 // c:1597 — `if (mailpath && *mailpath && **mailpath)`
1835 let mailpath = crate::ported::params::getaparam("MAILPATH");
1836 let has_mailpath = mailpath
1837 .as_ref()
1838 .map(|p| !p.is_empty() && p.first().map(|s| !s.is_empty()).unwrap_or(false))
1839 .unwrap_or(false);
1840 if has_mailpath {
1841 // c:1598 `checkmailpath(mailpath);` — emit each message to the
1842 // terminal (C writes directly to `shout`).
1843 for m in checkmailpath(mailpath.as_deref().unwrap()) {
1844 println!("{}", m);
1845 }
1846 } else {
1847 // c:1600-1608 — `if ((mailfile = getsparam("MAIL")) && *mailfile)
1848 // { x[0]=mailfile; x[1]=NULL; checkmailpath(x); }`
1849 crate::ported::signals::queue_signals(); // c:1600
1850 if let Some(mailfile) = crate::ported::params::getsparam("MAIL") {
1851 if !mailfile.is_empty() {
1852 let x = vec![mailfile]; // c:1604-1605
1853 for m in checkmailpath(&x) {
1854 println!("{}", m); // c:1606
1855 }
1856 }
1857 }
1858 crate::ported::signals::unqueue_signals(); // c:1608
1859 }
1860 LAST_MAILCHECK.store(currentmailcheck, Ordering::Relaxed); // c:1610
1861 }
1862
1863 // c:1613-1618 — `if (prepromptfns) for (...) ppnode->func();`
1864 let snapshot: Vec<fn()> = PREPROMPT_FNS.lock().unwrap().clone();
1865 for f in snapshot {
1866 f();
1867 }
1868}
1869
1870/// `static time_t lastmailcheck;` (Src/utils.c:1447) — the timestamp of the
1871/// previous mail check. `checkmail()` reads it (mtime/atime comparisons happen
1872/// against the PREVIOUS check) and writes it after walking the paths.
1873pub static LAST_MAILCHECK: AtomicI64 = AtomicI64::new(0);
1874
1875/// Direct port of `void checkmailpath(char **s)` from `Src/utils.c:1620`.
1876/// Walks each `PATH` / `PATH?message` MAILPATH component:
1877/// - empty component → `zerr` (c:1634-1636)
1878/// - `mailstat` failure → `zerr` unless ENOENT (c:1637-1639)
1879/// - directory → list entries (`PATH/entry[?message]`) and recurse
1880/// (c:1640-1669)
1881/// - file, interactive (`shout`) only: "You have new mail." or the
1882/// `?`-suffixed message expanded with `$_` bound to the file
1883/// (`parsestr`+`singsub`, c:1670-1699); and, under `MAILWARNING`,
1884/// "The mail in PATH has been read." (c:1700-1704)
1885///
1886/// **Signature note:** C is `void` and writes to `shout`; the zshrs caller
1887/// drives output, so this returns the messages (in C emission order) instead.
1888/// The new-mail / read-warning conditions are now ported faithfully — the
1889/// previous adhoc port used an unfaithful "mtime within 60s" heuristic and its
1890/// result was discarded by the caller, so the feature did nothing.
1891pub fn checkmailpath(s: &[String]) -> Vec<String> {
1892 // c:1620
1893 let mut messages = Vec::new();
1894 let interactive = *crate::ported::init::shout.lock().unwrap() != 0; // c:1670 `else if (shout)`
1895 let lastmailcheck = LAST_MAILCHECK.load(Ordering::Relaxed);
1896
1897 for path in s {
1898 // c:1627-1633 — split on the first '?': `file` before, `u` after (or None).
1899 let (file, u) = match path.find('?') {
1900 Some(p) => (&path[..p], Some(&path[p + 1..])),
1901 None => (path.as_str(), None),
1902 };
1903
1904 if file.is_empty() {
1905 // c:1634-1636 — `if (**s == 0) zerr("empty MAILPATH component: %s", *s);`
1906 zerr(&format!("empty MAILPATH component: {}", path));
1907 continue;
1908 }
1909
1910 let mut st: libc::stat = unsafe { std::mem::zeroed() };
1911 let real = unmeta(file); // c:1637 — mailstat(unmeta(*s), &st)
1912 if mailstat(&real, &mut st) == -1 {
1913 // c:1638-1639 — report anything other than "no such file".
1914 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1915 if e != libc::ENOENT {
1916 zerr(&format!("{}: {}", zsh_errno_msg(e), path));
1917 }
1918 } else if (st.st_mode as libc::mode_t & libc::S_IFMT) == libc::S_IFDIR {
1919 // c:1640-1669 — a directory: enumerate entries and recurse.
1920 match fs::read_dir(&real) {
1921 Err(_) => {
1922 let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
1923 zerr(&format!("{}: {}", zsh_errno_msg(e), path)); // c:1647
1924 }
1925 Ok(mut rd) => {
1926 let mut arr = Vec::new();
1927 // c:1653-1662 — `while ((fn = zreaddir(lock, 1)) && !errflag)`.
1928 while let Some(fname) = zreaddir(&mut rd, 1) {
1929 if errflag.load(Ordering::Relaxed) != 0 {
1930 break;
1931 }
1932 // c:1654-1657 — keep the `?message` suffix on each entry.
1933 arr.push(match u {
1934 Some(u) => format!("{}/{}?{}", path, fname, u),
1935 None => format!("{}/{}", path, fname),
1936 });
1937 }
1938 messages.extend(checkmailpath(&arr)); // c:1667 — recurse
1939 }
1940 }
1941 } else if interactive {
1942 // c:1671-1672 — new mail: non-empty, not yet read, newer than the
1943 // last check.
1944 if st.st_size != 0 && st.st_atime <= st.st_mtime && st.st_mtime >= lastmailcheck {
1945 match u {
1946 // c:1673-1675 — `fprintf(shout, "You have new mail.\n");`
1947 None => messages.push("You have new mail.".to_string()),
1948 // c:1676-1698 — custom message: bind `$_` to the file, then
1949 // parsestr + singsub the message (prompt-style expansion).
1950 Some(u) => {
1951 let usav = crate::ported::init::zunderscore.lock().unwrap().clone();
1952 crate::ported::exec::setunderscore(path); // c:1685 setunderscore(*s)
1953 if let Ok(parsed) = crate::ported::lex::parsestr(u) {
1954 // c:1688-1691 — parse ok → singsub then emit.
1955 messages.push(crate::ported::subst::singsub(&parsed));
1956 }
1957 crate::ported::exec::setunderscore(&usav); // c:1694-1697 restore $_
1958 }
1959 }
1960 }
1961 // c:1700-1704 — MAILWARNING: file read since the last check.
1962 if crate::ported::zsh_h::isset(crate::ported::zsh_h::MAILWARNING)
1963 && st.st_atime > st.st_mtime
1964 && st.st_atime > lastmailcheck
1965 && st.st_size != 0
1966 {
1967 messages.push(format!("The mail in {} has been read.", unmeta(file)));
1968 }
1969 }
1970 }
1971 messages
1972}
1973
1974/// Port of `printprompt4()` from `Src/utils.c:1718`.
1975///
1976/// Render the PS4 / PROMPT4 prefix and write it to stderr. zsh's
1977/// implementation reads `prompt4` global, suppresses XTRACE during
1978/// promptexpand (so subshells inside `%(?…)` don't recursively trace),
1979/// then fprintf's the expanded prefix to xtrerr. zshrs uses the same
1980/// suppress-XTRACE-around-expand pattern; ksh/sh emulation defaults
1981/// to `+ ` per Src/init.c:1192, zsh default to `+%N:%i> `.
1982///
1983/// The C source's caller (Src/exec.c::tracingcond etc.) follows this
1984/// with the per-line/per-arg fprintf — same shape mirrored at the two
1985/// zshrs call sites in fusevm_bridge.rs (BUILTIN_XTRACE_LINE / ARGS).
1986pub(crate) fn printprompt4() {
1987 // c:utils.c:1720 — `if (!isset(XTRACE)) return;`. C tests
1988 // `xtrerr` first then conditionally; the read-the-option early-
1989 // return path is equivalent for our purposes since we don't ship
1990 // the `xtrerr` separate-stream support.
1991 if !isset(XTRACE) {
1992 return;
1993 }
1994 // c:utils.c:1722-1724 — `if (prompt4) { ... s = dupstring(prompt4);`
1995 // C `prompt4` is a global initialized in init.c:1192 from the
1996 // emulation bits; PS4/PROMPT4 paramtab entries alias it via
1997 // IPDEF7R/IPDEF7 (params.c:381, 421). Read from paramtab to
1998 // honour user-set values, fall back to the same emulation
1999 // default C uses at init.c:1192.
2000 let posix = EMULATION(EMULATE_KSH | EMULATE_SH); // c:init.c:1192
2001 let prefix_template = getsparam("PS4")
2002 .or_else(|| getsparam("PROMPT4"))
2003 .unwrap_or_else(|| {
2004 if posix {
2005 "+ ".to_string() // c:init.c:1192
2006 } else {
2007 "+%N:%i> ".to_string() // c:init.c:1193
2008 }
2009 });
2010 // c:utils.c:1723,1726,1730 — `t = opts[XTRACE]; opts[XTRACE] = 0;
2011 // promptexpand(...); opts[XTRACE] = t;`
2012 let saved = isset(XTRACE);
2013 opt_state_set(&opt_name(XTRACE), false);
2014 let prefix = crate::prompt::expand_prompt(&prefix_template);
2015 opt_state_set(&opt_name(XTRACE), saved);
2016 // c:utils.c:1736 — `fprintf(xtrerr, "%s", s)`. Append to the xtrerr
2017 // line buffer rather than writing straight to stderr; the emitter
2018 // appends the command/args and flushes the whole line in one write,
2019 // matching C's single `fflush(xtrerr)` (so concurrent pipeline-stage
2020 // trace lines never interleave).
2021 crate::fusevm_bridge::xtrerr_fputs(&prefix);
2022}
2023
2024/// Port of `freestr(void *a)` from `Src/utils.c:1739`.
2025///
2026/// C body:
2027/// ```c
2028/// void freestr(void *a) { zsfree(a); }
2029/// ```
2030/// The C function is registered as the `freenode` callback for
2031/// hashtables holding plain string values. The Rust port consumes
2032/// `a` by value; Rust's `Drop` runs the equivalent of `zsfree` when
2033/// the `String` is moved into this fn and falls out of scope at the
2034/// closing brace — the no-op body is the correct port. Param name
2035/// `a` matches C exactly per Rule E.
2036pub fn freestr(_a: String) {}
2037
2038/// Port of `int gettempfile(const char *prefix, int use_heap, char **tempname)`
2039/// from Src/utils.c:2231. Creates a fresh tempfile with `O_RDWR|O_CREAT|O_EXCL`
2040/// and mode 0600 under umask 0177; returns `(fd, path)` on success.
2041///
2042/// C signature uses an out-param for the filename and returns the fd; Rust
2043/// returns both as a tuple. Matches the C control-flow: open with O_EXCL,
2044/// retry up to 16 times on EEXIST (the non-mkstemp branch, c:2255-2269).
2045pub fn gettempfile(prefix: Option<&str>) -> Option<(i32, String)> {
2046 // c:2231
2047 #[cfg(unix)]
2048 {
2049 queue_signals(); // c:2239
2050 let old_umask = unsafe { libc::umask(0o177) }; // c:2240
2051 let mut failures = 0; // c:2255
2052 let mut result: Option<(i32, String)> = None;
2053 loop {
2054 let fn_ = match gettempname(prefix, false) {
2055 // c:2260
2056 Some(n) => n,
2057 None => break,
2058 };
2059 let cn = match CString::new(fn_.clone()) {
2060 Ok(c) => c,
2061 Err(_) => break,
2062 };
2063 let fd = unsafe {
2064 libc::open(
2065 cn.as_ptr(),
2066 libc::O_RDWR | libc::O_CREAT | libc::O_EXCL, // c:2264
2067 0o600 as libc::c_int,
2068 )
2069 };
2070 if fd >= 0 {
2071 result = Some((fd, fn_));
2072 break;
2073 }
2074 let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
2075 if err != libc::EEXIST {
2076 // c:2269
2077 break;
2078 }
2079 failures += 1;
2080 if failures >= 16 {
2081 // c:2269
2082 break;
2083 }
2084 }
2085 unsafe {
2086 libc::umask(old_umask);
2087 } // c:2273
2088 unqueue_signals(); // c:2274
2089 result
2090 }
2091 #[cfg(not(unix))]
2092 {
2093 let _ = prefix;
2094 None
2095 }
2096}
2097
2098/// Port of `void gettyinfo(struct ttyinfo *ti)` from Src/utils.c:1746.
2099///
2100/// The saved baseline terminal mode — C's `struct ttyinfo shttyinfo`
2101/// (Src/init.c). Captured ONCE while the terminal is still in its
2102/// original (cooked / canonical) mode, before ZLE ever puts it into
2103/// raw mode via `zsetterm()`. `trashzle()` restores this baseline via
2104/// `settyinfo(&shttyinfo)` so that external commands the user runs
2105/// (`cat`, `less`, …) execute with a cooked terminal — where `^D` is
2106/// EOF, `^C` is SIGINT, and line editing works. Without it the tty
2107/// stayed in ZLE raw mode across command execution, so `cat` never
2108/// saw EOF on `^D`.
2109#[cfg(unix)]
2110pub static SHTTYINFO: std::sync::Mutex<Option<libc::termios>> = std::sync::Mutex::new(None);
2111
2112/// Reads the current termios from the global `SHTTY`. Returns
2113/// `None` when SHTTY is closed or the call fails (matching C's
2114/// silent return when SHTTY == -1).
2115/// C body (single statement): `fdgettyinfo(SHTTY, ti);`
2116/// Rust returns `Option<termios>` instead of taking an out-ptr;
2117/// the SHTTY=-1 case naturally returns Err from fdgettyinfo → None.
2118#[cfg(unix)]
2119pub fn gettyinfo() -> Option<libc::termios> {
2120 // c:1746
2121 fdgettyinfo(SHTTY.load(Ordering::Relaxed)).ok() // c:1748
2122}
2123
2124/// Emit the `$PS4` xtrace prefix to stderr.
2125/// Read terminal mode from a file descriptor.
2126/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1753. C source uses
2127/// `tcgetattr(SHTTY, &ti->tio)`; we return the populated termios
2128/// or an io::Error on failure (caller equivalent to zsh's `zerr`).
2129#[cfg(unix)]
2130/// WARNING: param names don't match C — Rust=(fd) vs C=(SHTTY, ti)
2131pub fn fdgettyinfo(fd: i32) -> io::Result<libc::termios> {
2132 let mut tio: libc::termios = unsafe { std::mem::zeroed() };
2133 if unsafe { libc::tcgetattr(fd, &mut tio) } == -1 {
2134 Err(io::Error::last_os_error())
2135 } else {
2136 Ok(tio)
2137 }
2138}
2139
2140#[cfg(not(unix))]
2141/// Port of `fdgettyinfo(int SHTTY, struct ttyinfo *ti)` from `Src/utils.c:1753`.
2142/// WARNING: param names don't match C — Rust=(_fd) vs C=(SHTTY, ti)
2143pub fn fdgettyinfo(_fd: i32) -> std::io::Result<()> {
2144 Err(std::io::Error::new(
2145 std::io::ErrorKind::Unsupported,
2146 "no termios",
2147 ))
2148}
2149
2150/// Port of `void settyinfo(struct ttyinfo *ti)` from Src/utils.c:1778.
2151///
2152/// Restores the termios state on the global `SHTTY` with EINTR
2153/// retry; no-op when SHTTY is closed.
2154/// C body (single statement): `fdsettyinfo(SHTTY, ti);`
2155/// Rust returns bool; SHTTY=-1 yields Err → false from fdsettyinfo.
2156#[cfg(unix)]
2157pub fn settyinfo(ti: &libc::termios) -> bool {
2158 // c:1778
2159 fdsettyinfo(SHTTY.load(Ordering::Relaxed), ti).is_ok() // c:1780
2160}
2161
2162/// Apply terminal mode to a file descriptor, with EINTR retry.
2163/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from Src/utils.c:1785. C source loops
2164/// `while (tcsetattr(SHTTY, TCSADRAIN, &ti->tio) == -1 && errno
2165/// == EINTR)` — same retry shape here.
2166#[cfg(unix)]
2167pub fn fdsettyinfo(SHTTY2: i32, ti: &libc::termios) -> io::Result<()> {
2168 loop {
2169 if unsafe { libc::tcsetattr(SHTTY2, libc::TCSADRAIN, ti) } != -1 {
2170 return Ok(());
2171 }
2172 let err = io::Error::last_os_error();
2173 if err.kind() != io::ErrorKind::Interrupted {
2174 return Err(err);
2175 }
2176 }
2177}
2178
2179#[cfg(not(unix))]
2180/// Port of `fdsettyinfo(int SHTTY, struct ttyinfo *ti)` from `Src/utils.c:1785`.
2181pub fn fdsettyinfo(SHTTY: i32, ti: &()) -> std::io::Result<()> {
2182 Err(std::io::Error::new(
2183 std::io::ErrorKind::Unsupported,
2184 "no termios",
2185 ))
2186}
2187
2188// window size changed // c:1831
2189/// Port of `adjustlines(int signalled)` from Src/utils.c:1831 — TIOCGWINSZ
2190/// lookup that seeds `$LINES`. The C variant updates the global
2191/// `zterm_lines` and returns whether it changed; this Rust port
2192/// returns the row count directly. Falls back to `$LINES` env var,
2193/// then 24, mirroring the C source's `tclines > 0 ? tclines : 24`
2194/// fallback at line 1844.
2195/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2196pub fn adjustlines() -> usize {
2197 // c:1831
2198 #[cfg(unix)]
2199 {
2200 unsafe {
2201 let mut ws: libc::winsize = std::mem::zeroed();
2202 if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_row > 0 {
2203 return ws.ws_row as usize;
2204 }
2205 }
2206 }
2207 // c:1844 fallback — paramtab `$LINES`, not OS env.
2208 getsparam("LINES")
2209 .and_then(|s| s.parse().ok())
2210 .unwrap_or(24)
2211}
2212
2213/// Port of `adjustcolumns(int signalled)` from Src/utils.c:1856 — TIOCGWINSZ
2214/// lookup that seeds `$COLUMNS`. The C variant updates the global
2215/// `zterm_columns` and returns whether it changed; this Rust port
2216/// returns the column count directly. Falls back to `$COLUMNS` env
2217/// var, then 80, mirroring the C source's `tccolumns > 0 ? tccolumns : 80`
2218/// fallback at line 1869.
2219/// WARNING: param names don't match C — Rust=() vs C=(signalled)
2220pub fn adjustcolumns() -> usize {
2221 // c:1856
2222 #[cfg(unix)]
2223 {
2224 unsafe {
2225 let mut ws: libc::winsize = std::mem::zeroed();
2226 if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
2227 return ws.ws_col as usize;
2228 }
2229 }
2230 }
2231 // c:1820 fallback — `if (zterm_columns <= 0) zterm_columns =
2232 // tccolumns > 0 ? tccolumns : 80`. C consults
2233 // `getsparam("COLUMNS")` (paramtab), not OS env.
2234 getsparam("COLUMNS")
2235 .and_then(|s| s.parse().ok())
2236 .unwrap_or(80)
2237}
2238
2239// window size changed // c:1824
2240/// Port of `void adjustwinsize(int from)` from `Src/utils.c:1889`.
2241/// SIGWINCH handler + LINES/COLUMNS-update entry. Reads the tty's
2242/// current geometry via `TIOCGWINSZ` ioctl, updates the cached
2243/// `zterm_lines`/`zterm_columns`, and writes `$LINES` / `$COLUMNS`
2244/// when they're already set in the environment.
2245/// ```c
2246/// void
2247/// adjustwinsize(int from)
2248/// {
2249/// static int getwinsz = 1;
2250/// int ttyrows = shttyinfo.winsize.ws_row;
2251/// int ttycols = shttyinfo.winsize.ws_col;
2252/// int resetzle = 0;
2253/// if (getwinsz || from == 1) {
2254/// if (SHTTY == -1) return;
2255/// if (ioctl(SHTTY, TIOCGWINSZ, &shttyinfo.winsize) == 0) {
2256/// resetzle = (ttyrows != ... || ttycols != ...);
2257/// ...
2258/// } else {
2259/// shttyinfo.winsize.ws_row = zterm_lines;
2260/// shttyinfo.winsize.ws_col = zterm_columns;
2261/// }
2262/// }
2263/// switch (from) {
2264/// case 0: case 1:
2265/// getwinsz = 0;
2266/// if (adjustlines(from) && zgetenv("LINES")) setiparam("LINES", zterm_lines);
2267/// if (adjustcolumns(from) && zgetenv("COLUMNS")) setiparam("COLUMNS", zterm_columns);
2268/// getwinsz = 1;
2269/// break;
2270/// case 2: resetzle = adjustlines(0); break;
2271/// case 3: resetzle = adjustcolumns(0); break;
2272/// }
2273/// if (interact && resetzle) zleentry(ZLE_CMD_REFRESH);
2274/// }
2275/// ```
2276pub fn adjustwinsize(from: i32) -> (usize, usize) {
2277 // c:1889
2278
2279 // c:1891 — `static int getwinsz = 1;`
2280 let getwinsz = ADJUSTWINSIZE_GETWINSZ.load(Ordering::SeqCst);
2281
2282 let mut ttyrows: i32 = 0;
2283 let mut ttycols: i32 = 0;
2284
2285 // c:1898-1917 — TIOCGWINSZ probe.
2286 if getwinsz != 0 || from == 1 {
2287 // c:1898
2288 let shtty = SHTTY.load(Ordering::Relaxed);
2289 if shtty == -1 {
2290 // c:1900
2291 return (adjustcolumns(), adjustlines()); // c:1901
2292 }
2293 #[cfg(unix)]
2294 unsafe {
2295 let mut ws: libc::winsize = std::mem::zeroed();
2296 if libc::ioctl(shtty, libc::TIOCGWINSZ, &mut ws as *mut _) == 0 {
2297 // c:1902
2298 ttyrows = ws.ws_row as i32; // c:1907
2299 ttycols = ws.ws_col as i32; // c:1908
2300 }
2301 }
2302 }
2303
2304 let mut resetzle = 0i32;
2305 match from {
2306 // c:1921
2307 0 | 1 => {
2308 // c:1922-1923
2309 ADJUSTWINSIZE_GETWINSZ.store(0, Ordering::SeqCst); // c:1924
2310 // c:1931-1934 — C: `if (adjustlines(from) && zgetenv("LINES"))
2311 // setiparam("LINES", zterm_lines);` (same for COLUMNS). The
2312 // zgetenv gate only guards re-publishing to an ENV-exported
2313 // LINES — in C the param itself is ALWAYS live because its
2314 // valptr aliases the `zterm_lines` global that adjustlines
2315 // just wrote (createparamtable IPDEF5 → zlevarsetfn/intvargetfn
2316 // on &zterm_lines). zshrs params store their own u_val copy
2317 // (no valptr aliasing), so the probe result must be written
2318 // through setiparam unconditionally — gating on the env left
2319 // `$LINES`/`$COLUMNS` at 0 for every interactive shell
2320 // (LINES is not in the environment; zsh doesn't export it).
2321 // Effect: `$(( LINES / 3 ))` = 0 → division-by-zero in
2322 // history-search-multi-word pagination (_hsmw_main:81), and
2323 // every LINES/COLUMNS-derived layout was wrong. Recursion is
2324 // safe: setiparam → zlevarsetfn → adjustwinsize(2|3) never
2325 // re-enters this arm.
2326 let lines = if ttyrows > 0 {
2327 ttyrows as i64 // c:1837 — zterm_lines = shttyinfo.winsize.ws_row
2328 } else {
2329 adjustlines() as i64 // c:1844 — tclines/24 fallback chain
2330 };
2331 setiparam("LINES", lines); // c:1932
2332 let cols = if ttycols > 0 {
2333 ttycols as i64 // c:1862 — zterm_columns = ws_col
2334 } else {
2335 adjustcolumns() as i64 // c:1869 — tccolumns/80 fallback chain
2336 };
2337 setiparam("COLUMNS", cols); // c:1934
2338 ADJUSTWINSIZE_GETWINSZ.store(1, Ordering::SeqCst); // c:1935
2339 }
2340 2 => {
2341 // c:1937
2342 resetzle = adjustlines() as i32; // c:1938
2343 }
2344 3 => {
2345 // c:1940
2346 resetzle = adjustcolumns() as i32; // c:1941
2347 }
2348 _ => {}
2349 }
2350
2351 // c:1946-1958 — resetzle + zleentry(ZLE_CMD_REFRESH) when interact.
2352 if from >= 2 && resetzle != 0 {
2353 // ZLE refresh dispatch via zleentry(ZLE_CMD_REFRESH) lands here
2354 // once the C signal handler shape ports.
2355 let _ = ttyrows;
2356 let _ = ttycols;
2357 }
2358
2359 (adjustcolumns(), adjustlines())
2360}
2361
2362/// Port of `static int getwinsz` from `Src/utils.c:1891`. Local
2363/// reentry guard inside adjustwinsize — bumped to 0 around the
2364/// setiparam recursion so the recursive call short-circuits.
2365pub static ADJUSTWINSIZE_GETWINSZ: std::sync::atomic::AtomicI32 =
2366 std::sync::atomic::AtomicI32::new(1); // c:1891
2367
2368/// Port of `check_fd_table(int fd)` from `Src/utils.c:1968-1983`.
2369/// ```c
2370/// if (fd <= max_zsh_fd) return;
2371/// if (fd >= fdtable_size) {
2372/// int old_size = fdtable_size;
2373/// while (fd >= fdtable_size)
2374/// fdtable = zrealloc(fdtable, (fdtable_size *= 2) * sizeof(*fdtable));
2375/// memset(fdtable + old_size, 0, (fdtable_size - old_size) * sizeof(*fdtable));
2376/// }
2377/// max_zsh_fd = fd;
2378/// ```
2379/// C semantics: GROW the `fdtable` array so it can index `fd`, then
2380/// update `max_zsh_fd`. Returns void.
2381///
2382/// The Rust port keeps the same signature for name-parity but the
2383/// fdtable global isn't yet modeled — so this is a no-op shim. The
2384/// previous Rust impl was an `fcntl(F_GETFD)` validity check —
2385/// COMPLETELY DIFFERENT SEMANTICS from C (which doesn't validate
2386/// the fd at all, just grows the table). Fixed to no-op + bool
2387/// return for caller compatibility (no live callers).
2388pub fn check_fd_table(fd: i32) -> bool {
2389 // c:1969
2390 // c:1971-1972 — `if (fd <= max_zsh_fd) return;`
2391 let cur_max = MAX_ZSH_FD.load(Ordering::Relaxed); // c:1971
2392 if fd <= cur_max {
2393 // c:1971
2394 return true; // c:1972 (return; in C)
2395 }
2396 if fd < 0 {
2397 // defensive — fdtable index must be ≥0
2398 return false;
2399 }
2400 // c:1974-1981 — `if (fd >= fdtable_size) { while (fd >= fdtable_size)
2401 // fdtable_size *= 2; zrealloc; memset(new_slots, 0); }`.
2402 // Rust Vec::resize handles the realloc + zero-fill in one call (the
2403 // expansion bumps capacity geometrically too).
2404 {
2405 let mut g = fdtable_lock().lock().unwrap();
2406 if (fd as usize) >= g.len() {
2407 g.resize((fd as usize) + 1, FDT_UNUSED); // c:1975-1979 grow
2408 }
2409 }
2410 // c:1982 — `max_zsh_fd = fd;`.
2411 MAX_ZSH_FD.store(fd, Ordering::Relaxed); // c:1982
2412 true
2413}
2414
2415/// Port of `movefd(int fd)` from `Src/utils.c:1989-2012`.
2416/// ```c
2417/// if (fd != -1 && fd < 10) {
2418/// int fe = fcntl(fd, F_DUPFD, 10);
2419/// zclose(fd); // unconditional close, even when fe == -1
2420/// fd = fe;
2421/// }
2422/// if (fd != -1) {
2423/// check_fd_table(fd);
2424/// fdtable[fd] = FDT_INTERNAL;
2425/// }
2426/// return fd;
2427/// ```
2428/// Two divergences fixed this iteration:
2429/// 1. Old Rust closed the source fd ONLY on success; C closes
2430/// unconditionally per the c:1999-2004 comment "probably better
2431/// to avoid a leak."
2432/// 2. Old Rust added `FD_CLOEXEC` after the dup; C does NOT —
2433/// CLOEXEC is added later by `addmodulefd`/`addlockfd` callers
2434/// that need it. movefd itself does not.
2435pub fn movefd(fd: i32) -> i32 {
2436 #[cfg(unix)]
2437 {
2438 let mut fd = fd;
2439 if fd != -1 && fd < 10 {
2440 // c:1992
2441 let fe = unsafe { libc::fcntl(fd, libc::F_DUPFD, 10) }; // c:1994
2442 unsafe { libc::close(fd) }; // c:2004 zclose(fd) — unconditional
2443 fd = fe; // c:2005
2444 }
2445 // c:2007-2010 — `if (fd != -1) { check_fd_table(fd); fdtable[fd]
2446 // = FDT_INTERNAL; }`. The fdtable global IS now modeled (port
2447 // of utils.c:63 — `Vec<u8>` behind `fdtable_lock`). Mark the
2448 // new fd as FDT_INTERNAL so the rest of the shell knows the
2449 // shell is using it (a later `addmodulefd` / `addlockfd`
2450 // upgrade may reclassify; raw external code shouldn't touch
2451 // it). The previous port skipped this so internal-fd tracking
2452 // (which `closeallelse` and forkexec rely on) silently
2453 // never saw zshrs-internal fds.
2454 if fd != -1 {
2455 // c:2007
2456 check_fd_table(fd); // c:2008
2457 fdtable_set(fd, FDT_INTERNAL); // c:2009
2458 }
2459 fd
2460 }
2461 #[cfg(not(unix))]
2462 {
2463 fd
2464 }
2465}
2466
2467/// Port of `redup(int x, int y)` from `Src/utils.c:2021`.
2468///
2469/// C signature: `int redup(int x, int y)`.
2470/// "Move fd x to y. If x == -1, fd y is closed. Returns y for
2471/// success, -1 for failure." (c:2014-2016 docstring.)
2472///
2473/// Body mirrors c:2023-2068: when `x == -1`, close `y` (return `y`);
2474/// when `x == y`, no-op (return `y`); otherwise `dup2(x, y)` +
2475/// `close(x)`.
2476///
2477/// C body fdtable updates (c:2053-2063) that the previous Rust port
2478/// SKIPPED with a stale "fdtable global not yet ported" comment —
2479/// fdtable IS now ported and these updates are load-bearing:
2480/// * `fdtable[y] = fdtable[x]` — the new fd inherits the old fd's
2481/// ownership category (FDT_INTERNAL / FDT_MODULE / etc.).
2482/// * If the inherited type is `FDT_FLOCK` / `FDT_FLOCK_EXEC`, promote
2483/// to `FDT_INTERNAL` (the dup'd fd doesn't carry the flock).
2484/// * If `fdtable[x] == FDT_FLOCK`, decrement `fdtable_flocks` (the
2485/// original lock-holding fd is about to be closed).
2486///
2487/// Without these updates, redup'd fds had stale `FDT_UNUSED` ownership
2488/// and `closeallelse(FDT_EXTERNAL)` etc. couldn't classify them.
2489pub fn redup(x: i32, y: i32) -> i32 {
2490 // c:2021
2491 let mut ret = y; // c:2023
2492 #[cfg(unix)]
2493 {
2494 if x < 0 {
2495 // c:2047
2496 zclose(y); // c:2048
2497 } else if x != y {
2498 // c:2049
2499 // c:2053-2057 — successful dup2: copy fdtable + FLOCK promote.
2500 if unsafe { libc::dup2(x, y) } == -1 {
2501 // c:2050
2502 ret = -1; // c:2051
2503 } else {
2504 check_fd_table(y); // c:2053
2505 let kind_x = fdtable_get(x); // c:2054
2506 let kind_y = if kind_x == FDT_FLOCK || kind_x == FDT_FLOCK_EXEC {
2507 FDT_INTERNAL // c:2055-2056
2508 } else {
2509 kind_x // c:2054
2510 };
2511 fdtable_set(y, kind_y);
2512 }
2513 // c:2062-2063 — `if (fdtable[x] == FDT_FLOCK) fdtable_flocks--;`
2514 // The original lock-holding fd is about to be closed, so the
2515 // flock-count tracker must drop. Even on dup2 failure C still
2516 // closes x, so this runs in both arms.
2517 if fdtable_get(x) == FDT_FLOCK {
2518 // c:2062
2519 FDTABLE_FLOCKS.fetch_sub(1, Ordering::SeqCst); // c:2063
2520 }
2521 zclose(x); // c:2064
2522 }
2523 }
2524 #[cfg(not(unix))]
2525 {
2526 let _ = (x, y);
2527 }
2528 ret // c:2067
2529}
2530
2531/// Port of `addmodulefd(int fd, int fdt)` from `Src/utils.c:2090-2097`.
2532/// ```c
2533/// if (fd >= 0) {
2534/// check_fd_table(fd);
2535/// fdtable[fd] = fdt;
2536/// }
2537/// ```
2538/// Two divergences fixed this iteration:
2539/// 1. C accepts an `fdt` parameter (FDT_MODULE / FDT_INTERNAL /
2540/// FDT_EXTERNAL — see callers in Src/Modules/{random,socket,tcp,
2541/// db_gdbm}.c). Rust port hardcoded `FDT_MODULE` — silently
2542/// ignored caller intent. Now takes `fdt` per C.
2543/// 2. C does NOT set `FD_CLOEXEC`. Rust port was adding it
2544/// unconditionally — divergent. CLOEXEC is the caller's
2545/// responsibility based on the fdt semantics.
2546pub fn addmodulefd(fd: i32, fdt: i32) {
2547 // c:2091
2548 // c:2093 — `if (fd >= 0)`.
2549 if fd >= 0 {
2550 // c:2094 — `check_fd_table(fd)` — grow fdtable to cover fd.
2551 check_fd_table(fd); // c:2094
2552 // c:2095 — `fdtable[fd] = fdt`. Routes through the canonical
2553 // helper which mirrors C's direct `fdtable[fd] = fdt` write.
2554 fdtable_set(fd, fdt); // c:2095
2555 }
2556}
2557
2558/// Port of `addlockfd(int fd, int cloexec)` from `Src/utils.c:2111-2121`.
2559/// ```c
2560/// if (cloexec) {
2561/// if (fdtable[fd] != FDT_FLOCK)
2562/// fdtable_flocks++;
2563/// fdtable[fd] = FDT_FLOCK;
2564/// } else {
2565/// fdtable[fd] = FDT_FLOCK_EXEC;
2566/// }
2567/// ```
2568/// Critical divergence fixed: C updates `fdtable[fd]` to FDT_FLOCK
2569/// (cloexec=true) or FDT_FLOCK_EXEC (cloexec=false). Previous Rust
2570/// port did the OPPOSITE — called `fcntl(F_SETFD, FD_CLOEXEC)` when
2571/// `cloexec` was true. That's wrong on two counts:
2572/// 1. C's "cloexec" parameter selects the FDT category, not a
2573/// libc fcntl flag.
2574/// 2. FDT_FLOCK means "lock survives across exec via fd inheritance"
2575/// — the OPPOSITE of close-on-exec. The Rust port was adding
2576/// CLOEXEC for the very case where the fd should be inheritable.
2577pub fn addlockfd(fd: i32, cloexec: bool) {
2578 // c:2112
2579 if cloexec {
2580 // c:2114
2581 // c:2115-2117 — track flock count, set FDT_FLOCK.
2582 if fdtable_get(fd) != FDT_FLOCK {
2583 FDTABLE_FLOCKS.fetch_add(1, Ordering::SeqCst);
2584 }
2585 fdtable_set(fd, FDT_FLOCK);
2586 } else {
2587 // c:2118
2588 // c:2119 — FDT_FLOCK_EXEC means flock inherited by exec.
2589 fdtable_set(fd, FDT_FLOCK_EXEC);
2590 }
2591}
2592
2593// FDTABLE_FLOCKS canonical declaration lives at utils.rs:5219
2594// (the exec.c global with same name). Reuse, don't redeclare.
2595
2596/// Close the given fd, and clear it from fdtable. // c:2123
2597/// Port of `int zclose(int fd)` from `Src/utils.c:2127`.
2598pub fn zclose(fd: i32) -> i32 {
2599 // c:2127
2600 if fd >= 0 {
2601 // c:2129
2602 // c:2130-2133 — comment carry: "Careful: we allow closing of
2603 // arbitrary fd's, beyond max_zsh_fd. In that case we don't
2604 // try anything clever."
2605 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed); // c:2134
2606 if fd <= max_fd {
2607 // c:2134
2608 // !!! ZSHRS THREADING ADAPTATION — deviates from C here !!!
2609 // C closes blindly; it is single-threaded, so a shell-recorded
2610 // fd number can never be concurrently recycled by someone else.
2611 // zshrs runs 18 worker threads whose Rust-owned fds (ReadDir
2612 // dirfds, SQLite, File) recycle freed numbers immediately — a
2613 // stale zclose then closes a FOREIGN fd, and std panics in
2614 // ReadDir::drop ("unexpected error during closedir: EBADF",
2615 // observed from the compinit bg scan). An in-range fd the
2616 // fdtable does NOT own is by definition not ours to close.
2617 if fdtable_get(fd) == FDT_UNUSED {
2618 tracing::debug!(fd, "zclose: skipping unowned in-range fd (thread-safety guard)");
2619 return 0;
2620 }
2621 if fdtable_get(fd) == FDT_FLOCK {
2622 // c:2135
2623 FDTABLE_FLOCKS.fetch_sub(1, Ordering::Relaxed);
2624 // c:2136
2625 }
2626 fdtable_set(fd, FDT_UNUSED); // c:2137
2627 // c:2138-2139 — shrink max_zsh_fd past trailing UNUSED slots.
2628 let mut m = MAX_ZSH_FD.load(Ordering::Relaxed);
2629 while m > 0 && fdtable_get(m) == FDT_UNUSED {
2630 m -= 1;
2631 }
2632 MAX_ZSH_FD.store(m, Ordering::Relaxed);
2633 // c:2140-2143 — coproc fd tracking.
2634 if fd == coprocin.load(Ordering::Relaxed) {
2635 coprocin.store(-1, Ordering::Relaxed);
2636 }
2637 if fd == coprocout.load(Ordering::Relaxed) {
2638 coprocout.store(-1, Ordering::Relaxed);
2639 }
2640 }
2641 #[cfg(unix)]
2642 unsafe {
2643 return libc::close(fd);
2644 } // c:2145
2645 #[cfg(not(unix))]
2646 return 0;
2647 }
2648 -1 // c:2147
2649}
2650
2651/// Port of `int zcloselockfd(int fd)` from `Src/utils.c:2155-2164`.
2652/// ```c
2653/// if (fd > max_zsh_fd) return -1;
2654/// if (fdtable[fd] != FDT_FLOCK && fdtable[fd] != FDT_FLOCK_EXEC)
2655/// return -1;
2656/// zclose(fd);
2657/// return 0;
2658/// ```
2659/// "Close an fd returning 0 if used for locking; return -1 if it
2660/// isn't." Caller (`bin_zsystem_flock -u`) uses the -1 return to
2661/// distinguish "fd not in the flock table" from "successfully
2662/// released a lock."
2663///
2664/// Previously the Rust port skipped the FDT_FLOCK / FDT_FLOCK_EXEC
2665/// check entirely and always returned 0 — meaning `zsystem flock -u
2666/// <unlocked-fd>` reported success on any fd. Now that the
2667/// `addlockfd` port (this iteration) populates the fdtable with the
2668/// canonical FDT_FLOCK / FDT_FLOCK_EXEC slots, the check can fire
2669/// faithfully.
2670pub fn zcloselockfd(fd: i32) -> i32 {
2671 // c:2156
2672 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
2673 // c:2158 — `if (fd > max_zsh_fd) return -1;`.
2674 if fd > max_fd {
2675 return -1;
2676 }
2677 // c:2160-2161 — `if (fdtable[fd] != FDT_FLOCK && != FDT_FLOCK_EXEC)
2678 // return -1;`.
2679 let slot = fdtable_get(fd);
2680 if slot != FDT_FLOCK && slot != FDT_FLOCK_EXEC {
2681 return -1;
2682 }
2683 zclose(fd); // c:2162
2684 0 // c:2163
2685}
2686
2687/// Port of `char *gettempname(const char *prefix, int use_heap)` from Src/utils.c:2178.
2688///
2689/// Returns a unique tempfile name templated like C `mktemp(3)` —
2690/// `{prefix}.XXXXXX`. Falls back to `getsparam("TMPPREFIX")` and
2691/// then `DEFAULT_TMPPREFIX` when `prefix` is None. Does NOT create
2692/// the file (matches C — only `gettempfile` creates).
2693pub fn gettempname(prefix: Option<&str>, _use_heap: bool) -> Option<String> {
2694 // c:2178
2695 let suffix = if prefix.is_some() {
2696 ".XXXXXX"
2697 } else {
2698 "XXXXXX"
2699 }; // c:2178
2700 queue_signals(); // c:2182
2701 let prefix_owned: String = match prefix {
2702 // c:2183
2703 Some(p) => p.to_string(),
2704 // c:2184 — `getsparam("TMPPREFIX")`. Read from paramtab (not OS
2705 // env); fall back to compile-time default when unset.
2706 None => getsparam("TMPPREFIX")
2707 .unwrap_or_else(|| crate::ported::config_h::DEFAULT_TMPPREFIX.to_string()),
2708 };
2709 let template = format!("{}{}", prefix_owned, suffix); // c:2186-2188
2710 // C uses mktemp(3) which mutates the X's into a unique name // c:2192/2219
2711 // without creating the file. Rust has no mktemp; emulate with
2712 // pid+timestamp. Caller is responsible for O_EXCL open.
2713 let pid = std::process::id();
2714 let nanos = std::time::SystemTime::now()
2715 .duration_since(UNIX_EPOCH)
2716 .map(|d| d.as_nanos())
2717 .unwrap_or(0);
2718 let unique = format!("{:x}{:x}", pid, nanos & 0xffffff);
2719 let name = template.replace("XXXXXX", &unique);
2720 unqueue_signals(); // c:2221
2721 Some(name)
2722}
2723
2724/// Check if metafied - port from zsh/Src/utils.c has_token()
2725// Check if a string contains a token // c:2282
2726/// Port of `has_token(const char *s)` from `Src/utils.c:2282` — used by
2727/// `ecstrcode` (parse.rs:989) to flip the token-marker bit on the
2728/// encoded string offset. Token markers live in 0x83..=0x9f (Pound,
2729/// Stringg, Hat, Star, ..., Bnull, Nularg). Earlier impl checked
2730/// only 0x83 (Meta) which missed Dash/Equals/Inbrack/Inbrace/etc.,
2731/// so any string containing those (e.g. `dart-lang/dart` →
2732/// `dart\u{9b}lang/dart`) got encoded with the no-token bit set,
2733/// breaking byte parity with C's wordcode-emitter output.
2734/// Port of `int has_token(const char *s)` from `Src/utils.c:2280-2288`.
2735/// ```c
2736/// while (*s)
2737/// if (itok(*s++)) return 1;
2738/// return 0;
2739/// ```
2740/// Routes through the canonical `itok()` typtab-driven predicate.
2741/// Previous Rust port used hardcoded `0x83..=0x9f` which was:
2742/// - Wrong on the low end: includes `0x83` (Meta, NOT a token).
2743/// - Wrong on the high end: missing `0xa0..=0xa1` (part of the
2744/// Snull..Nularg range that `itok` legitimately covers).
2745/// The full canonical ITOK range per `Src/zsh.h:152-159` is
2746/// `0x84..=0xa1` (Pound..Nularg), with possible gaps in the middle
2747/// (Snull at 0x9d follows Bang at 0x9c).
2748pub fn has_token(s: &str) -> bool {
2749 // c:2282
2750 s.bytes().any(itok) // c:2285
2751}
2752
2753// Delete a character in a string // c:2294
2754/// Remove character from string (from utils.c chuck)
2755pub fn chuck(s: &mut String, pos: usize) {
2756 // c:2294
2757 if pos < s.len() {
2758 s.remove(pos);
2759 }
2760}
2761
2762/// Convert character to lowercase
2763/// To-lowercase that respects locale.
2764/// Port of `tulower(int c)` from Src/utils.c.
2765pub fn tulower(c: char) -> char {
2766 // c:2302
2767 c.to_lowercase().next().unwrap_or(c)
2768}
2769
2770/// Convert character to uppercase
2771/// To-uppercase that respects locale.
2772/// Port of `tuupper(int c)` from Src/utils.c.
2773pub fn tuupper(c: char) -> char {
2774 // c:2310
2775 c.to_uppercase().next().unwrap_or(c)
2776}
2777
2778/// Port of `void ztrncpy(char *s, char *t, int len)` from `Src/utils.c:2320`.
2779///
2780/// C body (c:2320-2326):
2781/// ```c
2782/// while (len--)
2783/// *s++ = *t++;
2784/// *s = '\0';
2785/// ```
2786///
2787/// Copy `len` bytes from `t` into `s`, then NUL-terminate. C does no
2788/// bounds check; the caller must ensure `s` has `len + 1` bytes of
2789/// capacity. Rust port preserves the (s, t, len) parameter shape:
2790/// `s: &mut String` (output), `t: &str` (source), `len: usize` (copy
2791/// count). The C NUL terminator at c:2325 is implicit in Rust's
2792/// length-prefixed String. The Rust port clamps `len` to `t.len()` to
2793/// avoid the out-of-bounds read C's `*s++ = *t++` would perform when
2794/// `len > strlen(t)` (UB in C; explicitly bounded in Rust).
2795pub fn ztrncpy(s: &mut String, t: &str, len: usize) {
2796 // c:2320
2797 s.clear(); // C overwrites from start of *s
2798 let take = len.min(t.len()); // c:2322 while (len--)
2799 s.push_str(&t[..take]); // c:2322 *s++ = *t++
2800 // c:2325 — `*s = '\0';` (implicit in Rust String length)
2801}
2802
2803/// Port of `void strucpy(char **s, char *t)` from `Src/utils.c:2331-2337`.
2804/// ```c
2805/// char *u = *s;
2806/// while ((*u++ = *t++));
2807/// *s = u - 1; // leave *s pointing at the NUL terminator
2808/// ```
2809/// Body: `strcpy(*s, t)` + advance `*s` to point at the new
2810/// NUL terminator. The "u" in the name is a C convention for the
2811/// local pointer-walker, NOT "upper-case" — the function does NOT
2812/// change case.
2813///
2814/// Rust API: param name `s` matches C exactly per Rule E (despite Rust
2815/// convention of `dest` for output buffers). The C pointer-advance
2816/// `*s = u - 1` translates to "the new end of s is at s.len()", which
2817/// is implicit in Rust's owning-String model.
2818pub fn strucpy(s: &mut String, t: &str) {
2819 // c:2331
2820 s.push_str(t); // c:2335 `*u++ = *t++` loop
2821 // c:2336 — `*s = u - 1;` — pointer-advance is implicit in Rust
2822 // (`s.len()` is the new end-of-string position).
2823}
2824
2825/// Port of `void struncpy(char **s, char *t, int n)` from `Src/utils.c:2341-2350`.
2826/// ```c
2827/// char *u = *s;
2828/// while (n-- && (*u = *t++)) u++;
2829/// *s = u;
2830/// if (n > 0) *u = '\0';
2831/// ```
2832/// Body: copy up to `n` bytes from `t` to `*s`, NUL-terminate.
2833/// Note c:2348 — "just one null-byte will do, unlike strncpy(3)";
2834/// doesn't pad with NULs.
2835///
2836/// Param names match C exactly per Rule E.
2837pub fn struncpy(s: &mut String, t: &str, n: usize) {
2838 // c:2341
2839 // c:2345 — `while (n-- && (*u = *t++)) u++;` — copy up to n
2840 // bytes, stop at NUL (which in Rust &str is the end-of-string).
2841 let take = n.min(t.len());
2842 s.push_str(&t[..take]);
2843}
2844
2845/// Array length - port from arrlen()
2846/// Port of `arrlen(char **s)` from `Src/utils.c:2357`.
2847pub fn arrlen<T>(s: &[T]) -> usize {
2848 // c:2357
2849 s.len()
2850}
2851
2852// Return TRUE iff arrlen(s) >= lower_bound, but more efficiently. // c:2382
2853/// Check if array length >= n (from utils.c arrlen_ge)
2854pub fn arrlen_ge<T>(arr: &[T], n: usize) -> bool {
2855 // c:2369
2856 arr.len() >= n
2857}
2858
2859// Return TRUE iff arrlen(s) > lower_bound, but more efficiently. // c:2400
2860/// Check if array length > n (from utils.c arrlen_gt)
2861pub fn arrlen_gt<T>(arr: &[T], n: usize) -> bool {
2862 // c:2382
2863 arr.len() > n
2864}
2865
2866// Return TRUE iff arrlen(s) <= upper_bound, but more efficiently. // c:2409
2867/// Check if array length <= n (from utils.c arrlen_le)
2868pub fn arrlen_le<T>(arr: &[T], n: usize) -> bool {
2869 // c:2391
2870 arr.len() <= n
2871}
2872
2873// Return TRUE iff arrlen(s) < upper_bound, but more efficiently. // c:2400
2874/// Check if array length < n (from utils.c arrlen_lt)
2875pub fn arrlen_lt<T>(arr: &[T], n: usize) -> bool {
2876 // c:2400
2877 arr.len() < n
2878}
2879
2880/// Direct port of `int skipparens(char inpar, char outpar, char **s)`
2881/// from `Src/utils.c:2409`. Walks `*s` past a balanced `inpar`/`outpar`
2882/// pair, advancing the input slice in place. Returns 0 on full
2883/// balance, `-1` when `**s != inpar` (no opening paren), or positive
2884/// when the scan ran off the end of `*s` with `level > 0`
2885/// (unbalanced).
2886///
2887/// Rust signature: `s: &mut &str` mirrors C's `char **s` out-arg —
2888/// the caller's slice handle is rewritten to point past the closing
2889/// paren (or to the empty tail on unbalanced input).
2890/// WARNING: param names don't match C — Rust=(open, close, s) vs C=(inpar, outpar, s)
2891pub fn skipparens(open: char, close: char, s: &mut &str) -> i32 {
2892 // c:2409
2893 let mut chars = s.char_indices();
2894 // c:2412 — `if (**s != inpar) return -1;`
2895 match chars.next() {
2896 Some((_, c)) if c == open => {}
2897 _ => return -1,
2898 }
2899 // c:2414 — `for (level = 1; *++*s && level;)`
2900 let mut level: i32 = 1;
2901 let mut consumed = open.len_utf8();
2902 for (i, c) in chars {
2903 consumed = i + c.len_utf8();
2904 if c == open {
2905 level += 1; // c:2416-2417
2906 } else if c == close {
2907 level -= 1; // c:2418-2419
2908 }
2909 if level == 0 {
2910 break;
2911 }
2912 }
2913 *s = &s[consumed..];
2914 // c:2421 — `return level;`. 0 on balanced match, positive on
2915 // ran-off-end (unbalanced input).
2916 level
2917}
2918
2919/// zlong with base autodetection. Returns the parsed value AND a
2920/// `&str` slice pointing to the first unconsumed character —
2921/// matching C's `char **t` out-arg.
2922///
2923/// C signature: `zlong zstrtol(const char *s, char **t, int base)`.
2924/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
2925/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Otherwise `base`
2926/// must be in [2, 36]; values outside that range emit `zerr` and
2927/// return 0.
2928///
2929/// On overflow, mirrors C's "number truncated after N digits"
2930/// behaviour: emits a `zwarn` and returns the truncated value
2931/// (last in-range computation).
2932/// Port of `zstrtol(const char *s, char **t, int base)` from `Src/utils.c:2427`.
2933/// WARNING: param names don't match C — Rust=(s, base) vs C=(s, t, base)
2934pub fn zstrtol(s: &str, base: i32) -> (i64, &str) {
2935 // c:2427
2936 zstrtol_underscore(s, base, false) // c:2427
2937}
2938
2939/// Port of `zstrtol_underscore(const char *s, char **t, int base, int underscore)` from `Src/utils.c:2438`.
2940///
2941/// C signature: `zlong zstrtol_underscore(const char *s, char **t,
2942/// int base, int underscore)`.
2943///
2944/// Convert string to zlong with optional `_` digit-separator support.
2945/// `underscore != 0` allows underscores to appear inside the digit
2946/// run (skipped during accumulation). Returns the parsed value AND
2947/// the unconsumed-tail slice (matching C's `*t = (char *)s` writeback
2948/// at line 2516).
2949///
2950/// `base == 0` triggers prefix-based detection (`0x`/`0X`→16,
2951/// `0b`/`0B`→2, leading `0`→8, otherwise 10). Bases outside [2, 36]
2952/// emit `zerr` and return `(0, original_s)`. Overflow truncates and
2953/// emits a `zwarn` per c:2510-2512.
2954pub fn zstrtol_underscore(s: &str, base: i32, underscore: bool) -> (i64, &str) {
2955 // c:2438
2956 let bytes = s.as_bytes();
2957 let mut i = 0usize;
2958 // c:2444-2445 — skip leading `inblank` whitespace.
2959 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
2960 i += 1;
2961 }
2962 // c:2447-2450 — handle sign.
2963 let neg = if i < bytes.len() && (bytes[i] == b'-') {
2964 // c:2447 IS_DASH
2965 i += 1;
2966 true
2967 } else if i < bytes.len() && bytes[i] == b'+' {
2968 // c:2449
2969 i += 1;
2970 false
2971 } else {
2972 false
2973 };
2974
2975 // c:2452-2461 — base autodetect when base == 0.
2976 let mut base = base;
2977 if base == 0 {
2978 // c:2452
2979 if i >= bytes.len() || bytes[i] != b'0' {
2980 // c:2453
2981 base = 10; // c:2454
2982 } else {
2983 i += 1; // c:2455 ++s
2984 if i < bytes.len() && (bytes[i] == b'x' || bytes[i] == b'X') {
2985 // c:2455
2986 base = 16;
2987 i += 1;
2988 } else if i < bytes.len() && (bytes[i] == b'b' || bytes[i] == b'B') {
2989 // c:2457
2990 base = 2;
2991 i += 1;
2992 } else {
2993 // c:2459
2994 base = 8; // c:2460
2995 }
2996 }
2997 }
2998 let inp_idx = i; // c:2462 inp = s
2999 if base < 2 || base > 36 {
3000 // c:2463
3001 zerr(&format!(
3002 "invalid base (must be 2 to 36 inclusive): {}",
3003 base
3004 )); // c:2464
3005 return (0, s); // c:2465 return 0
3006 }
3007
3008 let mut calc: u64 = 0; // c:2441
3009 let mut trunc_idx: Option<usize> = None; // c:2440 trunc = NULL
3010 if base <= 10 {
3011 // c:2466
3012 // c:2467-2479 — digit accumulator, low bases.
3013 let max_d = b'0' + base as u8;
3014 while i < bytes.len() {
3015 let c = bytes[i];
3016 if c >= b'0' && c < max_d {
3017 if trunc_idx.is_none() {
3018 let newcalc = calc
3019 .wrapping_mul(base as u64)
3020 .wrapping_add((c - b'0') as u64);
3021 if newcalc < calc {
3022 // c:2473
3023 trunc_idx = Some(i); // c:2474
3024 } else {
3025 calc = newcalc;
3026 }
3027 }
3028 i += 1;
3029 } else if underscore && c == b'_' {
3030 // c:2467 underscore && *s == '_'
3031 i += 1;
3032 } else {
3033 break;
3034 }
3035 }
3036 } else {
3037 // c:2480
3038 // c:2481-2495 — digit accumulator, high bases (>10).
3039 while i < bytes.len() {
3040 let c = bytes[i];
3041 let digit = if c.is_ascii_digit() {
3042 // c:2481 idigit
3043 Some((c - b'0') as u32)
3044 } else if c >= b'a' && c < b'a' + base as u8 - 10 {
3045 // c:2482
3046 Some(((c & 0x1f) + 9) as u32) // c:2486 (*s & 0x1f) + 9
3047 } else if c >= b'A' && c < b'A' + base as u8 - 10 {
3048 // c:2483
3049 Some(((c & 0x1f) + 9) as u32)
3050 } else if underscore && c == b'_' {
3051 // c:2484
3052 i += 1;
3053 continue;
3054 } else {
3055 None
3056 };
3057 match digit {
3058 Some(d) => {
3059 if trunc_idx.is_none() {
3060 let newcalc = calc.wrapping_mul(base as u64).wrapping_add(d as u64);
3061 if newcalc < calc {
3062 // c:2489
3063 trunc_idx = Some(i); // c:2490
3064 } else {
3065 calc = newcalc;
3066 }
3067 }
3068 i += 1;
3069 }
3070 None => break,
3071 }
3072 }
3073 }
3074
3075 // c:2504-2511 — special-case: signed-overflow into top bit.
3076 // The lowest negative number triggers the first test but is
3077 // representable correctly; check it explicitly.
3078 if trunc_idx.is_none() && (calc as i64) < 0 {
3079 // c:2504
3080 let top_bit_only = calc & !(1u64 << 63); // c:2506
3081 if !neg || top_bit_only != 0 {
3082 // c:2505
3083 trunc_idx = Some(i.saturating_sub(1)); // c:2508
3084 calc /= base as u64; // c:2509
3085 }
3086 }
3087
3088 if let Some(t) = trunc_idx {
3089 // c:2512
3090 let digits = t.saturating_sub(inp_idx);
3091 let inp_str = &s[inp_idx..];
3092 zwarn(&format!(
3093 "number truncated after {} digits: {}",
3094 digits, inp_str
3095 )); // c:2513
3096 }
3097
3098 let result = if neg {
3099 // c:2517
3100 -(calc as i64)
3101 } else {
3102 calc as i64
3103 };
3104 (result, &s[i..])
3105}
3106
3107/// Parse unsigned integer with underscore support
3108/// Port from zsh/Src/utils.c zstrtoul_underscore() lines 2528-2575
3109/// Port of `zstrtoul_underscore(const char *s, zulong *retval)` from
3110/// `Src/utils.c:2527-2590`. C body:
3111/// ```c
3112/// if (*s == '+') s++;
3113/// if (*s != '0') base = 10;
3114/// else if (*++s == 'x' || *s == 'X') base = 16, s++;
3115/// else if (*s == 'b' || *s == 'B') base = 2, s++;
3116/// else base = isset(OCTALZEROES) ? 8 : 10;
3117/// ```
3118/// The leading-zero case is option-gated on OCTALZEROES — without
3119/// the option (default), `0777` parses as decimal 777, NOT octal 511.
3120/// Previously the Rust port always treated leading-zero as octal —
3121/// divergent for the default shell setup.
3122pub fn zstrtoul_underscore(s: &str) -> Option<u64> {
3123 // c:2529
3124 let s = s.trim();
3125 let s = s.strip_prefix('+').unwrap_or(s); // c:2533-2534
3126
3127 let (base, rest) = if s.starts_with("0x") || s.starts_with("0X") {
3128 // c:2538
3129 (16, &s[2..])
3130 } else if s.starts_with("0b") || s.starts_with("0B") {
3131 // c:2540
3132 (2, &s[2..])
3133 } else if s.starts_with('0') && s.len() > 1 && isset(OCTALZEROES)
3134 // c:2543
3135 {
3136 (8, &s[1..])
3137 } else {
3138 (10, s)
3139 };
3140
3141 let rest = rest.replace('_', "");
3142 u64::from_str_radix(&rest, base).ok()
3143}
3144
3145/// Port of `int setblock_fd(int turnonblocking, int fd, long *modep)`
3146/// from `Src/utils.c:2579`. Toggles O_NONBLOCK on `fd`.
3147///
3148/// **Signature matches C exactly** (param order `turnonblocking, fd`):
3149/// - `turnonblocking=1` → clear O_NONBLOCK (enable blocking).
3150/// - `turnonblocking=0` → set O_NONBLOCK (disable blocking).
3151///
3152/// Returns:
3153/// - `(true, mode)` if the fd's state was changed.
3154/// - `(false, -1)` if the fd is a regular file (C skips regular
3155/// files entirely — only operates on pipes/sockets/ttys per
3156/// `c:2599 if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`).
3157/// - `(false, mode)` if state was already correct.
3158///
3159/// Rust returns `(state_changed, modep_value)` as a tuple to mirror
3160/// C's `int` return + `long *modep` out-param.
3161pub fn setblock_fd(turnonblocking: bool, fd: i32) -> (bool, libc::c_long) {
3162 // c:2579
3163 #[cfg(unix)]
3164 unsafe {
3165 // c:2598-2600 — `if (!fstat(fd, &st) && !S_ISREG(st.st_mode))`.
3166 let mut st: libc::stat = std::mem::zeroed();
3167 if libc::fstat(fd, &mut st) != 0 {
3168 return (false, -1);
3169 }
3170 // c:2599 — skip regular files (no nonblock concept).
3171 let mode_bits = st.st_mode as u32;
3172 if (mode_bits & libc::S_IFMT as u32) == libc::S_IFREG as u32 {
3173 return (false, -1); // c:2614 *modep = -1; return 0
3174 }
3175 // c:2601 — `*modep = fcntl(fd, F_GETFL, 0);`
3176 let modep = libc::fcntl(fd, libc::F_GETFL, 0) as libc::c_long;
3177 if modep < 0 {
3178 return (false, -1); // c:2602 if (*modep != -1)
3179 }
3180 const NONBLOCK: libc::c_long = libc::O_NONBLOCK as libc::c_long;
3181 if !turnonblocking {
3182 // c:2603-2606 — want to KNOW if blocking was off; set it on.
3183 if (modep & NONBLOCK) != 0 {
3184 return (true, modep); // already nonblock — no-op, but "off"
3185 }
3186 if libc::fcntl(fd, libc::F_SETFL, modep | NONBLOCK) == 0 {
3187 return (true, modep); // c:2606
3188 }
3189 } else {
3190 // c:2607-2611 — want to clear NONBLOCK if currently set.
3191 if (modep & NONBLOCK) != 0 && libc::fcntl(fd, libc::F_SETFL, modep & !NONBLOCK) == 0 {
3192 return (true, modep); // c:2611 state changed
3193 }
3194 }
3195 (false, modep)
3196 }
3197 #[cfg(not(unix))]
3198 {
3199 let _ = (turnonblocking, fd);
3200 (false, -1)
3201 }
3202}
3203
3204/// Port of `int setblock_stdin(void)` from `Src/utils.c:2620-2625`.
3205/// ```c
3206/// long mode;
3207/// return setblock_fd(1, 0, &mode);
3208/// ```
3209/// `turnonblocking=1, fd=0` — turn ON blocking on stdin (clear
3210/// O_NONBLOCK).
3211pub fn setblock_stdin() -> i32 {
3212 // c:2624 — `return setblock_fd(1, 0, &mode);`.
3213 let (changed, _mode) = setblock_fd(true, 0); // c:2624
3214 changed as i32
3215}
3216
3217/// Port of `int read_poll(int fd, int *readchar, int polltty, zlong microseconds)`
3218/// from `Src/utils.c:2645-2738`.
3219///
3220/// Check whether input is pending on `fd` within `timeout_us`
3221/// microseconds. Faithful port of all three C paths:
3222/// 1. The `polltty` non-canonical-tty path (VMIN/VTIME termios
3223/// setup so a raw tty can be polled — c:2665-2696).
3224/// 2. The `HAVE_SELECT` wait (c:2697-2705). Rust uses `poll(2)`
3225/// as the idiom substitution for `select(2)`; `poll` return
3226/// semantics are mapped onto C's `ret` so the `ret < 0`
3227/// error-fallback still triggers.
3228/// 3. The final `setblock_fd` + non-blocking `read` fallback that
3229/// captures a byte when `select`/`poll` errored (c:2718-2729).
3230///
3231/// `readchar` is C's `int *readchar` out-param: when a byte is
3232/// actually consumed during the poll it is written through
3233/// `readchar` (so `read -t -k` on a raw tty gets the byte). `polltty`
3234/// is C's `int polltty` truthiness. Returns C's `(ret > 0)`.
3235///
3236/// WARNING: param order differs from C — Rust=(fd, readchar,
3237/// polltty, timeout_us) vs C=(fd, readchar, polltty, microseconds);
3238/// C's `microseconds` is `timeout_us` here.
3239pub fn read_poll(fd: i32, readchar: &mut i32, polltty: bool, timeout_us: i64) -> bool {
3240 // c:2645
3241 #[cfg(unix)]
3242 {
3243 let mut ret: i32 = -1; // c:2647
3244 let mut mode: libc::c_long = -1; // c:2648
3245 // C reassigns the `polltty` param; hold it as the VMIN-derived int.
3246 let mut polltty: i32 = polltty as i32; // c:2645
3247 let mut ti: Option<libc::termios> = None; // c:2659 `struct ttyinfo ti;`
3248
3249 // c:2662-2663 — `if (fd < 0 || (polltty && !isatty(fd))) polltty = 0;`
3250 if fd < 0 || (polltty != 0 && unsafe { libc::isatty(fd) } == 0) {
3251 polltty = 0; // no tty to poll
3252 }
3253
3254 // c:2665-2696 — HAS_TIO && !__CYGWIN__: non-canonical VMIN poll setup.
3255 if polltty != 0 && fd >= 0 {
3256 // c:2686 — `gettyinfo(&ti);` (operates on global SHTTY, as in C).
3257 if let Some(mut t) = gettyinfo() {
3258 // c:2687 — `if ((polltty = ti.tio.c_cc[VMIN]))`
3259 polltty = t.c_cc[libc::VMIN] as i32;
3260 if polltty != 0 {
3261 t.c_cc[libc::VMIN] = 0; // c:2688
3262 // c:2690 — termios timeout is 10ths of a second.
3263 t.c_cc[libc::VTIME] = (timeout_us / 100_000) as libc::cc_t; // c:2690
3264 settyinfo(&t); // c:2691
3265 }
3266 ti = Some(t);
3267 } else {
3268 // gettyinfo failed (SHTTY closed) — nothing to poll via VMIN.
3269 polltty = 0;
3270 }
3271 }
3272
3273 // c:2697-2705 — HAVE_SELECT wait, using poll(2) as the select(2) idiom.
3274 let timeout_ms = (timeout_us / 1000) as i32; // c:2698-2699 (expire_tv)
3275 if fd > -1 {
3276 // c:2701-2703
3277 let mut fds = [libc::pollfd {
3278 fd: fd as RawFd,
3279 events: libc::POLLIN,
3280 revents: 0,
3281 }];
3282 let raw = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
3283 ret = if raw > 0 {
3284 // Map poll revents onto select's "fd is readable" ret>0.
3285 if (fds[0].revents & libc::POLLIN) != 0 {
3286 1
3287 } else {
3288 0
3289 }
3290 } else {
3291 raw // 0 = timeout, -1 = error (triggers fallback below)
3292 };
3293 } else {
3294 // c:2705 — `select(0, NULL, NULL, NULL, &expire_tv)`: pure sleep.
3295 ret = unsafe { libc::poll(std::ptr::null_mut(), 0, timeout_ms) };
3296 }
3297
3298 // c:2718 — `if (fd >= 0 && ret < 0 && !errflag)`
3299 if fd >= 0 && ret < 0 && errflag.load(Ordering::Relaxed) == 0 {
3300 // c:2723 — final attempt: non-blocking read of a single char.
3301 // `(polltty || setblock_fd(0, fd, &mode))`
3302 let can_read = if polltty != 0 {
3303 true // polltty short-circuits setblock_fd; mode stays -1
3304 } else {
3305 let (changed, m) = setblock_fd(false, fd); // c:2723
3306 mode = m; // C writes *modep unconditionally
3307 changed
3308 };
3309 if can_read {
3310 let mut c: u8 = 0;
3311 let n = unsafe {
3312 libc::read(fd, &mut c as *mut u8 as *mut libc::c_void, 1)
3313 };
3314 if n > 0 {
3315 *readchar = c as i32; // c:2724
3316 ret = 1; // c:2725
3317 }
3318 }
3319 // c:2727-2728 — `if (mode != -1) fcntl(fd, F_SETFL, mode);`
3320 if mode != -1 {
3321 unsafe {
3322 libc::fcntl(fd, libc::F_SETFL, mode);
3323 }
3324 }
3325 }
3326
3327 // c:2730-2736 — HAS_TIO: restore canonical VMIN=1 / VTIME=0.
3328 if polltty != 0 {
3329 if let Some(mut t) = ti {
3330 t.c_cc[libc::VMIN] = 1; // c:2732
3331 t.c_cc[libc::VTIME] = 0; // c:2733
3332 settyinfo(&t); // c:2734
3333 }
3334 }
3335
3336 ret > 0 // c:2737
3337 }
3338 #[cfg(not(unix))]
3339 {
3340 let _ = (fd, readchar, polltty, timeout_us);
3341 false
3342 }
3343}
3344
3345/// Compute time difference in microseconds (from utils.c timespec_diff_us)
3346/// Port of `timespec_diff_us(const struct timespec *t1, const struct timespec *t2)` from `Src/utils.c:2752`.
3347/// Rust idiom replacement: `Instant::duration_since().as_micros()`
3348/// replaces the C tv_sec/tv_nsec arithmetic + sign-flip dance.
3349pub fn timespec_diff_us(t1: &std::time::Instant, t2: &std::time::Instant) -> i64 {
3350 if *t2 > *t1 {
3351 t2.duration_since(*t1).as_micros() as i64
3352 } else {
3353 -(t1.duration_since(*t2).as_micros() as i64)
3354 }
3355}
3356
3357/// Port of `int zmonotime(time_t *tloc)` from Src/utils.c:2780.
3358///
3359/// "Like time(), but uses the monotonic clock." Reads CLOCK_MONOTONIC
3360/// and returns `tv_sec`; writes the same value through `tloc` if
3361/// non-NULL (Rust: `Some(&mut t)`).
3362pub fn zmonotime(tloc: Option<&mut i64>) -> i64 {
3363 // c:2780
3364 #[cfg(unix)]
3365 {
3366 // c:2782 — `struct timespec ts;`
3367 let mut ts = libc::timespec {
3368 // c:2782
3369 tv_sec: 0,
3370 tv_nsec: 0,
3371 };
3372 // c:2783 — `zgettime_monotonic_if_available(&ts);`
3373 unsafe {
3374 libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); // c:2783
3375 }
3376 // c:2784-2785 — `if (tloc) *tloc = ts.tv_sec;`
3377 if let Some(t) = tloc {
3378 // c:2784
3379 *t = ts.tv_sec as i64; // c:2785
3380 }
3381 ts.tv_sec as i64 // c:2786 return ts.tv_sec
3382 }
3383 #[cfg(not(unix))]
3384 {
3385 let _ = tloc;
3386 0
3387 }
3388}
3389
3390/// Check if string looks like a number
3391/// Check whether a string parses as a decimal integer.
3392/// Sleep for a given number of seconds (fractional)
3393/// Sleep for a fractional number of seconds.
3394/// Port of `int zsleep(long us)` from Src/utils.c:2797.
3395///
3396/// "Sleep for the given number of microseconds." Wraps
3397/// `nanosleep(2)` with EINTR retry, returning 1 on completion, 0
3398/// on permanent error.
3399pub fn zsleep(us: i64) -> i32 {
3400 // c:2797
3401 let mut sleeptime = libc::timespec {
3402 // c:2797-2803
3403 tv_sec: (us / 1_000_000) as libc::time_t,
3404 tv_nsec: ((us % 1_000_000) * 1000) as libc::c_long,
3405 };
3406 #[cfg(unix)]
3407 {
3408 loop {
3409 // c:2804
3410 let mut rem = libc::timespec {
3411 tv_sec: 0,
3412 tv_nsec: 0,
3413 };
3414 let ret = unsafe { libc::nanosleep(&sleeptime, &mut rem) }; // c:2806
3415 if ret == 0 {
3416 // c:2808
3417 return 1;
3418 }
3419 let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
3420 if err != libc::EINTR {
3421 // c:2810
3422 return 0; // c:2811
3423 }
3424 sleeptime = rem; // c:2812
3425 }
3426 }
3427 #[cfg(not(unix))]
3428 {
3429 let _ = sleeptime;
3430 std::thread::sleep(std::time::Duration::from_micros(us.max(0) as u64));
3431 1
3432 }
3433}
3434
3435/// Sleep random amount up to max microseconds (from utils.c zsleep_random)
3436/// Port of `int zsleep_random(long max_us, time_t end_time)`
3437/// from Src/utils.c:2833.
3438///
3439/// "Sleep for time (fairly) randomly up to max_us microseconds.
3440/// Don't let the time extend beyond end_time. end_time is compared
3441/// to the current *monotonic* clock time. Return 1 if that seemed
3442/// to work, else 0."
3443pub fn zsleep_random(max_us: i64, end_time: i64) -> i32 {
3444 // c:2833
3445 let now = zmonotime(None); // c:2833
3446 let r16 = unsafe { libc::rand() } & 0xFFFF; // c:2845
3447 let mut r: i64 = (max_us >> 16) * (r16 as i64); // c:2852
3448 while r != 0 && now + (r / 1_000_000) > end_time {
3449 // c:2858
3450 r >>= 1; // c:2859
3451 }
3452 if r != 0 {
3453 // c:2860
3454 zsleep(r) // c:2861
3455 } else {
3456 0 // c:2862
3457 }
3458}
3459
3460/// Port of `int checkrmall(char *s)` from `Src/utils.c:2867`.
3461///
3462/// C body (c:2867-2919): count files in directory `s` (capped at 100,
3463/// honoring GLOBDOTS), emit one of 4 prompts based on count, optionally
3464/// sleep 10s under RMSTARWAIT, then `getquery("ny", 1)` for confirm.
3465///
3466/// Previous Rust port was a FAKE — single generic prompt, no file
3467/// count, no shout/errflag checks, no RMSTARWAIT, wrong valid-chars
3468/// order ("yn" instead of "ny" so 'n' isn't the default-first-arm).
3469/// Lost 9 of the 10 C semantic branches.
3470pub fn checkrmall(s: &str) -> bool {
3471 // c:2867
3472 // c:2871-2872 — `if (!shout) return 1;` — no controlling tty for
3473 // prompt output, default to yes. Rust analogue: if stderr isn't
3474 // a tty, skip the prompt and approve.
3475 let shout_is_tty = unsafe { libc::isatty(2) != 0 }; // c:2871
3476 if !shout_is_tty {
3477 // c:2871
3478 return true; // c:2872
3479 }
3480 // c:2873-2878 — `if (*s != '/')` build absolute via pwd prefix.
3481 let s_owned: String; // c:2873
3482 let s_abs: &str = if !s.starts_with('/') {
3483 // c:2873
3484 let pwd = getsparam("PWD").unwrap_or_default(); // c:2874 pwd[1]
3485 s_owned = if pwd.len() > 1 {
3486 // c:2874 if (pwd[1])
3487 crate::ported::string::tricat(&pwd, "/", s) // c:2875 zhtricat
3488 } else {
3489 // c:2876
3490 crate::ported::string::dyncat("/", s) // c:2877
3491 };
3492 s_owned.as_str()
3493 } else {
3494 s
3495 };
3496 let max_count: i32 = 100; // c:2879
3497 let mut count: i32 = 0; // c:2870 count = 0
3498 // c:2880-2892 — opendir + readdir loop, count entries (skip
3499 // dotfiles when !GLOBDOTS), cap at max_count.
3500 let ignoredots = !isset(GLOBDOTS); // c:2881
3501 // c:2880 — `if (!(dir = opendir(unmeta(s)))) return;` then
3502 // `while ((fn = zreaddir(dir, 1))) { ... }`.
3503 if let Ok(mut dir) = fs::read_dir(s_abs) {
3504 while let Some(fname) = zreaddir(&mut dir, 1) {
3505 if ignoredots && fname.starts_with('.') {
3506 // c:2885
3507 continue; // c:2886
3508 }
3509 count += 1; // c:2887
3510 if count > max_count {
3511 // c:2888
3512 break; // c:2889
3513 }
3514 }
3515 // c:closedir(dir) auto on drop
3516 }
3517 // c:2893-2904 — four-arm prompt based on file count.
3518 let stderr_h = io::stderr();
3519 let mut stderr_w = stderr_h.lock();
3520 if count > max_count {
3521 // c:2893
3522 let _ = write!(
3523 stderr_w,
3524 "zsh: sure you want to delete more than {} files in ",
3525 max_count
3526 ); // c:2894
3527 } else if count == 1 {
3528 // c:2896
3529 let _ = write!(stderr_w, "zsh: sure you want to delete the only file in ");
3530 // c:2897
3531 } else if count > 0 {
3532 // c:2898
3533 let _ = write!(
3534 stderr_w,
3535 "zsh: sure you want to delete all {} files in ",
3536 count
3537 ); // c:2899
3538 } else {
3539 // c:2901
3540 // c:2902 — `We don't know how many files the glob will expand to`
3541 let _ = write!(stderr_w, "zsh: sure you want to delete all the files in ");
3542 // c:2903
3543 }
3544 // c:2905 — `nicezputs(s, shout)` — escape non-printables in path.
3545 let _ = nicezputs(s_abs, &mut stderr_w); // c:2905
3546 // c:2906-2912 — RMSTARWAIT: print "? (waiting ten seconds)",
3547 // flush, beep, sleep 10, then newline.
3548 if isset(RMSTARWAIT) {
3549 // c:2906
3550 let _ = write!(stderr_w, "? (waiting ten seconds)"); // c:2907
3551 let _ = stderr_w.flush(); // c:2908
3552 drop(stderr_w); // release lock around zbeep + sleep
3553 zbeep(); // c:2909
3554 std::thread::sleep(std::time::Duration::from_secs(10)); // c:2910
3555 let _ = writeln!(io::stderr()); // c:2911 fputc('\n', shout)
3556 } else {
3557 drop(stderr_w);
3558 }
3559 // c:2913-2914 — `if (errflag) return 0;`
3560 if errflag.load(Ordering::Relaxed) != 0 {
3561 // c:2913
3562 return false; // c:2914
3563 }
3564 // c:2915-2917 — emit "[yn]? ", flush, beep.
3565 let _ = io::stderr().write_all(b" [yn]? "); // c:2915 nicezputs of " [yn]? "
3566 let _ = io::stderr().flush(); // c:2916
3567 zbeep(); // c:2917
3568 // c:2918 — `return (getquery("ny", 1) == 'y');`. Default-first-
3569 // char is 'n' (no) — getquery's first valid char is the default
3570 // returned when the user just hits Enter.
3571 getquery(Some("ny"), 1) == b'y' as i32 // c:2918
3572}
3573
3574/// Port of `ssize_t read_loop(int fd, char *buf, size_t len)` from
3575/// `Src/utils.c:2923`.
3576///
3577/// C body (c:2923-2945):
3578/// ```c
3579/// while (1) {
3580/// ssize_t ret = read(fd, buf, len);
3581/// if (ret == len) break;
3582/// if (ret <= 0) {
3583/// if (ret < 0) {
3584/// if (errno == EINTR) continue; // c:2933
3585/// if (fd != SHTTY) // c:2935
3586/// zwarn("read failed: %e", errno); // c:2936
3587/// }
3588/// return ret;
3589/// }
3590/// buf += ret; len -= ret;
3591/// }
3592/// ```
3593///
3594/// The previous Rust port omitted the `zwarn("read failed: %e", errno)`
3595/// emission at c:2935-2936. That's a real diagnostic divergence:
3596/// any non-SHTTY read failure in zsh emits a stderr message; the
3597/// Rust port silently propagated the io::Error to the caller, which
3598/// in most call sites was discarded (`let _ = read_loop(...)`).
3599/// Users debugging an interrupted file read (e.g. `< /broken/fd`)
3600/// would see no error message at all.
3601/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
3602pub fn read_loop(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
3603 // c:2923
3604 #[cfg(unix)]
3605 {
3606 let mut total = 0;
3607 while total < buf.len() {
3608 let n = unsafe {
3609 libc::read(
3610 // c:2928
3611 fd,
3612 buf[total..].as_mut_ptr() as *mut libc::c_void,
3613 buf.len() - total,
3614 )
3615 };
3616 if n <= 0 {
3617 // c:2931
3618 if n < 0 {
3619 let e = io::Error::last_os_error();
3620 if e.kind() == io::ErrorKind::Interrupted {
3621 // c:2933
3622 continue; // c:2934
3623 }
3624 // c:2935-2936 — `if (fd != SHTTY) zwarn("read failed: %e", errno);`
3625 let shtty = SHTTY.load(Ordering::Relaxed);
3626 if fd != shtty {
3627 // c:2935
3628 zwarn(
3629 // c:2936
3630 &format!("read failed: {}", e),
3631 );
3632 }
3633 return Err(e);
3634 }
3635 break;
3636 }
3637 total += n as usize;
3638 }
3639 Ok(total)
3640 }
3641 #[cfg(not(unix))]
3642 {
3643 let _ = (fd, buf);
3644 Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
3645 }
3646}
3647
3648/// Port of `ssize_t write_loop(int fd, const char *buf, size_t len)` from
3649/// `Src/utils.c:2949`.
3650///
3651/// C body (c:2949-2970):
3652/// ```c
3653/// while (1) {
3654/// ssize_t ret = write(fd, buf, len);
3655/// if (ret == len) break;
3656/// if (ret < 0) {
3657/// if (errno == EINTR) continue;
3658/// if (fd != SHTTY) // c:2960
3659/// zwarn("write failed: %e", errno); // c:2961
3660/// return -1;
3661/// }
3662/// buf += ret; len -= ret;
3663/// }
3664/// ```
3665///
3666/// Same divergence as `read_loop`: previous Rust port omitted the
3667/// `zwarn("write failed: %e", errno)` emission for non-SHTTY fds.
3668/// WARNING: param names don't match C — Rust=(fd, buf) vs C=(fd, buf, len)
3669pub fn write_loop(fd: i32, buf: &[u8]) -> io::Result<usize> {
3670 // c:2949
3671 #[cfg(unix)]
3672 {
3673 let mut total = 0;
3674 while total < buf.len() {
3675 let n = unsafe {
3676 libc::write(
3677 // c:2954
3678 fd,
3679 buf[total..].as_ptr() as *const libc::c_void,
3680 buf.len() - total,
3681 )
3682 };
3683 if n <= 0 {
3684 if n < 0 {
3685 let e = io::Error::last_os_error();
3686 if e.kind() == io::ErrorKind::Interrupted {
3687 // c:2958
3688 continue; // c:2959
3689 }
3690 // c:2960-2961 — `if (fd != SHTTY) zwarn("write failed: %e", errno);`
3691 let shtty = SHTTY.load(Ordering::Relaxed);
3692 if fd != shtty {
3693 // c:2960
3694 zwarn(
3695 // c:2961
3696 &format!("write failed: {}", e),
3697 );
3698 }
3699 return Err(e);
3700 }
3701 break;
3702 }
3703 total += n as usize;
3704 }
3705 Ok(total)
3706 }
3707 #[cfg(not(unix))]
3708 {
3709 let _ = (fd, buf);
3710 Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
3711 }
3712}
3713
3714/// Read a single character (from utils.c read1char).
3715///
3716/// Port of `static int read1char(int echo)` from `Src/utils.c:2972`.
3717///
3718/// C body (c:2972-2988):
3719/// ```c
3720/// char c;
3721/// int q = queue_signal_level();
3722/// dont_queue_signals();
3723/// while (read(SHTTY, &c, 1) != 1) {
3724/// if (errno != EINTR || errflag || retflag || breaks || contflag) {
3725/// restore_queue_signals(q);
3726/// return -1;
3727/// }
3728/// }
3729/// restore_queue_signals(q);
3730/// if (echo) write_loop(SHTTY, &c, 1);
3731/// return (unsigned char) c;
3732/// ```
3733///
3734/// Returns the byte read (0..=255) on success, `-1` on error. Three
3735/// divergences in the previous Rust port:
3736/// 1. **Read from stdin** instead of SHTTY (the tty fd). zsh reads
3737/// single chars during `read -k`, `bindkey`, query prompts —
3738/// always against the controlling terminal, not the standard
3739/// input which may be a pipe.
3740/// 2. **No `echo` parameter.** When `echo=1` C writes the byte
3741/// back to SHTTY so the user sees what they typed.
3742/// 3. **No `errflag` / `retflag` / `breaks` / `contflag` early-exit.**
3743/// A SIGINT-driven errflag should abort the read; previous Rust
3744/// port had no break path.
3745/// WARNING: returns i32 not `Option<char>` to match C's signature
3746/// (`(unsigned char) c` for success, `-1` for error).
3747pub fn read1char(echo: i32) -> i32 {
3748 // c:2972
3749 #[cfg(unix)]
3750 {
3751 let shtty = SHTTY.load(Ordering::Relaxed);
3752 if shtty < 0 {
3753 return -1;
3754 }
3755 let mut c: u8 = 0;
3756 loop {
3757 let rc = unsafe { libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1) };
3758 if rc == 1 {
3759 // c:2978
3760 break;
3761 }
3762 // c:2979 — `if (errno != EINTR || errflag || retflag || breaks
3763 // || contflag) return -1;`
3764 let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
3765 if err != libc::EINTR {
3766 return -1;
3767 }
3768 // Check errflag interrupt flag (the most observable of the
3769 // four guards; retflag/breaks/contflag are control-flow
3770 // states that don't apply outside the exec engine).
3771 if errflag.load(Ordering::Relaxed) != 0 {
3772 return -1;
3773 }
3774 }
3775 // c:2985-2986 — `if (echo) write_loop(SHTTY, &c, 1);`
3776 if echo != 0 {
3777 // c:2985
3778 let _ = write_loop(shtty, &[c]); // c:2986
3779 }
3780 // c:2987 — `return (unsigned char) c;`
3781 c as i32
3782 }
3783 #[cfg(not(unix))]
3784 {
3785 let _ = echo;
3786 -1
3787 }
3788}
3789
3790/// Port of `int noquery(int purge)` from Src/utils.c:2992.
3791///
3792/// "If anything has been typed before the query, return without
3793/// asking. Optionally also purge the input queue." Returns the
3794/// number of bytes pending on `SHTTY` (via FIONREAD ioctl); when
3795/// `purge` is set, drains them before returning.
3796pub fn noquery(purge: bool) -> i32 {
3797 // c:2992
3798 let mut val: libc::c_int = 0; // c:2992
3799 #[cfg(unix)]
3800 {
3801 let shtty = SHTTY.load(Ordering::Relaxed); // c:2999
3802 if shtty == -1 {
3803 return 0;
3804 }
3805 unsafe {
3806 // c:2999
3807 libc::ioctl(shtty, libc::FIONREAD, &mut val as *mut libc::c_int);
3808 }
3809 if purge {
3810 // c:3000
3811 let mut c: u8 = 0;
3812 for _ in 0..val {
3813 // c:3001
3814 let _ = unsafe {
3815 // c:3002
3816 libc::read(shtty, &mut c as *mut u8 as *mut libc::c_void, 1)
3817 };
3818 }
3819 }
3820 }
3821 val // c:3009
3822}
3823
3824/// Port of `int getquery(char *valid_chars, int purge)` from
3825/// `Src/utils.c:3014`.
3826///
3827/// Read a single keystroke from the TTY in raw mode and return its
3828/// character code (as `int`, mirroring C). If `valid_chars` is `None`
3829/// (C `NULL`) any byte is accepted. If `valid_chars` is `Some(s)`, only
3830/// chars in `s` (plus `\n` which maps to `s[0]`) are accepted; other
3831/// input rings the bell and re-prompts. `Y`/`N` are pre-normalised to
3832/// `y`/`n` per c:3041-3045.
3833///
3834/// `purge != 0` triggers `noquery(purge)`'s queue-purge path — if input
3835/// is queued, returns `'n'` without reading.
3836pub fn getquery(valid_chars: Option<&str>, purge: i32) -> i32 {
3837 // c:3014
3838 // c:3016 — `int c, d, nl = 0;`
3839 let mut c: i32;
3840 let mut d: i32;
3841 let mut nl: i32 = 0;
3842 // c:3017 — `int isem = !strcmp(term, "emacs");`
3843 // Stub: `term` is the $TERM environment global, declared in
3844 // `Src/init.c` (extern in zsh.h). Local stub reads $TERM from
3845 // paramtab; absent → empty string.
3846 let term: String = getsparam("TERM").unwrap_or_default();
3847 let isem: bool = term == "emacs";
3848 // c:3018 — `struct ttyinfo ti;`
3849 // Rust: termios is the canonical TTY-state type returned by gettyinfo.
3850 let mut ti: libc::termios; // c:3018
3851
3852 attachtty(mypgrp.load(Ordering::Relaxed)); // c:3020 attachtty(mypgrp)
3853
3854 // c:3022 — `gettyinfo(&ti);`
3855 ti = match gettyinfo() {
3856 Some(t) => t,
3857 None => return -1,
3858 };
3859 // c:3023-3030 — `ti.tio.c_lflag &= ~ECHO; if (!isem) {
3860 // ti.tio.c_lflag &= ~ICANON;
3861 // ti.tio.c_cc[VMIN] = 1;
3862 // ti.tio.c_cc[VTIME] = 0; }`
3863 #[cfg(unix)]
3864 {
3865 ti.c_lflag &= !(libc::ECHO); // c:3024
3866 if !isem {
3867 // c:3025
3868 ti.c_lflag &= !(libc::ICANON); // c:3026
3869 ti.c_cc[libc::VMIN] = 1; // c:3027
3870 ti.c_cc[libc::VTIME] = 0; // c:3028
3871 }
3872 }
3873 // c:3037 — `settyinfo(&ti);`
3874 settyinfo(&ti);
3875
3876 // c:3039 — `if (noquery(purge))`
3877 if noquery(purge != 0) != 0 {
3878 // Stub: `shttyinfo` is the canonical saved-TTY-state global,
3879 // declared in `Src/init.c` (extern in zsh.h:1856). Without the
3880 // global tracked here we re-fetch current termios as a degraded
3881 // best-effort restore.
3882 if !isem {
3883 // c:3040
3884 if let Some(saved) = gettyinfo() {
3885 // c:3041 settyinfo(&shttyinfo)
3886 settyinfo(&saved);
3887 }
3888 }
3889 let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"n\n"); // c:3042
3890 return b'n' as i32; // c:3043
3891 }
3892
3893 // c:3046-3061 — `while ((c = read1char(0)) >= 0) { ... }`
3894 c = -1;
3895 loop {
3896 let cc = read1char(0); // c:3046
3897 if cc < 0 {
3898 c = cc;
3899 break;
3900 }
3901 c = cc;
3902 if c == b'Y' as i32 {
3903 // c:3047
3904 c = b'y' as i32; // c:3048
3905 } else if c == b'N' as i32 {
3906 // c:3049
3907 c = b'n' as i32; // c:3050
3908 }
3909 if valid_chars.is_none() {
3910 // c:3051
3911 break; // c:3052
3912 }
3913 if c == b'\n' as i32 {
3914 // c:3053
3915 // c:3054 — `c = *valid_chars;`
3916 c = valid_chars.unwrap().bytes().next().unwrap_or(b'\n') as i32;
3917 nl = 1; // c:3055
3918 break; // c:3056
3919 }
3920 // c:3058 — `if (strchr(valid_chars, c))`
3921 if valid_chars.unwrap().bytes().any(|b| b as i32 == c) {
3922 nl = 1; // c:3059
3923 break; // c:3060
3924 }
3925 zbeep(); // c:3062
3926 }
3927 if c >= 0 {
3928 // c:3064
3929 // c:3065-3066 — `char buf = (char)c; write_loop(SHTTY, &buf, 1);`
3930 let buf = [c as u8]; // c:3065
3931 let _ = write_loop(SHTTY.load(Ordering::Relaxed), &buf); // c:3066
3932 }
3933 if nl != 0 {
3934 // c:3068
3935 let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n"); // c:3069
3936 }
3937
3938 if isem {
3939 // c:3071
3940 if c != b'\n' as i32 {
3941 // c:3072
3942 // c:3073 — `while ((d = read1char(1)) >= 0 && d != '\n');`
3943 loop {
3944 d = read1char(1);
3945 if d < 0 || d == b'\n' as i32 {
3946 break;
3947 }
3948 }
3949 }
3950 } else if c != b'\n' as i32 && valid_chars.is_none() {
3951 // c:3075
3952 // c:3077-3094 — MULTIBYTE_SUPPORT branch: drain trailing bytes
3953 // of an incomplete multibyte sequence.
3954 if isset(MULTIBYTE) && c >= 0 {
3955 // c:3077
3956 // c:3083-3093 — `for (;;) { ret = mbrlen(&cc, 1, &mbs);
3957 // if (ret != MB_INCOMPLETE) break;
3958 // c = read1char(1); if (c < 0) break;
3959 // cc = (char)c; }`
3960 // Rust: model MB_INCOMPLETE detection by checking whether
3961 // the byte buffer so far forms a complete UTF-8 prefix.
3962 // `cc` carries the current byte under examination.
3963 let mut cc: u8 = c as u8; // c:3082
3964 let mut accum: Vec<u8> = vec![cc];
3965 loop {
3966 // mbrlen returns MB_INCOMPLETE when the partial buffer
3967 // doesn't yet form a complete UTF-8 character. Rust
3968 // equivalent: from_utf8 errors with error_len() == None
3969 // when more bytes are needed.
3970 let incomplete = match std::str::from_utf8(&accum) {
3971 Ok(_) => false,
3972 Err(e) => e.error_len().is_none(),
3973 };
3974 if !incomplete {
3975 // c:3088
3976 break;
3977 }
3978 let nc = read1char(1); // c:3089
3979 if nc < 0 {
3980 // c:3090
3981 break; // c:3091
3982 }
3983 cc = nc as u8; // c:3092
3984 accum.push(cc);
3985 }
3986 let _ = cc;
3987 }
3988 // c:3097 — `write_loop(SHTTY, "\n", 1);`
3989 let _ = write_loop(SHTTY.load(Ordering::Relaxed), b"\n");
3990 }
3991
3992 // c:3101 — `settyinfo(&shttyinfo);` — restore saved TTY state.
3993 // Stub-degraded path: refetch current termios. The proper port
3994 // requires wiring an `shttyinfo` global in init.rs.
3995 if let Some(saved) = gettyinfo() {
3996 settyinfo(&saved);
3997 }
3998
3999 c // c:3102 return c
4000}
4001
4002// `spscan` (Src/utils.c:3109) — canonical port lives below the
4003// thread_local block in this file. The pre-existing 3-arg
4004// `(name, candidates[], threshold) → Option<String>` shape was a
4005// drift fake (C is `void spscan(HashNode hn, scanflags)`); deleted
4006// because it had zero call sites and conflicted with the faithful
4007// port spckword needs.
4008
4009// spellcheck a word // c:3123
4010// fix s ; if hist is nonzero, fix the history list too // c:3124
4011
4012// File-static state shared with the (inlined) spscan callback. C uses
4013// raw file-statics at utils.c:3045-3050 (`best`, `d`, `guess`, `ic`,
4014// `spckpat`, `spnamepat`); Rust port mirrors them as thread_locals
4015// (per-evaluator per PORT_PLAN.md bucket-1) so concurrent worker
4016// threads each have their own correction state.
4017thread_local! {
4018 /// Port of `static int d;` from `Src/utils.c:3045`. Best dist seen.
4019 static SPCK_D: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
4020 /// Port of `static char *best;` from `Src/utils.c:3046`. Best-match.
4021 static SPCK_BEST: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4022 /// Port of `static char *guess;` from `Src/utils.c:3047`. Word to fix.
4023 static SPCK_GUESS: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
4024 /// Port of `static Patprog spckpat;` from `Src/utils.c:3049`.
4025 static SPCK_PAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4026 = const { std::cell::RefCell::new(None) };
4027 /// Port of `static Patprog spnamepat;` from `Src/utils.c:3050`.
4028 static SPCK_NAMEPAT: std::cell::RefCell<Option<crate::ported::pattern::Patprog>>
4029 = const { std::cell::RefCell::new(None) };
4030}
4031
4032/// Port of `static void spscan(HashNode hn, UNUSED(int scanflags))` from
4033/// `Src/utils.c:3074`. Inlined per-call below because hashtable.rs's
4034/// scan helpers don't model C's `scanhashtable(table, ..., scanfn, ...)`
4035/// callback shape — Rust call sites iterate the table directly. C
4036/// body:
4037/// ```c
4038/// int nd = spdist(hn->nam, guess, (int) strlen(guess) / 4 + 1);
4039/// if (nd < d && (!spckpat || !pattry(spckpat, hn->nam))) {
4040/// best = hn->nam;
4041/// d = nd;
4042/// }
4043/// ```
4044fn spscan(name: &str) {
4045 // c:3074
4046 let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4047 if guess.is_empty() {
4048 return;
4049 }
4050 let thresh = guess.len() / 4 + 1; // c:3076 `(int) strlen(guess) / 4 + 1`
4051 let nd = spdist(name, &guess, thresh) as i32; // c:3076
4052 let d = SPCK_D.with(|c| c.get());
4053 if nd < d {
4054 // c:3077
4055 // c:3078 — `if (!spckpat || !pattry(spckpat, hn->nam))`.
4056 let allow = SPCK_PAT.with(|p| {
4057 p.borrow()
4058 .as_ref()
4059 .map(|prog| !crate::ported::pattern::pattry(prog, name))
4060 .unwrap_or(true)
4061 });
4062 if allow {
4063 SPCK_BEST.with(|b| *b.borrow_mut() = Some(name.to_string())); // c:3079
4064 SPCK_D.with(|c| c.set(nd)); // c:3080
4065 }
4066 }
4067}
4068
4069/// Port of `void spckword(char **s, int hist, int cmd, int ask)` from
4070/// `Src/utils.c:3128`.
4071///
4072/// Spell-check `*s`. If a near-match is found in the appropriate
4073/// hashtable (params for `$x`, command tables for command position,
4074/// directory entries otherwise) AND the user accepts the correction
4075/// (`ask=0` auto-accepts), replace `*s` in place with the corrected
4076/// form and (if `hist!=0`) rewrite the history entry too.
4077///
4078/// Faithful 1:1 line-by-line port. Interactive prompting (c:3273-3287)
4079/// is stubbed to auto-accept when `ask=1` since `getquery` /
4080/// `promptexpand` / `shout` / `zbeep` aren't yet wired in zshrs —
4081/// flagged with WARNING at the prompt site.
4082///
4083/// Caller updates: previous Rust signature `(word, candidates[], threshold)
4084/// → Option<String>` is gone — `lex.rs` builds candidate lists itself,
4085/// but C's spckword scans the canonical hashtables directly. The new
4086/// signature matches C exactly: takes `&mut String` for in-place fix.
4087pub fn spckword(s: &mut String, hist: i32, cmd: i32, ask: i32) {
4088 use crate::ported::hashtable::{
4089 aliastab_lock, cmdnamtab_lock, fillcmdnamtable, pathchecked, reswdtab_lock, shfunctab_lock,
4090 };
4091 use crate::ported::params::{getsparam, paramtab};
4092 use crate::ported::pattern::{patcompile, PAT_HEAPDUP};
4093 use crate::ported::zsh_h::{
4094 isset, Dash, Equals, Stringg as StringTok, Tilde, AUTOCD, HASHLISTALL,
4095 };
4096 // c:3130-3133 — locals.
4097 let _t: Option<String>;
4098 let mut ic: char = '\0'; // c:3131
4099 let mut preflen: usize = 0; // c:3132
4100 // c:3133 — `autocd = cmd && isset(AUTOCD) && strcmp(*s, ".") && strcmp(*s, "..")`.
4101 let autocd = cmd != 0 && isset(AUTOCD) && s != "." && s != "..";
4102
4103 // c:3135-3136 — `if (!(*s)[0] || !(*s)[1]) return;`
4104 if s.len() < 2 {
4105 return;
4106 }
4107 // c:3137-3140 — HISTFLAG_NOEXEC or leading %/- skip.
4108 let bytes = s.as_bytes();
4109 let first = bytes[0] as char;
4110 let histdone = crate::ported::hist::histdone.load(std::sync::atomic::Ordering::Relaxed); // c:3137
4111 if (histdone & HISTFLAG_NOEXEC) != 0
4112 || (if cmd != 0 {
4113 first == '%' // c:3139 — leading % is a job
4114 } else {
4115 first == '-' || first == Dash // c:3139 — leading hyphen is an option
4116 })
4117 {
4118 return; // c:3140
4119 }
4120 // c:3141-3142 — `if (!strcmp(*s, "in")) return;`.
4121 if s == "in" {
4122 return; // c:3142
4123 }
4124 // c:3143-3155 — `cmd` branch: skip if it's already a known
4125 // function/builtin/cmdname/alias/reswd. Optional HASHLISTALL
4126 // re-fill of cmdnamtab on miss.
4127 if cmd != 0 {
4128 // c:3143
4129 let known = shfunctab_lock() // c:3144
4130 .read()
4131 .map(|t| t.get(s).is_some())
4132 .unwrap_or(false)
4133 || BUILTINS // c:3145
4134 .iter()
4135 .any(|b| b.node.nam == *s)
4136 || cmdnamtab_lock() // c:3146
4137 .read()
4138 .map(|t| t.get(s).is_some())
4139 .unwrap_or(false)
4140 || aliastab_lock() // c:3147
4141 .read()
4142 .map(|t| t.get(s).is_some())
4143 .unwrap_or(false)
4144 || reswdtab_lock() // c:3148
4145 .read()
4146 .map(|t| t.get(s).is_some())
4147 .unwrap_or(false);
4148 if known {
4149 return; // c:3149
4150 }
4151 // c:3150-3154 — HASHLISTALL: bulk-hash $PATH then retry.
4152 if isset(HASHLISTALL) {
4153 // c:3150
4154 let path: Vec<String> =
4155 getsparam("PATH") // c:3151
4156 .map(|p| p.split(':').map(String::from).collect())
4157 .unwrap_or_default();
4158 fillcmdnamtable(&path); // c:3151
4159 if cmdnamtab_lock() // c:3152
4160 .read()
4161 .map(|t| t.get(s).is_some())
4162 .unwrap_or(false)
4163 {
4164 return; // c:3153
4165 }
4166 }
4167 }
4168 // c:3156-3165 — Tilde/Equals/String prefix skip + itok/Dash
4169 // detok + early-return on any other tokenized char.
4170 let mut start = 0usize;
4171 let bytes = s.as_bytes(); // re-bind after potential string ops
4172 if !bytes.is_empty() {
4173 let c0 = bytes[0] as char;
4174 if c0 == Tilde || c0 == Equals || c0 == StringTok {
4175 // c:3157
4176 start = 1; // c:3158 t++
4177 }
4178 }
4179 // Scan from `start` for tokenized bytes.
4180 {
4181 let mut buf = s.clone().into_bytes();
4182 let mut i = start;
4183 let mut had_dash_only = true; // accumulator
4184 while i < buf.len() {
4185 let b = buf[i];
4186 if itok(b) {
4187 // c:3160
4188 if b as char == Dash {
4189 // c:3161
4190 buf[i] = b'-'; // c:3162
4191 } else {
4192 return; // c:3164
4193 }
4194 } else {
4195 had_dash_only = had_dash_only && false;
4196 }
4197 i += 1;
4198 }
4199 let _ = had_dash_only;
4200 *s = String::from_utf8_lossy(&buf).into_owned();
4201 }
4202 // c:3166 — `best = NULL;`
4203 SPCK_BEST.with(|b| *b.borrow_mut() = None);
4204 SPCK_D.with(|c| c.set(100)); // initialised at each table-scan branch in C; mirror up-front.
4205 // c:3167-3169 — `for (t = *s; *t; t++) if (*t == '/') break;`
4206 // `t` is the position of the first slash (or end of string).
4207 let t_pos = s.find('/').unwrap_or(s.len()); // c:3167
4208 // c:3170-3171 — `if (**s == Tilde && !*t) return;`
4209 let bytes = s.as_bytes();
4210 if !bytes.is_empty() && (bytes[0] as char) == Tilde && t_pos == bytes.len() {
4211 // c:3170
4212 return; // c:3171
4213 }
4214
4215 // c:3173-3178 — compile CORRECT_IGNORE pattern if set.
4216 if let Some(ci) = getsparam("CORRECT_IGNORE") {
4217 // c:3173
4218 let prog = patcompile(
4219 &{
4220 let mut __pat_tok = (&ci).to_string();
4221 crate::ported::glob::tokenize(&mut __pat_tok);
4222 __pat_tok
4223 },
4224 PAT_HEAPDUP,
4225 None,
4226 ); // c:3176
4227 SPCK_PAT.with(|p| *p.borrow_mut() = prog);
4228 } else {
4229 SPCK_PAT.with(|p| *p.borrow_mut() = None); // c:3178
4230 }
4231 // c:3180-3185 — compile CORRECT_IGNORE_FILE pattern if set.
4232 if let Some(ci) = getsparam("CORRECT_IGNORE_FILE") {
4233 // c:3180
4234 let prog = patcompile(
4235 &{
4236 let mut __pat_tok = (&ci).to_string();
4237 crate::ported::glob::tokenize(&mut __pat_tok);
4238 __pat_tok
4239 },
4240 PAT_HEAPDUP,
4241 None,
4242 ); // c:3183
4243 SPCK_NAMEPAT.with(|p| *p.borrow_mut() = prog);
4244 } else {
4245 SPCK_NAMEPAT.with(|p| *p.borrow_mut() = None); // c:3185
4246 }
4247
4248 let bytes = s.as_bytes();
4249 let first = if bytes.is_empty() {
4250 '\0'
4251 } else {
4252 bytes[0] as char
4253 };
4254
4255 // c:3187-3193 — `**s == String && !*t`: $-prefixed name → scan paramtab.
4256 if first == StringTok && t_pos == bytes.len() {
4257 // c:3187
4258 // c:3188 — `guess = *s + 1;` strip leading $.
4259 let guess = s[1..].to_string();
4260 // c:3189-3190 — `if (itype_end(guess, INAMESPC, 1) == guess) return;`
4261 if itype_end(&guess, crate::ported::ztype_h::INAMESPC, true) == 0 {
4262 // c:3189
4263 return; // c:3190
4264 }
4265 ic = StringTok; // c:3191
4266 SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4267 SPCK_D.with(|c| c.set(100)); // c:3192
4268 if let Ok(t) = paramtab().read() {
4269 // c:3193 scanhashtable(paramtab, ..., spscan, ...)
4270 for k in t.keys() {
4271 spscan(k);
4272 }
4273 }
4274 // c:3194-3202 — `**s == Equals`: =cmd → hashcmd; then scan aliases+cmdnam.
4275 } else if first == Equals {
4276 // c:3194
4277 if t_pos != bytes.len() {
4278 // c:3195
4279 return; // c:3196
4280 }
4281 // c:3197-3198 — `if (hashcmd(guess = *s + 1, pathchecked)) return;`
4282 let guess = s[1..].to_string();
4283 let path: Vec<String> = getsparam("PATH")
4284 .map(|p| p.split(':').map(String::from).collect())
4285 .unwrap_or_default();
4286 let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4287 if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4288 return; // c:3198
4289 }
4290 SPCK_D.with(|c| c.set(100)); // c:3199
4291 ic = Equals; // c:3200
4292 SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess));
4293 if let Ok(t) = aliastab_lock().read() {
4294 // c:3201
4295 for (k, _) in t.iter() {
4296 spscan(k);
4297 }
4298 }
4299 if let Ok(t) = cmdnamtab_lock().read() {
4300 // c:3202
4301 for (k, _) in t.iter() {
4302 spscan(k);
4303 }
4304 }
4305 // c:3203-3248 — default branch: filename / dir spell-check.
4306 } else {
4307 // c:3203
4308 let mut guess = s.clone(); // c:3204
4309 // c:3205-3218 — Tilde / String inline-expand prefix handling.
4310 if !guess.is_empty()
4311 && ((guess.as_bytes()[0] as char) == Tilde
4312 || (guess.as_bytes()[0] as char) == StringTok)
4313 {
4314 // c:3205
4315 ic = guess.as_bytes()[0] as char; // c:3207
4316 if t_pos + 1 >= s.len() {
4317 // c:3208 — `if (!*++t) return;`
4318 return; // c:3209
4319 }
4320 // c:3210-3214 — `noerrs=2; singsub(&guess); noerrs = ne;`
4321 let saved_noerrs =
4322 crate::ported::exec::noerrs.load(std::sync::atomic::Ordering::Relaxed);
4323 crate::ported::exec::noerrs.store(2, std::sync::atomic::Ordering::Relaxed); // c:3212
4324 guess = crate::ported::subst::singsub(&guess); // c:3213
4325 crate::ported::exec::noerrs.store(saved_noerrs, std::sync::atomic::Ordering::Relaxed);
4326 if guess.is_empty() {
4327 return; // c:3216 `if (!guess) return;`
4328 }
4329 // c:3217 — `preflen = strlen(guess) - strlen(t);` t = original
4330 // s[t_pos..] (the post-slash remainder).
4331 let t_len = s.len() - t_pos;
4332 preflen = guess.len().saturating_sub(t_len);
4333 }
4334 // c:3219-3220 — `if (access(unmeta(guess), F_OK) == 0) return;`
4335 let cstr = match std::ffi::CString::new(unmeta(&guess).as_str()) {
4336 Ok(c) => c,
4337 Err(_) => return,
4338 };
4339 if unsafe { libc::access(cstr.as_ptr(), libc::F_OK) } == 0 {
4340 // c:3219
4341 return; // c:3220
4342 }
4343 // c:3221 — `best = spname(guess);`
4344 // The Rust spname has a signature-drift adaptation taking
4345 // `(name, dir)`; pass the parent dir extracted from guess.
4346 let path_obj = std::path::Path::new(&guess);
4347 let parent = path_obj
4348 .parent()
4349 .and_then(|p| p.to_str())
4350 .filter(|s| !s.is_empty())
4351 .unwrap_or(".");
4352 let basename = path_obj.file_name().and_then(|n| n.to_str()).unwrap_or("");
4353 let best = spname(basename, parent);
4354 SPCK_BEST.with(|b| *b.borrow_mut() = best);
4355 SPCK_GUESS.with(|g| *g.borrow_mut() = Some(guess.clone()));
4356 // c:3222-3247 — command-position default branch: hashcmd +
4357 // scan tables + autocd cdpath scan.
4358 if t_pos == s.len() && cmd != 0 {
4359 // c:3222
4360 // c:3223-3224 — hashcmd shortcut.
4361 let path: Vec<String> = getsparam("PATH")
4362 .map(|p| p.split(':').map(String::from).collect())
4363 .unwrap_or_default();
4364 let pc = pathchecked.load(std::sync::atomic::Ordering::Relaxed);
4365 if crate::ported::exec::hashcmd(&guess, &path[pc.min(path.len())..]).is_some() {
4366 return; // c:3224
4367 }
4368 SPCK_D.with(|c| c.set(100)); // c:3225
4369 // c:3226-3230 — scan reswd, alias, shfunc, builtin, cmdnam.
4370 if let Ok(t) = reswdtab_lock().read() {
4371 // c:3226
4372 for (k, _) in t.iter() {
4373 spscan(k);
4374 }
4375 }
4376 if let Ok(t) = aliastab_lock().read() {
4377 // c:3227
4378 for (k, _) in t.iter() {
4379 spscan(k);
4380 }
4381 }
4382 if let Ok(t) = shfunctab_lock().read() {
4383 // c:3228
4384 for (k, _) in t.iter() {
4385 spscan(k);
4386 }
4387 }
4388 // c:3229 — builtintab scan: BUILTINS is a static array.
4389 for b in BUILTINS.iter() {
4390 spscan(&b.node.nam);
4391 }
4392 if let Ok(t) = cmdnamtab_lock().read() {
4393 // c:3230
4394 for (k, _) in t.iter() {
4395 spscan(k);
4396 }
4397 }
4398 // c:3231-3247 — autocd $cdpath scan: for each cdpath
4399 // entry, find the closest filename match via mindist.
4400 // Strict `<` (not `<=` as in spscan) so a cdpath dir
4401 // wins only when STRICTLY better than the existing best
4402 // — preferring earlier cdpath dirs on ties.
4403 if autocd {
4404 // c:3232 — `if (cd_able_vars(unmeta(guess))) return;`
4405 let unmeta_guess = crate::ported::utils::unmeta(&guess);
4406 if crate::ported::builtin::cd_able_vars(&unmeta_guess).is_some() {
4407 return; // c:3233
4408 }
4409 // c:3234 — `for (pp = cdpath; *pp; pp++)`. Read CDPATH.
4410 let cdpath: Vec<String> = crate::ported::params::paramtab()
4411 .read()
4412 .ok()
4413 .and_then(|t| t.get("cdpath").and_then(|pm| pm.u_arr.clone()))
4414 .unwrap_or_default();
4415 let mut cur_d = SPCK_D.with(|c| c.get());
4416 let mut cur_best = SPCK_BEST.with(|b| b.borrow().clone());
4417 for pp in cdpath.iter() {
4418 // c:3235-3245 — `mindist(*pp, *s, bestcd, 1)`.
4419 if let Some((bestcd, thisdist)) = mindist(pp, &s) {
4420 // c:3239 — STRICT `<` (not `<=`) per C comment
4421 // at c:3236-3239.
4422 if (thisdist as i32) < cur_d {
4423 cur_best = Some(bestcd);
4424 cur_d = thisdist as i32;
4425 }
4426 }
4427 }
4428 SPCK_BEST.with(|b| *b.borrow_mut() = cur_best);
4429 SPCK_D.with(|c| c.set(cur_d));
4430 }
4431 }
4432 }
4433 // c:3250-3251 — `if (errflag) return;`
4434 if (errflag.load(std::sync::atomic::Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
4435 return; // c:3251
4436 }
4437 // c:3252 — `if (best && strlen(best) > 1 && strcmp(best, guess))`.
4438 let best = SPCK_BEST.with(|b| b.borrow().clone());
4439 let guess = SPCK_GUESS.with(|g| g.borrow().clone()).unwrap_or_default();
4440 let Some(mut best) = best else {
4441 return;
4442 };
4443 if best.len() <= 1 || best == guess {
4444 return;
4445 }
4446 // c:3253-3272 — assemble the prefixed/de-tokenized replacement.
4447 if ic != '\0' {
4448 // c:3254
4449 if preflen > 0 {
4450 // c:3256
4451 // c:3258-3259 — `if (strncmp(guess, best, preflen)) return;`
4452 if !best.starts_with(&guess[..preflen.min(guess.len())]) {
4453 return; // c:3259
4454 }
4455 // c:3261-3263 — `u = ...s[0..t-*s] + best[preflen..]`.
4456 let t_off = s.len() - (s.len() - t_pos);
4457 let mut u = String::with_capacity(t_off + best.len() - preflen + 1);
4458 u.push_str(&s[..t_off]);
4459 u.push_str(&best[preflen..]);
4460 best = u;
4461 } else {
4462 // c:3264 — `u = "\0" + best;` (prepend NUL placeholder).
4463 best = format!("\0{}", best);
4464 }
4465 // c:3269-3271 — `best = u; guess = *s; *guess = *best = ztokens[ic - Pound];`
4466 let pound = crate::ported::zsh_h::Pound as u8;
4467 let zt = crate::ported::lex::ztokens.as_bytes();
4468 let token_char = if (ic as u8) >= pound {
4469 let idx = (ic as u8 - pound) as usize;
4470 if idx < zt.len() {
4471 zt[idx] as char
4472 } else {
4473 ic
4474 }
4475 } else {
4476 ic
4477 };
4478 // Set first char of both `*s` (the original) and `best`.
4479 if !s.is_empty() {
4480 let mut sb = s.clone().into_bytes();
4481 sb[0] = token_char as u8;
4482 *s = String::from_utf8_lossy(&sb).into_owned();
4483 }
4484 if !best.is_empty() {
4485 let mut bb = best.into_bytes();
4486 bb[0] = token_char as u8;
4487 best = String::from_utf8_lossy(&bb).into_owned();
4488 }
4489 }
4490 // c:3273-3289 — interactive prompt (`ask`) or auto-accept.
4491 let x: char;
4492 if ask != 0 {
4493 // c:3273
4494 // WARNING — DIVERGENCE: `noquery()`, `shout`, `promptexpand`,
4495 // `zputs(stream)`, `zbeep`, `getquery("nyae", 0)` aren't yet
4496 // wired in zshrs (interactive ZLE prompt machinery). Default
4497 // to 'n' (decline) when ask=1 — preserves the C behavior of
4498 // declining when shout is NULL (c:3286-3287). Re-enable the
4499 // interactive flow when promptexpand/getquery land.
4500 x = 'n';
4501 } else {
4502 x = 'y'; // c:3289
4503 }
4504 // c:3290-3300 — apply chosen action.
4505 if x == 'y' {
4506 // c:3290
4507 *s = best; // c:3291 `*s = dupstring(best);`
4508 if hist != 0 {
4509 // c:3292
4510 // c:3293 — `hwrep(best);` (history rewrite). Stubbed: hist
4511 // rewrite plumbing isn't yet hooked into the lex caller.
4512 }
4513 } else if x == 'a' {
4514 // c:3294
4515 crate::ported::hist::histdone
4516 .fetch_or(HISTFLAG_NOEXEC, std::sync::atomic::Ordering::Relaxed); // c:3295
4517 } else if x == 'e' {
4518 // c:3296
4519 crate::ported::hist::histdone.fetch_or(
4520 HISTFLAG_NOEXEC | crate::ported::zsh_h::HISTFLAG_RECALL,
4521 std::sync::atomic::Ordering::Relaxed,
4522 ); // c:3297
4523 }
4524 // c:3299-3300 — `if (ic) **s = ic;` — restore prefix sigil.
4525 if ic != '\0' && !s.is_empty() {
4526 let mut sb = s.clone().into_bytes();
4527 sb[0] = ic as u8;
4528 *s = String::from_utf8_lossy(&sb).into_owned();
4529 }
4530}
4531
4532/// Port of `ztrftimebuf(int *bufsizeptr, int decr)` from `Src/utils.c:3312`.
4533///
4534/// ```c
4535/// static int ztrftimebuf(int *bufsizeptr, int decr) {
4536/// if (*bufsizeptr <= decr) return 1;
4537/// *bufsizeptr -= decr;
4538/// return 0;
4539/// }
4540/// ```
4541///
4542/// "Helper for ztrftime: try to fit decr more bytes (plus a NUL)
4543/// in the buffer, and a new string length to decrement from that.
4544/// Returns 0 if the new length fits, 1 otherwise."
4545///
4546/// Previous Rust port had wrong semantics — returned `needed.max(256)`
4547/// (a buffer-sizing helper) instead of the C "decrement-and-check"
4548/// semantics. New port matches C: takes &mut bufsize + decr,
4549/// returns i32 (0 = fit, 1 = doesn't fit).
4550pub fn ztrftimebuf(bufsizeptr: &mut i32, decr: i32) -> i32 {
4551 if *bufsizeptr <= decr {
4552 return 1;
4553 }
4554 *bufsizeptr -= decr;
4555 0
4556}
4557
4558/// Format time struct (from utils.c ztrftime)
4559/// Port of `ztrftime(char *buf, int bufsize, char *fmt, struct tm *tm, long nsec)` from `Src/utils.c:3337`.
4560/// WARNING: param names don't match C — Rust=(fmt, time) vs C=(buf, bufsize, fmt, tm, nsec)
4561// Rust idiom replacement: `SystemTime::duration_since(UNIX_EPOCH)`
4562// + libc::localtime covers the C tm-struct populate; the C source's
4563// 192-line body builds the fmt-walk by hand because C has no
4564// strftime extension support — Rust delegates to libc::strftime via
4565// the strftime crate equivalent inline.
4566/// `ztrftime` — see implementation.
4567///
4568/// `use_gmt` controls whether time fields are interpreted as UTC (true,
4569/// libc::gmtime) or local time (false, libc::localtime). C's ztrftime
4570/// takes a `struct tm *` produced by the caller via gmtime/localtime
4571/// (e.g. Src/Modules/stat.c:201 picks between them based on STF_GMT);
4572/// since the Rust signature takes `SystemTime` we hoist that choice into
4573/// this bool param.
4574pub fn ztrftime(fmt: &str, time: std::time::SystemTime, use_gmt: bool) -> String {
4575 // C takes `struct tm *` + nsec directly, so negative time_t
4576 // (pre-1970) flows through localtime untouched. SystemTime
4577 // pre-epoch surfaces as duration_since Err — recover the signed
4578 // seconds rather than collapsing to 0 (the old unwrap_or_default
4579 // pinned every pre-epoch timestamp to the epoch).
4580 let (secs, nsec): (i64, u64) = match time.duration_since(UNIX_EPOCH) {
4581 Ok(d) => (d.as_secs() as i64, d.subsec_nanos() as u64),
4582 Err(e) => {
4583 let d = e.duration();
4584 let s = -(d.as_secs() as i64);
4585 let n = d.subsec_nanos() as u64;
4586 // sub-second part rolls the seconds down one: -1.25s
4587 // before epoch is secs=-2, nsec=750ms in tm terms.
4588 if n > 0 {
4589 (s - 1, 1_000_000_000 - n)
4590 } else {
4591 (s, 0)
4592 }
4593 }
4594 };
4595
4596 #[cfg(unix)]
4597 unsafe {
4598 let tm = if use_gmt {
4599 libc::gmtime(&secs)
4600 } else {
4601 libc::localtime(&secs)
4602 };
4603 if tm.is_null() {
4604 return String::new();
4605 }
4606 let tm_ref = &*tm;
4607
4608 // c:3398-3406 — pre-pass: walk fmt and substitute zsh-specific
4609 // extensions BEFORE delegating to libc::strftime. The C source
4610 // handles these inline; Rust pre-rewrites them into literal
4611 // numbers so libc::strftime sees only standard specifiers.
4612 // %K = 24-hr clock, no leading zero (0-23)
4613 // %L = 12-hr clock, no leading zero (1-12)
4614 // %f = day of month, no leading zero (1-31)
4615 // %.N = fractional seconds, N digits (0-9)
4616 let mut preprocessed = String::with_capacity(fmt.len());
4617 let bytes = fmt.as_bytes();
4618 let mut i = 0;
4619 while i < bytes.len() {
4620 if bytes[i] == b'%' && i + 1 < bytes.len() {
4621 // c:3374-3384 — parse optional `N.` prefix (digit count
4622 // for the `%.` fractional-seconds specifier).
4623 let mut j = i + 1;
4624 while j < bytes.len() && bytes[j].is_ascii_digit() {
4625 j += 1;
4626 }
4627 let digs: u32 = if j > i + 1 {
4628 std::str::from_utf8(&bytes[i + 1..j])
4629 .unwrap_or("3")
4630 .parse()
4631 .unwrap_or(3)
4632 } else {
4633 3
4634 };
4635 // c:3408 — `switch (*fmt++)` examines the specifier
4636 // char (after the digit prefix).
4637 if j < bytes.len() && bytes[j] == b'.' {
4638 // c:3409 — fractional-seconds. Default 3 digits if
4639 // none specified (j == i+1).
4640 let digs = digs.min(9);
4641 let trunc_div: u64 = 10u64.pow(9 - digs);
4642 let val = nsec / trunc_div;
4643 preprocessed.push_str(&format!("{:0width$}", val, width = digs as usize));
4644 i = j + 1;
4645 continue;
4646 }
4647 let next = bytes[i + 1];
4648 // c:3445-3460 — %K / %L / %f extensions.
4649 match next {
4650 b'K' => {
4651 preprocessed.push_str(&format!("{}", tm_ref.tm_hour));
4652 i += 2;
4653 continue;
4654 }
4655 b'L' => {
4656 let mut h12 = tm_ref.tm_hour % 12;
4657 if h12 == 0 {
4658 h12 = 12;
4659 }
4660 preprocessed.push_str(&format!("{}", h12));
4661 i += 2;
4662 continue;
4663 }
4664 b'f' => {
4665 preprocessed.push_str(&format!("{}", tm_ref.tm_mday));
4666 i += 2;
4667 continue;
4668 }
4669 b'N' => {
4670 // c:3491-3495 — `%N`: nanoseconds, always 9 digits
4671 // (`sprintf(buf, "%09ld", nsec)`).
4672 preprocessed.push_str(&format!("{:09}", nsec));
4673 i += 2;
4674 continue;
4675 }
4676 _ => {}
4677 }
4678 }
4679 preprocessed.push(bytes[i] as char);
4680 i += 1;
4681 }
4682
4683 let mut buf = vec![0u8; 256];
4684 let c_fmt = CString::new(preprocessed).unwrap_or_default();
4685 let len = libc::strftime(
4686 buf.as_mut_ptr() as *mut libc::c_char,
4687 buf.len(),
4688 c_fmt.as_ptr(),
4689 tm,
4690 );
4691
4692 if len > 0 {
4693 buf.truncate(len);
4694 String::from_utf8_lossy(&buf).to_string()
4695 } else {
4696 String::new()
4697 }
4698 }
4699
4700 #[cfg(not(unix))]
4701 {
4702 let _ = (fmt, secs, nsec);
4703 String::new()
4704 }
4705}
4706
4707/// Join array with delimiter (from utils.c zjoin)
4708/// Port of `zjoin(char **arr, int delim, int heap)` from `Src/utils.c:3622`.
4709/// Joins `arr` with `delim` between elements. When `delim` is itself a meta
4710/// byte (`imeta(delim)` — NUL, `Meta`, `Marker`, or a token in the
4711/// Pound..Nularg range; see `inittyptab` c:4195-4201), C writes the delimiter
4712/// in *metafied* form (`Meta` byte then `delim ^ 32`, c:3634-3637) rather than
4713/// the raw byte (c:3639). The earlier port emitted the raw char always, which
4714/// was byte-wrong for meta delimiters — notably `zjoin(array, '\0', …)` (NUL is
4715/// imeta), where C emits `0x83 0x20`, not a raw `0x00`.
4716///
4717/// WARNING: param names don't match C — Rust=(arr, delim) vs C=(arr, delim, heap).
4718/// The `heap` arg is dropped (Rust `String` owns its storage). As with
4719/// [`metafy`], the `Meta` byte cannot live in valid UTF-8, so the final
4720/// `String` boundary uses the same `from_utf8`/lossy fallback — byte-exact for
4721/// every ASCII (non-meta) delimiter, lossy only on meta delimiters. Callers
4722/// needing byte-exact meta-delimited output should join at the byte level.
4723pub fn zjoin(arr: &[String], delim: char) -> String {
4724 // c:3622
4725 // c:3629-3630 — empty array → "".
4726 if arr.is_empty() {
4727 return String::new();
4728 }
4729 let db = delim as u32;
4730 // c:3634 — `imeta(delim)`. Only byte-valued delimiters can be meta.
4731 let delim_meta = db < 0x100 && imeta_byte(db as u8);
4732 let mut out: Vec<u8> = Vec::new();
4733 for (i, s) in arr.iter().enumerate() {
4734 if i != 0 {
4735 // The separator that C writes after every element then chops off
4736 // the trailing one (c:3641) is equivalent to interposing it.
4737 if delim_meta {
4738 out.push(Meta); // c:3635
4739 out.push((db as u8) ^ 32); // c:3636
4740 } else if db < 0x100 {
4741 out.push(db as u8); // c:3639 — raw single byte
4742 } else {
4743 // Non-byte char delimiter (no C analogue): emit its UTF-8.
4744 let mut buf = [0u8; 4];
4745 out.extend_from_slice(delim.encode_utf8(&mut buf).as_bytes());
4746 }
4747 }
4748 out.extend_from_slice(s.as_bytes()); // c:3633 — strucpy(&ptr, *s)
4749 }
4750 String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
4751}
4752
4753/// Port of `char **colonsplit(char *s, int uniq)` from
4754/// `Src/utils.c:3650`. Splits `s` on `:`; when `uniq` is set,
4755/// duplicate segments are dropped (linear scan against the
4756/// already-emitted prefix).
4757/// ```c
4758/// char **
4759/// colonsplit(char *s, int uniq)
4760/// {
4761/// int ct;
4762/// char *t, **ret, **ptr, **p;
4763/// for (t = s, ct = 0; *t; t++)
4764/// if (*t == ':') ct++;
4765/// ptr = ret = zalloc(sizeof(char *) * (ct + 2));
4766/// t = s;
4767/// do {
4768/// s = t;
4769/// for (; *t && *t != ':'; t++);
4770/// if (uniq)
4771/// for (p = ret; p < ptr; p++)
4772/// if (strlen(*p) == t - s && !strncmp(*p, s, t - s))
4773/// goto cont;
4774/// *ptr = zalloc((t - s) + 1);
4775/// ztrncpy(*ptr++, s, t - s);
4776/// cont: ;
4777/// } while (*t++);
4778/// *ptr = NULL;
4779/// return ret;
4780/// }
4781/// ```
4782pub fn colonsplit(s: &str, uniq: bool) -> Vec<String> {
4783 // c:3650
4784 // c:3655-3657 — count colons.
4785 let ct = s.matches(':').count(); // c:3655
4786 let mut ret: Vec<String> = Vec::with_capacity(ct + 2); // c:3658 zalloc((ct+2)*sizeof(char *))
4787
4788 // c:3661-3673 — do-while loop walking segments.
4789 let bytes = s.as_bytes();
4790 let mut t: usize = 0;
4791 loop {
4792 let seg_start = t; // c:3662 s = t
4793 // c:3664 — `for (; *t && *t != ':'; t++)`
4794 while t < bytes.len() && bytes[t] != b':' {
4795 t += 1;
4796 }
4797 let seg = &s[seg_start..t];
4798 // c:3665-3668 — uniq dedupe.
4799 if !uniq || !ret.iter().any(|p| p == seg) {
4800 // c:3665
4801 ret.push(seg.to_string()); // c:3670 zalloc + ztrncpy
4802 }
4803 // c:3673 — `while (*t++)` — break if at end, else step past colon.
4804 if t >= bytes.len() {
4805 break;
4806 } // c:3673
4807 t += 1; // c:3673 t++
4808 }
4809 // c:3674 — `*ptr = NULL;` — Rust Vec needs no sentinel.
4810 ret // c:3675
4811}
4812
4813/// Port of `skipwsep(char **s)` from `Src/utils.c:3680`.
4814///
4815/// Skip whitespace separators (`iwsep`-true bytes) at the start of
4816/// `s`. Returns `(remaining_str, count)` — `count` is the number of
4817/// bytes / chars skipped. C body:
4818///
4819/// ```c
4820/// while (*t && iwsep(*t == Meta ? t[1] ^ 32 : *t)) {
4821/// if (*t == Meta) t++;
4822/// t++;
4823/// i++;
4824/// }
4825/// ```
4826///
4827/// The C version honours Meta-encoded bytes (`Meta` followed by
4828/// `^32`-XOR'd byte). Rust port mirrors that byte-by-byte.
4829pub fn skipwsep(s: &str) -> (&str, usize) {
4830 let bytes = s.as_bytes();
4831 let mut i: usize = 0;
4832 let mut count: usize = 0;
4833 while i < bytes.len() {
4834 let b = if bytes[i] == Meta && i + 1 < bytes.len() {
4835 bytes[i + 1] ^ 32
4836 } else {
4837 bytes[i]
4838 };
4839 if !iwsep(b) {
4840 break;
4841 }
4842 if bytes[i] == Meta {
4843 i += 1;
4844 }
4845 i += 1;
4846 count += 1;
4847 }
4848 (&s[i..], count)
4849}
4850
4851/// Port of `spacesplit(char *s, int allownull, int heap, int quote)` from
4852/// `Src/utils.c:3711`. Splits `s` on `$IFS` via the `ISEP`/`IWSEP` char
4853/// classes (TYPTAB, kept in sync with `$IFS` by `inittyptab`, which
4854/// re-runs on every IFS set — params.rs:8691, c:4795), NOT on hardcoded
4855/// whitespace. zsh's word-splitting rules: runs of IFS-WHITESPACE
4856/// collapse and the leading/trailing ones yield real `""` fields (which
4857/// callers elide); each IFS-NON-WHITESPACE char delimits a field, and
4858/// consecutive ones yield `nulstring` (`Nularg`, `Src/subst.c:36`
4859/// `{Nularg,'\0'}`) empty fields, which survive and are later stripped to
4860/// `""` by remnulargs.
4861///
4862/// WARNING: param names don't match C — Rust=(s, allownull) vs C=(s,
4863/// allownull, heap, quote). The `quote` arm (backslash-escaped seps) and
4864/// `heap` C-buffer param drop: all zshrs callers pass quote=0, and Rust
4865/// owns its String storage. The previous port split only on hardcoded
4866/// `[' ','\t','\n']`, ignoring `$IFS` entirely (Bug #636).
4867pub fn spacesplit(s: &str, allownull: bool) -> Vec<String> {
4868 // c:3711
4869 use crate::ported::ztype_h::zistype;
4870 let bytes = s.as_bytes();
4871 // Meta-aware decode of the logical char value + its byte length at a
4872 // byte position (mirrors skipwsep's `*x == Meta ? x[1] ^ 32 : *x`).
4873 let char_at = |i: usize| -> (u8, usize) {
4874 // c:Src/zsh.h Meta — `\u{83}` prefix; next byte XOR 0x20.
4875 if bytes[i] == Meta && i + 1 < bytes.len() {
4876 (bytes[i + 1] ^ 32, 2)
4877 } else {
4878 (bytes[i], 1)
4879 }
4880 };
4881 // skipwsep(&s) — advance past a run of IWSEP (IFS-whitespace). c:3730
4882 let skipwsep_at = |mut i: usize| -> usize {
4883 while i < bytes.len() {
4884 let (c, l) = char_at(i);
4885 if !iwsep(c) {
4886 break;
4887 }
4888 i += l;
4889 }
4890 i
4891 };
4892 // itype_end(s, ISEP, 1) — `once` advances past exactly ONE ISEP char
4893 // if present, else leaves the index unchanged (c:4462 `if (once) break`).
4894 let isep_one = |i: usize| -> usize {
4895 if i < bytes.len() {
4896 let (c, l) = char_at(i);
4897 if zistype(c, ISEP as u32) {
4898 return i + l;
4899 }
4900 }
4901 i
4902 };
4903 // findsep(&s, NULL, 0) — advance to the next ISEP char. c:3784
4904 let findsep_at = |mut i: usize| -> usize {
4905 while i < bytes.len() {
4906 let (c, l) = char_at(i);
4907 if zistype(c, ISEP as u32) {
4908 break;
4909 }
4910 i += l;
4911 }
4912 i
4913 };
4914 let nulstring = || Nularg.to_string(); // c:subst.c:36 nulstring = {Nularg,0}
4915
4916 let mut ret: Vec<String> = Vec::new();
4917 let mut si = 0usize;
4918 let mut t = si; // c:3729 — `t = s;`
4919 si = skipwsep_at(si); // c:3730 — `skipwsep(&s);`
4920 // c:3732-3735 — leading-field handling.
4921 if si < bytes.len() && isep_one(si) != si {
4922 // c:3733 — at an IFS-non-whitespace sep after skipping whitespace.
4923 ret.push(if allownull {
4924 String::new()
4925 } else {
4926 nulstring()
4927 });
4928 } else if !allownull && t != si {
4929 // c:3734-3735 — leading IFS-whitespace was skipped → real "".
4930 ret.push(String::new());
4931 }
4932 // c:3736-3756 — main word loop.
4933 while si < bytes.len() {
4934 let iend = isep_one(si); // c:3737 itype_end(s, ISEP, 1)
4935 if iend != si {
4936 // c:3738-3741 — consume the single non-ws sep + following WS.
4937 si = iend;
4938 si = skipwsep_at(si);
4939 }
4940 t = si; // c:3746 — `t = s;`
4941 si = findsep_at(si); // c:3747 — `findsep(&s, NULL, quote);`
4942 if si > t || allownull {
4943 // c:3748-3751 — emit the word `t..si`.
4944 ret.push(s[t..si].to_string());
4945 } else {
4946 // c:3753 — empty field between non-ws seps → nulstring.
4947 ret.push(nulstring());
4948 }
4949 t = si; // c:3754 — `t = s;`
4950 si = skipwsep_at(si); // c:3755 — `skipwsep(&s);`
4951 }
4952 // c:3757 — trailing IFS-whitespace → real "".
4953 if !allownull && t != si {
4954 ret.push(String::new());
4955 }
4956 ret
4957}
4958
4959/// Port of `findsep(char **s, char *sep, int quote)` from `Src/utils.c:3784`.
4960///
4961/// c:3762 — "Find a separator. Return 0 if already at separator, 1 if
4962/// separator found later, else -1." `pos` is C's walking `*s`: on
4963/// return it points at the separator (or end-of-string) so the caller
4964/// reads the word as `s[entry_pos..pos]`.
4965///
4966/// `sep` (c:3772):
4967/// - `None` → split on normal `$IFS` separators (ISEP chars).
4968/// - `Some(b"")` → no real separator: advance past one character.
4969/// - `Some(seq)` → look for the (possibly multi-byte) literal `seq`.
4970///
4971/// `quote` (c:3776) — a `\` before a separator suppresses it, and `\\`
4972/// collapses to `\`. This only applies when `sep` is `None` and it
4973/// strips backslashes in place, so the buffer must be modifiable
4974/// (C demands "something modifiable"; Rust takes `&mut String`).
4975///
4976/// The standalone form has no in-tree caller yet — the hot split paths
4977/// (`splitstring`, `findword`, `wordcount`) inline this logic — but it
4978/// is kept as a faithful, callable name-parity anchor.
4979/// WARNING: param names match C semantics — `pos` is C's `*s`.
4980pub fn findsep(s: &mut String, pos: &mut usize, sep: Option<&[u8]>, quote: bool) -> i32 {
4981 // c:3784
4982 use crate::ported::zsh_h::{MB_METACHARINIT, MB_METACHARLEN, MB_METACHARLENCONV};
4983 use crate::ported::ztype_h::{isep, ISEP, WC_ZISTYPE};
4984
4985 MB_METACHARINIT(); // c:3792
4986 let is_isep = |c: Option<char>| c.map(|c| WC_ZISTYPE(c, ISEP as u32)).unwrap_or(false);
4987
4988 match sep {
4989 // c:3793-3824 — default separators (ISEP), with optional quoting.
4990 None => {
4991 let start = *pos;
4992 let mut t = *pos;
4993 while t < s.len() {
4994 let b = s.as_bytes()[t];
4995 if quote && b == b'\\' {
4996 // c:3795 — `if (quote && *t == '\\')`
4997 if t + 1 < s.len() && s.as_bytes()[t + 1] == b'\\' {
4998 // c:3796-3799 — `\\` → `\`: drop one backslash,
4999 // advance past the one we keep (ilen = 1).
5000 chuck(s, t);
5001 t += 1;
5002 continue;
5003 }
5004 // c:3801 — measure the char *after* the backslash.
5005 let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t + 1..]);
5006 if is_isep(c) {
5007 // c:3802-3804 — escaped separator: strip the
5008 // backslash, then advance over the now-bare char.
5009 chuck(s, t);
5010 t += ilen.max(1);
5011 continue;
5012 }
5013 // c:3806-3810 — backslash is a normal byte.
5014 if isep(b) {
5015 break; // (never taken: '\\' is not an ISEP)
5016 }
5017 t += 1; // ilen = 1
5018 } else {
5019 // c:3814-3818 — ordinary character.
5020 let (ilen, c) = MB_METACHARLENCONV(&s.as_bytes()[t..]);
5021 if is_isep(c) {
5022 break; // c:3817
5023 }
5024 t += ilen.max(1);
5025 }
5026 }
5027 let i = if t > start { 1 } else { 0 }; // c:3821 `i = (t > *s)`
5028 *pos = t; // c:3822
5029 i // c:3823
5030 }
5031 // c:3825-3834 — empty separator: advance past the first char.
5032 Some(seq) if seq.is_empty() => {
5033 if *pos < s.len() {
5034 *pos += MB_METACHARLEN(&s.as_bytes()[*pos..]); // c:3831
5035 1 // c:3832
5036 } else {
5037 -1 // c:3833
5038 }
5039 }
5040 // c:3835-3845 — explicit (possibly multi-byte) literal separator.
5041 Some(seq) => {
5042 let mut i = 0i32;
5043 while *pos < s.len() {
5044 // c:3840 — `for (t=sep, tt=*s; *t && *tt && *t==*tt; ...)`
5045 let bytes = s.as_bytes();
5046 let mut k = 0usize;
5047 while k < seq.len() && *pos + k < bytes.len() && seq[k] == bytes[*pos + k] {
5048 k += 1;
5049 }
5050 if k == seq.len() {
5051 return if i > 0 { 1 } else { 0 }; // c:3841 `return (i > 0)`
5052 }
5053 *pos += MB_METACHARLEN(&bytes[*pos..]); // c:3842
5054 i += 1;
5055 }
5056 -1 // c:3844
5057 }
5058 }
5059}
5060
5061/// Find word at position (from utils.c findword)
5062/// Port of `findword(char **s, char *sep)` from `Src/utils.c:3849`.
5063pub fn findword<'a>(s: &'a str, sep: Option<&'a str>) -> Option<(&'a str, &'a str)> {
5064 let s = match sep {
5065 Some(_) => s,
5066 None => s.trim_start(),
5067 };
5068 if s.is_empty() {
5069 return None;
5070 }
5071 match sep {
5072 Some(sep) => {
5073 if let Some(pos) = s.find(sep) {
5074 Some((&s[..pos], &s[pos + sep.len()..]))
5075 } else {
5076 Some((s, ""))
5077 }
5078 }
5079 None => {
5080 let end = s.find(|c: char| c.is_ascii_whitespace()).unwrap_or(s.len());
5081 Some((&s[..end], &s[end..]))
5082 }
5083 }
5084}
5085
5086/// Port of `wordcount(char *s, char *sep, int mul)` from `Src/utils.c:3879`.
5087///
5088/// Returns the number of words in `s` that would result from splitting
5089/// on `sep` (or `$IFS` when `sep` is None). `mul` controls how
5090/// consecutive empty fields are counted:
5091/// - `mul == 0`: don't count leading/trailing/consecutive empties.
5092/// - `mul > 0`: count consecutive empties (each `sep` boundary
5093/// produces one word, even if the surrounding text is empty).
5094/// - `mul < 0`: count empty trailing fields (final separator after
5095/// the last non-empty field counts as one extra empty word).
5096///
5097/// C body (paraphrased):
5098/// ```c
5099/// if (sep) {
5100/// r = 1;
5101/// sl = strlen(sep);
5102/// for (; (c = findsep(&s, sep, 0)) >= 0; s += sl)
5103/// if ((c || mul) && (sl || *(s + sl)))
5104/// r++;
5105/// } else {
5106/// /* IFS-based: walk skipwsep / itype_end(s, ISEP, 1) */
5107/// }
5108/// ```
5109///
5110/// This port walks the metafied byte stream directly to mirror C's
5111/// pointer arithmetic. The `sep`-based branch is exact; the IFS
5112/// branch uses [`iwsep`] for whitespace-separator detection (C's
5113/// `ISEP` char class collapsed to whitespace, which is the common
5114/// case for default `$IFS` = `" \t\n"`).
5115pub fn wordcount(s: &str, sep: Option<&str>, mul: i32) -> i32 {
5116 // c:3879
5117 let bytes = s.as_bytes();
5118 if let Some(sep) = sep {
5119 // C: r = 1; sl = strlen(sep); for (; findsep(&s,sep,0) >= 0; s+=sl)
5120 // if ((c || mul) && (sl || *(s+sl))) r++;
5121 let sep_bytes = sep.as_bytes();
5122 let sl = sep_bytes.len();
5123 let mut r: i32 = 1;
5124 let mut pos = 0;
5125 while pos <= bytes.len() {
5126 let rest = &bytes[pos..];
5127 let c_offset = match sep_bytes.is_empty() {
5128 true => Some(0usize),
5129 false => rest.windows(sl).position(|w| w == sep_bytes),
5130 };
5131 let Some(c) = c_offset else { break };
5132 // C `c` is the chars before the separator; `(sl || *(s+sl))`
5133 // means: if sl is zero (empty sep), only count when there's a
5134 // following char. Otherwise (sl > 0), the second clause is true
5135 // when sep is non-empty AND there are bytes after sep.
5136 let after_off = pos + c + sl;
5137 let following_nonempty = after_off < bytes.len();
5138 let cond_b = sl != 0 || following_nonempty;
5139 if (c != 0 || mul != 0) && cond_b {
5140 r += 1;
5141 }
5142 if sl == 0 {
5143 // Avoid infinite loop on empty sep — mirrors C's findsep
5144 // which advances by 1 byte when sep is empty.
5145 pos += 1;
5146 } else {
5147 pos += c + sl;
5148 }
5149 }
5150 r
5151 } else {
5152 // IFS branch (sep == NULL). C source uses itype_end(s, ISEP, 1)
5153 // to skip ISEP chars (default $IFS = " \t\n"). We use iwsep.
5154 let mut s_pos = 0usize;
5155 let t_orig = s_pos;
5156 let mut r: i32 = 0;
5157 // C: if (mul <= 0) skipwsep(&s);
5158 if mul <= 0 {
5159 while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5160 s_pos += 1;
5161 }
5162 }
5163 // C: if ((*s && itype_end(s,ISEP,1)!=s) || (mul<0 && t!=s)) r++;
5164 let has_word_now = s_pos < bytes.len() && !iwsep(bytes[s_pos]);
5165 if has_word_now || (mul < 0 && t_orig != s_pos) {
5166 r += 1;
5167 }
5168 // C: for (; *s; r++) { advance over word + maybe-skipwsep + findsep + maybe-skipwsep }
5169 while s_pos < bytes.len() {
5170 // Advance past the current word (non-ISEP chars).
5171 let word_start = s_pos;
5172 while s_pos < bytes.len() && !iwsep(bytes[s_pos]) {
5173 s_pos += 1;
5174 }
5175 if s_pos > word_start && mul <= 0 {
5176 while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5177 s_pos += 1;
5178 }
5179 }
5180 // C: (void)findsep(&s, NULL, 0) — advance past one sep run.
5181 // Already handled above when mul<=0; for mul>0 we still need
5182 // to consume one separator byte to make progress.
5183 if s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5184 s_pos += 1;
5185 }
5186 let t_after = s_pos;
5187 if mul <= 0 {
5188 while s_pos < bytes.len() && iwsep(bytes[s_pos]) {
5189 s_pos += 1;
5190 }
5191 }
5192 if s_pos < bytes.len() {
5193 r += 1;
5194 } else {
5195 // C: if (mul < 0 && t != s) r++;
5196 if mul < 0 && t_after != s_pos {
5197 r += 1;
5198 }
5199 break;
5200 }
5201 }
5202 r
5203 }
5204}
5205
5206/// Join array with separator - port from zsh/Src/utils.c sepjoin() lines 3926-3958
5207///
5208/// If sep is None, uses first char of IFS (defaults to space).
5209/// Join an array with separator.
5210/// Port of `sepjoin(char **s, char *sep, int heap)` from Src/utils.c:3928.
5211/// WARNING: param names don't match C — Rust=(arr, sep) vs C=(s, sep, heap)
5212// Rust idiom replacement: `slice::join` covers the C `zalloc`+`strcpy`
5213// loop with running length pre-compute; the `heap` arg drops since
5214// String owns its own allocation.
5215/// `sepjoin` — see implementation.
5216pub fn sepjoin(arr: &[String], sep: Option<&str>) -> String {
5217 // c:3928
5218 // c:3934-3935 — if (!*s) return heap ? dupstring("") : ztrdup("");
5219 if arr.is_empty() {
5220 return String::new();
5221 }
5222 // c:3936-3945 — `if (!sep)` default-sep arm:
5223 // if (ifs && *ifs != ' ') sep = dupstrpfx(ifs, MB_METACHARLEN(ifs));
5224 // else sep = " ";
5225 // IFS SET-but-EMPTY takes the first branch (`*ifs` is '\0' != ' ')
5226 // and dupstrpfx("", …) yields an EMPTY separator — `"$*"` with
5227 // IFS="" concatenates. Only IFS UNSET (ifs == NULL → getsparam
5228 // None) or starting with a space falls back to " ".
5229 let ifs_storage: String;
5230 let sep_str: &str = match sep {
5231 Some(s) => s, // c:3936
5232 None => {
5233 ifs_storage = match getsparam("IFS") {
5234 // c:3938-3940 — set, first char not space (empty → "").
5235 Some(ifs) if !ifs.starts_with(' ') => ifs
5236 .chars()
5237 .next()
5238 .map(|c| c.to_string())
5239 .unwrap_or_default(),
5240 // c:3941-3944 — unset (NULL) or leading space → " ".
5241 _ => " ".to_string(),
5242 };
5243 &ifs_storage
5244 }
5245 };
5246 // c:3947-3956 — pre-compute total length, alloc, copy elements
5247 // interleaving sep. Rust slice::join collapses all that
5248 // into the one canonical call.
5249 arr.join(sep_str)
5250}
5251
5252/// Split string by separator - port from zsh/Src/utils.c sepsplit() lines 3961-3992
5253///
5254/// If sep is None, performs IFS-style word splitting (spacesplit).
5255/// Otherwise splits on the given separator string.
5256/// allownull: if true, allows empty strings in result
5257/// Split a string on `IFS` separators.
5258/// Port of `sepsplit(char *s, char *sep, int allownull, int heap)` from Src/utils.c:3962.
5259/// WARNING: param names don't match C — Rust=(s, sep, allownull) vs C=(s, sep, allownull, heap)
5260pub fn sepsplit(s: &str, sep: Option<&str>, allownull: bool) -> Vec<String> {
5261 // c:3962
5262 // Handle Nularg at start (zsh internal marker) - line 3968
5263 let s = if s.starts_with('\x00') && s.len() > 1 {
5264 &s[1..]
5265 } else {
5266 s
5267 };
5268
5269 match sep {
5270 None => spacesplit(s, allownull),
5271 Some("") => {
5272 // Empty separator: split into characters
5273 if allownull {
5274 s.chars().map(|c| c.to_string()).collect()
5275 } else {
5276 s.chars()
5277 .map(|c| c.to_string())
5278 .filter(|c| !c.is_empty())
5279 .collect()
5280 }
5281 }
5282 Some(sep) => {
5283 let parts: Vec<String> = s.split(sep).map(|p| p.to_string()).collect();
5284 if allownull {
5285 parts
5286 } else {
5287 parts.into_iter().filter(|p| !p.is_empty()).collect()
5288 }
5289 }
5290 }
5291}
5292
5293/// Port of `getshfunc(char *nam)` from `Src/utils.c:3998`.
5294///
5295/// C body:
5296/// ```c
5297/// Shfunc getshfunc(char *nam) {
5298/// return (Shfunc) shfunctab->getnode(shfunctab, nam);
5299/// }
5300/// ```
5301///
5302/// Routes through the global `shfunctab` singleton in
5303/// hashtable.rs (hashtable::shfunctab_lock). Returns an owned
5304/// `shfunc` clone so callers can read `flags` (to detect
5305/// `PM_UNDEFINED` autoload stubs), `funcdef`, `filename`, and
5306/// `body` without holding the table lock. Owned clone vs C's
5307/// `*Shfunc` raw pointer trades one allocation for Rust
5308/// lifetime safety — `getshfunc` is a function-lookup site,
5309/// not per-statement, so the cost is irrelevant.
5310///
5311/// Returning `Option<String>` of just `body` (the old contract)
5312/// made every PM_UNDEFINED autoload stub invisible because
5313/// `body=None` collapsed via `and_then` to `None`, which callers
5314/// then read as "function doesn't exist."
5315pub fn getshfunc(nam: &str) -> Option<shfunc> {
5316 let tab = shfunctab_lock().read().expect("shfunctab poisoned");
5317 tab.get(nam).cloned()
5318}
5319
5320/// Port of `char **subst_string_by_func(Shfunc func, char *arg1, char *orig)`
5321/// from Src/utils.c:4017.
5322///
5323/// Calls the named shell function with `[func, arg1?, orig]` as
5324/// positional args under `sfcontext = SFC_SUBST` and returns the
5325/// `$reply` array on success. Routes through `callhookfunc` (the
5326/// static-linked equivalent of `doshfunc`), then reads `$reply`
5327/// from the env-var fallback because the global `paramtab` is not
5328/// yet a singleton in the Rust port (params::getaparam takes a
5329/// `&ParamTable` arg).
5330pub fn subst_string_by_func(
5331 func_name: &str,
5332 arg1: Option<&str>,
5333 orig: &str,
5334) -> Option<Vec<String>> // c:4017
5335{
5336 let osc = SFCONTEXT.load(Ordering::Relaxed); // c:4019
5337 let osm = STOPMSG.load(Ordering::Relaxed);
5338 let old_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
5339 let mut args: Vec<String> = Vec::with_capacity(3); // c:4020-4026
5340 args.push(func_name.to_string()); // c:4023
5341 if let Some(a) = arg1 {
5342 // c:4024
5343 args.push(a.to_string()); // c:4025
5344 }
5345 args.push(orig.to_string()); // c:4026
5346 SFCONTEXT // c:4027
5347 .store(SFC_SUBST, Ordering::Relaxed);
5348 INCOMPFUNC.store(0, Ordering::Relaxed); // c:4028
5349
5350 // c:4030 — `if (doshfunc(func, l, 1))`. Direct doshfunc call
5351 // mirrors C. body_runner routes through the host body-only entry
5352 // (matching the same shfunc that `callhookfunc` would resolve).
5353 let shf_clone: Option<crate::ported::zsh_h::shfunc> = shfunctab_lock()
5354 .read()
5355 .ok()
5356 .and_then(|t| t.get(func_name).cloned());
5357 let rc = if let Some(mut shf) = shf_clone {
5358 let name_for_body = func_name.to_string();
5359 let body_args = args.clone();
5360 let body_runner = move || -> i32 {
5361 crate::ported::exec::run_function_body(&name_for_body, &body_args[1..]).unwrap_or(0)
5362 };
5363 crate::ported::exec::doshfunc(&mut shf, args.clone(), true, body_runner)
5364 } else {
5365 callhookfunc(func_name, Some(&args), 0, std::ptr::null_mut())
5366 };
5367 // c:4033 — `ret = getaparam("reply")` against paramtab. `reply`
5368 // is a shell-local PM_ARRAY entry, never exported to env.
5369 let ret: Option<Vec<String>> = if rc != 0 {
5370 None // c:4031
5371 } else {
5372 getaparam("reply") // c:4033
5373 };
5374
5375 SFCONTEXT.store(osc, Ordering::Relaxed); // c:4035
5376 STOPMSG.store(osm, Ordering::Relaxed); // c:4036
5377 INCOMPFUNC.store(old_incompfunc, Ordering::Relaxed); // c:4037
5378 ret // c:4038
5379}
5380
5381/// Port of `char **subst_string_by_hook(char *name, char *arg1, char *orig)`
5382/// from Src/utils.c:4049.
5383///
5384/// Looks up `name` as a shell function and calls
5385/// `subst_string_by_func` on it. If that returns no result, walks
5386/// `${name}_hook` as an array of function names, trying each in
5387/// order until one yields a `$reply`.
5388pub fn subst_string_by_hook(name: &str, arg1: Option<&str>, orig: &str) -> Option<Vec<String>> // c:4049
5389{
5390 let mut ret: Option<Vec<String>> = None;
5391 if getshfunc(name).is_some() {
5392 // c:4054
5393 ret = subst_string_by_func(name, arg1, orig); // c:4055
5394 }
5395 if ret.is_none() {
5396 // c:4058
5397 let arrnam = format!("{}_hook", name); // c:4061-4063
5398 // c:4065 — `arr = getaparam(arrnam)`. The previous Rust port
5399 // read `env::var(arrnam)` and NUL-split — wrong: the hook
5400 // array is a shell-local PM_ARRAY in paramtab, not env.
5401 if let Some(arr) = getaparam(&arrnam) {
5402 // c:4065
5403 for f in arr.iter() {
5404 // c:4068
5405 if f.is_empty() {
5406 continue;
5407 }
5408 if getshfunc(f).is_some() {
5409 // c:4069
5410 ret = subst_string_by_func(f, arg1, orig); // c:4070
5411 if ret.is_some() {
5412 // c:4071
5413 break; // c:4072
5414 }
5415 }
5416 }
5417 }
5418 }
5419 ret // c:4094
5420}
5421
5422/// Make single-element array (from utils.c mkarray)
5423/// Port of `mkarray(char *s)` from `Src/utils.c:4083`.
5424pub fn mkarray(s: Option<&str>) -> Vec<String> {
5425 match s {
5426 Some(val) => vec![val.to_string()],
5427 None => Vec::new(),
5428 }
5429}
5430
5431/// Make single-element array on heap (from utils.c hmkarray)
5432/// Port of `hmkarray(char *s)` from `Src/utils.c:4094`.
5433pub fn hmkarray(s: &str) -> Vec<String> {
5434 if s.is_empty() {
5435 Vec::new()
5436 } else {
5437 vec![s.to_string()]
5438 }
5439}
5440
5441/// Port of `void zbeep(void)` from Src/utils.c:4105.
5442///
5443/// Honours `$ZBEEP` (a key-string sequence) when set and the BEEP
5444/// option when unset; emits the BEL char (\007) to SHTTY by
5445/// default. The Rust port writes via `write_loop` to mirror C's
5446/// raw-write semantics.
5447pub fn zbeep() {
5448 // c:4105
5449 queue_signals(); // c:4105
5450 if let Ok(zbeep) = std::env::var("ZBEEP") {
5451 // c:4109
5452 let (decoded, _) = getkeystring(&zbeep); // c:4111
5453 #[cfg(unix)]
5454 {
5455 let shtty = SHTTY.load(Ordering::Relaxed);
5456 if shtty != -1 {
5457 let _ = write_loop(shtty, decoded.as_bytes()); // c:4112
5458 } else {
5459 eprint!("{}", decoded);
5460 }
5461 }
5462 #[cfg(not(unix))]
5463 eprint!("{}", decoded);
5464 } else if isset(BEEP) {
5465 // c:4113
5466 #[cfg(unix)]
5467 {
5468 let shtty = SHTTY.load(Ordering::Relaxed);
5469 if shtty != -1 {
5470 let _ = write_loop(shtty, b"\x07"); // c:4114
5471 } else {
5472 eprint!("\x07");
5473 }
5474 }
5475 #[cfg(not(unix))]
5476 eprint!("\x07");
5477 }
5478 unqueue_signals(); // c:4115
5479}
5480
5481/// Free array (no-op in Rust, provided for API compat)
5482/// Port of `freearray(char **s)` from `Src/utils.c:4120`.
5483pub fn freearray(s: Vec<String>) {
5484 // c:4124 — DPUTS(!s, "freearray() with zero argument")
5485 // Rust takes Vec<String> by value (never null), so the C !s
5486 // check maps to no condition that can fire in Rust. Document
5487 // the gap.
5488 let _ = &s; // c:4124 (no-op; Vec is never NULL in Rust)
5489 // Rust Drop handles this
5490}
5491
5492/// Split on '=' returning (name, value) (from utils.c equalsplit)
5493/// Port of `equalsplit(char *s, char **t)` from `Src/utils.c:4133`.
5494/// WARNING: param names don't match C — Rust=(s) vs C=(s, t)
5495pub fn equalsplit(s: &str) -> Option<(String, String)> {
5496 let eq = s.find('=')?;
5497 Some((s[..eq].to_string(), s[eq + 1..].to_string()))
5498}
5499
5500// initialize the ztypes table // c:4151
5501/// Port of `inittyptab()` from `Src/utils.c:4155`. Initialise the
5502/// `typtab[256]` lookup table that backs the `idigit`/`ialnum`/etc.
5503/// predicates in `ztype_h`.
5504///
5505/// C body (c:4155-4250) does:
5506/// 1. Zero the table.
5507/// 2. Mark 0..=31 and 128..=159 + 127 as ICNTRL.
5508/// 3. Mark '0'..='9' as IDIGIT|IALNUM|IWORD|IIDENT|IUSER.
5509/// 4. Mark 'a'..='z' and 'A'..='Z' as IALPHA|IALNUM|IIDENT|IUSER|IWORD.
5510/// 5. Mark '_' as IIDENT|IUSER.
5511/// 6. Mark '-', '.', Dash as IUSER.
5512/// 7. Mark ' '/'\t' as IBLANK|INBLANK; '\n' as INBLANK.
5513/// 8. Mark '\0', Meta, Marker as IMETA.
5514/// 9. Mark Pound..LAST_NORMAL_TOK as ITOK|IMETA.
5515/// 10. Mark Snull..Nularg as ITOK|IMETA|INULL.
5516/// 11. Walk $IFS adding ISEP and IWSEP for blanks.
5517///
5518/// This first-pass port covers steps 1-7. Steps 8-11 require Meta /
5519/// Marker / Pound / Snull / Nularg constants from zsh_h/zsh.h and
5520/// the `ifs` global; the remaining Meta/IFS marks are skipped until
5521/// those land. Idempotent — safe to call multiple times.
5522pub fn inittyptab() {
5523 // utils.c:4155
5524
5525 // c:4160 — `if (!(typtab_flags & ZTF_INIT))` one-off init.
5526 {
5527 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5528 if (flags & ZTF_INIT) == 0 {
5529 TYPTAB_FLAGS.store(ZTF_INIT, Ordering::Relaxed);
5530 }
5531 }
5532
5533 // Local scratch, flushed to the atomic table at fn end — C mutates
5534 // typtab[] in place unlocked; the atomic store-per-slot flush keeps
5535 // readers lock-free (see TYPTAB doc in ztype_h.rs).
5536 let mut t = [0u32; 256]; // c:4168 memset(typtab, 0, sizeof(typtab))
5537 // c:4169-4170 — control chars 0..32 and 128..160.
5538 for c in 0..32u32 {
5539 t[c as usize] = ICNTRL as u32;
5540 t[(c + 128) as usize] = ICNTRL as u32;
5541 }
5542 // c:4171 — `typtab[127] = ICNTRL;`
5543 t[127] = ICNTRL as u32;
5544 // c:4172-4173 — '0'..='9'.
5545 for c in (b'0' as usize)..=(b'9' as usize) {
5546 t[c] = (IDIGIT | IALNUM | IWORD | IIDENT | IUSER) as u32;
5547 }
5548 // c:4174-4175 — 'a'..='z' and matching 'A'..='Z'.
5549 for c in (b'a' as usize)..=(b'z' as usize) {
5550 let upper = c - (b'a' as usize) + (b'A' as usize);
5551 let bits = (IALPHA | IALNUM | IIDENT | IUSER | IWORD) as u32;
5552 t[c] = bits;
5553 t[upper] = bits;
5554 }
5555 // c:4190 — `typtab['_'] = IIDENT | IUSER;`
5556 t[b'_' as usize] = (IIDENT | IUSER) as u32;
5557 // c:4191 — `typtab['-'] = typtab['.'] = typtab[(unsigned char) Dash] = IUSER;`.
5558 t[b'-' as usize] = IUSER as u32;
5559 t[b'.' as usize] = IUSER as u32;
5560 // c:4191 — `Dash` token marker (0x9b per zsh.h:182, "Only in patterns").
5561 // Marking it IUSER lets pattern-side $-named-character paths
5562 // accept it as a user-name byte. Previously omitted in the port.
5563 t[crate::ported::zsh_h::Dash as usize] = IUSER as u32;
5564 // c:4192-4194 — blanks.
5565 t[b' ' as usize] |= (IBLANK | INBLANK) as u32;
5566 t[b'\t' as usize] |= (IBLANK | INBLANK) as u32;
5567 t[b'\n' as usize] |= INBLANK as u32;
5568
5569 // c:4195 — `typtab['\0'] |= IMETA;`. Previously omitted in the
5570 // Rust port — '\0' had only ICNTRL (set by the 0..32 loop at
5571 // c:4169) and was missing IMETA. C `imeta('\0')` is true (the
5572 // NUL byte must be Meta-encoded as `Meta + ('\0' ^ 32)`); the
5573 // Rust `imeta()` typtab predicate returned false, so any code
5574 // routing through `imeta(c)` (e.g. `input::shingetline`)
5575 // failed to Meta-encode NUL bytes from stdin, corrupting the
5576 // SHIN buffer when piped binary data hit a `\0`.
5577 t[0] |= IMETA as u32; // c:4195
5578 // c:4196-4197 — Meta + Marker marked IMETA.
5579 {
5580 t[Meta as usize] |= IMETA as u32;
5581 t[Marker as usize] |= IMETA as u32;
5582 }
5583
5584 // c:4133-4134 — `for (t0 = Pound; t0 <= LAST_NORMAL_TOK; t0++)
5585 // typtab[t0] |= ITOK | IMETA;`
5586 // Marks all char-rewrite token markers (Pound, Stringg, Hat,
5587 // Star, ...). Without this, `itok(Stringg)` returns false and
5588 // `is_valid_assignment_target("$NAME")` wrongly accepts the
5589 // leading `$` as part of an identifier prefix → `$=cmd` lexes
5590 // as ENVSTRING instead of STRING.
5591 {
5592 let lo = Pound as usize;
5593 let hi = LAST_NORMAL_TOK as usize;
5594 for t0 in lo..=hi {
5595 t[t0] |= (ITOK | IMETA) as u32;
5596 }
5597 }
5598
5599 // c:4135-4136 — `for (t0 = Snull; t0 <= Nularg; t0++)
5600 // typtab[t0] |= ITOK | IMETA | INULL;`
5601 {
5602 let lo = Snull as usize;
5603 let hi = Nularg as usize;
5604 for t0 in lo..=hi {
5605 t[t0] |= (ITOK | IMETA | INULL) as u32;
5606 }
5607 }
5608
5609 // c:4202-4231 — IFS walk. Sets ISEP on every IFS char and IWSEP
5610 // on the blank (inblank) subset. Reads the current `ifs` global
5611 // (defaulting to DEFAULT_IFS), demetafies `Meta+X` pairs, and
5612 // skips a doubled blank (`s[1]==c`) so the IWSEP bit doesn't
5613 // mark "blank repeated → no-skip" IFS chars. Mirrors C exactly.
5614 {
5615 // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; ...)`.
5616 // C reads the global `ifs` variable (the same one `ifssetfn`
5617 // writes). The Rust port walks paramtab first (so the GSU
5618 // dispatch path matches C); on miss, fall through to ifs_lock
5619 // directly so a fresh `ifssetfn` update before any paramtab
5620 // entry exists is still visible to inittyptab.
5621 // c:4216 — `for (s = ifs ? ifs : CURRENT_DEFAULT_IFS; *s; s++)`.
5622 // The default-IFS fallback is keyed on the `ifs` POINTER being
5623 // NULL (i.e. IFS unset), NOT on it pointing at an empty string.
5624 // `IFS=''` leaves a non-NULL empty `ifs`, the loop body never
5625 // runs, and typtab ends up with ZERO ISEP/IWSEP bits — which is
5626 // exactly how an empty IFS disables all word splitting
5627 // (`IFS=''; v='a b c'; print -rl -- ${=v}` is one word). The
5628 // port used to coerce "" → DEFAULT_IFS at both the paramtab read
5629 // and the fallback, so an empty IFS still split on whitespace.
5630 let pt_ifs: Option<String> = crate::ported::params::paramtab()
5631 .read()
5632 .ok()
5633 .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)));
5634 let src: String = match pt_ifs {
5635 // IFS is SET (possibly to ""): walk its chars verbatim.
5636 Some(v) => v,
5637 // No paramtab entry yet. Fall through to the ifs_lock global so
5638 // a fresh ifssetfn update landing before the paramtab entry
5639 // exists is still visible; an empty global there means "not
5640 // initialised", which is C's NULL → CURRENT_DEFAULT_IFS.
5641 None => {
5642 let g = crate::ported::params::ifs_lock()
5643 .lock()
5644 .map(|g| g.clone())
5645 .unwrap_or_default();
5646 if g.is_empty() {
5647 DEFAULT_IFS.to_string() // c:4216 (ifs == NULL)
5648 } else {
5649 g
5650 }
5651 }
5652 };
5653 let bytes = src.as_bytes();
5654 let mut i = 0;
5655 while i < bytes.len() {
5656 // c:4217 — `int c = (unsigned char) (*s == Meta ? *++s ^ 32 : *s)`.
5657 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
5658 i += 1;
5659 bytes[i] ^ 32
5660 } else {
5661 bytes[i]
5662 };
5663 // c:4218-4223 — MULTIBYTE non-ASCII skip. Bytes >= 0x80
5664 // (after demetafy) are not classified by typtab — they
5665 // reach wcsitype via WC_ZISTYPE instead.
5666 if c >= 0x80 {
5667 i += 1;
5668 continue;
5669 }
5670 let cu = c as usize;
5671 // c:4224-4229 — `if (inblank(c))` — for default-IFS
5672 // chars space/tab/newline, mark IWSEP unless the next
5673 // byte repeats the same char.
5674 let is_inblank = (t[cu] & (INBLANK as u32)) != 0;
5675 if is_inblank {
5676 if i + 1 < bytes.len() && bytes[i + 1] == c {
5677 i += 1; // c:4226 — skip the dup
5678 } else {
5679 t[cu] |= IWSEP as u32; // c:4228
5680 }
5681 }
5682 // c:4230 — `typtab[c] |= ISEP;`
5683 t[cu] |= ISEP as u32;
5684 i += 1;
5685 }
5686 }
5687
5688 // c:4232-4252 — wordchars walk. ORs IWORD onto every byte in
5689 // `$WORDCHARS` (or DEFAULT_WORDCHARS when unset). Used by every
5690 // word-class lookup in pattern matching, `${var:#word}`, etc.
5691 // Drops to ASCII-only under MULTIBYTE_SUPPORT (the non-ASCII path
5692 // routes through wordchars_wide).
5693 {
5694 // Same fallback as IFS above: paramtab first, then wordchars_lock
5695 // so wordcharssetfn updates that bypass paramtab still reach
5696 // the typtab rebuild.
5697 let wc = crate::ported::params::paramtab()
5698 .read()
5699 .ok()
5700 .and_then(|t| {
5701 t.get("WORDCHARS")
5702 .map(|pm| crate::ported::params::wordcharsgetfn(pm))
5703 })
5704 .filter(|s| !s.is_empty())
5705 .unwrap_or_else(|| {
5706 crate::ported::params::wordchars_lock()
5707 .lock()
5708 .map(|g| g.clone())
5709 .unwrap_or_default()
5710 });
5711 let src: String = if wc.is_empty() {
5712 DEFAULT_WORDCHARS.to_string()
5713 } else {
5714 wc
5715 };
5716 let bytes = src.as_bytes();
5717 let mut i = 0;
5718 while i < bytes.len() {
5719 // c:4238 — Meta+X demetafy.
5720 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
5721 i += 1;
5722 bytes[i] ^ 32
5723 } else {
5724 bytes[i]
5725 };
5726 // c:4239-4249 — MULTIBYTE non-ASCII skip.
5727 if c < 0x80 {
5728 t[c as usize] |= IWORD as u32; // c:4251
5729 }
5730 i += 1;
5731 }
5732 }
5733
5734 // c:4253-4254 — SPECCHARS walk. ORs ISPECIAL onto every member
5735 // of the hardcoded SPECCHARS string. Drives glob-special and
5736 // quote-special detection.
5737 {
5738 for &b in SPECCHARS.as_bytes() {
5739 t[b as usize] |= ISPECIAL as u32; // c:4254
5740 }
5741 }
5742
5743 // c:4255-4256 — comma special only when ZTF_SP_COMMA was set
5744 // via `makecommaspecial(1)`. KSH_GLOB / extended-glob path.
5745 {
5746 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5747 if (flags & ZTF_SP_COMMA) != 0 {
5748 // c:4255
5749 t[b',' as usize] |= ISPECIAL as u32; // c:4256
5750 }
5751 }
5752
5753 // c:4257-4261 — bangchar special when BANGHIST + interact +
5754 // bangchar != 0. Sets ZTF_BANGCHAR flag bit then marks the
5755 // bangchar byte ISPECIAL.
5756 {
5757 let bangchar2 = bangchar.load(Ordering::SeqCst) as usize;
5758 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5759 let interact_flag = (flags & ZTF_INTERACT) != 0;
5760 let banghist = isset(BANGHIST);
5761 if banghist && bangchar2 != 0 && bangchar2 < 256 && interact_flag {
5762 // c:4257
5763 TYPTAB_FLAGS.fetch_or(ZTF_BANGCHAR, Ordering::Relaxed); // c:4258
5764 t[bangchar2] |= ISPECIAL as u32; // c:4259
5765 } else {
5766 TYPTAB_FLAGS.fetch_and(!ZTF_BANGCHAR, Ordering::Relaxed); // c:4261
5767 }
5768 }
5769
5770 // c:4262-4263 — PATCHARS walk. ORs IPATTERN onto every member.
5771 // Used by pattern compilation to detect glob metachars.
5772 {
5773 for &b in PATCHARS.as_bytes() {
5774 t[b as usize] |= IPATTERN as u32; // c:4263
5775 }
5776 }
5777
5778 // Flush the scratch to the lock-free table (see header note).
5779 for (i, v) in t.iter().enumerate() {
5780 TYPTAB[i].store(*v, Ordering::Relaxed);
5781 }
5782}
5783
5784/// Port of `void makecommaspecial(int yesno)` from Src/utils.c:4270.
5785///
5786/// Toggles `ZTF_SP_COMMA` and the `ISPECIAL` bit on `,` in the
5787/// global typtab — used by glob/extended-glob to flag `,`
5788/// (KSH_GLOB) as a metacharacter.
5789pub fn makecommaspecial(yesno: bool) {
5790 // c:4270
5791 if yesno {
5792 // c:4272
5793 TYPTAB_FLAGS.fetch_or(ZTF_SP_COMMA, Ordering::Relaxed); // c:4273
5794 TYPTAB[b',' as usize].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4274
5795 } else {
5796 TYPTAB_FLAGS.fetch_and(!ZTF_SP_COMMA, Ordering::Relaxed); // c:4276
5797 TYPTAB[b',' as usize].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4277
5798 }
5799}
5800
5801/// Port of `void makebangspecial(int yesno)` from Src/utils.c:4283.
5802///
5803/// Toggles `ISPECIAL` on the current `bangchar`. When `yesno==0`
5804/// always clears; when nonzero, sets only if `ZTF_BANGCHAR` was
5805/// stored by `inittyptab` (i.e. BANGHIST is on).
5806pub fn makebangspecial(yesno: bool) {
5807 // c:4283
5808 let bc = bangchar.load(Ordering::SeqCst) as usize;
5809 if bc == 0 || bc >= 256 {
5810 return;
5811 }
5812 let flags = TYPTAB_FLAGS.load(Ordering::Relaxed);
5813 if !yesno {
5814 // c:4289
5815 TYPTAB[bc].fetch_and(!(ISPECIAL as u32), Ordering::Relaxed); // c:4290
5816 } else if (flags & ZTF_BANGCHAR) != 0 {
5817 // c:4291
5818 TYPTAB[bc].fetch_or(ISPECIAL as u32, Ordering::Relaxed); // c:4292
5819 }
5820}
5821
5822/// Port of `wcsiblank(wint_t wc)` from `Src/utils.c:4302`.
5823///
5824/// ```c
5825/// mod_export int wcsiblank(wint_t wc) {
5826/// if (iswspace(wc) && wc != L'\n')
5827/// return 1;
5828/// return 0;
5829/// }
5830/// ```
5831///
5832/// "wide-character version of the iblank() macro" — true for any
5833/// whitespace EXCEPT newline. The previous Rust port included
5834/// newline (since `c.is_whitespace()` returns true for '\n') —
5835/// wrong for callers that use this to find token boundaries.
5836pub fn wcsiblank(wc: char) -> bool {
5837 wc.is_whitespace() && wc != '\n'
5838}
5839
5840/// Port of `int wcsitype(wchar_t c, int itype)` from Src/utils.c:4321.
5841///
5842/// "zistype macro extended to support wide characters. Works for
5843/// IIDENT, IWORD, IALNUM, ISEP."
5844///
5845/// The Rust port checks whether `c` falls in the typtab class
5846/// represented by `itype`. ASCII chars consult the global TYPTAB
5847/// (the same one C's `zistype()` macro indexes); non-ASCII chars
5848/// route through Unicode predicates that mirror the C `iswalnum`
5849/// fallback at line 4346.
5850pub fn wcsitype(c: char, itype: u32) -> bool {
5851 // c:4321
5852 if !isset(MULTIBYTE) {
5853 // c:4327
5854 if (c as u32) < 256 {
5855 return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
5856 }
5857 return false;
5858 }
5859 if (c as u32) < 128 {
5860 // c:4343
5861 return (TYPTAB[c as usize].load(Ordering::Relaxed) & itype) != 0;
5862 }
5863 let cls = itype as u16;
5864 if cls == IIDENT {
5865 // c:4347
5866 if isset(POSIXIDENTIFIERS) {
5867 return false; // c:4348
5868 }
5869 return c.is_alphanumeric(); // c:4350
5870 }
5871 if cls == IWORD {
5872 // c:4352
5873 if c.is_alphanumeric() {
5874 return true;
5875 } // c:4353
5876 // C: IS_COMBINING(c) — no Rust crate-free combining-mark
5877 // predicate. zero-width chars (combining, zero-width-joiner,
5878 // etc.) are treated as word per c:4362.
5879 if unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0 {
5880 // c:4362
5881 return true;
5882 }
5883 // c:4364 — `wmemchr(wordchars_wide.chars, c, …)`. Reads from
5884 // the canonical `wordchars` global (writable by
5885 // `wordcharssetfn` at `Src/params.c:5143`). Previously routed
5886 // through `std::env::var("WORDCHARS")` which is the libc
5887 // process environment — never reflects runtime `WORDCHARS=:`
5888 // assignments inside the shell.
5889 let w = crate::ported::params::paramtab()
5890 .read()
5891 .ok()
5892 .and_then(|t| {
5893 t.get("WORDCHARS")
5894 .map(|pm| crate::ported::params::wordcharsgetfn(pm))
5895 })
5896 .unwrap_or_default();
5897 return w.chars().any(|x| x == c);
5898 }
5899 if cls == ISEP {
5900 // c:4366
5901 // c:4367 — same canonical-global pattern for IFS.
5902 let ifs = crate::ported::params::paramtab()
5903 .read()
5904 .ok()
5905 .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
5906 .unwrap_or_default();
5907 return ifs.chars().any(|x| x == c);
5908 }
5909 let _ = IALNUM;
5910 c.is_alphanumeric() // c:4370
5911}
5912
5913/// Check if a character type at end of string (from utils.c itype_end)
5914/// Port of `char *itype_end(const char *ptr, int itype, int once)` from `Src/utils.c:4395`.
5915///
5916/// Walks `ptr` byte-by-byte advancing past every char whose
5917/// type-bits include `itype`. Returns the byte offset where the
5918/// walk stops (C returns the advanced pointer; the byte offset is
5919/// the Rust equivalent — `ptr + return_value` reproduces the C
5920/// pointer).
5921///
5922/// C body branches (ported line-for-line):
5923///
5924/// 1. **INAMESPC special-case** (c:4399-4413). If itype == INAMESPC
5925/// and we're not POSIXIDENTIFIERS-strict outside ksh emulation,
5926/// recurse on `ptr+(*ptr=='.')` with IIDENT to allow dotted
5927/// ksh93 namespace names. The recursion result determines
5928/// whether to advance past the dot or fall through.
5929///
5930/// 2. **MULTIBYTE branch** (c:4416-4470, gated `isset(MULTIBYTE)
5931/// && (itype != IIDENT || !isset(POSIXIDENTIFIERS))`). Walks
5932/// multibyte chars via mb_metacharlenconv (multibyte len +
5933/// wchar conversion). Per-codepoint test:
5934/// - itok byte (raw command line): test the byte via zistype
5935/// - Meta-prefixed pair: extract real byte via XOR, test via
5936/// zistype
5937/// - ASCII (< 0x80): test via zistype
5938/// - Non-ASCII codepoint: route through wcsitype (which
5939/// already handles IWORD/ISEP via WORDCHARS/IFS paramtab
5940/// lookup matching c:4364/c:4367)
5941///
5942/// 3. **Non-multibyte branch** (c:4474-4480). Simple Meta-byte +
5943/// zistype byte loop.
5944///
5945/// `once = true` stops after the first matching char (used for
5946/// "is this a valid first char" checks per c:1576 etc).
5947pub fn itype_end(s: &str, mut itype: u32, once: bool) -> usize {
5948 use crate::ported::zsh_h::{isset, Meta, EMULATE_KSH, EMULATION, MULTIBYTE, POSIXIDENTIFIERS};
5949 use crate::ported::ztype_h::{itok, zistype, IIDENT, INAMESPC};
5950
5951 // c:4399-4413 — INAMESPC handling with ksh93 namespace dot.
5952 let mut start = 0usize;
5953 if itype == INAMESPC {
5954 itype = IIDENT as u32;
5955 if !isset(POSIXIDENTIFIERS) || EMULATION(EMULATE_KSH) {
5956 // c:4403 — `t = itype_end(ptr + (*ptr == '.'), itype, 0);`
5957 let first_is_dot = s.as_bytes().first().copied() == Some(b'.');
5958 let recurse_start = if first_is_dot { 1 } else { 0 };
5959 let t = recurse_start + itype_end(&s[recurse_start..], itype, false);
5960 // c:4404-4410 — `if (t > ptr + (*ptr == '.')) {
5961 // if (*t == '.') ptr = t + 1; /* Fall through */
5962 // else if (!once) return t;
5963 // }`
5964 if t > recurse_start {
5965 let t_byte = s.as_bytes().get(t).copied();
5966 if t_byte == Some(b'.') {
5967 start = t + 1; // c:4407
5968 } else if !once {
5969 return t; // c:4409
5970 }
5971 }
5972 }
5973 }
5974
5975 let bytes = s.as_bytes();
5976 let mb = isset(MULTIBYTE) && (itype != IIDENT as u32 || !isset(POSIXIDENTIFIERS));
5977
5978 if mb {
5979 // c:4416-4470 — multibyte walk. mb_charinit() in C is a
5980 // no-op stateless reset; not needed in Rust since str::chars
5981 // is already stateless UTF-8.
5982 let mut i = start;
5983 while i < bytes.len() {
5984 let b = bytes[i];
5985 // c:4422-4425 — itok: raw token byte, test via zistype,
5986 // advance 1.
5987 if itok(b) {
5988 if !zistype(b, itype) {
5989 break;
5990 }
5991 i += 1;
5992 } else if b == Meta {
5993 // c:4438-4440 (WEOF arm) + non-MB Meta fallback:
5994 // Meta-prefixed pair = single unescaped char via XOR.
5995 let nxt = bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20;
5996 if (nxt as u32) > 127 || !zistype(nxt, itype) {
5997 break;
5998 }
5999 i += 2;
6000 } else if b < 0x80 {
6001 // c:4441-4443 (len == 1 && isascii) — ASCII: direct
6002 // zistype on the byte.
6003 if !zistype(b, itype) {
6004 break;
6005 }
6006 i += 1;
6007 } else {
6008 // c:4446-4467 — non-ASCII codepoint. Decode the UTF-8
6009 // sequence starting at `i` and route through wcsitype
6010 // (which already handles IWORD via WORDCHARS paramtab
6011 // c:4364, ISEP via IFS paramtab c:4367, default via
6012 // iswalnum/is_alphanumeric c:4370).
6013 let tail = match std::str::from_utf8(&bytes[i..]) {
6014 Ok(t) => t,
6015 Err(_) => break, // invalid UTF-8 — stop walk
6016 };
6017 let ch = match tail.chars().next() {
6018 Some(c) => c,
6019 None => break,
6020 };
6021 if !wcsitype(ch, itype) {
6022 break;
6023 }
6024 i += ch.len_utf8();
6025 }
6026 if once {
6027 break;
6028 }
6029 }
6030 i
6031 } else {
6032 // c:4474-4480 — non-multibyte arm. Simple byte loop with
6033 // Meta-prefix collapse.
6034 let mut i = start;
6035 while i < bytes.len() {
6036 let b = bytes[i];
6037 let chr = if b == Meta {
6038 bytes.get(i + 1).copied().unwrap_or(0) ^ 0x20
6039 } else {
6040 b
6041 };
6042 if !zistype(chr, itype) {
6043 break;
6044 }
6045 i += if b == Meta { 2 } else { 1 };
6046 if once {
6047 break;
6048 }
6049 }
6050 i
6051 }
6052}
6053
6054/// Duplicate array (from utils.c arrdup)
6055/// Port of `arrdup(char **s)` from `Src/utils.c:4493`.
6056pub fn arrdup(s: &[String]) -> Vec<String> {
6057 s.to_vec()
6058}
6059
6060/// Duplicate array with max elements (from utils.c arrdup_max)
6061/// Port of `arrdup_max(char **s, unsigned max)` from `Src/utils.c:4508`.
6062pub fn arrdup_max(s: &[String], max: usize) -> Vec<String> {
6063 s.iter().take(max).cloned().collect()
6064}
6065
6066/// Duplicate array with zsh allocation (from utils.c zarrdup)
6067/// Port of `zarrdup(char **s)` from `Src/utils.c:4532`.
6068pub fn zarrdup(s: &[String]) -> Vec<String> {
6069 s.to_vec()
6070}
6071
6072/// Duplicate array of wide strings (from utils.c wcs_zarrdup) - same as zarrdup in Rust
6073/// Port of `wcs_zarrdup(wchar_t **s)` from `Src/utils.c:4547`.
6074pub fn wcs_zarrdup(s: &[String]) -> Vec<String> {
6075 s.to_vec()
6076}
6077
6078/// Spelling correction: find closest match (from utils.c spname)
6079/// Port of `spname(char *oldname)` from `Src/utils.c:4562`.
6080/// WARNING: param names don't match C — Rust=(name, dir) vs C=(oldname)
6081pub fn spname(name: &str, dir: &str) -> Option<String> {
6082 let entries = match fs::read_dir(dir) {
6083 Ok(e) => e,
6084 Err(_) => return None,
6085 };
6086
6087 let mut best = None;
6088 let mut best_dist = 4; // threshold
6089
6090 for entry in entries.flatten() {
6091 if let Some(entry_name) = entry.file_name().to_str() {
6092 let dist = spdist(name, entry_name, best_dist);
6093 if dist < best_dist {
6094 best_dist = dist;
6095 best = Some(entry_name.to_string());
6096 }
6097 }
6098 }
6099 best
6100}
6101
6102/// Spelling correction with full path (from utils.c mindist)
6103/// Port of `mindist(char *dir, char *mindistguess, char *mindistbest, int wantdir)` from `Src/utils.c:4624`.
6104/// WARNING: param names don't match C — Rust=(dir, name) vs C=(dir, mindistguess, mindistbest, wantdir)
6105pub fn mindist(dir: &str, name: &str) -> Option<(String, usize)> {
6106 let entries = match fs::read_dir(dir) {
6107 Ok(e) => e,
6108 Err(_) => return None,
6109 };
6110
6111 let mut best = None;
6112 let mut best_dist = 4;
6113
6114 for entry in entries.flatten() {
6115 if let Some(entry_name) = entry.file_name().to_str() {
6116 let dist = spdist(name, entry_name, best_dist);
6117 if dist < best_dist {
6118 best_dist = dist;
6119 best = Some(entry_name.to_string());
6120 }
6121 }
6122 }
6123 best.map(|name| (name, best_dist))
6124}
6125
6126// spellcheck a word // c:3123
6127// fix s ; if hist is nonzero, fix the history list too // c:3124
6128/// Compute edit distance between two strings (for spelling correction)
6129/// Direct port of `int spdist(char *s, char *t, int thresh)` from
6130/// `Src/utils.c:4675-4750`. Drives the CORRECT-option typo prompt:
6131/// returns 0 for identical, 1 for case-only mistakes, 2 for one
6132/// transposition / missing letter / QWERTY-adjacent mistype, 200 for
6133/// anything farther.
6134///
6135/// **This is NOT a Levenshtein DP — that was the previous Rust impl
6136/// dressed as a port.** The C source uses a keyboard-adjacency model
6137/// (qwertykeymap / dvorakkeymap arrays + tulower equality) which is
6138/// the actual behaviour `setopt CORRECT` depends on. Restored
6139/// faithfully.
6140pub fn spdist(s: &str, t: &str, thresh: usize) -> usize {
6141 // c:4679-4690 — qwerty keymap (rows of 14 chars each: numeric row,
6142 // QWERTY top row, home row, bottom row, then shift versions).
6143 // Embedded `\n` / `\t` mark off-grid cells.
6144 const QWERTYKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6145\t1234567890-=\t\
6146\tqwertyuiop[]\t\
6147\tasdfghjkl;'\n\t\
6148\tzxcvbnm,./\t\t\t\
6149\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6150\t!@#$%^&*()_+\t\
6151\tQWERTYUIOP{}\t\
6152\tASDFGHJKL:\"\n\t\
6153\tZXCVBNM<>?\n\n\t\
6154\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
6155 // c:4691-4702 — dvorak keymap, same shape.
6156 const DVORAKKEYMAP: &str = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6157\t1234567890[]\t\
6158\t',.pyfgcrl/=\t\
6159\taoeuidhtns-\n\t\
6160\t;qjkxbmwvz\t\t\t\
6161\n\n\n\n\n\n\n\n\n\n\n\n\n\n\
6162\t!@#$%^&*(){}\t\
6163\t\"<>PYFGCRL?+\t\
6164\tAOEUIDHTNS_\n\t\
6165\t:QJKXBMWVZ\n\n\t\
6166\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
6167
6168 // c:4703-4707 — `keymap = isset(DVORAK) ? dvorakkeymap : qwertykeymap;`
6169 let keymap = if isset(DVORAK) {
6170 DVORAKKEYMAP.as_bytes()
6171 } else {
6172 QWERTYKEYMAP.as_bytes()
6173 };
6174
6175 let s_b = s.as_bytes();
6176 let t_b = t.as_bytes();
6177
6178 // c:4709-4710 — `if (!strcmp(s, t)) return 0;`
6179 if s == t {
6180 return 0;
6181 }
6182
6183 // c:4712-4714 — `for (p, q; *p && tulower(*p) == tulower(*q); p++, q++);
6184 // if (!*p && !*q) return 1;` — case-only mismatch.
6185 let mut p = 0usize;
6186 let mut q = 0usize;
6187 while p < s_b.len() && q < t_b.len() && tulower(s_b[p] as char) == tulower(t_b[q] as char) {
6188 p += 1;
6189 q += 1;
6190 }
6191 if p == s_b.len() && q == t_b.len() {
6192 return 1;
6193 }
6194
6195 // c:4715-4716 — `if (!thresh) return 200;`
6196 if thresh == 0 {
6197 return 200;
6198 }
6199
6200 // c:4717-4727 — first walk: detect transposition / missing letter at
6201 // first divergence.
6202 p = 0;
6203 q = 0;
6204 while p < s_b.len() && q < t_b.len() {
6205 if s_b[p] == t_b[q] {
6206 // c:4718-4719 — match: skip (don't count `aa` as transposed).
6207 p += 1;
6208 q += 1;
6209 continue;
6210 }
6211 // c:4720-4721 — `if (p[1] == q[0] && q[1] == p[0]) return
6212 // spdist(p+2, q+2, thresh-1) + 1;` transposition.
6213 if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p + 1] == t_b[q] && t_b[q + 1] == s_b[p] {
6214 // SAFETY: bytes are ASCII for the keymap test below; from_utf8
6215 // is fine on a non-ASCII tail here because spdist takes &str.
6216 let s_tail = std::str::from_utf8(&s_b[p + 2..]).unwrap_or("");
6217 let t_tail = std::str::from_utf8(&t_b[q + 2..]).unwrap_or("");
6218 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
6219 }
6220 // c:4722-4723 — `if (p[1] == q[0]) return spdist(p+1, q, thresh-1) + 2;`
6221 if p + 1 < s_b.len() && s_b[p + 1] == t_b[q] {
6222 let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6223 let t_tail = std::str::from_utf8(&t_b[q..]).unwrap_or("");
6224 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6225 }
6226 // c:4724-4725 — `if (p[0] == q[1]) return spdist(p, q+1, thresh-1) + 2;`
6227 if q + 1 < t_b.len() && s_b[p] == t_b[q + 1] {
6228 let s_tail = std::str::from_utf8(&s_b[p..]).unwrap_or("");
6229 let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6230 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6231 }
6232 // c:4726-4727 — `if (*p != *q) break;`
6233 break;
6234 }
6235 // c:4728-4729 — `if ((!*p && strlen(q) == 1) || (!*q && strlen(p) == 1))
6236 // return 2;` — single trailing-char insertion.
6237 if (p == s_b.len() && (t_b.len() - q) == 1) || (q == t_b.len() && (s_b.len() - p) == 1) {
6238 return 2;
6239 }
6240 // c:4730-4748 — second walk: keyboard-adjacency mistype detection.
6241 p = 0;
6242 q = 0;
6243 while p < s_b.len() && q < t_b.len() {
6244 if p + 1 < s_b.len() && q + 1 < t_b.len() && s_b[p] != t_b[q] && s_b[p + 1] == t_b[q + 1] {
6245 // c:4737-4738 — `if (!(z = strchr(keymap, p[0])) || *z == '\n' ||
6246 // *z == '\t') return spdist(p+1, q+1,
6247 // thresh-1) + 1;`
6248 let pos = keymap.iter().position(|&b| b == s_b[p]);
6249 let z_ok = match pos {
6250 Some(i) => keymap[i] != b'\n' && keymap[i] != b'\t',
6251 None => false,
6252 };
6253 if !z_ok {
6254 let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6255 let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6256 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 1;
6257 }
6258 // c:4739 — `t0 = z - keymap;`
6259 let t0 = pos.unwrap() as isize;
6260 // c:4740-4744 — eight adjacency offsets (-15,-14,-13,-1,+1,
6261 // +13,+14,+15) → keyboard neighbours.
6262 let offsets: [isize; 8] = [-15, -14, -13, -1, 1, 13, 14, 15];
6263 let adjacent = offsets.iter().any(|&off| {
6264 let idx = t0 + off;
6265 if idx >= 0 && (idx as usize) < keymap.len() {
6266 keymap[idx as usize] == t_b[q]
6267 } else {
6268 false
6269 }
6270 });
6271 if adjacent {
6272 // c:4745 — `return spdist(p+1, q+1, thresh-1) + 2;`
6273 let s_tail = std::str::from_utf8(&s_b[p + 1..]).unwrap_or("");
6274 let t_tail = std::str::from_utf8(&t_b[q + 1..]).unwrap_or("");
6275 return spdist(s_tail, t_tail, thresh.saturating_sub(1)) + 2;
6276 }
6277 // c:4746 — `return 200;`
6278 return 200;
6279 } else if p < s_b.len() && q < t_b.len() && s_b[p] != t_b[q] {
6280 // c:4747-4748 — `else if (*p != *q) break;`
6281 break;
6282 }
6283 p += 1;
6284 q += 1;
6285 }
6286 // c:4749 — `return 200;`
6287 200
6288}
6289
6290/// Set terminal to cbreak mode (from utils.c setcbreak)
6291#[cfg(unix)]
6292/// Port of `setcbreak` from `Src/utils.c:4756`.
6293pub fn setcbreak() -> bool {
6294 if let Some(mut ti) = gettyinfo() {
6295 ti.c_lflag &= !(libc::ICANON | libc::ECHO);
6296 ti.c_cc[libc::VMIN] = 1;
6297 ti.c_cc[libc::VTIME] = 0;
6298 settyinfo(&ti)
6299 } else {
6300 false
6301 }
6302}
6303
6304#[cfg(not(unix))]
6305/// Port of `setcbreak` from `Src/utils.c:4756`.
6306pub fn setcbreak() -> bool {
6307 false
6308}
6309
6310/// Port of `void attachtty(pid_t pgrp)` from Src/utils.c:4775.
6311/// Hands the controlling terminal to `pgrp`. Gated by `jobbing &&
6312/// interact`; falls back to `mypgrp` and disables MONITOR on permanent
6313/// failure (matching the C source's recursion + `opts[MONITOR]=0` path).
6314#[cfg(unix)]
6315pub fn attachtty(pgrp: i32) {
6316 // c:4775
6317
6318 if !(jobbing() && interact()) {
6319 return; // c:4779
6320 }
6321 let shtty = SHTTY.load(Ordering::Relaxed); // c:4781
6322 if shtty == -1 {
6323 return;
6324 }
6325 let ep = ATTACHTTY_EP.load(Ordering::Relaxed);
6326 let rc = unsafe { libc::tcsetpgrp(shtty, pgrp) }; // c:4781
6327 if rc == -1 && ep == 0 {
6328 // c:4781
6329 let mypgrp_val = *crate::ported::jobs::MYPGRP // c:4792
6330 .get_or_init(|| Mutex::new(0))
6331 .lock()
6332 .unwrap();
6333 if pgrp != mypgrp_val && unsafe { libc::kill(-pgrp, 0) } == -1 {
6334 attachtty(mypgrp_val); // c:4793
6335 } else {
6336 let errno_val = io::Error::last_os_error().raw_os_error().unwrap_or(0);
6337 if errno_val != libc::ENOTTY {
6338 // c:4795
6339 zwarn(&format!(
6340 "can't set tty pgrp: {}", // c:4797
6341 io::Error::from_raw_os_error(errno_val)
6342 ));
6343 let _ = io::stderr().flush(); // c:4798
6344 }
6345 opt_state_set("monitor", false); // c:4815 opts[MONITOR]=0
6346 ATTACHTTY_EP.store(1, Ordering::Relaxed); // c:4815
6347 }
6348 } else if rc != -1 {
6349 // c:4815
6350 *crate::ported::jobs::LAST_ATTACHED_PGRP // c:4815
6351 .get_or_init(|| Mutex::new(0))
6352 .lock()
6353 .unwrap() = pgrp;
6354 }
6355}
6356
6357/// Port of `pid_t gettygrp(void)` from Src/utils.c:4815.
6358#[cfg(unix)]
6359pub fn gettygrp() -> i32 {
6360 // c:4815
6361 let shtty = SHTTY.load(Ordering::Relaxed);
6362 if shtty == -1 {
6363 // c:4819
6364 return -1; // c:4820
6365 }
6366 unsafe { libc::tcgetpgrp(shtty) } // c:4823
6367}
6368
6369/// Convert raw bytes (possibly containing NUL / 0x83-0x9b) to
6370/// zsh's metafied form: each `imeta(b)` byte becomes `Meta` (0x83)
6371/// followed by `b ^ 32`.
6372///
6373/// Port of `metafy(char *buf, int len, int heap)` from Src/utils.c:4856. The C source takes a
6374/// `heap` mode controlling whether the result is `zalloc`'d /
6375/// `zhalloc`'d / written into a static buffer / appended to the
6376/// existing buffer; in Rust we always return an owned `String`
6377/// since allocation strategy is uniform. The byte-level transform
6378/// is identical: walk the input, count metafy hits, allocate
6379/// `len + meta` bytes, expand each `Meta+X` pair in reverse.
6380/// Rust idiom replacement: forward byte-walk + Vec::push covers the
6381/// C two-pass (count + alloc + reverse-expand) approach; Vec grows
6382/// on demand so the pre-count is unnecessary in Rust.
6383/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len, heap)
6384pub fn metafy(buf: &str) -> String {
6385 // c:4856
6386 let bytes = buf.as_bytes();
6387 let mut out = Vec::with_capacity(bytes.len());
6388 for &b in bytes {
6389 // C: `#define imeta(c) ((c) >= Meta)` from Src/zsh.h —
6390 // every byte >= 0x83 needs escaping. The previous
6391 // narrow-range check `(0x83..=0x9b)` was a bug: bytes
6392 // 0x9c..=0xff (e.g. UTF-8 continuation bytes, high-Latin
6393 // characters) escaped C's imeta() but not the Rust
6394 // version, which then fed un-escaped bytes downstream
6395 // and corrupted Meta-aware loops.
6396 if imeta_byte(b) {
6397 out.push(Meta);
6398 out.push(b ^ 32);
6399 } else {
6400 out.push(b);
6401 }
6402 }
6403 // metafied bytes are in [0..=0x7f]∪{0x83}∪[expanded ^ 32 range];
6404 // String::from_utf8 may fail on the high bytes — fall back to
6405 // lossy. The lossy fallback inserts U+FFFD for invalid
6406 // sequences, which is fine for String-level callers (lexer /
6407 // pattern compile / display) but breaks byte-round-trip with
6408 // `unmetafy`. Byte-round-trip callers should use `metafy_bytes`
6409 // (which preserves the metafied byte sequence verbatim).
6410 String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
6411}
6412
6413/// Port of `ztrdup_metafy(const char *s)` from `Src/utils.c:4929`.
6414///
6415/// ```c
6416/// mod_export char *
6417/// ztrdup_metafy(const char *s)
6418/// {
6419/// if (!s) return NULL;
6420/// return metafy((char *)s, -1, META_DUP);
6421/// }
6422/// ```
6423pub fn ztrdup_metafy(s: &str) -> String {
6424 metafy(s)
6425}
6426
6427/// Port of `unmetafy(char *s, int *len)` from `Src/utils.c:4954`.
6428///
6429/// Take a metafied byte buffer in `s` and convert it in place to
6430/// its literal representation. C signature:
6431///
6432/// ```c
6433/// char *unmetafy(char *s, int *len);
6434/// ```
6435///
6436/// The Rust port mutates `s` in place and returns the resulting
6437/// length (mirroring C's `*len` out-parameter). C control flow:
6438///
6439/// ```c
6440/// for (p = s; *p && *p != Meta; p++); // skip prefix with no Meta
6441/// for (t = p; (*t = *p++);) // walk the rest
6442/// if (*t++ == Meta && *p)
6443/// t[-1] = *p++ ^ 32; // un-escape: XOR with 32
6444/// ```
6445///
6446/// Same algorithm here, byte-indexed against the Vec rather than
6447/// pointer-walked.
6448/// WARNING: param names don't match C — Rust=(s) vs C=(s, len)
6449pub fn unmetafy(s: &mut Vec<u8>) -> usize {
6450 // c:4954
6451 // First loop: find the first `Meta` byte. Everything before it
6452 // stays as-is, so we don't need to copy.
6453 let mut p: usize = 0;
6454 while p < s.len() && s[p] != Meta {
6455 p += 1;
6456 }
6457 // Second loop: walk from `p` onward, copying each byte into the
6458 // `t` slot (which trails `p` by one position per Meta-escape
6459 // we collapse).
6460 let mut t: usize = p;
6461 while p < s.len() {
6462 let b = s[p];
6463 s[t] = b;
6464 p += 1;
6465 if b == Meta && p < s.len() {
6466 // C: t[-1] = *p++ ^ 32; — overwrite the just-written
6467 // Meta with the un-escaped byte.
6468 s[t] = s[p] ^ 32;
6469 p += 1;
6470 }
6471 t += 1;
6472 }
6473 s.truncate(t);
6474 t
6475}
6476
6477/// Port of `int metalen(const char *s, int len)` from `Src/utils.c:4971-4983`.
6478/// ```c
6479/// int mlen = len;
6480/// while (len--) {
6481/// if (*s++ == Meta) { mlen++; s++; }
6482/// }
6483/// return mlen;
6484/// ```
6485/// Doc: "Return the character length of a metafied substring, given
6486/// the unmetafied substring length." So **input `len` is the
6487/// UNMETAFIED char count, output is the METAFIED byte count.**
6488///
6489/// Previously the Rust port had INVERTED semantics: looped while
6490/// `i < len` byte-walk, returned char count. That's the reverse
6491/// operation. Pin the correct C contract.
6492pub fn metalen(s: &str, len: usize) -> usize {
6493 // c:4972
6494 let bytes = s.as_bytes();
6495 let mut mlen = len; // c:4974
6496 let mut remaining = len;
6497 let mut i = 0;
6498 while remaining > 0 && i < bytes.len() {
6499 // c:4976 `while (len--)`
6500 if bytes[i] == Meta {
6501 // c:4977
6502 mlen += 1; // c:4978
6503 i += 2; // c:4979 s++ (already advanced past Meta)
6504 } else {
6505 i += 1;
6506 }
6507 remaining -= 1;
6508 }
6509 mlen
6510}
6511
6512/// Port of `unmeta(const char *file_name)` from `Src/utils.c:4994`.
6513///
6514/// Convert a zsh internal (metafied) string to a system-call-safe
6515/// form (e.g. for passing to `open(2)`).
6516///
6517/// C body shape (c:4994-5010):
6518/// ```c
6519/// meta = 0;
6520/// for (t = file_name; *t; t++)
6521/// if (*t == Meta) { meta = 1; break; }
6522/// if (!meta) return (char *) file_name; // no-copy fast path
6523/// for (t = file_name, p = fn; *t; p++)
6524/// if ((*p = *t++) == Meta && *t)
6525/// *p = *t++ ^ 32;
6526/// ```
6527// Rust idiom replacement: byte-scan fast path + `unmetafy` covers
6528// the C in-place decode + alloc-on-copy dance; String owns its own
6529// allocation so no `heap` flag needed.
6530/// `unmeta` — see implementation.
6531pub fn unmeta(s: &str) -> String {
6532 // c:4994
6533 let bytes = s.as_bytes();
6534 // c:4995-4996 — Meta-byte scan; no-copy fast path.
6535 if !bytes.iter().any(|&b| b == Meta) {
6536 return s.to_string();
6537 }
6538 let mut buf = bytes.to_vec();
6539 let len = unmetafy(&mut buf); // c:4999-5001
6540 buf.truncate(len);
6541 String::from_utf8_lossy(&buf).into_owned()
6542}
6543
6544/// Port of `convchar_t unmeta_one(const char *in, int *sz)` from `Src/utils.c:5056-5086`.
6545/// Non-MULTIBYTE branch (c:5077-5083):
6546/// ```c
6547/// if (in[0] == Meta) { *sz = 2; wc = (unsigned char)(in[1] ^ 32); }
6548/// else { *sz = 1; wc = (unsigned char) in[0]; }
6549/// ```
6550/// Returns `(decoded_char, bytes_consumed)`. The c:5070 NULL/empty
6551/// guard returns `(0, 0)`. Previously used hardcoded `0x83` for the
6552/// Meta byte — now routes through the canonical `Meta` constant
6553/// (defined as `'\u{83}'` at zsh.h:144).
6554/// WARNING: param names don't match C — Rust=(s) vs C=(in, sz)
6555pub fn unmeta_one(s: &str) -> (char, usize) {
6556 // c:5058
6557 let bytes = s.as_bytes();
6558 // c:5070 — `if (!in || !*in) return 0;`
6559 if bytes.is_empty() {
6560 return ('\0', 0);
6561 }
6562 // c:5077 — `if (in[0] == Meta)`.
6563 if bytes[0] == Meta && bytes.len() > 1 {
6564 // c:5078-5079 — `*sz = 2; wc = (unsigned char)(in[1] ^ 32);`.
6565 ((bytes[1] ^ 32) as char, 2)
6566 } else {
6567 // c:5081-5082 — `*sz = 1; wc = (unsigned char) in[0];`.
6568 (bytes[0] as char, 1)
6569 }
6570}
6571
6572/// Port of `ztrcmp(char const *s1, char const *s2)` from `Src/utils.c:5106`.
6573///
6574/// Byte-walking compare with lazy Meta resolution. C body skips
6575/// matching bytes wholesale, then on the first differing byte
6576/// un-meta-fies just that byte and compares. Faster than
6577/// `unmeta(s1).cmp(unmeta(s2))` because:
6578/// 1. No allocation up front,
6579/// 2. Only the first differing position pays the un-meta cost.
6580///
6581/// ```c
6582/// while(*s1 && *s1 == *s2) { s1++; s2++; }
6583/// if (!(c1 = *s1)) c1 = -1;
6584/// else if (c1 == Meta) c1 = *++s1 ^ 32;
6585/// if (!(c2 = *s2)) c2 = -1;
6586/// else if (c2 == Meta) c2 = *++s2 ^ 32;
6587/// return c1 - c2;
6588/// ```
6589pub fn ztrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
6590 // c:5106
6591 let b1 = s1.as_bytes();
6592 let b2 = s2.as_bytes();
6593 let mut i1 = 0;
6594 let mut i2 = 0;
6595 // Skip the matching prefix.
6596 while i1 < b1.len() && i2 < b2.len() && b1[i1] == b2[i2] {
6597 i1 += 1;
6598 i2 += 1;
6599 }
6600 // Resolve c1: -1 for end-of-string, else the next byte
6601 // (un-meta-fied if it's a Meta marker).
6602 let c1: i32 = if i1 >= b1.len() {
6603 -1
6604 } else if b1[i1] == Meta && i1 + 1 < b1.len() {
6605 (b1[i1 + 1] ^ 32) as i32
6606 } else {
6607 b1[i1] as i32
6608 };
6609 let c2: i32 = if i2 >= b2.len() {
6610 -1
6611 } else if b2[i2] == Meta && i2 + 1 < b2.len() {
6612 (b2[i2 + 1] ^ 32) as i32
6613 } else {
6614 b2[i2] as i32
6615 };
6616 c1.cmp(&c2)
6617}
6618
6619// pastebuf() DELETED — was a misplaced fn. The real `pastebuf()`
6620// lives in `Src/Zle/zle_misc.c:558` (a ZLE clipboard helper that
6621// pastes a Cutbuffer into the line-edit buffer). It has nothing to
6622// do with metafication, despite this file's prior body which
6623// reimplemented half of `metafy`. The real metafy lives below at
6624// `pub fn metafy()` (port of utils.c:4856).
6625
6626/// Unmetafied string length (from utils.c ztrlen lines 5135-5152)
6627pub fn ztrlen(s: &str) -> usize {
6628 // c:5136
6629 let mut len = 0;
6630 let chars: Vec<char> = s.chars().collect();
6631 let mut i = 0;
6632 while i < chars.len() {
6633 len += 1;
6634 if chars[i] as u32 == Meta as u32 && i + 1 < chars.len() {
6635 i += 2;
6636 } else {
6637 i += 1;
6638 }
6639 }
6640 len
6641}
6642
6643/// Port of `ztrlenend(char const *s, char const *eptr)` from `Src/utils.c:5162`.
6644///
6645/// ```c
6646/// for (l = 0; s < eptr; l++) {
6647/// if (*s++ == Meta) s++; // skip past Meta-escaped pair
6648/// }
6649/// return l;
6650/// ```
6651///
6652/// Count the unmetafied character length from `s` up to `end`
6653/// bytes. Each Meta-escaped pair counts as 1 character.
6654/// Previous Rust port called `chars().count()` which counts UTF-8
6655/// codepoints, not byte-walked Meta-pairs — wrong semantics.
6656pub fn ztrlenend(s: &str, eptr: usize) -> usize {
6657 let bytes = s.as_bytes();
6658 let cap = eptr.min(bytes.len());
6659 let mut l = 0;
6660 let mut i = 0;
6661 while i < cap {
6662 if bytes[i] == Meta {
6663 // Meta sentinel + escaped byte = 1 visible char.
6664 i += 2;
6665 } else {
6666 i += 1;
6667 }
6668 l += 1;
6669 }
6670 l
6671}
6672
6673/// Port of `int ztrsub(char const *t, char const *s)` from `Src/utils.c:5185-5203`.
6674/// ```c
6675/// int l = t - s;
6676/// while (s != t) {
6677/// if (*s++ == Meta) { s++; l--; }
6678/// }
6679/// return l;
6680/// ```
6681/// "Subtract two pointers in a metafied string." `s` is the start
6682/// pointer, `t` is the end pointer; both point into the SAME buffer.
6683/// Returns the count of unmetafied chars in `[s, t)` — same as
6684/// `ztrlen` of the substring but expressed via pointer arithmetic.
6685///
6686/// Rust API: takes the full buffer and a byte-offset pair
6687/// `[start..end)` since Rust can't replicate raw-pointer subtraction
6688/// across distinct `&str` arguments without UB risk. The semantics
6689/// stay faithful: count of unmetafied chars between two offsets.
6690/// Previous Rust port took two unrelated `&str` and computed
6691/// `ztrlen(&t[..t.len()-s.len()])` — a fundamentally different
6692/// operation that worked only when `s` was the suffix of `t`.
6693/// WARNING: param names don't match C — Rust=(buf, start, end) vs C=(t, s)
6694pub fn ztrsub(buf: &str, start: usize, end: usize) -> usize {
6695 // c:5187
6696 let bytes = buf.as_bytes();
6697 let end = end.min(bytes.len());
6698 let start = start.min(end);
6699 let mut l = (end - start) as isize; // c:5189
6700 let mut i = start;
6701 while i < end {
6702 if bytes[i] == Meta {
6703 // c:5192
6704 i += 2; // c:5198
6705 l -= 1; // c:5199
6706 } else {
6707 i += 1;
6708 }
6709 }
6710 l.max(0) as usize
6711}
6712
6713/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
6714/// `Src/utils.c:5217`.
6715///
6716/// Port of `char *zreaddir(DIR *dir, int ignoredots)` from
6717/// `Src/utils.c:5217-5240`. Pulls the next entry from a DIR* the
6718/// caller owns; returns the entry's bare name (metafied String) or
6719/// `None` at EOF. The C source's `ignoredots` flag (c:5232) skips
6720/// `.` and `..` when set; callers (`spnamepat` at c:4648) that
6721/// want dot entries pass 0.
6722///
6723/// Caller pattern matches C exactly:
6724/// let mut dir = fs::read_dir(path)?;
6725/// while let Some(name) = zreaddir(&mut dir, 1) { ... }
6726/// // dir auto-closed on drop (= closedir)
6727pub fn zreaddir(dir: &mut fs::ReadDir, ignoredots: i32) -> Option<String> {
6728 // c:5217
6729 for entry in dir.by_ref() {
6730 // c:5221 readdir loop
6731 let Ok(e) = entry else { continue };
6732 let Ok(name) = e.file_name().into_string() else {
6733 continue;
6734 };
6735 // c:5232 — `if (ignoredots && de->d_name[0] == '.' &&
6736 // (!de->d_name[1] || (de->d_name[1] == '.' && !de->d_name[2])))`.
6737 if ignoredots != 0 && (name == "." || name == "..") {
6738 continue; // c:5234
6739 }
6740 return Some(name); // c:5238 return de->d_name
6741 }
6742 None // c:5240 — fell off the end
6743}
6744
6745/// Port of `int zputs(char const *s, FILE *stream)` from `Src/utils.c:5263-5282`.
6746///
6747/// ```c
6748/// while (*s) {
6749/// if (*s == Meta) c = *++s ^ 32;
6750/// else if (itok(*s)) { s++; continue; }
6751/// else c = *s;
6752/// s++;
6753/// if (fputc(c, stream) < 0) return EOF;
6754/// }
6755/// return 0;
6756/// ```
6757///
6758/// Writes `s` to `stream` with Meta+X pair decoding and ITOK-byte
6759/// skipping. Returns `0` on success, `-1` on write error (mirroring
6760/// C's `EOF` sentinel for the int return).
6761pub fn zputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
6762 // c:5265
6763 let bytes = s.as_bytes(); // c:5265 *s walk
6764 let mut i = 0;
6765 while i < bytes.len() {
6766 // c:5267 while (*s)
6767 let c: u8; // c:5268 char c
6768 if bytes[i] == Meta {
6769 // c:5269 if (*s == Meta)
6770 // c:5270 — `c = *++s ^ 32;`
6771 if i + 1 < bytes.len() {
6772 c = bytes[i + 1] ^ 32;
6773 i += 1; // c:5270 ++s
6774 } else {
6775 i += 1;
6776 continue;
6777 }
6778 } else if itok(bytes[i]) {
6779 // c:5271 else if (itok(*s))
6780 // c:5272 — `s++; continue;` (skip token byte)
6781 i += 1;
6782 continue;
6783 } else {
6784 c = bytes[i]; // c:5274 else c = *s
6785 }
6786 i += 1; // c:5276 s++
6787 if stream.write_all(&[c]).is_err() {
6788 // c:5277 fputc(c, stream)
6789 return -1; // c:5278 return EOF
6790 }
6791 }
6792 0 // c:5280 return 0
6793}
6794
6795/// Port of `nicedup(char const *s, int heap)` from `Src/utils.c:5289`
6796/// (single-byte build) and `Src/utils.c:5530` (multibyte build).
6797///
6798/// C bodies:
6799/// ```c
6800/// /* Src/utils.c:5289 — !MULTIBYTE_SUPPORT */
6801/// char *retstr;
6802/// (void)sb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
6803/// return retstr;
6804///
6805/// /* Src/utils.c:5530 — MULTIBYTE_SUPPORT */
6806/// char *retstr;
6807/// (void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);
6808/// return retstr;
6809/// ```
6810///
6811/// zshrs targets MULTIBYTE_SUPPORT (matches every modern zsh
6812/// build) — route through `mb_niceformat`. Previous Rust port
6813/// went through `sb_niceformat`, which is the !MULTIBYTE path —
6814/// strips wide-char handling and corrupts non-ASCII via byte-mask
6815/// `nicechar(c & 0xff)`.
6816///
6817/// C signature faithful: `(s, heap) -> String`. `heap` selects between
6818/// arena (`NICEFLAG_HEAP`) and persistent (default `ztrdup`) allocation
6819/// in C; Rust has a single allocator so both paths produce the same
6820/// owned `String`, but the `heap` parameter is preserved per Rule S1.
6821pub fn nicedup(s: &str, heap: i32) -> String {
6822 // c:5530
6823 // c:5532 — `char *retstr;`
6824 let retstr: Option<String>;
6825 // c:5534 — `(void)mb_niceformat(s, NULL, &retstr, heap ? NICEFLAG_HEAP : 0);`
6826 let mut slot: Option<String> = None;
6827 let _ = mb_niceformat(
6828 s,
6829 None,
6830 Some(&mut slot),
6831 if heap != 0 { NICEFLAG_HEAP } else { 0 },
6832 );
6833 retstr = slot;
6834 // c:5536 — `return retstr;`
6835 retstr.unwrap_or_default()
6836}
6837
6838/// Nice-format and duplicate string.
6839/// Port of `nicedupstring(char const *s)` from `Src/utils.c:5301`.
6840/// C body: `return nicedup(s, 1);` — heap-arena allocation form.
6841pub fn nicedupstring(s: &str) -> String {
6842 // c:5301
6843 nicedup(s, 1) // c:5303
6844}
6845
6846/// Nicely format a string
6847/// Port of `nicezputs(char const *s, FILE *stream)` from `Src/utils.c`.
6848///
6849/// Under `MULTIBYTE_SUPPORT` (the daily-driver path), C defines
6850/// `nicezputs(str, outs)` as a macro at zsh.h:3274:
6851/// `(void)mb_niceformat((str), (outs), NULL, 0)`.
6852/// Without MULTIBYTE (c:5313): `sb_niceformat(s, stream, NULL, 0)`.
6853///
6854/// The previous Rust impl was `s.chars().map(nicechar).collect()` —
6855/// wrong on every front:
6856/// 1. No unmetafy step (Meta-byte pairs surfaced as `\M-…` mangled).
6857/// 2. `nicechar` is byte-based (`c & 0xff`), corrupting non-ASCII
6858/// multibyte codepoints into spurious `\M-X` escapes.
6859/// 3. No `itok` ZSH-token-byte handling.
6860///
6861/// Route through `mb_niceformat` to match the C macro under
6862/// MULTIBYTE — which itself unmetafies and uses `wcs_nicechar` for
6863/// proper wide-char handling.
6864pub fn nicezputs(s: &str, stream: &mut dyn std::io::Write) -> i32 {
6865 // c:5313 (non-MULTIBYTE) / zsh.h:3274 (MULTIBYTE macro:
6866 // `(void)mb_niceformat((str), (outs), NULL, 0)`).
6867 let _ = mb_niceformat(s, Some(stream), None, 0);
6868 0 // c:5316 return 0
6869}
6870
6871/// Port of `niceztrlen(char const *s)` from `Src/utils.c:5324`.
6872///
6873/// Returns the length (in bytes) of the visible representation of
6874/// the metafied string `s`. C body walks each char via `nicechar`
6875/// and accumulates `strlen(nicechar(c))`; the Rust port mirrors
6876/// this via [`sb_niceformat`] which is the same render-then-measure
6877/// path.
6878pub fn niceztrlen(s: &str) -> usize {
6879 // c equivalent: l = sb_niceformat(s, NULL, NULL, 0); return l;
6880 sb_niceformat(s, None, None, 0)
6881}
6882
6883/// Multibyte-aware nice-format of a string.
6884/// Port of `mb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from Src/utils.c:5366. Walks the
6885/// (un-metafied) string char-by-char; for each control byte or
6886/// invalid sequence emits a `^X`/`\\xNN` representation, otherwise
6887/// passes the char through. The C source threads an
6888/// `mbstate_t` through `mbrtowc()` and falls back to single-byte
6889/// `\M-` notation on `MB_INVALID`; the Rust port uses Rust's
6890/// chars iterator which already produces valid scalar values, so
6891/// invalid-byte fallback collapses to the control-char branch.
6892/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream, outstrp, flags)
6893// Rust idiom replacement: `chars()` + `wcs_nicechar` covers the C
6894// mbrtowc loop with `MB_INVALID` fallback (Rust UTF-8 guarantees
6895// valid scalars, so the invalid-byte arm collapses).
6896/// `mb_niceformat` — see implementation.
6897pub fn mb_niceformat(
6898 s: &str,
6899 mut stream: Option<&mut dyn std::io::Write>,
6900 outstrp: Option<&mut Option<String>>,
6901 flags: i32,
6902) -> usize {
6903 // c:5366
6904 let mut l: usize = 0; // c:5368 size_t l = 0
6905 let mut newl: usize; // c:5368 size_t newl
6906 // c:5369 — `int umlen, outalloc, outleft, eol = 0;`. outalloc/outleft
6907 // model C's buffer-growth math; Rust String auto-grows so the realloc
6908 // loop at c:5430-5440 collapses to push_str. umlen and eol carry.
6909 let mut umlen: usize; // c:5369
6910 let mut eol: bool = false; // c:5369 int eol = 0
6911 // c:5370 — `wchar_t c;` (carried inside loop in Rust)
6912 // c:5371 — `char *ums, *ptr, *fmt, *outstr, *outptr;`
6913 let mut ums: Vec<u8>; // c:5371 char *ums
6914 let mut ptr: usize; // c:5371 char *ptr
6915 let mut fmt: String; // c:5371 char *fmt
6916 let mut outstr: Option<String>; // c:5371 char *outstr
6917 // c:5372 — `mbstate_t mbs;` (Rust UTF-8 has no shift state; tracked
6918 // by the `valid_up_to` / `error_len` logic below).
6919
6920 // c:5374-5380 — `if (outstrp) outptr = outstr = zalloc(5*strlen(s));`
6921 if outstrp.is_some() {
6922 // c:5374
6923 outstr = Some(String::with_capacity(5 * s.len())); // c:5376
6924 } else {
6925 // c:5377
6926 outstr = None; // c:5379 outstr = NULL
6927 }
6928
6929 // c:5382 — `ums = ztrdup(s);`
6930 // c:5383-5387 — comment-only block carried verbatim:
6931 /*
6932 * is this necessary at this point? niceztrlen does this
6933 * but it's used in lots of places. however, one day this may
6934 * be, too.
6935 */ // c:5383-5387
6936 // c:5388 — `untokenize(ums);` — Rust port uses the char-based
6937 // `lex::untokenize` (only fires on codepoints in the 0x84..=0xa1
6938 // ITOK range, never on UTF-8 continuation bytes); the byte-level
6939 // shape of C's untokenize is unsafe on raw UTF-8 input because a
6940 // continuation byte like 0x97 (part of `字` = 0xE5 0xAD 0x97) sits
6941 // inside the ITOK range and would be misclassified as a token.
6942 // c:5389 — `ptr = unmetafy(ums, ¨en);` — `unmeta` is the safe
6943 // UTF-8 wrapper that runs unmetafy only when Meta bytes are
6944 // present and otherwise no-ops.
6945 //
6946 // `unmetafy_str` (not `unmeta`) is what produces the raw byte buffer C
6947 // expects here. zshrs metafies at the CHARACTER level — an undecodable byte
6948 // is stored as U+0083 followed by `char::from(byte ^ 32)` — and U+0083
6949 // itself encodes as the two UTF-8 bytes C2 83. `unmeta` scans for a 0x83
6950 // BYTE, so it finds the trailing byte of that pair and mangles the string:
6951 // `$'\M-a'` came back out as `\M-^C\M-A` instead of `\M-a`.
6952 let detok = untokenize(s);
6953 ums = unmetafy_str(&detok);
6954 umlen = ums.len(); // c:5389 *umlen
6955 ptr = 0; // c:5389 ptr starts at 0 in ums
6956
6957 // c:5391 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
6958 while umlen > 0 {
6959 // c:5392
6960 // c:5393 — `cnt = eol ? MB_INVALID : mbrtowc(&c, ptr, umlen, &mbs);`
6961 // Rust equivalent: try to decode one UTF-8 scalar from ums[ptr..],
6962 // honoring `eol` (force the invalid arm when set).
6963 let cnt: usize;
6964 let decoded_c: Option<char>;
6965 if eol {
6966 // c:5396 — MB_INVALID arm via `eol = 1`.
6967 decoded_c = None;
6968 cnt = 1;
6969 } else {
6970 let remaining = &ums[ptr..ptr + umlen];
6971 match std::str::from_utf8(remaining) {
6972 Ok(s_slice) => {
6973 // Valid UTF-8 throughout remaining; consume one char.
6974 if let Some(ch) = s_slice.chars().next() {
6975 decoded_c = Some(ch);
6976 cnt = ch.len_utf8();
6977 } else {
6978 // Empty? Shouldn't happen given umlen > 0, but
6979 // mirror MB_INVALID.
6980 decoded_c = None;
6981 cnt = 1;
6982 }
6983 }
6984 Err(e) => {
6985 let valid_up_to = e.valid_up_to();
6986 if valid_up_to > 0 {
6987 // Decode the first valid char then continue.
6988 let valid_slice =
6989 unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
6990 let ch = valid_slice.chars().next().unwrap();
6991 decoded_c = Some(ch);
6992 cnt = ch.len_utf8();
6993 } else if e.error_len().is_none() {
6994 // Incomplete sequence at end of input — MB_INCOMPLETE.
6995 // c:5394 — `case MB_INCOMPLETE: eol = 1; FALL THROUGH`
6996 eol = true;
6997 decoded_c = None; // MB_INVALID
6998 cnt = 1;
6999 } else {
7000 // Definite invalid byte — MB_INVALID.
7001 decoded_c = None;
7002 cnt = 1;
7003 }
7004 }
7005 }
7006 }
7007
7008 // c:5396-5403 / c:5404-5424 switch dispatch on cnt/decoded_c.
7009 let cnt_used: usize;
7010 match decoded_c {
7011 None => {
7012 // c:5397 — `case MB_INVALID:` (or fall-through from
7013 // MB_INCOMPLETE at c:5394).
7014 // c:5400 — `fmt = nicechar_sel(*ptr, flags & NICEFLAG_QUOTE);`
7015 fmt = nicechar_sel(ums[ptr] as char, (flags & NICEFLAG_QUOTE) != 0);
7016 newl = fmt.len(); // c:5401 newl = strlen(fmt)
7017 cnt_used = 1; // c:5402 cnt = 1
7018 // c:5403 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless)
7019 }
7020 Some(c) if c == '\0' => {
7021 // c:5406 — `case 0:` — '\0' decodes to 0; consume 1 byte
7022 // and fall through to default.
7023 cnt_used = 1; // c:5409
7024 // c:5411-5421 default arm (FALL THROUGH from case 0)
7025 if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
7026 // c:5413
7027 fmt = "\\'".to_string(); // c:5414
7028 newl = 2; // c:5415
7029 } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
7030 // c:5417
7031 fmt = "\\\\".to_string(); // c:5418
7032 newl = 2; // c:5419
7033 } else {
7034 // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
7035 // flags & NICEFLAG_QUOTE);`
7036 let mut width: usize = 0;
7037 fmt =
7038 wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
7039 newl = width;
7040 }
7041 }
7042 Some(c) => {
7043 // c:5411 — `default:` arm (cnt > 0, valid char).
7044 cnt_used = cnt;
7045 if c == '\'' && (flags & NICEFLAG_QUOTE) != 0 {
7046 // c:5413
7047 fmt = "\\'".to_string(); // c:5414
7048 newl = 2; // c:5415
7049 } else if c == '\\' && (flags & NICEFLAG_QUOTE) != 0 {
7050 // c:5417
7051 fmt = "\\\\".to_string(); // c:5418
7052 newl = 2; // c:5419
7053 } else {
7054 // c:5422 — `fmt = wcs_nicechar_sel(c, &newl, NULL,
7055 // flags & NICEFLAG_QUOTE);`
7056 let mut width: usize = 0;
7057 fmt =
7058 wcs_nicechar_sel(c, Some(&mut width), None, (flags & NICEFLAG_QUOTE) != 0);
7059 newl = width;
7060 }
7061 }
7062 }
7063
7064 umlen -= cnt_used; // c:5427 umlen -= cnt
7065 ptr += cnt_used; // c:5428 ptr += cnt
7066 l += newl; // c:5429 l += newl
7067
7068 if let Some(ref mut w) = stream {
7069 // c:5431 if (stream)
7070 // c:5432 — `zputs(fmt, stream);`
7071 let _ = w.write_all(fmt.as_bytes());
7072 }
7073 if let Some(ref mut buf) = outstr {
7074 // c:5433 if (outstr)
7075 // c:5434-5446 — append fmt to outstr, growing on demand. Rust
7076 // String auto-grows; the realloc loop collapses to push_str.
7077 buf.push_str(&fmt); // c:5446 memcpy(outptr, fmt, outlen)
7078 }
7079 let _ = fmt;
7080 }
7081
7082 // c:5451 — `free(ums);` (Rust drop at scope exit)
7083 drop(ums);
7084 if let Some(slot) = outstrp {
7085 // c:5452 if (outstrp)
7086 // c:5453 — `*outptr = '\0';` (no-op for Rust String)
7087 // c:5455-5460 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping. Rust has
7088 // a single allocator so all three paths produce identical owned
7089 // String contents; transfer ownership into caller's slot.
7090 *slot = outstr.take();
7091 }
7092
7093 l // c:5462 return l
7094}
7095
7096/// Port of `is_mb_niceformat(const char *s)` from `Src/utils.c:5474`.
7097///
7098/// Predicate: would any character in `s` need representation by
7099/// `mb_niceformat` / `nicedup`? C body:
7100/// ```c
7101/// ums = ztrdup(s);
7102/// untokenize(ums);
7103/// ptr = unmetafy(ums, ¨en);
7104/// while (umlen > 0) {
7105/// cnt = mbrtowc(&c, ptr, umlen, &mbs);
7106/// switch (cnt) {
7107/// case MB_INCOMPLETE: case MB_INVALID:
7108/// if (is_nicechar(*ptr)) { ret = 1; break; }
7109/// cnt = 1;
7110/// memset(&mbs, 0, sizeof mbs);
7111/// break;
7112/// case 0: cnt = 1; /* FALLTHROUGH */
7113/// default:
7114/// if (is_wcs_nicechar(c)) ret = 1;
7115/// break;
7116/// }
7117/// if (ret) break;
7118/// umlen -= cnt; ptr += cnt;
7119/// }
7120/// ```
7121///
7122/// Rust port: unmetafy in place via [`unmetafy`], then walk the
7123/// resulting bytes. For valid UTF-8 sequences, check
7124/// `is_wcs_nicechar(scalar)`; for invalid bytes, check
7125/// `is_nicechar(byte)`. Either path bailing positive returns true.
7126pub fn is_mb_niceformat(s: &str) -> i32 {
7127 // c:5474
7128 // c:5476 — `int umlen, ret = 0;`
7129 let umlen: usize; // c:5476
7130 let mut ret: i32 = 0; // c:5476
7131 // c:5477 — `char *ums, *ptr;` (eptr modelled by bytes.len())
7132 let mut ums: Vec<u8>; // c:5477
7133 let mut ptr: usize; // c:5477
7134
7135 // c:5481-5483 — `ums = ztrdup(s); untokenize(ums); ptr =
7136 // unmetafy(ums, ¨en);`. Use BYTE-level `unmetafy_str`,
7137 // not char-level `unmeta`: `unmeta` decodes a metafied eight-bit byte
7138 // (Meta 0x83 + 0xC1 for `$'\M-a'`) into the valid Rust char U+00E1
7139 // (`á`), which the UTF-8 scan below reads as a PRINTABLE Latin-1 letter
7140 // → is_mb_niceformat=0 → `(q+)`/quotedzputs left the raw byte unquoted
7141 // (`\341`) where zsh emits `$'\M-a'`. `unmetafy_str` preserves the raw
7142 // 0xE1 byte (C's `unmetafy`), so the MB_INVALID arm's is_nicechar(0xE1)
7143 // fires (high-bit → nice) exactly as C does.
7144 let detok = untokenize(s);
7145 ums = unmetafy_str(&detok);
7146 umlen = ums.len(); // c:5483 *umlen
7147 ptr = 0; // c:5483 ptr starts at 0
7148
7149 // c:5485 — `memset(&mbs, 0, sizeof mbs);` (Rust: stateless UTF-8)
7150 while ret == 0 && ptr < ums.len() {
7151 // c:5486 while (umlen > 0)
7152 let remaining = &ums[ptr..];
7153 match std::str::from_utf8(remaining) {
7154 Ok(s_slice) => {
7155 // c:5503-5511 — `default: if (is_wcs_nicechar(c)) ret = 1;`
7156 for ch in s_slice.chars() {
7157 if is_wcs_nicechar(ch) {
7158 // c:5508
7159 ret = 1; // c:5509
7160 break;
7161 }
7162 }
7163 break;
7164 }
7165 Err(e) => {
7166 let valid_up_to = e.valid_up_to();
7167 if valid_up_to > 0 {
7168 let valid = unsafe { std::str::from_utf8_unchecked(&remaining[..valid_up_to]) };
7169 for ch in valid.chars() {
7170 if is_wcs_nicechar(ch) {
7171 // c:5508
7172 ret = 1; // c:5509
7173 break;
7174 }
7175 }
7176 if ret != 0 {
7177 break;
7178 }
7179 ptr += valid_up_to;
7180 continue;
7181 }
7182 // c:5493-5498 — `case MB_INVALID: if (is_nicechar(*ptr))
7183 // ret = 1; break;`
7184 if is_nicechar(remaining[0] as char) {
7185 // c:5494
7186 ret = 1; // c:5495
7187 break;
7188 }
7189 ptr += 1;
7190 }
7191 }
7192 }
7193
7194 drop(ums); // c:5519 free(ums)
7195 ret // c:5523 return ret
7196}
7197
7198/// Multibyte metachar length with conversion (from utils.c mb_metacharlenconv_r)
7199/// Port of `mb_metacharlenconv_r(const char *s, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5548`.
7200/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, wcp, mbsp)
7201// Rust idiom replacement: Rust strings are UTF-8 with valid scalar
7202// values, so `chars().next()` covers the mbrtowc state machine; no
7203// mbstate_t threading required.
7204/// `mb_metacharlenconv_r` — see implementation.
7205pub fn mb_metacharlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
7206 if let Some(c) = s[pos..].chars().next() {
7207 (c.len_utf8(), Some(c))
7208 } else {
7209 (0, None)
7210 }
7211}
7212
7213/// Multibyte-aware metafied-string char advance.
7214/// Port of `mb_metacharlenconv(const char *s, wint_t *wcp)` from Src/utils.c:5611. Returns
7215/// `(bytes_consumed, scalar_char)` for the next char in `s`. C
7216/// source dispatches to `mb_metacharlenconv_r()` for true
7217/// multibyte; we use Rust's UTF-8 char iterator which already
7218/// handles multi-byte correctly. ASCII fast-path: `Meta+X` is 2
7219/// bytes consumed → `(2, X^32)`; bare ASCII is `(1, c)`.
7220/// WARNING: param names don't match C — Rust=(s) vs C=(s, wcp)
7221pub fn mb_metacharlenconv(s: &str) -> (usize, Option<char>) {
7222 let bytes = s.as_bytes();
7223 if bytes.is_empty() {
7224 return (0, None);
7225 }
7226 if bytes[0] == 0x83 && bytes.len() >= 2 {
7227 // Meta+X pair → unescape.
7228 let raw = bytes[1] as u32 ^ 32;
7229 return (2, char::from_u32(raw));
7230 }
7231 if bytes[0] <= 0x7f {
7232 return (1, Some(bytes[0] as char));
7233 }
7234 // Multi-byte UTF-8 — let Rust decode.
7235 if let Some(c) = s.chars().next() {
7236 return (c.len_utf8(), Some(c));
7237 }
7238 (1, None)
7239}
7240
7241/// Multibyte metastring length to end (from utils.c mb_metastrlenend)
7242/// Port of `mb_metastrlenend(char *ptr, int width, char *eptr)` from `Src/utils.c:5655`.
7243///
7244/// Counts characters (`width == false`) or display columns
7245/// (`width == true`) in a metafied string. C walks the metafied
7246/// byte stream, un-metafies inline (`if (*ptr == Meta) inchar =
7247/// *++ptr ^ 32`, c:5672), and feeds one byte at a time to
7248/// `mbrtowc` to assemble each multibyte character (c:5691); a
7249/// complete character counts 1 (or its `WCWIDTH`), an invalid byte
7250/// counts as a single character (c:5712-5714).
7251///
7252/// Rust-port note: zshrs stores both metafied byte-pairs (`Meta` +
7253/// `byte ^ 32`, produced by `getkeystring` for `$'\xNN'` escapes)
7254/// AND real multibyte `char`s (the literal `$'é'` path) in one
7255/// `String`, whereas C only ever holds metafied bytes. So the port
7256/// first reduces the slice to its raw byte stream via `unmetafy_str`
7257/// (metafied pair → its byte; real `char` → its UTF-8 bytes — the
7258/// same bytes C's inline demetafy would yield), then runs C's
7259/// `mbrtowc` character-assembly loop over those bytes. Rust's UTF-8
7260/// validation is the `mbrtowc` analogue: the shortest valid prefix
7261/// is one character; a byte that begins no valid sequence counts as
7262/// one (c:5712).
7263pub fn mb_metastrlenend(ptr: &str, width: bool, eptr: usize) -> usize {
7264 // c:5672 — un-metafy the (optionally end-bounded) slice to raw bytes.
7265 let bytes = unmetafy_str(&ptr[..eptr.min(ptr.len())]);
7266 let mut num: usize = 0; // c:5658 size_t ret / counter
7267 let mut i: usize = 0;
7268 while i < bytes.len() {
7269 // c:5678-5687 — 7-bit US-ASCII subset: one character, skip mbrtowc.
7270 if bytes[i] <= 0x7f {
7271 num += 1;
7272 i += 1;
7273 continue;
7274 }
7275 // c:5691 — mbrtowc: take the shortest valid UTF-8 character.
7276 let mut num_in_char: usize = 1; // c:5701 trailing-octet span
7277 let hi = (i + 4).min(bytes.len());
7278 for end in (i + 1)..=hi {
7279 if std::str::from_utf8(&bytes[i..end]).is_ok() {
7280 num_in_char = end - i;
7281 break;
7282 }
7283 }
7284 if width {
7285 // c:5717-5723 — WCWIDTH of the assembled character (≥0).
7286 let wcw = std::str::from_utf8(&bytes[i..i + num_in_char])
7287 .ok()
7288 .and_then(|s| s.chars().next())
7289 .and_then(unicode_width::UnicodeWidthChar::width)
7290 .unwrap_or(0);
7291 num += wcw;
7292 } else {
7293 // c:5727 — one character (complete, or invalid treated as one).
7294 num += 1;
7295 }
7296 i += num_in_char;
7297 }
7298 num
7299}
7300
7301/// Multibyte char length with conversion (from utils.c mb_charlenconv_r)
7302/// Port of `mb_charlenconv_r(const char *s, int slen, wint_t *wcp, mbstate_t *mbsp)` from `Src/utils.c:5747`.
7303/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp, mbsp)
7304pub fn mb_charlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
7305 mb_metacharlenconv_r(s, pos)
7306}
7307
7308/// Multibyte char length (from utils.c mb_charlenconv)
7309/// Port of `mb_charlenconv(const char *s, int slen, wint_t *wcp)` from `Src/utils.c:5793`.
7310/// WARNING: param names don't match C — Rust=(s, pos) vs C=(s, slen, wcp)
7311pub fn mb_charlenconv(s: &str, pos: usize) -> usize {
7312 s[pos..].chars().next().map(|c| c.len_utf8()).unwrap_or(0)
7313}
7314
7315/// Port of `int metacharlenconv(const char *x, int *c)` from `Src/utils.c:5810-5826`.
7316/// ```c
7317/// if (*x == Meta) {
7318/// if (c) *c = x[1] ^ 32;
7319/// return 2;
7320/// }
7321/// if (c) *c = (char)*x;
7322/// return 1;
7323/// ```
7324/// Single-byte metafied char advance — Meta+X is 2 bytes (decode
7325/// via XOR 32), plain byte is 1 byte. Previously used hardcoded
7326/// `0x83`; now routes through the canonical `Meta` const for
7327/// maintainability parity.
7328/// WARNING: param names don't match C — Rust=(s) vs C=(x, c)
7329pub fn metacharlenconv(s: &str) -> (usize, Option<char>) {
7330 // c:5811
7331 let bytes = s.as_bytes();
7332 if bytes.is_empty() {
7333 return (0, None);
7334 }
7335 if bytes[0] == Meta && bytes.len() >= 2 {
7336 // c:5818
7337 let raw = bytes[1] as u32 ^ 32; // c:5820
7338 return (2, char::from_u32(raw)); // c:5821
7339 }
7340 (1, Some(bytes[0] as char)) // c:5823-5825
7341}
7342
7343/// Plain (non-metafy) char advance.
7344/// Port of `charlenconv(const char *x, int len, int *c)` from Src/utils.c:5832 — the
7345/// non-MULTIBYTE_SUPPORT branch. Single-byte read with
7346/// `len`-bound check; matches the C source's `if (!len)` early
7347/// exit.
7348/// WARNING: param names don't match C — Rust=(s, len) vs C=(x, len, c)
7349pub fn charlenconv(s: &str, len: usize) -> (usize, Option<char>) {
7350 if len == 0 {
7351 return (0, None);
7352 }
7353 let bytes = s.as_bytes();
7354 if bytes.is_empty() {
7355 return (0, None);
7356 }
7357 (1, Some(bytes[0] as char))
7358}
7359
7360/// Port of `size_t sb_niceformat(const char *s, FILE *stream, char **outstrp, int flags)` from `Src/utils.c:5849-5910`.
7361/// ```c
7362/// ums = ztrdup(s); untokenize(ums); ptr = unmetafy(ums, ¨en);
7363/// while (ptr < eptr) {
7364/// int c = (unsigned char) *ptr;
7365/// if (c == '\'' && (flags & NICEFLAG_QUOTE)) fmt = "\\'";
7366/// else if (c == '\\' && (flags & NICEFLAG_QUOTE)) fmt = "\\\\";
7367/// else fmt = nicechar_sel(c, ...);
7368/// }
7369/// ```
7370/// Single-byte nice format. **Unmetafies the input first** (c:5872),
7371/// then calls `nicechar_sel` for EVERY byte (not just controls).
7372///
7373/// Param mapping (Rule S1, faithful to C):
7374/// - `s: &str` ← C `const char *s`
7375/// - `stream: Option<&mut dyn Write>` ← C `FILE *stream` (`None` ≡ `NULL`)
7376/// - `outstrp: Option<&mut Option<String>>` ← C `char **outstrp`
7377/// (outer `None` ≡ `NULL`; inner `Option<String>` is the storage slot
7378/// the caller passes as `&local`, mirroring `char *retstr;
7379/// sb_niceformat(s, NULL, &retstr, ...);`).
7380/// - returns `usize` ← C `size_t l`.
7381pub fn sb_niceformat(
7382 s: &str,
7383 mut stream: Option<&mut dyn std::io::Write>,
7384 outstrp: Option<&mut Option<String>>,
7385 flags: i32,
7386) -> usize {
7387 // c:5851
7388 let mut l: usize = 0; // c:5853 size_t l = 0
7389 let mut newl: usize; // c:5853 size_t newl
7390 // c:5854 — `int umlen, outalloc, outleft;`. outalloc/outleft model
7391 // C's buffer-growth math; Rust String auto-grows so the realloc
7392 // loop at c:5897-5906 collapses to push_str. umlen carries through.
7393 let umlen: usize; // c:5854
7394 // c:5855 — `char *ums, *ptr, *eptr, *fmt, *outstr, *outptr;`
7395 let mut ums: Vec<u8>; // c:5855 char *ums
7396 let mut ptr: usize; // c:5855 char *ptr
7397 let eptr: usize; // c:5855 char *eptr
7398 let mut fmt: String; // c:5855 char *fmt
7399 let mut outstr: Option<String>; // c:5855 char *outstr
7400
7401 // c:5857-5863 — `if (outstrp) outptr = outstr = zalloc(2*strlen(s));`
7402 if outstrp.is_some() {
7403 // c:5857
7404 outstr = Some(String::with_capacity(2 * s.len())); // c:5859
7405 } else {
7406 // c:5860
7407 outstr = None; // c:5862 outstr = NULL
7408 }
7409
7410 // c:5865 — `ums = ztrdup(s);`
7411 ums = s.as_bytes().to_vec();
7412 // c:5866-5870 — comment-only block carried verbatim:
7413 /*
7414 * is this necessary at this point? niceztrlen does this
7415 * but it's used in lots of places. however, one day this may
7416 * be, too.
7417 */ // c:5866-5870
7418 // c:5871 — `untokenize(ums);` inlined byte-level (lex::untokenize
7419 // is char-based; C does byte-walks). Mirrors `Src/exec.c:2077-2099`
7420 // exec.c untokenize().
7421 let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; // ZTOKENS — Src/lex.c:38
7422 let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
7423 for &c in &ums {
7424 // exec.c:2082
7425 if (0x84u8..=0xa1u8).contains(&c) {
7426 // exec.c:2083 itok(c)
7427 if c != 0xa1u8 {
7428 // exec.c:2086 c != Nularg
7429 let idx = (c - 0x84) as usize;
7430 if idx < ztokens_table.len() {
7431 detok.push(ztokens_table[idx]);
7432 }
7433 }
7434 } else {
7435 detok.push(c); // exec.c:2094
7436 }
7437 }
7438 ums = detok;
7439 // c:5872 — `ptr = unmetafy(ums, ¨en);`
7440 umlen = unmetafy(&mut ums);
7441 ums.truncate(umlen);
7442 eptr = umlen; // c:5873 eptr = ptr + umlen
7443 ptr = 0; // c:5872 ptr starts at 0 in ums
7444
7445 while ptr < eptr {
7446 // c:5875
7447 let c: i32 = ums[ptr] as i32; // c:5876 int c = (unsigned char) *ptr
7448 if c == b'\'' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
7449 // c:5877
7450 fmt = "\\'".to_string(); // c:5878
7451 newl = 2; // c:5879
7452 } else if c == b'\\' as i32 && (flags & NICEFLAG_QUOTE) != 0 {
7453 // c:5881
7454 fmt = "\\\\".to_string(); // c:5882
7455 newl = 2; // c:5883
7456 } else {
7457 // c:5885
7458 fmt = nicechar_sel(c as u8 as char, (flags & NICEFLAG_QUOTE) != 0); // c:5886
7459 newl = 1; // c:5887
7460 }
7461
7462 ptr += 1; // c:5890 ++ptr
7463 l += newl; // c:5891 l += newl
7464
7465 if let Some(ref mut w) = stream {
7466 // c:5893 if (stream)
7467 // c:5894 — `zputs(fmt, stream);`
7468 let _ = w.write_all(fmt.as_bytes());
7469 }
7470 if let Some(ref mut buf) = outstr {
7471 // c:5895 if (outstr)
7472 // c:5896-5912 — append fmt to outstr, growing on demand. Rust
7473 // String auto-grows; the realloc loop at c:5897-5906 collapses
7474 // to push_str (memcpy + outptr/outleft bookkeeping unneeded).
7475 buf.push_str(&fmt); // c:5907 memcpy(outptr, fmt, outlen)
7476 }
7477 // `fmt` is consumed at next iter assignment (C reuses pointer).
7478 let _ = fmt;
7479 }
7480
7481 // c:5915 — `free(ums);` (Rust drop at scope exit)
7482 drop(ums);
7483 if let Some(slot) = outstrp {
7484 // c:5916 if (outstrp)
7485 // c:5917 — `*outptr = '\0';` (no-op for String — push_str leaves
7486 // no embedded NUL, and Rust String is length-prefixed).
7487 // c:5919-5925 — NICEFLAG_NODUP / NICEFLAG_HEAP shaping: in C this
7488 // selects between ztrdup (perm) / dupstring (heap arena) / direct
7489 // ownership-transfer (NODUP). Rust has a single allocator so all
7490 // three paths produce identical owned String contents; transfer
7491 // ownership of `outstr` into the caller's slot.
7492 *slot = outstr.take();
7493 }
7494
7495 l // c:5928 return l
7496}
7497
7498/// Port of `int is_sb_niceformat(const char *s)` from `Src/utils.c:5937-5959`.
7499///
7500/// Predicate: would `sb_niceformat` change the input? Walks each byte
7501/// after unmetafy, returns `1` if any byte is "nice" per `is_nicechar`,
7502/// else `0` (C `int` return faithful to Rule S1).
7503pub fn is_sb_niceformat(s: &str) -> i32 {
7504 // c:5937
7505 // c:5939 — `int umlen, ret = 0;`
7506 let umlen: usize; // c:5939
7507 let mut ret: i32 = 0; // c:5939
7508 // c:5940 — `char *ums, *ptr, *eptr;`
7509 let mut ums: Vec<u8>; // c:5940 char *ums
7510 let mut ptr: usize; // c:5940 char *ptr
7511 let eptr: usize; // c:5940 char *eptr
7512
7513 ums = s.as_bytes().to_vec(); // c:5942 ums = ztrdup(s)
7514 // c:5943 — `untokenize(ums);` inlined byte-level (lex::untokenize is
7515 // char-based; C does byte-walks). Mirrors `Src/exec.c:2077-2099`.
7516 let ztokens_table = b"#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\"; // ZTOKENS — Src/lex.c:38
7517 let mut detok: Vec<u8> = Vec::with_capacity(ums.len());
7518 for &c in &ums {
7519 // exec.c:2082
7520 if (0x84u8..=0xa1u8).contains(&c) {
7521 // exec.c:2083 itok(c)
7522 if c != 0xa1u8 {
7523 // exec.c:2086 c != Nularg
7524 let idx = (c - 0x84) as usize;
7525 if idx < ztokens_table.len() {
7526 detok.push(ztokens_table[idx]);
7527 }
7528 }
7529 } else {
7530 detok.push(c); // exec.c:2094
7531 }
7532 }
7533 ums = detok;
7534 umlen = unmetafy(&mut ums); // c:5944 ptr = unmetafy(ums, ¨en)
7535 ums.truncate(umlen);
7536 eptr = umlen; // c:5945 eptr = ptr + umlen
7537 ptr = 0; // c:5944 ptr starts at 0 in ums
7538
7539 while ptr < eptr {
7540 // c:5947
7541 if is_nicechar(ums[ptr] as char) {
7542 // c:5948 is_nicechar(*ptr)
7543 ret = 1; // c:5949
7544 break; // c:5950
7545 }
7546 ptr += 1; // c:5952 ++ptr
7547 }
7548
7549 drop(ums); // c:5955 free(ums)
7550 ret // c:5957 return ret
7551}
7552
7553/// Tab expansion — direct port of `zexpandtabs(const char *s, int len, int width, int startpos, FILE *fout, int all)` in zsh/Src/utils.c:5975.
7554/// Writes `s` into `out` with TAB characters expanded to spaces against
7555/// a tabstop of `width`. `startpos` carries the cumulative emitted
7556/// column from previous calls (used by `print -X` which preserves
7557/// alignment across args). When `all_tabs` is false, only leading TABs
7558/// (those at the start of a line) are expanded; embedded TABs are
7559/// emitted verbatim and `startpos` is advanced by one tabstop. When
7560/// `all_tabs` is true, every TAB expands. Returns the new `startpos`.
7561pub(crate) fn zexpandtabs(
7562 s: &str,
7563 width: i32,
7564 startpos: i32,
7565 all_tabs: bool,
7566 out: &mut String,
7567) -> i32 {
7568 let mut startpos = startpos;
7569 let mut at_start = true;
7570 for c in s.chars() {
7571 if c == '\t' {
7572 if all_tabs || at_start {
7573 if width <= 0 || startpos % width == 0 {
7574 out.push(' ');
7575 startpos += 1;
7576 }
7577 if width > 0 {
7578 while startpos % width != 0 {
7579 out.push(' ');
7580 startpos += 1;
7581 }
7582 }
7583 } else {
7584 let rem = startpos % width;
7585 startpos += width - rem;
7586 out.push('\t');
7587 }
7588 continue;
7589 } else if c == '\n' || c == '\r' {
7590 out.push(c);
7591 startpos = 0;
7592 at_start = true;
7593 continue;
7594 }
7595 at_start = false;
7596 out.push(c);
7597 startpos += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as i32;
7598 }
7599 startpos
7600}
7601
7602/// Port of `int hasspecial(char const *s)` from `Src/utils.c:6072-6082`.
7603/// ```c
7604/// while (*s) {
7605/// if (ispecial(*s == Meta ? *++s ^ 32 : *s)) return 1;
7606/// s++;
7607/// }
7608/// return 0;
7609/// ```
7610/// Predicate: does `s` contain any byte that needs shell-quoting?
7611/// Routes through the canonical typtab-driven `ztype_h::ispecial`
7612/// which respects the `ZTF_SP_COMMA` (set by `makecommaspecial`)
7613/// and `ZTF_BANGCHAR` (BANGHIST + interact) flags. Previously used
7614/// a hardcoded char list — diverged from C for `,` (KSH_GLOB
7615/// extended-glob) and for the dynamic bangchar (`!` by default but
7616/// user-rewritable via `$HISTCHARS`).
7617pub fn hasspecial(s: &str) -> bool {
7618 // c:6072
7619 let bytes = s.as_bytes();
7620 let mut i = 0;
7621 while i < bytes.len() {
7622 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
7623 // c:6075 — `*s == Meta ? *++s ^ 32 : *s`.
7624 let v = bytes[i + 1] ^ 32;
7625 i += 2;
7626 v
7627 } else {
7628 let v = bytes[i];
7629 i += 1;
7630 v
7631 };
7632 if crate::ported::ztype_h::ispecial(c) {
7633 // c:6075
7634 return true;
7635 }
7636 }
7637 false
7638}
7639
7640/// Port of `static char *addunprintable(char *v, const char *u, const char *uend)`
7641/// from `Src/utils.c:6082-6124`. Renders unprintable bytes using
7642/// **shell-compatible C-string escapes**:
7643/// - `\0` (NUL, with `\000` form if next byte is octal-digit)
7644/// - `\a` (0x07 BEL), `\b` (BS), `\f` (FF), `\n` (LF),
7645/// `\r` (CR), `\t` (TAB), `\v` (VT)
7646/// - `\nnn` 3-digit octal fallback for any other control byte
7647///
7648/// Previously the Rust port emitted ZLE-style caret notation (`^X`,
7649/// `\u{:04x}`) — completely different convention. C semantics target
7650/// `printf %q` / `$'...'` reuse where the output must round-trip
7651/// through `printf %b` to reconstruct the original byte. Caret
7652/// notation is what ZLE *displays* but not what `addunprintable`
7653/// emits.
7654/// WARNING: param names don't match C — Rust=(c) vs C=(v, u, uend)
7655pub fn addunprintable(c: char) -> String {
7656 // c:6082
7657 let b = c as u32 & 0xff;
7658 match b as u8 {
7659 // c:6097-6103 — `\0`. C peeks next byte and uses `\000` form
7660 // when followed by an octal digit. Rust port works on a single
7661 // char so emits just `\0`; callers needing the disambiguation
7662 // form can pass the lookahead byte separately.
7663 0x00 => "\\0".to_string(),
7664 0x07 => "\\a".to_string(), // c:6106
7665 0x08 => "\\b".to_string(), // c:6107
7666 0x0c => "\\f".to_string(), // c:6108
7667 0x0a => "\\n".to_string(), // c:6109
7668 0x0d => "\\r".to_string(), // c:6110
7669 0x09 => "\\t".to_string(), // c:6111
7670 0x0b => "\\v".to_string(), // c:6112
7671 // c:6114-6119 — `\nnn` 3-digit octal default.
7672 _ => format!("\\{:o}{:o}{:o}", (b >> 6) & 7, (b >> 3) & 7, b & 7),
7673 }
7674}
7675
7676/// One element of a metafied string, as C's `MB_METACHARLENCONV` loop sees it:
7677/// either a decoded character, or a raw byte that is not one.
7678///
7679/// A byte that cannot appear in a Rust `String` (an invalid-UTF-8 byte such as
7680/// the 0xE1 produced by `$'\M-a'`) is stored metafied — `Meta` (0x83) followed
7681/// by `byte ^ 32` — so a plain `.chars()` walk sees two innocuous characters
7682/// where the shell means one raw byte, and passes them straight through. C hits
7683/// exactly this case as `WEOF` out of `MB_METACHARLENCONV` (c:6210) and routes
7684/// it to `addunprintable()`.
7685#[derive(Clone, Copy, PartialEq)]
7686enum MetaChar {
7687 Ch(char),
7688 Raw(u8),
7689}
7690
7691/// Quote a string according to the specified type
7692/// Port from zsh/Src/utils.c quotestring() (lines 6141-6452)
7693/// Quote a string per the requested bslashquote style.
7694/// Port of `quotestring(const char *s, int instring)` from Src/utils.c — used by `print
7695/// -%q`, `${(q)var}`, completion-output escaping, history
7696/// re-emission.
7697/// WARNING: param names don't match C — Rust=(s, quote_type) vs C=(s, instring)
7698pub fn quotestring(s: &str, quote_type: i32) -> String {
7699 // c:6207-6210 — C walks the string with MB_METACHARINIT/MB_METACHARLENCONV,
7700 // which yields one decoded character at a time, or WEOF for a byte that does
7701 // not decode. These three closures are that walk, kept inline exactly as C
7702 // keeps it inline in each arm below.
7703 //
7704 // `meta_chars` is the decoder: a byte that cannot live in a Rust `String`
7705 // (an invalid-UTF-8 byte such as the 0xE1 from `$'\M-a'`) is stored metafied
7706 // — Meta (0x83) then `byte ^ 32` — so a naive `.chars()` walk sees two
7707 // innocuous characters where the shell means one raw byte and passes them
7708 // straight through. That is C's WEOF case, and it must reach addunprintable.
7709 let meta_chars = |s: &str| -> Vec<MetaChar> {
7710 let mut out = Vec::with_capacity(s.len());
7711 let mut it = s.chars().peekable();
7712 while let Some(c) = it.next() {
7713 if c as u32 == Meta as u32 {
7714 if let Some(&n) = it.peek() {
7715 let nu = n as u32;
7716 if (0x80..=0xff).contains(&nu) {
7717 it.next();
7718 out.push(MetaChar::Raw((nu as u8) ^ 32));
7719 continue;
7720 }
7721 }
7722 }
7723 out.push(MetaChar::Ch(c));
7724 }
7725 out
7726 };
7727 // c:6212 — `cc != WEOF && WC_ISPRINT(cc)`. A raw byte is never printable: it
7728 // IS the WEOF case.
7729 let mc_printable = |mc: MetaChar| -> bool {
7730 match mc {
7731 MetaChar::Ch(c) => !c.is_control(),
7732 MetaChar::Raw(_) => false,
7733 }
7734 };
7735 // c:6082-6124 addunprintable — emit a non-printable element BYTE by byte, so
7736 // a non-printable multibyte character becomes one escape per UTF-8 byte.
7737 // `\0` widens to `\000` when an octal digit follows so the escape cannot
7738 // swallow it (c:6097-6103). There is deliberately no `\e` case: C has none,
7739 // so ESC comes out as the octal `\033`.
7740 let push_unprintable = |out: &mut String, mc: MetaChar, next: Option<MetaChar>| {
7741 let mut buf = [0u8; 4];
7742 let bytes: &[u8] = match mc {
7743 MetaChar::Ch(c) => c.encode_utf8(&mut buf).as_bytes(),
7744 MetaChar::Raw(b) => {
7745 buf[0] = b;
7746 &buf[0..1]
7747 }
7748 };
7749 for i in 0..bytes.len() {
7750 let b = bytes[i];
7751 if b == 0 {
7752 out.push_str("\\0");
7753 let follows = if i + 1 < bytes.len() {
7754 Some(bytes[i + 1])
7755 } else {
7756 next.map(|n| match n {
7757 MetaChar::Ch(c) => {
7758 let mut nb = [0u8; 4];
7759 c.encode_utf8(&mut nb).as_bytes()[0]
7760 }
7761 MetaChar::Raw(rb) => rb,
7762 })
7763 };
7764 if matches!(follows, Some(b'0'..=b'7')) {
7765 out.push_str("00");
7766 }
7767 } else {
7768 out.push_str(&addunprintable(b as char));
7769 }
7770 }
7771 };
7772
7773 // c:6141
7774 if s.is_empty() {
7775 return if quote_type == QT_NONE {
7776 String::new()
7777 } else if quote_type == QT_BACKSLASH {
7778 // c:6194 `if (!*s && shownull)` — shownull is 0 for plain
7779 // QT_BACKSLASH, so an empty string produces NO quotes.
7780 String::new()
7781 } else if quote_type == QT_BACKSLASH_SHOWNULL {
7782 // c:6165 sets shownull=1, so empty → '' (c:6194 adds the pair).
7783 "''".to_string()
7784 } else if quote_type == QT_SINGLE || quote_type == QT_SINGLE_OPTIONAL {
7785 "''".to_string()
7786 } else if quote_type == QT_DOUBLE {
7787 "\"\"".to_string()
7788 } else if quote_type == QT_DOLLARS {
7789 "$''".to_string()
7790 } else {
7791 String::new()
7792 };
7793 }
7794
7795 if quote_type == QT_NONE {
7796 s.to_string()
7797 } else if quote_type == QT_BACKSLASH_PATTERN {
7798 // Only bslashquote pattern characters (lines 6242-6247)
7799 let mut result = String::with_capacity(s.len() * 2);
7800 for c in s.chars() {
7801 if matches!(
7802 c,
7803 '*' | '?' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '#' | '^' | '~'
7804 ) {
7805 result.push('\\');
7806 }
7807 result.push(c);
7808 }
7809 result
7810 } else if quote_type == QT_BACKSLASH || quote_type == QT_BACKSLASH_SHOWNULL {
7811 // c:Src/utils.c:6260-6452 QT_BACKSLASH. Three sub-cases per
7812 // input char (after the special-syntax handling above):
7813 // - `\n` → emit `$'\n'` literal 5 bytes (c:6366-6371)
7814 // - special-and-printable → emit `\<char>` (c:6385-6395)
7815 // - non-printable (ctrl, multibyte outside printable
7816 // range) → emit `$'<addunprintable>'` (c:6412-6422)
7817 // Bug #144/#149 in docs/BUGS.md: previous port mapped ALL
7818 // ispecial chars to `\<char>`, so `\n`/`\t`/etc. came out
7819 // as `\<actual-byte>` instead of `$'\n'`/`$'\t'`.
7820 //
7821 // c:6301-6306 — `=` and `~` are NOT escaped unless one of:
7822 // (B) the char is at position 0 (u == s)
7823 // (C) MAGICEQUALSUBST is set AND the previous byte is `=`
7824 // or `:` (covers `var==val`, `var:=val`)
7825 // (D) the char is `~` AND EXTENDEDGLOB is set
7826 // Without these conditions, mid-word `=`/`~` falls through
7827 // to the literal-emit path (c:6418-6434).
7828 let mut result = String::with_capacity(s.len() * 2);
7829 let mut prev: char = '\0'; // would-be u[-1]
7830 let mcs = meta_chars(s);
7831 for i in 0..mcs.len() {
7832 let mc = mcs[i];
7833 let c = match mc {
7834 MetaChar::Ch(c) => c,
7835 // A raw byte is never `\n` and never ispecial — it is the
7836 // not-printable case below. Stand in with its byte value so the
7837 // `=`/`~` lookbehind still advances correctly.
7838 MetaChar::Raw(b) => b as char,
7839 };
7840 // c:6301-6306 gate for `=`/`~` (condition A/B/C/D).
7841 let eq_tilde_gate = if c == '=' || c == '~' {
7842 i == 0 // c:6303 u == s
7843 || (isset(MAGICEQUALSUBST) && (prev == '=' || prev == ':')) // c:6304-6305
7844 || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
7845 } else {
7846 true // c:6302 *u != '=' && *u != '~'
7847 };
7848 if mc == MetaChar::Ch('\n') {
7849 // c:6366-6371 — newline gets dedicated `$'\n'` form.
7850 result.push_str("$'\\n'");
7851 } else if matches!(mc, MetaChar::Ch(_))
7852 && ispecial(c)
7853 && eq_tilde_gate
7854 && c.is_ascii()
7855 && !c.is_ascii_control()
7856 {
7857 // c:6385-6395 — printable special → `\<char>`.
7858 result.push('\\');
7859 result.push(c);
7860 } else if !mc_printable(mc) {
7861 // c:6412-6422 — anything not printable (a control char, a
7862 // non-printable multibyte char, or a raw undecodable byte) is
7863 // wrapped one element at a time as `$'<addunprintable>'`.
7864 result.push_str("$'");
7865 push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
7866 result.push('\'');
7867 } else {
7868 result.push(c);
7869 }
7870 prev = c;
7871 }
7872 result
7873 } else if quote_type == QT_SINGLE {
7874 // c:Src/utils.c:6300-6395 QT_SINGLE — `(qq)` flag.
7875 // The outer ispecial-gated branch at c:6301-6313 only
7876 // triggers special handling when the QT-specific
7877 // subcondition matches; for QT_SINGLE that's `*u == '\''`.
7878 // Newlines do NOT match, so they fall through to the
7879 // pass-through path at c:6394 and are emitted LITERALLY
7880 // inside the surrounding `'…'`. Bug #199 in docs/BUGS.md:
7881 // previous port emitted `'$'\n''` (split-quote with
7882 // `$'\n'` escape between segments) for newline, diverging
7883 // from zsh's literal-newline-in-single-quotes output.
7884 let mut result = String::with_capacity(s.len() + 4);
7885 result.push('\'');
7886 for c in s.chars() {
7887 if c == '\'' {
7888 // c:6364-6379 — apostrophe close-reopen sequence.
7889 result.push_str("'\\''");
7890 } else {
7891 // Newline + everything else: pass through literally.
7892 result.push(c);
7893 }
7894 }
7895 result.push('\'');
7896 result
7897 } else if quote_type == QT_SINGLE_OPTIONAL {
7898 // c:Src/utils.c:6314-6385 QT_SINGLE_OPTIONAL — minimum
7899 // quoting. Walks the string with two states:
7900 // quotesub=1 — not currently inside a quote span. Bare
7901 // apostrophes get `\'` (backslash form). Any OTHER
7902 // special char triggers back-filling: insert `'` at
7903 // `quotestart` (start of unquoted prefix), shifting
7904 // subsequent chars right, then push char, transition
7905 // to quotesub=2.
7906 // quotesub=2 — currently inside a `'…'` span. Bare
7907 // apostrophes break the span: push `'\\'`, transition
7908 // back to quotesub=1 with quotestart=position-after.
7909 // Other specials append in-place.
7910 // End: if quotesub=2, close with `'`.
7911 // For "hello world" this yields `'hello world'` (back-
7912 // filled at start). For "it's" it yields `it\'s` (no quote
7913 // span ever opens). The naive per-char approach without
7914 // back-filling produced `hello' 'world` — parity bug.
7915 //
7916 // c:6301-6306 — the SAME `=`/`~` gate the QT_BACKSLASH arm applies.
7917 // `ispecial` alone would open a quote span on any mid-word `=` or `~`,
7918 // so `a=b` came out as `'a=b'` where zsh leaves it bare.
7919 let chars: Vec<char> = s.chars().collect();
7920 let forces_quote = |i: usize, c: char| -> bool {
7921 if !ispecial(c) {
7922 return false; // c:6301
7923 }
7924 if c == '=' || c == '~' {
7925 i == 0 // c:6303 u == s
7926 || (isset(MAGICEQUALSUBST) && (chars[i - 1] == '=' || chars[i - 1] == ':')) // c:6304-6305
7927 || (c == '~' && isset(EXTENDEDGLOB)) // c:6306
7928 } else {
7929 true // c:6302
7930 }
7931 };
7932 let needs_quoting = chars.iter().enumerate().any(|(i, &c)| forces_quote(i, c));
7933 if !needs_quoting {
7934 return s.to_string();
7935 }
7936 let mut result: Vec<char> = Vec::with_capacity(s.len() + 4);
7937 let mut quotestart: usize = 0; // index in `result` where the next `'` would go
7938 let mut quotesub: u8 = 1; // 1 = not quoting, 2 = inside `'…'`
7939 for (i, &c) in chars.iter().enumerate() {
7940 if c == '\'' {
7941 if quotesub == 2 {
7942 // close current quote span, then `\'`, then
7943 // mark that we may need to reopen on next special
7944 result.push('\'');
7945 result.push('\\');
7946 result.push('\'');
7947 quotesub = 1;
7948 quotestart = result.len();
7949 } else {
7950 result.push('\\');
7951 result.push('\'');
7952 quotestart = result.len();
7953 }
7954 } else if forces_quote(i, c) {
7955 if quotesub == 1 {
7956 // Back-fill: insert `'` at quotestart, shifting
7957 // everything after right by 1.
7958 result.insert(quotestart, '\'');
7959 quotesub = 2;
7960 }
7961 result.push(c);
7962 } else {
7963 result.push(c);
7964 }
7965 }
7966 if quotesub == 2 {
7967 result.push('\'');
7968 }
7969 result.into_iter().collect()
7970 } else if quote_type == QT_DOUBLE {
7971 // Double quote: "string" (lines 6272-6280, 6311-6312)
7972 let mut result = String::with_capacity(s.len() + 4);
7973 result.push('"');
7974 for c in s.chars() {
7975 if matches!(c, '$' | '`' | '"' | '\\') {
7976 result.push('\\');
7977 }
7978 result.push(c);
7979 }
7980 result.push('"');
7981 result
7982 } else if quote_type == QT_DOLLARS {
7983 // c:6203-6241 — `$'…'` quoting. Each element is either printable, in
7984 // which case it is emitted verbatim (backslashing `\`, `'`, and — under
7985 // BANGHIST — the history character), or it is NOT, in which case C hands
7986 // its raw bytes to addunprintable(): a named escape (`\t`, `\n`, `\a`, …)
7987 // or a 3-digit octal one.
7988 //
7989 // Two things this must NOT do, both of which the previous port did:
7990 // * pass an undecodable byte through raw. `$'\M-a'` is the byte 0xE1,
7991 // which is not valid UTF-8 and is stored metafied; walking `.chars()`
7992 // re-emitted it verbatim instead of `\341`, so the output could not
7993 // be re-parsed.
7994 // * emit `\e` for ESC. C's addunprintable has no `\e` case — ESC comes
7995 // out as the octal `\033`.
7996 let mut result = String::with_capacity(s.len() + 4);
7997 result.push_str("$'");
7998 let mcs = meta_chars(s);
7999 let bang = crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
8000 for i in 0..mcs.len() {
8001 let mc = mcs[i];
8002 if let (true, MetaChar::Ch(c)) = (mc_printable(mc), mc) {
8003 // c:6214-6224
8004 if c == '\\' || c == '\'' || (isset(BANGHIST) && bang != 0 && c as u32 == bang as u32)
8005 {
8006 result.push('\\');
8007 }
8008 result.push(c);
8009 } else {
8010 // c:6226-6229
8011 push_unprintable(&mut result, mc, mcs.get(i + 1).copied());
8012 }
8013 }
8014 result.push('\'');
8015 result
8016 } else if quote_type == QT_BACKTICK {
8017 // Backtick quoting (minimal - just escape backticks)
8018 s.replace('`', "\\`")
8019 } else {
8020 // Unknown quote_type — treat as no-op to match C's `default:` arm.
8021 s.to_string()
8022 }
8023}
8024
8025// ===========================================================
8026// xtrace helpers moved from src/ported/vm_helper.
8027// printprompt4 is a direct port of utils.c:1718-1735; quotedzputs
8028// is its argument-formatter companion (zsh formats `set -x` lines
8029// via the same utils.c path).
8030// ===========================================================
8031
8032/// Port of `quotedzputs(char const *s, FILE *stream)` from `Src/utils.c:6464`.
8033///
8034/// Quote a string for re-readable output (`set -x`, `typeset -p`,
8035/// `set` listing, etc.). zsh's algorithm under MULTIBYTE_SUPPORT
8036/// (c:6464-6543):
8037/// 1. empty input → `''` (c:6470-6475)
8038/// 2. needs nice-format (controls / non-printables) → emit
8039/// `$'<mb_niceformat output>'` (c:6478-6492)
8040/// 3. no SPECCHARS member → return string unchanged (c:6511-6517)
8041/// 4. otherwise wrap in `'…'` (Bourne) or RCQUOTES form,
8042/// with embedded `'` rewritten as `'\''` / `''` (c:6533-6587)
8043///
8044/// Previous Rust port omitted step 2 (the `$'…'` branch).
8045/// Result: strings containing control bytes (`\n`, `\t`, escape
8046/// sequences) were single-quoted instead of `$'…'`-quoted,
8047/// breaking round-trip through `typeset -p` / `set` / `set -x`
8048/// because POSIX single-quotes are *strong* — embedded `\n` would
8049/// be re-fed as a literal newline rather than the C-escape.
8050///
8051/// The C signature is `char *quotedzputs(char const *s, FILE *stream)`
8052/// — when `stream` is non-NULL it writes there and returns NULL,
8053/// otherwise it returns the quoted string. Rust's variant covers
8054/// only the `stream==NULL` form (the `set -x` callers all want the
8055/// string back, not direct stdout writing). The `stream==Some` form
8056/// is fputs/fputc-direct in C; Rust callers that need writer-based
8057/// output should compose `print!`/`write!` with this fn's return.
8058/// WARNING: param names don't match C — Rust=(s) vs C=(s, stream)
8059pub(crate) fn quotedzputs(s: &str) -> String {
8060 // c:6464
8061 // c:6469-6475 — `if (!*s)` empty string emits `''` literal.
8062 if s.is_empty() {
8063 return "''".to_string(); // c:6472
8064 }
8065 // c:6477-6508 — `is_mb_niceformat(s)` / `is_sb_niceformat(s)` arm:
8066 // if the string contains nice-formatted chars (controls,
8067 // non-printables), wrap in `$'…'` using sb/mb_niceformat with
8068 // NICEFLAG_QUOTE so embedded `'`/`\` get backslash-escaped.
8069 if is_mb_niceformat(s) != 0 {
8070 // c:6478-6492 (MULTIBYTE_SUPPORT branch): use mb_niceformat
8071 // with NICEFLAG_QUOTE so multi-byte chars round-trip through
8072 // wcs_nicechar (raw UTF-8 for printable wides, `\u`/`\U` for
8073 // large codepoints) AND `'`/`\\` get backslash-escaped.
8074 // Under !MULTIBYTE_SUPPORT (c:6494-6508) C would use
8075 // sb_niceformat instead. Static-link path: MULTIBYTE is
8076 // always available in Rust.
8077 // c:6485-6492 — `mb_niceformat(s, NULL, &substr,
8078 // NICEFLAG_QUOTE|NICEFLAG_NODUP);`
8079 let mut substr: Option<String> = None;
8080 let _ = mb_niceformat(s, None, Some(&mut substr), NICEFLAG_QUOTE | NICEFLAG_NODUP);
8081 return format!("$'{}'", substr.unwrap_or_default()); // c:6488
8082 }
8083 // c:6511-6518 — `if (!hasspecial(s)) return dupstring(s);`.
8084 if !hasspecial(s) {
8085 // c:6511
8086 return s.to_string(); // c:6516
8087 }
8088 // c:6520-6529 — outstr buffer alloc (zhalloc). Rust uses growable
8089 // String; the C `l = strlen(s) + 2 + (per-' overhead)` size hint
8090 // is just allocation tuning and doesn't affect output.
8091 let mut out = String::with_capacity(s.len() + 2);
8092 let bytes = s.as_bytes();
8093 let csh_junkie = isset(CSHJUNKIEQUOTES); // c:6554 / c:6612
8094
8095 if isset(RCQUOTES) {
8096 // c:6533 — RCQUOTES: wrap entire string in `'…'`; each
8097 // embedded `'` becomes `''` (the rc-style doubled quote).
8098 out.push('\''); // c:6539
8099 // c:6540-6547 — C walks METAFIED bytes; the Rust String is
8100 // UTF-8, so the faithful transposition walks CHARS (a byte
8101 // walk Latin-1-casts every multibyte char: em-dash E2 80 94
8102 // became "â" + a token byte that downstream passes ate).
8103 // Token chars (Dash U+009B, Meta U+0083) are codepoints here.
8104 let chars_v: Vec<char> = s.chars().collect();
8105 let mut i = 0;
8106 while i < chars_v.len() {
8107 let c = if chars_v[i] == Dash {
8108 // c:6541 — `if (*s == Dash) c = '-';`
8109 i += 1;
8110 '-'
8111 } else if chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len() {
8112 // c:6543 — `else if (*s == Meta) c = *++s ^ 32;`
8113 let n = chars_v[i + 1] as u32;
8114 i += 2;
8115 char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
8116 } else {
8117 // c:6546 — `else c = *s;`
8118 let dec = chars_v[i];
8119 i += 1;
8120 dec
8121 };
8122 if c == '\'' {
8123 // c:6548-6553 — `if (c == '\'') *ptr++ = '\'';` (the
8124 // rc-quote doubling).
8125 out.push('\''); // c:6553
8126 } else if c == '\n' && csh_junkie {
8127 // c:6554-6560 — `if (c == '\n' && isset(CSHJUNKIEQUOTES))
8128 // *ptr++ = '\\';`
8129 out.push('\\'); // c:6559
8130 }
8131 // c:6561-6570 — emit c (metafy on imeta).
8132 out.push(c); // c:6569 (non-stream branch always re-metafies;
8133 // Rust String holds decoded chars directly)
8134 }
8135 out.push('\''); // c:6576
8136 } else {
8137 // c:6578-6637 — Bourne-style quoting, "avoiding empty quoted
8138 // strings". Tracks `inquote` so that `it's` becomes
8139 // `'it'\''s'` (no empty `''` runs).
8140 let mut inquote = false; // c:6466 (initialised at top of C fn)
8141 // c:6579-6586 — char-wise decode, same transposition as the
8142 // RCQUOTES arm above (C's byte walk is over metafied bytes).
8143 let chars_v: Vec<char> = s.chars().collect();
8144 let mut i = 0;
8145 while i < chars_v.len() {
8146 let c = if chars_v[i] == Dash {
8147 i += 1;
8148 '-' // c:6581
8149 } else if chars_v[i] as u32 == Meta as u32 && i + 1 < chars_v.len() {
8150 let n = chars_v[i + 1] as u32; // c:6583
8151 i += 2;
8152 char::from_u32(n ^ 32).unwrap_or(chars_v[i - 1])
8153 } else {
8154 let dec = chars_v[i]; // c:6585
8155 i += 1;
8156 dec
8157 };
8158 if c == '\'' {
8159 // c:6587-6602 — `'` closes any open inquote then emits
8160 // `\'` outside the quotes.
8161 if inquote {
8162 out.push('\''); // c:6593
8163 inquote = false; // c:6594
8164 }
8165 out.push('\\'); // c:6600
8166 out.push('\''); // c:6601
8167 } else {
8168 // c:6603-6629 — other chars open a quote run if not
8169 // already open, optionally backslash-escape `\n` under
8170 // CSHJUNKIEQUOTES, then emit the byte.
8171 if !inquote {
8172 out.push('\''); // c:6609
8173 inquote = true; // c:6610
8174 }
8175 if c == '\n' && csh_junkie {
8176 out.push('\\'); // c:6617
8177 }
8178 out.push(c); // c:6627 (imeta-encoding handled by Rust
8179 // String storage in the non-stream form)
8180 }
8181 }
8182 if inquote {
8183 out.push('\''); // c:6636
8184 }
8185 }
8186 // c:6639-6640 — `if (!stream) *ptr++ = '\0';` — Rust String already
8187 // NUL-terminated implicitly; no-op.
8188 out // c:6642
8189}
8190
8191/// Port of `char *dquotedztrdup(char const *s)` from `Src/utils.c:6648-6723`.
8192/// Two arms (selected by `isset(CSHJUNKIEQUOTES)` at c:6655):
8193///
8194/// **CSHJUNKIEQUOTES path** (c:6656-6686): the csh-junk-quote style
8195/// where only the non-special sections are wrapped in `"..."` and
8196/// special chars (`"`, `$`, `` ` ``) appear OUTSIDE the quotes with
8197/// backslash escape. `\n` inside the quotes gets an extra `\` so it
8198/// round-trips through history.
8199///
8200/// **Default path** (c:6687-6719): wraps the whole string in `"..."`.
8201/// `\` is doubled to `\\`. `"`, `$`, `` ` `` get backslash-escaped.
8202/// A trailing `\` gets an extra `\` appended (the `pending` quirk).
8203///
8204/// Previously the Rust port only implemented the default arm; the
8205/// CSHJUNKIEQUOTES path is now ported faithfully.
8206pub fn dquotedztrdup(s: &str) -> String {
8207 // c:6648
8208 let mut out = String::with_capacity(s.len() * 4 + 2);
8209 let bytes = s.as_bytes();
8210 // c:6655 — `if (isset(CSHJUNKIEQUOTES))`.
8211 if isset(CSHJUNKIEQUOTES) {
8212 let mut inquote = false;
8213 let mut i = 0;
8214 while i < bytes.len() {
8215 // c:6661-6662 — Meta byte decode.
8216 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
8217 i += 2;
8218 (bytes[i - 1] ^ 32) as char
8219 } else {
8220 i += 1;
8221 bytes[i - 1] as char
8222 };
8223 match c {
8224 // c:6664-6673 — `"` / `$` / `` ` `` — close quote
8225 // (if open), then `\<c>`.
8226 '"' | '$' | '`' => {
8227 if inquote {
8228 out.push('"');
8229 inquote = false;
8230 }
8231 out.push('\\');
8232 out.push(c);
8233 }
8234 // c:6674-6682 — default arm: open quote if needed,
8235 // backslash-escape newline, emit char.
8236 _ => {
8237 if !inquote {
8238 out.push('"');
8239 inquote = true;
8240 }
8241 if c == '\n' {
8242 out.push('\\');
8243 }
8244 out.push(c);
8245 }
8246 }
8247 }
8248 // c:6685-6686 — close trailing quote.
8249 if inquote {
8250 out.push('"');
8251 }
8252 } else {
8253 // c:6687-6718 — default (non-CSH) arm.
8254 out.push('"');
8255 let mut pending = false;
8256 let mut i = 0;
8257 while i < bytes.len() {
8258 let c = if bytes[i] == Meta && i + 1 < bytes.len() {
8259 i += 2;
8260 (bytes[i - 1] ^ 32) as char
8261 } else {
8262 i += 1;
8263 bytes[i - 1] as char
8264 };
8265 match c {
8266 '\\' => {
8267 if pending {
8268 out.push('\\');
8269 }
8270 out.push('\\');
8271 pending = true;
8272 }
8273 '"' | '$' | '`' => {
8274 if pending {
8275 out.push('\\');
8276 }
8277 out.push('\\');
8278 out.push(c);
8279 pending = false;
8280 }
8281 other => {
8282 out.push(other);
8283 pending = false;
8284 }
8285 }
8286 }
8287 if pending {
8288 out.push('\\');
8289 }
8290 out.push('"');
8291 }
8292 // c:6720 — `ret = metafy(buf, p - buf, META_DUP);` re-metafy result.
8293 metafy(&out)
8294}
8295
8296/// Port of `dquotedzputs(char const *s, FILE *stream)` from
8297/// Src/utils.c:6729. C body (4 lines):
8298/// `char *d = dquotedztrdup(s);
8299/// int ret = zputs(d, stream);
8300/// zsfree(d);
8301/// return ret;`
8302/// Rust returns the quoted string directly (callers compose via
8303/// `format!` rather than streaming through a FILE*), so the zputs
8304/// call drops.
8305pub fn dquotedzputs(s: &str) -> String {
8306 // c:6729
8307 dquotedztrdup(s) // c:6731
8308}
8309
8310/// Convert UCS-4 to UTF-8 (from utils.c ucs4toutf8)
8311/// Port of `ucs4toutf8(char *dest, unsigned int wval)` from `Src/utils.c:6743`.
8312///
8313/// C accepts 0..=0x7FFFFFFF (legacy UCS-4 / 6-byte UTF-8) — wider
8314/// than Unicode's 0..=0x10FFFF — to match `wctomb(3)` on Linux
8315/// with UTF-8 locale. The Rust `char::from_u32` path would reject
8316/// codepoints above 0x10FFFF (and surrogates), so this port mirrors
8317/// the C bit-pattern dispatch directly.
8318///
8319/// C body (c:6743-6779):
8320/// ```c
8321/// if (wval < 0x80) len = 1; // ASCII
8322/// else if (wval < 0x800) len = 2;
8323/// else if (wval < 0x10000) len = 3;
8324/// else if (wval < 0x200000) len = 4;
8325/// else if (wval < 0x4000000) len = 5;
8326/// else if (wval < 0x80000000) len = 6;
8327/// else { zerr("character not in range"); return -1; }
8328///
8329/// switch (len) { /* falls through except to the last case */
8330/// case 6: dest[5] = (wval & 0x3f) | 0x80; wval >>= 6;
8331/// case 5: dest[4] = (wval & 0x3f) | 0x80; wval >>= 6;
8332/// case 4: dest[3] = (wval & 0x3f) | 0x80; wval >>= 6;
8333/// case 3: dest[2] = (wval & 0x3f) | 0x80; wval >>= 6;
8334/// case 2: dest[1] = (wval & 0x3f) | 0x80; wval >>= 6;
8335/// *dest = wval | ((0xfc << (6 - len)) & 0xfc);
8336/// break;
8337/// case 1: *dest = wval;
8338/// }
8339/// return len;
8340/// ```
8341///
8342/// Returns the encoded bytes (1..=6 long) on success, None on
8343/// out-of-range. C's `zerr("character not in range")` (c:6763) is
8344/// not emitted here — the return signals the error.
8345/// WARNING: param names don't match C — Rust=(wval) vs C=(dest, wval)
8346pub fn ucs4toutf8(wval: u32) -> Option<String> {
8347 // c:6743
8348 let len: usize = if wval < 0x80 {
8349 1
8350 }
8351 // c:6750
8352 else if wval < 0x800 {
8353 2
8354 }
8355 // c:6752
8356 else if wval < 0x10000 {
8357 3
8358 }
8359 // c:6754
8360 else if wval < 0x200000 {
8361 4
8362 }
8363 // c:6756
8364 else if wval < 0x4000000 {
8365 5
8366 }
8367 // c:6758
8368 else if wval < 0x80000000 {
8369 6
8370 }
8371 // c:6760
8372 else {
8373 // c:6762
8374 zerr("character not in range"); // c:6763
8375 return None; // c:6764
8376 };
8377 let mut buf = [0u8; 6];
8378 let mut w = wval;
8379 // c:6767-6776 — fall-through switch building trailing bytes first.
8380 match len {
8381 1 => {
8382 buf[0] = w as u8;
8383 } // c:6775
8384 n => {
8385 // Trailing (len-1) bytes: each is `(w & 0x3f) | 0x80`,
8386 // shifting w right 6 between them (c:6768-6772).
8387 for i in (1..n).rev() {
8388 buf[i] = ((w & 0x3f) as u8) | 0x80;
8389 w >>= 6;
8390 }
8391 // Leading byte: `w | ((0xfc << (6 - len)) & 0xfc)` (c:6773).
8392 buf[0] = (w as u8) | (((0xfcu32 << (6 - n)) & 0xfc) as u8);
8393 }
8394 }
8395 Some(String::from_utf8_lossy(&buf[..len]).into_owned())
8396}
8397
8398/// Port of `ucs4tomb(unsigned int wval, char *buf)` from `Src/utils.c:6788`.
8399///
8400/// Encode a UCS-4 codepoint into the buffer `buf` using the current
8401/// locale's multibyte encoding. Returns the number of bytes written,
8402/// or -1 on conversion failure. C body uses `wctomb(3)` when
8403/// `__STDC_ISO_10646__` is defined (which it is on every modern
8404/// glibc / macOS libc), falls back to UTF-8 if the codeset is
8405/// `"UTF-8"`, and uses `iconv(3)` otherwise.
8406///
8407/// This Rust port mirrors the primary `wctomb` path via libc FFI;
8408/// the iconv fallback is unused on macOS/Linux modern builds.
8409/// On conversion failure, emits `zerr("character not in range")`
8410/// to match C source line 6794.
8411///
8412/// C body shape:
8413/// ```c
8414/// int count = wctomb(buf, (wchar_t)wval);
8415/// if (count == -1) zerr("character not in range");
8416/// return count;
8417/// ```
8418pub fn ucs4tomb(wval: u32, buf: &mut [u8]) -> i32 {
8419 // libc::wctomb requires at least MB_CUR_MAX bytes (typically 4
8420 // for UTF-8, 6 for some encodings). Use a stack buffer first,
8421 // then copy into the caller's buffer.
8422 // libc crate doesn't expose wctomb on all platforms; declare
8423 // the POSIX prototype directly. wchar_t is i32 on macOS/Linux
8424 // for our supported targets.
8425 extern "C" {
8426 fn wctomb(s: *mut libc::c_char, wc: libc::wchar_t) -> libc::c_int;
8427 }
8428 // libc::c_char is i8 on most targets but u8 on aarch64-linux. Use c_char
8429 // so the wctomb arg pointer type matches per-target without a cast.
8430 let mut local = [0 as libc::c_char; 16];
8431 let count = unsafe { wctomb(local.as_mut_ptr(), wval as libc::wchar_t) };
8432 if count < 0 {
8433 zerr("character not in range");
8434 return -1;
8435 }
8436 let n = count as usize;
8437 if n > buf.len() {
8438 zerr("character not in range");
8439 return -1;
8440 }
8441 for i in 0..n {
8442 buf[i] = local[i] as u8;
8443 }
8444 count
8445}
8446
8447/// Parse getkeystring escape sequences (from utils.c getkeystring)
8448/// Handles \n \t \r \e \a \b \f \v \\ \' \" \xNN \uNNNN \UNNNNNNNN \0NNN
8449/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from `Src/utils.c:6915`.
8450/// WARNING: param names don't match C — Rust=(s) vs C=(s, len, how, misc)
8451pub fn getkeystring(s: &str) -> (String, usize) {
8452 // c:6915
8453 let mut result = String::new();
8454 let mut chars = s.chars().peekable();
8455 let mut consumed = 0;
8456
8457 while let Some(c) = chars.next() {
8458 consumed += c.len_utf8();
8459 if c != '\\' {
8460 result.push(c);
8461 continue;
8462 }
8463 match chars.next() {
8464 Some('n') => {
8465 result.push('\n');
8466 consumed += 1;
8467 }
8468 Some('t') => {
8469 result.push('\t');
8470 consumed += 1;
8471 }
8472 Some('r') => {
8473 result.push('\r');
8474 consumed += 1;
8475 }
8476 Some('e') | Some('E') => {
8477 result.push('\x1b');
8478 consumed += 1;
8479 }
8480 Some('a') => {
8481 result.push('\x07');
8482 consumed += 1;
8483 }
8484 Some('b') => {
8485 result.push('\x08');
8486 consumed += 1;
8487 }
8488 Some('f') => {
8489 result.push('\x0c');
8490 consumed += 1;
8491 }
8492 Some('v') => {
8493 result.push('\x0b');
8494 consumed += 1;
8495 }
8496 Some('\\') => {
8497 result.push('\\');
8498 consumed += 1;
8499 }
8500 Some('\'') => {
8501 result.push('\'');
8502 consumed += 1;
8503 }
8504 Some('"') => {
8505 result.push('"');
8506 consumed += 1;
8507 }
8508 Some('x') => {
8509 consumed += 1;
8510 let mut hex = String::new();
8511 for _ in 0..2 {
8512 if let Some(&c) = chars.peek() {
8513 if c.is_ascii_hexdigit() {
8514 hex.push(chars.next().unwrap());
8515 consumed += 1;
8516 } else {
8517 break;
8518 }
8519 }
8520 }
8521 // c:Src/utils.c:7169-7170 — `\x` ALWAYS runs `*t++ =
8522 // zstrtol(s+1, &s, 16)`, even with no hex digit. zstrtol
8523 // over a non-hex byte parses zero digits and returns 0, so
8524 // a NUL byte is emitted (the offending char is processed
8525 // next). `$'\xg'` → NUL + 'g'; `$'\x'` → NUL. Previously
8526 // `from_str_radix("")` errored and nothing was pushed.
8527 let val: u8 = if hex.is_empty() {
8528 0 // c:7170 zstrtol on empty reads as 0
8529 } else {
8530 u8::from_str_radix(&hex, 16).unwrap_or(0)
8531 };
8532 // c:Src/utils.c — `\xNN` is one raw BYTE; metafied
8533 // per c:7289-7294 (vm_helper::meta_encode_byte) so
8534 // the String stays valid UTF-8. Bug #127.
8535 {
8536 // c:Src/utils.c metafy byte-encode step:
8537 // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8538 let b_ = val;
8539 if b_ < 0x80 {
8540 result.push(b_ as char);
8541 } else {
8542 result.push('\u{83}');
8543 result.push(char::from(b_ ^ 32));
8544 }
8545 }
8546 }
8547 Some('u') => {
8548 consumed += 1;
8549 let mut hex = String::new();
8550 for _ in 0..4 {
8551 if let Some(&c) = chars.peek() {
8552 if c.is_ascii_hexdigit() {
8553 hex.push(chars.next().unwrap());
8554 consumed += 1;
8555 } else {
8556 break;
8557 }
8558 }
8559 }
8560 if let Ok(val) = u32::from_str_radix(&hex, 16) {
8561 if let Some(c) = char::from_u32(val) {
8562 result.push(c);
8563 }
8564 }
8565 }
8566 Some('U') => {
8567 consumed += 1;
8568 let mut hex = String::new();
8569 for _ in 0..8 {
8570 if let Some(&c) = chars.peek() {
8571 if c.is_ascii_hexdigit() {
8572 hex.push(chars.next().unwrap());
8573 consumed += 1;
8574 } else {
8575 break;
8576 }
8577 }
8578 }
8579 if let Ok(val) = u32::from_str_radix(&hex, 16) {
8580 if let Some(c) = char::from_u32(val) {
8581 result.push(c);
8582 }
8583 }
8584 }
8585 Some(c @ '0'..='7') => {
8586 consumed += 1;
8587 let mut oct = String::new();
8588 oct.push(c);
8589 for _ in 0..2 {
8590 if let Some(&c) = chars.peek() {
8591 if ('0'..='7').contains(&c) {
8592 oct.push(chars.next().unwrap());
8593 consumed += 1;
8594 } else {
8595 break;
8596 }
8597 }
8598 }
8599 if let Ok(val) = u8::from_str_radix(&oct, 8) {
8600 // c:Src/utils.c — octal escape is one raw BYTE;
8601 // metafied per c:7289-7294. Bug #127.
8602 {
8603 // c:Src/utils.c metafy byte-encode step:
8604 // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8605 let b_ = val;
8606 if b_ < 0x80 {
8607 result.push(b_ as char);
8608 } else {
8609 result.push('\u{83}');
8610 result.push(char::from(b_ ^ 32));
8611 }
8612 }
8613 }
8614 }
8615 Some('c') => {
8616 consumed += 1;
8617 // \cX = control character
8618 if let Some(c) = chars.next() {
8619 consumed += 1;
8620 result.push((c as u8 & 0x1f) as char);
8621 }
8622 }
8623 Some(mod_letter @ ('C' | 'M')) => {
8624 // c:Src/utils.c:7029-7052 + c:7265-7275 — `\C` /
8625 // `\M` set control / meta flags; optional `-`
8626 // separator; then read the next char (possibly
8627 // through additional `\C`/`\M` chains) and apply
8628 // the mask: control → `& 0x9f` (or `\C-?` → 0x7f),
8629 // meta → `| 0x80`. Bug #113 in docs/BUGS.md: the
8630 // previous Rust port matched these in the default
8631 // arm and emitted literal `\C` / `\M`. This is the
8632 // second getkeystring (utils.rs); the in-tree
8633 // `getkeystring_dollar_quote` (lex.rs) also needs
8634 // the fix and gets it in the same commit.
8635 consumed += 1;
8636 let mut control = mod_letter == 'C';
8637 let mut meta = mod_letter == 'M';
8638 // Consume optional `-` separator + chained `\C`/`\M`.
8639 loop {
8640 if chars.peek() == Some(&'-') {
8641 chars.next();
8642 consumed += 1;
8643 continue;
8644 }
8645 // chained `\C` / `\M`?
8646 let mut iter_clone = chars.clone();
8647 if iter_clone.next() == Some('\\') {
8648 if let Some(nx) = iter_clone.next() {
8649 if nx == 'C' || nx == 'M' {
8650 chars.next(); // consume '\'
8651 chars.next(); // consume C/M
8652 consumed += 2;
8653 if nx == 'C' {
8654 control = true;
8655 } else {
8656 meta = true;
8657 }
8658 continue;
8659 }
8660 }
8661 }
8662 break;
8663 }
8664 // Read one base character (allowing nested simple
8665 // escapes for common cases).
8666 let base: Option<char> = if chars.peek() == Some(&'\\') {
8667 chars.next();
8668 consumed += 1;
8669 match chars.next() {
8670 Some('n') => {
8671 consumed += 1;
8672 Some('\n')
8673 }
8674 Some('t') => {
8675 consumed += 1;
8676 Some('\t')
8677 }
8678 Some('r') => {
8679 consumed += 1;
8680 Some('\r')
8681 }
8682 Some('a') => {
8683 consumed += 1;
8684 Some('\x07')
8685 }
8686 Some('b') => {
8687 consumed += 1;
8688 Some('\x08')
8689 }
8690 Some('e') | Some('E') => {
8691 consumed += 1;
8692 Some('\x1b')
8693 }
8694 Some('f') => {
8695 consumed += 1;
8696 Some('\x0c')
8697 }
8698 Some('v') => {
8699 consumed += 1;
8700 Some('\x0b')
8701 }
8702 Some('\\') => {
8703 consumed += 1;
8704 Some('\\')
8705 }
8706 Some('\'') => {
8707 consumed += 1;
8708 Some('\'')
8709 }
8710 Some('"') => {
8711 consumed += 1;
8712 Some('"')
8713 }
8714 Some(other) => {
8715 consumed += 1;
8716 Some(other)
8717 }
8718 None => None,
8719 }
8720 } else {
8721 chars.next().inspect(|c| {
8722 consumed += c.len_utf8();
8723 })
8724 };
8725 if let Some(ch) = base {
8726 let mut byte = ch as u32;
8727 if control {
8728 if byte == '?' as u32 {
8729 byte = 0x7f;
8730 } else {
8731 byte &= 0x9f;
8732 }
8733 }
8734 if meta {
8735 byte |= 0x80;
8736 }
8737 // c:Src/utils.c:7265-7275 — masked result is one
8738 // raw BYTE (`$'\M-i'` = 0xe9), metafied per
8739 // c:7289-7294. Multibyte base chars (> 0xff)
8740 // keep the codepoint form. Bug #127.
8741 if byte <= 0xff {
8742 {
8743 // c:Src/utils.c metafy byte-encode step:
8744 // `if (imeta(c)) {{ *p++ = Meta; *p++ = c ^ 32; }}`
8745 let b_ = byte as u8;
8746 if b_ < 0x80 {
8747 result.push(b_ as char);
8748 } else {
8749 result.push('\u{83}');
8750 result.push(char::from(b_ ^ 32));
8751 }
8752 }
8753 } else if let Some(c) = char::from_u32(byte) {
8754 result.push(c);
8755 }
8756 }
8757 }
8758 Some(c) if isset(BANGHIST)
8759 && crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst) != 0
8760 && c as u32
8761 == crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst) as u32 =>
8762 {
8763 // The reverse of quotestring's BANGHIST escape (c:Src/utils.c:6230
8764 // — `\!` is emitted for the history char): unquoting `$'a\!b'`
8765 // must strip the backslash so `${(Q)${(qqqq)v}}` round-trips.
8766 // In zsh this happens because `(Q)` re-lexes the string and
8767 // history expansion consumes the `\<bangchar>`; here the decoder
8768 // consumes it directly.
8769 consumed += 1;
8770 result.push(c);
8771 }
8772 Some(c) => {
8773 consumed += 1;
8774 result.push('\\');
8775 result.push(c);
8776 }
8777 None => {
8778 result.push('\\');
8779 }
8780 }
8781 }
8782 (result, consumed)
8783}
8784
8785/// Check if s is a prefix of t (from utils.c strpfx)
8786// Return non-zero if s is a prefix of t. // c:7345
8787/// `strpfx` — see implementation.
8788pub fn strpfx(s: &str, t: &str) -> bool {
8789 t.starts_with(s)
8790}
8791
8792/// Check if s is a suffix of t (from utils.c strsfx)
8793// Return non-zero if s is a suffix of t. // c:7345
8794/// `strsfx` — see implementation.
8795pub fn strsfx(s: &str, t: &str) -> bool {
8796 t.ends_with(s)
8797}
8798
8799/// Go up n directories (from utils.c upchdir)
8800/// Port of `upchdir(int n)` from `Src/utils.c:7356`.
8801pub fn upchdir(n: usize) -> io::Result<()> {
8802 let mut path = String::new();
8803 for i in 0..n {
8804 if i > 0 {
8805 path.push('/');
8806 }
8807 path.push_str("..");
8808 }
8809 std::env::set_current_dir(&path)?;
8810 Ok(())
8811}
8812
8813/// Port of `struct dirsav` from `Src/zsh.h:1159`.
8814///
8815/// ```c
8816/// struct dirsav {
8817/// int dirfd, level;
8818/// char *dirname;
8819/// dev_t dev;
8820/// ino_t ino;
8821/// };
8822/// ```
8823///
8824/// The previous Rust port omitted `dev` and `ino` which the
8825/// `restoredir` integrity check (utils.c:7592) reads. Adding them
8826/// so callers can verify the saved-and-restored cwd matches the
8827/// captured device + inode.
8828// `struct dirsav` lives in `dirsav` per Rule C
8829// (its C definition is `Src/zsh.h:1159`, not utils.c). The previous
8830// Rust port had a `pub struct DirSav` PascalCase duplicate of the
8831// canonical lowercase struct; deleted in favour of routing through
8832// `zsh_h::dirsav` directly.
8833
8834/// Port of `init_dirsav(Dirsav d)` from `Src/utils.c:7381`. Initialize a
8835/// `dirsav` struct to its empty/default state. C body memset's the
8836/// fields to 0 (dirfd to -1).
8837///
8838/// C signature: `void init_dirsav(Dirsav d)` where
8839/// `Dirsav = struct dirsav *`. Rust port returns the initialised
8840/// struct since callers always pair-with a fresh allocation.
8841/// WARNING: param names don't match C — Rust=() vs C=(path)
8842/// C body (3 lines):
8843/// `d->ino = d->dev = 0; d->dirname = NULL; d->dirfd = d->level = -1;`
8844/// The C `dirname = NULL` becomes `dirname: None`; Rust port prefills
8845/// dirname with current_dir for legacy callers that immediately read
8846/// it (mirrors what `setpwd()` does in C right after `init_dirsav`).
8847pub fn init_dirsav() -> dirsav {
8848 // c:7381
8849 dirsav {
8850 dirfd: -1,
8851 level: 0,
8852 dev: 0,
8853 ino: 0, // c:7383-7385
8854 dirname: std::env::current_dir()
8855 .ok()
8856 .map(|p| p.to_string_lossy().to_string()),
8857 }
8858}
8859
8860/// Port of `lchdir(char const *path, struct dirsav *d, int hard)` from `Src/utils.c:7400`.
8861///
8862/// c:7388 — "Change directory, without following symlinks." With `hard`
8863/// set, descends `path` one component at a time: `lstat`s each component
8864/// (so a symlink is never followed), `chdir`s into it, then re-`lstat`s
8865/// `.` and compares dev/ino against the component it just stat'd. A
8866/// symlink swapped under us between the `lstat` and the `chdir` is caught
8867/// by the mismatch and the saved directory is restored via `restoredir`.
8868/// `d` (when non-NULL) is filled so the caller can later restore the cwd.
8869///
8870/// Returns 0 on success, -1 on a normal descent failure, -2 if the cwd
8871/// could not even be restored afterwards.
8872///
8873/// zshrs targets macOS/Linux, so the live C arms are HAVE_LSTAT +
8874/// HAVE_FCHDIR; the no-lstat / no-fchdir fallbacks (which never compile
8875/// on our platforms) are elided. C restores `errno = err` before each
8876/// non-zero return so the caller can read the break-reason; that errno
8877/// propagation is elided here (no current caller inspects errno — the
8878/// -1/-2/0 return is the contract).
8879/// WARNING: param names match C — (path, d, hard).
8880#[cfg(unix)]
8881pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
8882 // c:7400
8883 use std::ffi::CString;
8884
8885 let mut ds = init_dirsav(); // c:7405 — local fallback `struct dirsav ds`
8886 let used_local = d.is_none(); // tracks C's `d == &ds`
8887 // c:7415-7418 — `if (!d) { init_dirsav(&ds); d = &ds; }`
8888 let d: &mut dirsav = d.unwrap_or(&mut ds);
8889
8890 let bytes = path.as_bytes();
8891 let mut level: i32;
8892
8893 // c:7419-7424 (HAVE_LSTAT arm) —
8894 // `if ((*path == '/' || !hard) && (d != &ds || hard))`
8895 if (bytes.first() == Some(&b'/') || hard == 0) && (!used_local || hard != 0) {
8896 level = -1; // c:7425
8897 } else {
8898 level = 0; // c:7431
8899 // c:7432-7435 — record dev/ino of `.` if not already captured.
8900 if d.dev == 0 && d.ino == 0 {
8901 if let Ok(meta) = fs::metadata(".") {
8902 d.dev = meta.dev(); // c:7433
8903 d.ino = meta.ino(); // c:7434
8904 }
8905 }
8906 }
8907
8908 // c:7439-7451 — soft (`!hard`) path: count components, set d->level,
8909 // and delegate to zchdir.
8910 if hard == 0 {
8911 if !used_local {
8912 // c:7443-7447 — count '/'-separated components into `level`.
8913 let mut p = 0usize;
8914 while p < bytes.len() {
8915 while p < bytes.len() && bytes[p] != b'/' {
8916 p += 1;
8917 }
8918 while p < bytes.len() && bytes[p] == b'/' {
8919 p += 1;
8920 }
8921 level += 1;
8922 }
8923 d.level = level; // c:7448
8924 }
8925 return zchdir(path); // c:7450
8926 }
8927
8928 // c:7453+ — hard path (HAVE_LSTAT + HAVE_FCHDIR).
8929 let mut close_dir = false; // c:7412
8930 // c:7455-7460 — save the starting dir via an fd if we don't have one.
8931 if d.dirfd < 0 {
8932 close_dir = true; // c:7456
8933 let dot = CString::new(".").unwrap();
8934 d.dirfd = unsafe { libc::open(dot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) }; // c:7457
8935 if d.dirfd < 0
8936 && zgetdir(Some(&mut *d)).is_some()
8937 && d.dirname
8938 .as_deref()
8939 .map(|s| !s.starts_with('/'))
8940 .unwrap_or(false)
8941 {
8942 // c:7458-7459 — cwd is relative; fall back to opening "..".
8943 let dotdot = CString::new("..").unwrap();
8944 d.dirfd = unsafe { libc::open(dotdot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
8945 }
8946 }
8947
8948 // c:7462-7464 — absolute path: start the descent from root.
8949 if bytes.first() == Some(&b'/') {
8950 let root = CString::new("/").unwrap();
8951 if unsafe { libc::chdir(root.as_ptr()) } < 0 {
8952 zwarn(&format!(
8953 "failed to chdir(/): {}",
8954 io::Error::last_os_error()
8955 )); // c:7464
8956 }
8957 }
8958
8959 let dot = CString::new(".").unwrap();
8960 let mut pos = 0usize; // index into `path`, replaces C `path`/`pptr`
8961 let mut err: i32 = 0; // c:7408
8962 loop {
8963 // c:7466-7467 — skip leading slashes.
8964 while pos < bytes.len() && bytes[pos] == b'/' {
8965 pos += 1;
8966 }
8967 // c:7468-7480 — end of path reached: success.
8968 if pos >= bytes.len() {
8969 if !used_local {
8970 d.level = level; // c:7472
8971 }
8972 // c:7474-7477 — close the saved-dir fd if we opened it.
8973 if d.dirfd >= 0 && close_dir {
8974 unsafe { libc::close(d.dirfd) };
8975 d.dirfd = -1;
8976 }
8977 return 0; // c:7479
8978 }
8979 // c:7481 — find the end of this path component.
8980 let start = pos;
8981 let mut end = start + 1;
8982 while end < bytes.len() && bytes[end] != b'/' {
8983 end += 1;
8984 }
8985 // c:7482-7485 — component too long.
8986 if end - start > libc::PATH_MAX as usize {
8987 err = libc::ENAMETOOLONG;
8988 break;
8989 }
8990 // c:7486-7488 — copy the component into `buf`.
8991 let comp = &bytes[start..end];
8992 pos = end;
8993 let cbuf = match CString::new(comp) {
8994 Ok(c) => c,
8995 Err(_) => {
8996 err = libc::ENOENT; // embedded NUL — lstat would fail anyway.
8997 break;
8998 }
8999 };
9000 // c:7489-7492 — lstat the component (does NOT follow a symlink).
9001 let mut st1: libc::stat = unsafe { std::mem::zeroed() };
9002 if unsafe { libc::lstat(cbuf.as_ptr(), &mut st1) } != 0 {
9003 err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9004 break;
9005 }
9006 // c:7493-7496 — must be a real directory, not a symlink/other.
9007 if st1.st_mode as u32 & libc::S_IFMT as u32 != libc::S_IFDIR as u32 {
9008 err = libc::ENOTDIR;
9009 break;
9010 }
9011 // c:7497-7500 — chdir into it.
9012 if unsafe { libc::chdir(cbuf.as_ptr()) } != 0 {
9013 err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9014 break;
9015 }
9016 if level >= 0 {
9017 level += 1; // c:7501-7502
9018 }
9019 // c:7503-7510 — re-lstat `.` and verify the dir we landed in is
9020 // the same dev/ino we lstat'd, catching a symlink swap race.
9021 let mut st2: libc::stat = unsafe { std::mem::zeroed() };
9022 if unsafe { libc::lstat(dot.as_ptr(), &mut st2) } != 0 {
9023 err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
9024 break;
9025 }
9026 if st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino {
9027 err = libc::ENOTDIR; // c:7508
9028 break;
9029 }
9030 }
9031
9032 // c:7512-7548 — descent failed; restore the saved directory.
9033 if restoredir(&mut *d) != 0 {
9034 let restoreerr = io::Error::last_os_error(); // c:7513
9035 // c:7519-7532 — restore failed too; force cwd to $HOME then "/".
9036 let mut reached = false;
9037 for i in 0..2 {
9038 // c:7521-7527 — destination: $HOME on pass 0, "/" on pass 1.
9039 let cdest = if i != 0 {
9040 "/".to_string()
9041 } else {
9042 match getsparam("HOME") {
9043 Some(h) => h, // c:7526
9044 None => continue, // c:7525 `if (!home) continue;`
9045 }
9046 };
9047 setsparam("PWD", &cdest); // c:7528-7529 `zsfree(pwd); pwd = ztrdup(cdest);`
9048 if let Ok(cd) = CString::new(cdest.as_bytes()) {
9049 if unsafe { libc::chdir(cd.as_ptr()) } == 0 {
9050 // c:7530
9051 reached = true;
9052 break;
9053 }
9054 }
9055 }
9056 if !reached {
9057 // c:7533-7534 — couldn't even reach "/".
9058 zerr(&format!(
9059 "lost current directory, failed to cd to /: {}",
9060 io::Error::last_os_error()
9061 ));
9062 } else {
9063 // c:7535-7537
9064 let pwd = getsparam("PWD").unwrap_or_default();
9065 zerr(&format!(
9066 "lost current directory: {}: changed to `{}'",
9067 restoreerr, pwd
9068 ));
9069 }
9070 // c:7540-7545 — close the saved-dir fd if we opened it.
9071 if d.dirfd >= 0 && close_dir {
9072 unsafe { libc::close(d.dirfd) };
9073 d.dirfd = -1;
9074 }
9075 let _ = err; // c:7546 `errno = err;` propagation elided (see doc).
9076 return -2; // c:7547
9077 }
9078 // c:7549-7558 — descent failed but the saved directory was restored.
9079 if d.dirfd >= 0 && close_dir {
9080 unsafe { libc::close(d.dirfd) };
9081 d.dirfd = -1;
9082 }
9083 let _ = err; // c:7557 `errno = err;` propagation elided (see doc).
9084 -1 // c:7558
9085}
9086
9087/// Non-unix stub: lchdir's symlink-safe descent is built on POSIX
9088/// `lstat`/`fchdir`/`chdir`, which have no Windows equivalent here.
9089#[cfg(not(unix))]
9090pub fn lchdir(path: &str, d: Option<&mut dirsav>, hard: i32) -> i32 {
9091 let _ = (path, d, hard);
9092 -1
9093}
9094
9095/// Port of `restoredir(struct dirsav *d)` from `Src/utils.c:7565`.
9096///
9097/// ```c
9098/// int restoredir(struct dirsav *d) {
9099/// if (d->dirname && *d->dirname == '/')
9100/// return chdir(d->dirname);
9101/// if (d->dirfd >= 0) {
9102/// if (!fchdir(d->dirfd)) {
9103/// if (!d->dirname) return 0;
9104/// else if (chdir(d->dirname)) {
9105/// close(d->dirfd); d->dirfd = -1; err = -2;
9106/// }
9107/// } else {
9108/// close(d->dirfd); d->dirfd = err = -1;
9109/// }
9110/// } else if (d->level > 0)
9111/// err = upchdir(d->level);
9112/// else if (d->level < 0) err = -1;
9113/// // dev/ino integrity check ...
9114/// }
9115/// ```
9116///
9117/// Restore the cwd captured in `d`. Absolute `dirname` short-
9118/// circuits to `chdir`. Otherwise tries `fchdir(dirfd)` (when
9119/// supported) then falls through to `upchdir(level)` for the
9120/// nested-fn-exit case. Returns 0 on success, non-zero on failure
9121/// (matching C's int return).
9122///
9123/// Signature change: previous Rust port took `saved: &str` and
9124/// returned `bool` — different shape from C, missed the dirfd /
9125/// level / dev / ino fields entirely.
9126pub fn restoredir(d: &mut dirsav) -> i32 {
9127 // C: if (d->dirname && *d->dirname == '/') return chdir(d->dirname);
9128 if let Some(name) = d.dirname.as_ref() {
9129 if name.starts_with('/') {
9130 return match std::env::set_current_dir(name) {
9131 Ok(_) => 0,
9132 Err(_) => -1,
9133 };
9134 }
9135 }
9136 let mut err: i32 = 0;
9137 // C: HAVE_FCHDIR path — try fchdir(dirfd) first.
9138 #[cfg(unix)]
9139 if d.dirfd >= 0 {
9140 let rc = unsafe { libc::fchdir(d.dirfd) };
9141 if rc == 0 {
9142 if d.dirname.is_none() {
9143 return 0;
9144 }
9145 let name = d.dirname.as_ref().unwrap();
9146 if std::env::set_current_dir(name).is_err() {
9147 unsafe { libc::close(d.dirfd) };
9148 d.dirfd = -1;
9149 err = -2;
9150 }
9151 } else {
9152 unsafe { libc::close(d.dirfd) };
9153 d.dirfd = -1;
9154 err = -1;
9155 }
9156 } else if d.level > 0 {
9157 // C: err = upchdir(d->level);
9158 let _ = upchdir(d.level as usize);
9159 } else if d.level < 0 {
9160 err = -1;
9161 }
9162 // C: dev/ino integrity check after the chdir/fchdir.
9163 if (d.dev != 0 || d.ino != 0) && err == 0 {
9164 if let Ok(meta) = fs::metadata(".") {
9165 if meta.ino() != d.ino || meta.dev() != d.dev {
9166 err = -1;
9167 }
9168 } else {
9169 err = -1;
9170 }
9171 }
9172 err
9173}
9174
9175/// Port of `privasserted()` from `Src/utils.c:7607`.
9176///
9177/// "Check whether the shell is running with privileges in effect.
9178/// This is the case if EITHER the euid is zero, OR (if the system
9179/// supports POSIX.1e (POSIX.6) capability sets) the process'
9180/// Effective or Inheritable capability sets are non-empty."
9181///
9182/// ```c
9183/// if (!geteuid()) return 1;
9184/// #ifdef HAVE_CAP_GET_PROC
9185/// cap_t caps = cap_get_proc();
9186/// if (caps) {
9187/// cap_flag_value_t val;
9188/// for (cap_value_t cap = 0;
9189/// !cap_get_flag(caps, cap, CAP_EFFECTIVE, &val); cap++)
9190/// if (val && cap != CAP_WAKE_ALARM) {
9191/// cap_free(caps);
9192/// return 1;
9193/// }
9194/// }
9195/// cap_free(caps);
9196/// #endif
9197/// return 0;
9198/// ```
9199///
9200/// The previous Rust port checked `getuid() != geteuid()` which is
9201/// the SUID-binary detection, not the "running with privileges"
9202/// check. The capability-set inspection requires libcap (gated
9203/// behind the `libcap` feature in `crate::ported::modules::cap`);
9204/// without it, only the euid==0 path is exercised — same as the
9205/// C `#else` arm when HAVE_CAP_GET_PROC isn't defined.
9206pub fn privasserted() -> bool {
9207 #[cfg(unix)]
9208 {
9209 if unsafe { libc::geteuid() } == 0 {
9210 return true;
9211 }
9212 }
9213 // POSIX.1e capabilities check (HAVE_CAP_GET_PROC) — only
9214 // active on Linux when zshrs is built with `--features
9215 // libcap`. The cap module's `cap_get_proc` returns Ok(text)
9216 // when the process has any capability set; we treat any
9217 // non-default-empty result as "privileges asserted".
9218 #[cfg(all(target_os = "linux", feature = "libcap"))]
9219 {
9220 // Pending: walk the capability set with cap_get_flag and
9221 // skip CAP_WAKE_ALARM as the C source does. The cap.rs
9222 // port doesn't yet expose the flag-iteration FFI; until
9223 // it does, conservative-true on any non-empty cap text.
9224 if let Ok(text) = crate::ported::modules::cap::cap_get_proc() {
9225 // Empty / default-empty cap text = no privileges.
9226 if !text.is_empty() && text != "=" {
9227 return true;
9228 }
9229 }
9230 }
9231 false
9232}
9233
9234/// Port of `mode_to_octal(mode_t mode)` from `Src/utils.c:7634`.
9235///
9236/// Convert a `mode_t` into the equivalent canonical octal value
9237/// by testing each `S_I*` flag explicitly and OR-ing the matching
9238/// octal bit. This is NOT just `mode & 07777` — on systems where
9239/// the libc `S_IRUSR`/etc. constants don't match the canonical
9240/// values (e.g. when zsh runs against a non-POSIX libc),
9241/// `mode & 07777` returns the libc representation. The C version
9242/// translates to canonical bits explicitly so callers get a stable
9243/// portable result.
9244///
9245/// ```c
9246/// int mode_to_octal(mode_t mode)
9247/// {
9248/// int m = 0;
9249/// if (mode & S_ISUID) m |= 04000;
9250/// ... (12 bit-by-bit mappings)
9251/// return m;
9252/// }
9253/// ```
9254pub fn mode_to_octal(mode: u32) -> i32 {
9255 // c:7634
9256 #[cfg(not(unix))]
9257 {
9258 // No POSIX permission bits on non-Unix; fall back to canonical
9259 // octal-bit layout via the same mask values C uses on POSIX.
9260 let mut o: i32 = 0;
9261 if mode & 0o4000 != 0 {
9262 o |= 0o4000;
9263 } // c:7638-7639
9264 if mode & 0o2000 != 0 {
9265 o |= 0o2000;
9266 } // c:7640-7641
9267 if mode & 0o1000 != 0 {
9268 o |= 0o1000;
9269 } // c:7642-7643
9270 if mode & 0o0400 != 0 {
9271 o |= 0o0400;
9272 } // c:7644-7645
9273 if mode & 0o0200 != 0 {
9274 o |= 0o0200;
9275 } // c:7646-7647
9276 if mode & 0o0100 != 0 {
9277 o |= 0o0100;
9278 } // c:7648-7649
9279 if mode & 0o0040 != 0 {
9280 o |= 0o0040;
9281 } // c:7650-7651
9282 if mode & 0o0020 != 0 {
9283 o |= 0o0020;
9284 } // c:7652-7653
9285 if mode & 0o0010 != 0 {
9286 o |= 0o0010;
9287 } // c:7654-7655
9288 if mode & 0o0004 != 0 {
9289 o |= 0o0004;
9290 } // c:7656-7657
9291 if mode & 0o0002 != 0 {
9292 o |= 0o0002;
9293 } // c:7658-7659
9294 if mode & 0o0001 != 0 {
9295 o |= 0o0001;
9296 } // c:7660-7661
9297 return o; // c:7662
9298 }
9299 #[cfg(unix)]
9300 {
9301 // c:7636 — int m = 0;
9302 let mut m: i32 = 0;
9303 // c:7638-7661 — 12 bit-by-bit mappings from libc S_I* → canonical octal.
9304 if mode & S_ISUID as u32 != 0 {
9305 m |= 0o4000;
9306 } // c:7638-7639
9307 if mode & S_ISGID as u32 != 0 {
9308 m |= 0o2000;
9309 } // c:7640-7641
9310 if mode & S_ISVTX as u32 != 0 {
9311 m |= 0o1000;
9312 } // c:7642-7643
9313 if mode & S_IRUSR as u32 != 0 {
9314 m |= 0o0400;
9315 } // c:7644-7645
9316 if mode & S_IWUSR as u32 != 0 {
9317 m |= 0o0200;
9318 } // c:7646-7647
9319 if mode & S_IXUSR as u32 != 0 {
9320 m |= 0o0100;
9321 } // c:7648-7649
9322 if mode & S_IRGRP as u32 != 0 {
9323 m |= 0o0040;
9324 } // c:7650-7651
9325 if mode & S_IWGRP as u32 != 0 {
9326 m |= 0o0020;
9327 } // c:7652-7653
9328 if mode & S_IXGRP as u32 != 0 {
9329 m |= 0o0010;
9330 } // c:7654-7655
9331 if mode & S_IROTH as u32 != 0 {
9332 m |= 0o0004;
9333 } // c:7656-7657
9334 if mode & S_IWOTH as u32 != 0 {
9335 m |= 0o0002;
9336 } // c:7658-7659
9337 if mode & S_IXOTH as u32 != 0 {
9338 m |= 0o0001;
9339 } // c:7660-7661
9340 m // c:7662
9341 }
9342}
9343
9344/// Port of `mailstat(char *path, struct stat *st)` from `Src/utils.c:7685`.
9345///
9346/// C signature: `int mailstat(char *path, struct stat *st)`.
9347/// Writes maildir aggregate stats into `*st` (or the native stat for
9348/// non-directory paths) and returns the underlying `stat(2)` return
9349/// (0 on success, -1 on error — matches C's `i = stat(path, st);
9350/// return i;`).
9351///
9352/// When `path` is a maildir directory (containing `cur/`, `tmp/`,
9353/// `new/` subdirs), walks `new/` + `cur/` and aggregates into `*st`:
9354/// nlink=1, S_IFDIR→S_IFREG, size=Σ message bytes,
9355/// blocks=Σ messages, atime=newest, mtime=newest. When the path is
9356/// a plain file, leaves the native `stat(2)` result in `*st`.
9357pub fn mailstat(path: &str, st: &mut libc::stat) -> i32 {
9358 // c:7685
9359 let c_path = match CString::new(path) {
9360 Ok(c) => c,
9361 Err(_) => return -1,
9362 };
9363 // C: if ((i = stat(path, st)) != 0 || !S_ISDIR(st->st_mode)) return i;
9364 let i = unsafe { libc::stat(c_path.as_ptr(), st as *mut _) }; // c:7693
9365 if i != 0 || (st.st_mode & libc::S_IFMT) != libc::S_IFDIR {
9366 // c:7693
9367 return i; // c:7693
9368 }
9369 // C 7700-7706: nlink=1, S_IFDIR → S_IFREG, zero size/blocks.
9370 st.st_nlink = 1; // c:7701
9371 st.st_mode &= !libc::S_IFDIR; // c:7702
9372 st.st_mode |= libc::S_IFREG; // c:7703
9373 st.st_size = 0; // c:7704
9374 st.st_blocks = 0; // c:7705
9375 // C 7707-7712: stat(path/cur). If absent or not a dir, return 0
9376 // with the partial out (just the IFREG-coerced root).
9377 let cur_path = match CString::new(format!("{}/cur", path)) {
9378 Ok(c) => c,
9379 Err(_) => return 0,
9380 };
9381 let mut sub: libc::stat = unsafe { std::mem::zeroed() };
9382 if unsafe { libc::stat(cur_path.as_ptr(), &mut sub) } != 0 // c:7708
9383 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9384 // c:7708
9385 {
9386 return 0; // c:7710
9387 }
9388 st.st_atime = sub.st_atime; // c:7712
9389 // C 7715-7722: stat(path/tmp).
9390 let tmp_path = match CString::new(format!("{}/tmp", path)) {
9391 Ok(c) => c,
9392 Err(_) => return 0,
9393 };
9394 if unsafe { libc::stat(tmp_path.as_ptr(), &mut sub) } != 0 // c:7716
9395 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9396 // c:7716
9397 {
9398 return 0; // c:7718
9399 }
9400 st.st_mtime = sub.st_mtime; // c:7720
9401 // C 7724-7730: stat(path/new). C overwrites mtime with new/'s mtime.
9402 let new_path = match CString::new(format!("{}/new", path)) {
9403 Ok(c) => c,
9404 Err(_) => return 0,
9405 };
9406 if unsafe { libc::stat(new_path.as_ptr(), &mut sub) } != 0 // c:7724
9407 || (sub.st_mode & libc::S_IFMT) != libc::S_IFDIR
9408 // c:7724
9409 {
9410 return 0; // c:7726
9411 }
9412 st.st_mtime = sub.st_mtime; // c:7728
9413 // C 7749-7778: walk new/ and cur/, sum size + blocks, track newest
9414 // atime / mtime.
9415 let mut atime: libc::time_t = 0; // c:7748
9416 let mut mtime: libc::time_t = 0; // c:7748
9417 for sub_name in ["new", "cur"] {
9418 let dir = format!("{}/{}", path, sub_name);
9419 let entries = match fs::read_dir(&dir) {
9420 Ok(e) => e,
9421 Err(_) => return 0,
9422 };
9423 for entry in entries.flatten() {
9424 let name = entry.file_name();
9425 let name_bytes = name.as_encoded_bytes();
9426 // C: if (fn->d_name[0] == '.') continue;
9427 if name_bytes.first() == Some(&b'.') {
9428 // c:7758
9429 continue;
9430 }
9431 let entry_path = match CString::new(entry.path().to_string_lossy().as_bytes()) {
9432 Ok(c) => c,
9433 Err(_) => continue,
9434 };
9435 let mut entry_st: libc::stat = unsafe { std::mem::zeroed() };
9436 if unsafe { libc::stat(entry_path.as_ptr(), &mut entry_st) } != 0 {
9437 // c:7762
9438 continue;
9439 }
9440 st.st_size += entry_st.st_size; // c:7766
9441 st.st_blocks += 1; // c:7767
9442 // C: if (atime != mtime && atime > atime_max) atime_max = atime;
9443 if entry_st.st_atime != entry_st.st_mtime && entry_st.st_atime > atime {
9444 // c:7769
9445 atime = entry_st.st_atime;
9446 }
9447 if entry_st.st_mtime > mtime {
9448 // c:7771
9449 mtime = entry_st.st_mtime;
9450 }
9451 }
9452 }
9453 // C 7783-7784: if (atime) st_ret.st_atime = atime;
9454 if atime != 0 {
9455 // c:7783
9456 st.st_atime = atime;
9457 }
9458 if mtime != 0 {
9459 // c:7785
9460 st.st_mtime = mtime;
9461 }
9462 0 // c:7787
9463}
9464
9465/// Script name for error messages
9466pub static mut SCRIPT_NAME: Option<String> = None;
9467/// Script filename
9468pub static mut SCRIPT_FILENAME: Option<String> = None;
9469
9470/// Print a fatal error to stderr.
9471/// Port of `zerr(VA_ALIST1(const char *fmt))` from Src/utils.c:172. C source sets `errflag`
9472/// after emitting `<prefix>: <msg>\n` so the running script aborts
9473/// at the next safe point. The Rust port currently just prints —
9474// =====================================================================
9475// Module-static state for the zerr/zwarn/zerrnam/zwarnnam family —
9476// port of the file-statics in Src/init.c + Src/exec.c that
9477// `zwarning()` (utils.c:142) reads to build the error prefix.
9478// =====================================================================
9479
9480// name of script being sourced // c:33
9481/// Port of `char *scriptname` from `Src/init.c`. Set when `source`
9482/// is reading a script; cleared on return. Used by `zwarning()`
9483/// (utils.c:147) as the diagnostic prefix.
9484static SCRIPTNAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9485
9486/// Port of `char *argzero` from `Src/init.c`. The shell's argv[0].
9487/// Used by `zwarning()` (utils.c:147) as the fallback diagnostic
9488/// prefix when scriptname is unset.
9489static ARGZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9490
9491/// Port of `char *scriptfilename` from `Src/init.c` (listed in
9492/// `zsh.export:356`). The FILE that the current code was PARSED
9493/// from. Distinct from `scriptname` (which doshfunc overrides on
9494/// function entry at c:5903). `scriptfilename` stays at the outer
9495/// file across function calls; PS4's `%x` reads it at
9496/// `Src/prompt.c:937`.
9497///
9498/// Set at init.c:479 (`-c` mode → `scriptname = scriptfilename
9499/// = "zsh"`), init.c:1367 (`source` enters the named file),
9500/// init.c:1558+1592+1667 (save/install/restore around `.` /
9501/// `source` bin_dot dispatch).
9502static SCRIPTFILENAME: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9503
9504/// Port of `char *posixzero` from `Src/params.c:76`. The original
9505/// argv[0] preserved unchanged by later mutations. Used by
9506/// `argzerogetfn` for `$0` under `isset(POSIXARGZERO)`.
9507static POSIXZERO: std::sync::OnceLock<Mutex<Option<String>>> = std::sync::OnceLock::new();
9508
9509// error flag: bits from enum errflag_bits // c:124
9510/// Port of `int errflag` from `Src/init.c`. Tracks whether an
9511/// error has been raised (`ERRFLAG_ERROR = 1`) or break/return
9512/// is in flight (`ERRFLAG_INT = 2`).
9513///
9514/// Direct global access: `errflag.load(Ordering::Relaxed)` reads
9515/// the current value, `errflag.fetch_or(ERRFLAG_ERROR, …)` matches
9516/// C's `errflag |= ERRFLAG_ERROR`, `errflag.store(0, …)` matches
9517/// C's `errflag = 0`.
9518#[allow(non_upper_case_globals)]
9519pub static errflag: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9520
9521/// Port of `int noerrs` from `Src/init.c`. Counter — when `> 0`,
9522/// suppresses error printing. `noerrs >= 2` also suppresses the
9523/// `errflag` set inside `zerr`/`zerrnam`.
9524static NOERRS: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
9525
9526/// Port of `int locallevel` from `Src/init.c`. Function-call depth
9527/// (0 = top-level, 1+ = inside a fn). `zwarning()` checks this in
9528/// the script-prefix path (utils.c:150).
9529// `LOCALLEVEL` removed — see `locallevel_lock` removal comment
9530// below. Canonical static lives in `LOCALLEVEL`
9531// (port of params.c:54).
9532
9533/// Port of `int lineno` from `Src/init.c`. Current line number;
9534/// `zerrmsg()` includes it in the diagnostic when locallevel > 0
9535/// or shinstdin is unset (utils.c:301).
9536static LINENO: std::sync::OnceLock<Mutex<i32>> = std::sync::OnceLock::new();
9537
9538/// Port of the `isset(SHINSTDIN)` check from utils.c:150. C reads
9539/// the SHINSTDIN option directly; the Rust port caches it here so
9540/// callers don't pull in the whole option-table for every error.
9541static SHINSTDIN_OPT: std::sync::OnceLock<Mutex<bool>> = std::sync::OnceLock::new();
9542
9543/// `ERRFLAG_ERROR` from `Src/zsh.h`. Set on `zerr`/`zerrnam` to
9544/// signal a fatal error has occurred.
9545pub const ERRFLAG_ERROR: i32 = 1;
9546
9547/// Port of `mod_export int incompfunc` from `Src/utils.c:46`.
9548/// Set non-zero while a `comp*` builtin is dispatching from inside
9549/// a user-defined completion function — guards `comparguments` /
9550/// `compset` / `compadd` / `compdescribe` / `comptags` / `compvalues`
9551/// / `compfiles` / `compgroups` / `compquote` against being called
9552/// outside the `compfunc` shfunc context (each builtin checks
9553/// `INCOMPFUNC` early and emits "can only be called from completion
9554/// function" when zero).
9555pub static INCOMPFUNC: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:46
9556
9557/// Port of `mod_export int resetneeded` from `Src/utils.c:1821`.
9558/// Set when the editor needs a redraw — incremented by widgets +
9559/// signal handlers (e.g. SIGWINCH); the next prompt loop tick clears
9560/// it after running zrefresh.
9561pub static RESETNEEDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1821
9562
9563/// Port of `mod_export int winchanged` from `Src/utils.c:1827`.
9564/// Set by the SIGWINCH handler — the next refresh re-queries the
9565/// terminal size and re-renders, then clears the flag.
9566pub static WINCHANGED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:1827
9567
9568/// Port of C's `GETKEY_*` flag enumeration from `Src/zsh.h:3143`.
9569/// Passed to `getkeystring_with` to alter escape interpretation:
9570/// - GETKEY_OCTAL_ESC: `\NNN` octal escapes are interpreted (1<<0)
9571/// - GETKEY_EMACS: unknown `\<char>` drops the backslash (1<<1)
9572/// - GETKEY_BACKSLASH_C: `\c` truncates the result (1<<3)
9573pub const GETKEY_OCTAL_ESC: u32 = 1 << 0; // c:zsh.h:3143
9574/// `GETKEY_EMACS` constant.
9575pub const GETKEY_EMACS: u32 = 1 << 1; // c:zsh.h:3150
9576/// `GETKEY_CTRL` constant.
9577pub const GETKEY_CTRL: u32 = 1 << 2; // c:zsh.h:3152
9578/// `GETKEY_BACKSLASH_C` constant.
9579pub const GETKEY_BACKSLASH_C: u32 = 1 << 3; // c:zsh.h:3154
9580
9581/// `GETKEYS_PRINT = GETKEY_OCTAL_ESC | GETKEY_BACKSLASH_C |
9582/// GETKEY_EMACS` per Src/zsh.h:3185 — the flag set `bin_print` uses
9583/// when interpreting escapes (with EMACS: unknown `\<c>` → `<c>`).
9584pub const GETKEYS_PRINT: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_BACKSLASH_C; // c:zsh.h:3185
9585
9586/// `GETKEYS_ECHO = GETKEY_BACKSLASH_C` per Src/zsh.h:3178 — the flag
9587/// set `echo` uses (and `print -e`). NOT GETKEY_EMACS: unknown
9588/// `\<c>` is preserved as literal `\<c>`. NOT GETKEY_OCTAL_ESC:
9589/// `\NNN` is NOT interpreted as octal. Only `\a \b \e \E \f \n \r
9590/// \t \v \\` (plus `\c` truncation) are handled.
9591///
9592/// Per builtin.c:4754-4760 — `bin_print` selects this for BIN_ECHO
9593/// or when `-e` is set; otherwise GETKEYS_PRINT.
9594pub const GETKEYS_ECHO: u32 = GETKEY_BACKSLASH_C; // c:zsh.h:3178
9595
9596/// `GETKEYS_BINDKEY = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL`
9597/// per Src/zsh.h:3187 — the flag set `print -b` uses. Like GETKEYS_PRINT
9598/// it has EMACS (so `\C-`/`\M-` backslash escapes are processed and
9599/// unknown `\<c>` collapses to `<c>`) but adds GETKEY_CTRL (enabling the
9600/// `^X` caret notation) and drops GETKEY_BACKSLASH_C (no `\c` truncation).
9601pub const GETKEYS_BINDKEY: u32 = GETKEY_OCTAL_ESC | GETKEY_EMACS | GETKEY_CTRL; // c:zsh.h:3187
9602
9603/// Static `int ep` from Src/utils.c:4775 — sticky flag suppressing the
9604/// `can't set tty pgrp` warning after the first failure.
9605static ATTACHTTY_EP: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:4775
9606
9607// =====================================================================
9608// !!! WARNING: RUST-ONLY HELPERS — NO DIRECT C COUNTERPART !!!
9609// =====================================================================
9610//
9611// These three ported (`fdtable_lock`, `fdtable_get`, `fdtable_set`) DO
9612// NOT EXIST as functions in `Src/utils.c`. The C source declares
9613// `fdtable` as a bare `unsigned char *` global (Src/utils.c:~63) and
9614// every call site reads/writes it as a direct array index:
9615//
9616// if (fdtable[fd] != FDT_UNUSED) ...
9617// fdtable[fd] = FDT_EXTERNAL;
9618//
9619// Rust can't hand out raw mutable indexes through a `Mutex<Vec<u8>>`
9620// safely without a borrow scope, so the Rust port wraps the same
9621// slot access in three `pub fn`s. Each call site to these helpers
9622// corresponds 1:1 to a `fdtable[fd] = X` or `fdtable[fd] != X`
9623// statement in the C source — they are NOT new policy, they only
9624// adapt the storage shape from `unsigned char *` to a growable
9625// `Mutex<Vec<i32>>`.
9626//
9627// Also adapts `growfdtable` (Src/utils.c:1965) by lazily growing the
9628// Vec inside `fdtable_set` instead of a separate `growfdtable`
9629// call — the C source calls `growfdtable(fd)` immediately before
9630// every `fdtable[fd] = X` write anyway.
9631//
9632// !!! Do NOT use these for any state that the C source does not
9633// already store in `fdtable[]`. Adding a new "fd kind" here is a
9634// scope expansion, not a port. !!!
9635// =====================================================================
9636
9637static FDTABLE: std::sync::OnceLock<Mutex<Vec<i32>>> = std::sync::OnceLock::new();
9638
9639/// Port of `int max_zsh_fd` global from `Src/exec.c:201`.
9640/// "The highest fd we know zsh is using" — bumped by `fdtable_set`
9641/// callers, shrunk by `zclose` when trailing slots fall to UNUSED.
9642pub static MAX_ZSH_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
9643
9644/// Port of `int fdtable_flocks` global from `Src/exec.c:204`.
9645/// Count of `FDT_FLOCK`-tagged fds; consulted by `closem()` to
9646/// decide whether to skip the flock-fd sweep.
9647pub static FDTABLE_FLOCKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
9648
9649/// Setter for `scriptname`. Called from `bin_dot` / `source`
9650/// when entering a script.
9651pub fn set_scriptname(name: Option<String>) {
9652 *scriptname_lock().lock().unwrap() = name;
9653}
9654
9655/// Read `scriptname` — direct mirror of the C file-scope
9656/// `char *scriptname;` at `Src/utils.c:36`. Exposed for the
9657/// prompt expander (`%N`), `bin_dot`, and ZLE source tracking.
9658pub fn scriptname_get() -> Option<String> {
9659 scriptname_lock().lock().unwrap().clone()
9660}
9661
9662/// Setter for `scriptfilename` — direct mirror of the C
9663/// `scriptfilename` global at `Src/init.c` (zsh.export:356).
9664/// Called from `bin_dot` at init.c:1592, restored at c:1667.
9665pub fn set_scriptfilename(name: Option<String>) {
9666 *scriptfilename_lock().lock().unwrap() = name;
9667}
9668
9669/// Read `scriptfilename` — direct mirror of the C global.
9670/// Used by PS4's `%x` (`Src/prompt.c:937`) and the function-call
9671/// frame at `Src/exec.c:1613` (`fstack.filename = scriptfilename`).
9672pub fn scriptfilename_get() -> Option<String> {
9673 scriptfilename_lock().lock().unwrap().clone()
9674}
9675
9676/// Read `locallevel` — function nesting depth. Routes through the
9677/// canonical `LOCALLEVEL` static (port of
9678/// `Src/params.c:54`). The prior Rust port stored a duplicate
9679/// `Mutex<i32>` here that split state from params.rs; both copies
9680/// were never kept in sync.
9681pub fn locallevel() -> i32 {
9682 LOCALLEVEL.load(Ordering::Relaxed)
9683}
9684
9685/// Bump `locallevel` (called by `startparamscope`).
9686pub fn inc_locallevel() {
9687 LOCALLEVEL.fetch_add(1, Ordering::Relaxed);
9688}
9689
9690/// Decrement `locallevel` (called by `endparamscope`).
9691pub fn dec_locallevel() {
9692 LOCALLEVEL.fetch_sub(1, Ordering::Relaxed);
9693}
9694
9695/// Setter for `argzero`. Called once at shell init from `parseargs`.
9696pub fn set_argzero(name: Option<String>) {
9697 // c:271 (init.c) — `argv0 = argzero = posixzero = *argv++;`. At
9698 // shell init both argzero and posixzero share the same source.
9699 // posixzero is preserved unchanged after later mutations (exec -a,
9700 // function frames) — argzero is what gets rewritten. If posixzero
9701 // hasn't been independently set yet, mirror argzero so the
9702 // POSIXARGZERO branch in `argzerogetfn` returns something useful.
9703 let mut posix_lock = posixzero_lock().lock().unwrap();
9704 if posix_lock.is_none() {
9705 *posix_lock = name.clone();
9706 }
9707 drop(posix_lock);
9708 *argzero_lock().lock().unwrap() = name;
9709}
9710
9711/// Read `argzero`. Used by `argzerogetfn` for `$0`.
9712pub fn argzero() -> Option<String> {
9713 argzero_lock().lock().unwrap().clone()
9714}
9715
9716/// Port of `char *posixzero` from `Src/params.c:76`. The original
9717/// `argv[0]` preserved from shell startup, unchanged by later `exec -a`
9718/// or function-call rewrites. C's `argzerogetfn` (params.c:4958)
9719/// returns this instead of `argzero` when `isset(POSIXARGZERO)`.
9720///
9721/// Set once at init via `set_posixzero` (called from the init path
9722/// in C at init.c:271/297/321). `set_argzero` mirrors the value here
9723/// on first call so the POSIXARGZERO branch has something to read
9724/// even if the init path hasn't run yet.
9725pub fn set_posixzero(name: Option<String>) {
9726 *posixzero_lock().lock().unwrap() = name;
9727}
9728
9729/// Read `posixzero`. C: `posixzero` (params.c:76 global). Used by
9730/// `argzerogetfn` when `isset(POSIXARGZERO)`.
9731pub fn posixzero() -> Option<String> {
9732 posixzero_lock().lock().unwrap().clone()
9733}
9734
9735/// Setter for `noerrs`. Increment to suppress error output;
9736/// decrement to restore.
9737pub fn set_noerrs(v: i32) {
9738 *noerrs_lock().lock().unwrap() = v;
9739}
9740
9741/// Setter for `locallevel`. Called by function-call entry/exit.
9742pub fn set_locallevel(v: i32) {
9743 LOCALLEVEL.store(v, Ordering::Relaxed);
9744}
9745
9746/// Setter for `lineno`. Called by the parser as it advances.
9747pub fn set_lineno(v: i32) {
9748 *lineno_lock().lock().unwrap() = v;
9749}
9750
9751/// Setter for the cached SHINSTDIN flag. Called by the option
9752/// machinery whenever `setopt shinstdin` / `unsetopt shinstdin`
9753/// fires.
9754pub fn set_shinstdin(v: bool) {
9755 *shinstdin_lock().lock().unwrap() = v;
9756}
9757
9758/// Check if string is a valid identifier
9759// `isident` DELETED — fake duplicate (cited `c:params.c:1288` from
9760// utils.rs, location mismatch). Canonical port is
9761// `isident` at `params.rs:2056`, which
9762// correctly handles namespace prefix (`ns.var`) per the real C
9763// body — the utils.rs copy dropped that arm. Callers now route
9764// through `isident` directly.
9765
9766// QuoteType enum + impl deleted — the canonical quote-type values are
9767// the bare `QT_*: i32` constants at `zsh_h.rs:175` (port of anonymous
9768// `enum { QT_NONE, QT_BACKSLASH, … }` from `Src/zsh.h:253-298`).
9769// `quotestring()` takes `quote_type: i32` matching C's signature.
9770
9771/// Map a `(q)` flag count to a `QT_*` value.
9772/// Port of the q-flag dispatch in `Src/subst.c` `paramsubst()` —
9773/// `(q)`=`QT_BACKSLASH`, `(qq)`=`QT_SINGLE`, `(qqq)`=`QT_DOUBLE`,
9774/// `(qqqq+)`=`QT_DOLLARS`.
9775pub fn qflag_quotetype(count: u32) -> i32 {
9776 match count {
9777 0 => QT_NONE,
9778 1 => QT_BACKSLASH,
9779 2 => QT_SINGLE,
9780 3 => QT_DOUBLE,
9781 _ => QT_DOLLARS,
9782 }
9783}
9784
9785/// Port of `ispecial()` macro from `Src/ztype.h:59` — `zistype(X,
9786/// ISPECIAL)`. C populates the ISPECIAL typtab bit at
9787/// `Src/utils.c:4253-4262`:
9788/// - Every byte in `SPECCHARS` (`Src/zsh.h:228` =
9789/// `"#$^*()=|{}[]\`<>?~;&\\n\\t \\\\\\'\\""`)
9790/// - `,` when `typtab_flags & ZTF_SP_COMMA` (set by
9791/// `makecommaspecial(1)` per c:4271)
9792/// - `bangchar` (default `!`) when `BANGHIST` is set AND
9793/// `ZTF_INTERACT` is set (interactive shell)
9794///
9795/// The Rust port enumerates `SPECCHARS` directly because the
9796/// typtab is not lazily initialised — production `ispecial` calls
9797/// happen after `init_typtab` runs at shell startup, but
9798/// `quotestring` is unit-tested in isolation where the typtab is
9799/// all-zero. Match the C SPECCHARS set byte-for-byte; the option-
9800/// driven augments (`,` and bangchar) are still NOT wired through
9801/// (a known gap — see the typtab-routing TODO at the param level).
9802fn ispecial(c: char) -> bool {
9803 // c:59 (Src/ztype.h)
9804 // c:228 (Src/zsh.h) `SPECCHARS` — exact byte set from the C
9805 // string literal. `^` and `{`/`}` were already present; `!`
9806 // is INTENTIONALLY OMITTED here because C only ISPECIAL-tags
9807 // bangchar under BANGHIST + interactive. Including `!`
9808 // unconditionally diverges from C for non-interactive scripts
9809 // and unbang'd input.
9810 matches!(
9811 c,
9812 '#' | '$' | '^' | '*' | '(' | ')' | '=' // c:228 first half
9813 | '|' | '{' | '}' | '[' | ']' | '`' // c:228 mid
9814 | '<' | '>' | '?' | '~' | ';' | '&' // c:228 specials
9815 | '\n' | '\t' | ' ' | '\\' | '\'' | '"' // c:228 whitespace/backslash/quotes
9816 )
9817}
9818
9819/// Convert integer to string with specified base — re-export of
9820/// the canonical port at `convbase_param`.
9821///
9822/// The previous Rust port at this file was a SECOND, divergent
9823/// implementation of `convbase`:
9824/// - Used LOWERCASE digit chars (`"0123456789abc..."`) for
9825/// bases 11..36, while C uses UPPERCASE (`(dig - 10) + 'A'`
9826/// at Src/params.c:5621).
9827/// - Skipped CBASES + OCTALZEROES prefix handling at c:5598-5604
9828/// (`0x`/`0` prefixes when option-gated).
9829/// - Returned wrong format for bases not handled by the explicit
9830/// match arms.
9831///
9832/// The canonical port lives at `params.rs::convbase_ptr` (c:5588
9833/// faithful body) + `params.rs::convbase` (c:5634 wrapper). Route
9834/// through there so utils.rs / params.rs agree byte-for-byte and
9835/// no divergent duplicate stays alive.
9836pub fn convbase(val: i64, base: u32) -> String {
9837 // c:5634 (Src/params.c)
9838 convbase_param(val, base)
9839}
9840
9841// `checkglobqual` DELETED — fake bracket-depth scanner cited
9842// `c:glob.c:1158` from utils.rs. Canonical port is
9843// `crate::ported::glob::checkglobqual(str, sl, nobareglob, sp)` at
9844// `glob.rs:813`, which matches the real C signature
9845// `(char *str, int sl, int nobareglob, char **sp)`. The utils.rs
9846// fake reduced the signature to a single bool and discarded the
9847// `sp` glob-qualifier-start out-pointer that callers need. Zero
9848// callers used the utils.rs version.
9849
9850// `dupstrpfx` DELETED — fake duplicate cited `c:string.c:161` from
9851// utils.rs (wrong location). Canonical port is
9852// `dupstrpfx` at `string.rs:161`. Zero
9853// callers used the utils.rs version.
9854
9855/// Get username from UID (from utils.c getpwuid handling)
9856pub fn statuidprint(uid: u32) -> Option<String> {
9857 #[cfg(unix)]
9858 {
9859 let pwd = unsafe { libc::getpwuid(uid) };
9860 if pwd.is_null() {
9861 return None;
9862 }
9863 let name = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_name) };
9864 name.to_str().ok().map(|s| s.to_string())
9865 }
9866 #[cfg(not(unix))]
9867 {
9868 let _ = uid;
9869 None
9870 }
9871}
9872
9873// `ztrdup` / `dyncat` / `tricat` / `bicat` DELETED — these were
9874// fake duplicates of the canonical ports in `src/ported/string.rs`
9875// (`Src/string.c:62`, `:131`, `:98`, `:145`). The utils.rs copies
9876// admitted they were not in utils.c via `c:string.c:NNN`
9877// annotations; PORT.md Rule 1 disallows the duplicate location.
9878// Callers now route through `crate::ported::string::{ztrdup,
9879// dyncat, tricat, bicat}` directly.
9880
9881// `iwsep` / `iwsep_byte` DELETED — these were fake hardcoded
9882// `c==' '||c=='\t'||c=='\n'` checks claiming to port `iwsep` from
9883// "Src/zsh.h", but the real macro lives at `Src/ztype.h:61`
9884// (`#define iwsep(X) zistype(X, IWSEP)`) and consults the `typtab[]`
9885// lookup — which mutates when IFS is reassigned. The canonical port
9886// is `iwsep(u8) -> bool` at `ztype_h.rs:133`.
9887// Internal utils.rs callers (skipwsep / spacesplit / findword) now
9888// route through it directly.
9889
9890// `imeta(c: char)` DELETED — fake `char`-arg wrapper cited
9891// `c:ztype.h:60` from utils.rs (wrong location, wrong signature).
9892// Canonical port is `crate::ported::ztype_h::imeta(u8) -> bool` at
9893// `ztype_h.rs:130`. C `imeta(X)` takes a byte; wrapping with `char`
9894// invented an `> 0xff` early-out that C doesn't have. Migrated 1
9895// caller (`zle/computil.rs:6194`).
9896
9897/// Get hostname
9898pub fn gethostname() -> String {
9899 #[cfg(unix)]
9900 {
9901 let mut buf = vec![0u8; 256];
9902 unsafe {
9903 if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) == 0 {
9904 let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
9905 return String::from_utf8_lossy(&buf[..len]).to_string();
9906 }
9907 }
9908 }
9909 // gethostname(2) failure fallback. C consults `$HOST` /
9910 // `cached_hostname`. Read paramtab; fall back to "localhost".
9911 getsparam("HOST")
9912 .or_else(|| getsparam("HOSTNAME"))
9913 .unwrap_or_else(|| "localhost".to_string())
9914}
9915
9916/// Get current working directory — re-export of the canonical
9917/// port at `compat::zgetcwd` (Src/compat.c:559).
9918// `zgetcwd` DELETED — fake `Option<String>` wrapper around the
9919// canonical `compat::zgetcwd() -> String` port (`Src/compat.c:559`).
9920// C `zgetcwd` never returns NULL (falls back to `"."` per the
9921// `dupstring(".")` arm), so the `Option` was caller-API churn, not
9922// a port. Callers route through `crate::ported::compat::zgetcwd()`
9923// directly.
9924
9925// `zchdir` duplicate deleted — canonical port at compat.rs:253
9926// matches C's `int zchdir(char *dir)` signature (returns i32, not
9927// bool). Zero callers used the utils.rs bool variant.
9928
9929// `realpath` / `mkdir` / `symlink` / `readlink` / `getenv` DELETED —
9930// five Rust-only convenience wrappers around std::fs / std::env /
9931// std::os::unix::fs with ZERO callers across the tree. Each was a
9932// thin std-lib re-export with no zsh C counterpart:
9933// - realpath → std::fs::canonicalize (libc realpath(3) — not in
9934// utils.c; the zsh source uses chrealpath at hist.c:1971)
9935// - mkdir → std::fs::create_dir (libc mkdir(2) — zsh's analogue
9936// is Modules/files.c::bin_mkdir, not a utils.c helper)
9937// - symlink → std::os::unix::fs::symlink (libc symlink(2))
9938// - readlink → std::fs::read_link (libc readlink(2))
9939// - getenv → std::env::var (libc getenv(3) — zsh's analogue is
9940// zgetenv in compat.c)
9941// PORT.md Rule 0/A: names must exist in upstream zsh C source as a
9942// `<name>(` function. None of these qualify; with zero callers they
9943// were dead code by definition. Callers needing the same semantics
9944// route through the canonical port (chrealpath / bin_mkdir / etc.)
9945// or std::fs directly.
9946
9947/// Per-prompt callback registry.
9948/// Port of the static `prepromptfns` LinkList in Src/utils.c:1319.
9949/// Holds the bare-fn pointers `addprepromptfn`/`delprepromptfn`
9950/// register and `preprompt()` walks.
9951static PREPROMPT_FNS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
9952
9953/// Set environment variable
9954pub fn setenv(name: &str, value: &str) {
9955 std::env::set_var(name, value);
9956}
9957
9958/// Unset environment variable
9959pub fn unsetenv(name: &str) {
9960 std::env::remove_var(name);
9961}
9962
9963/// Time-ordered timed-function registry.
9964/// Port of the `timedfns` LinkList Src/utils.c:1371 keeps for the
9965/// `sched` builtin. Sorted ascending by `when` (epoch seconds);
9966/// `addtimedfn` does an insertion sort (matches the C source's
9967/// `for (;;)` walk at lines 1394-1411).
9968pub static TIMED_FNS: Mutex<Vec<(i64, fn())>> = Mutex::new(Vec::new()); // c:1371 timedfns (mod_export in C)
9969
9970/// Get current user ID
9971pub fn getuid() -> u32 {
9972 #[cfg(unix)]
9973 unsafe {
9974 libc::getuid()
9975 }
9976 #[cfg(not(unix))]
9977 0
9978}
9979
9980/// Get effective user ID
9981pub fn geteuid() -> u32 {
9982 #[cfg(unix)]
9983 unsafe {
9984 libc::geteuid()
9985 }
9986 #[cfg(not(unix))]
9987 0
9988}
9989
9990/// Get current group ID
9991pub fn getgid() -> u32 {
9992 #[cfg(unix)]
9993 unsafe {
9994 libc::getgid()
9995 }
9996 #[cfg(not(unix))]
9997 0
9998}
9999
10000/// Get effective group ID
10001pub fn getegid() -> u32 {
10002 #[cfg(unix)]
10003 unsafe {
10004 libc::getegid()
10005 }
10006 #[cfg(not(unix))]
10007 0
10008}
10009
10010/// Get process ID
10011pub fn getpid() -> i32 {
10012 std::process::id() as i32
10013}
10014
10015/// Get parent process ID
10016pub fn getppid() -> i32 {
10017 #[cfg(unix)]
10018 unsafe {
10019 libc::getppid()
10020 }
10021 #[cfg(not(unix))]
10022 0
10023}
10024
10025/// `getkeystring` with the C `how` parameter (Src/utils.c:6915).
10026/// The plain `getkeystring(s)` shim above defaults `how=0` for
10027/// non-EMACS callers (zbeep, dollar-quote — they keep unknown
10028/// `\<char>` as literal `\<char>`). Pass `GETKEYS_PRINT` to get
10029/// the print/echo behavior (drop backslash on unknown).
10030/// `getkeystring` with the C `how` parameter AND the
10031/// `GETKEY_UPDATE_OFFSET` cursor-offset out-param (Src/utils.c:6915).
10032///
10033/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from
10034/// `Src/utils.c:6915`, specifically its `GETKEY_UPDATE_OFFSET` path.
10035/// When `how` sets `GETKEY_UPDATE_OFFSET` and `misc` is `Some`, `*misc`
10036/// is the C `*misc` completion offset — a byte index into `s` — and is
10037/// updated as escapes positioned before the cursor collapse:
10038/// - `-1` per collapsed `\<char>` escape whose backslash precedes the
10039/// cursor, `s - sstart < *misc` (c:6987); undone if the backslash is
10040/// kept literal in the default (non-EMACS) arm (c:7181-7183);
10041/// - additional `-6` for `\u`, `-10` (`-4`+`-6`) for `\U` (c:7073-7084),
10042/// then `+N` for the N output bytes emitted (c:7123-7124).
10043/// Octal/hex escapes get only the base `-1`, matching C's own incomplete
10044/// `/* HERE: GETKEY_UPDATE_OFFSET? */` handling (c:7155).
10045///
10046/// The offset is a byte index. For ASCII `$'...'` content (one byte per
10047/// char) it matches the C metafied-byte semantics exactly; the byte-vs-
10048/// char divergence for non-ASCII content is the pre-existing shared-cursor
10049/// limitation documented on `set_comp_sep` (compcore.rs), not new here.
10050///
10051/// Callers with no offset to track pass `misc = None`.
10052pub fn getkeystring_with(
10053 s: &str,
10054 how: u32,
10055 mut misc: Option<&mut i32>,
10056) -> (String, usize) {
10057 // c:utils.c:6915
10058 let update_off = (how & crate::ported::zsh_h::GETKEY_UPDATE_OFFSET as u32) != 0;
10059 let mut result = String::new();
10060 let mut chars = s.chars().peekable();
10061 let mut consumed = 0;
10062 // c:7194 — C's `^` handler is `control = 1; continue;`, so the control bit
10063 // stays PENDING across loop iterations and a following `\M`/`\C` escape
10064 // chains onto it (`^\M-a` → c:7034 `meta = 1 + control` → meta==2 → 0x81).
10065 // This port's `^` arm consumes its base character inline, which cannot
10066 // express that, so the flag carries the state to the modifier arm instead.
10067 let mut pending_control = false;
10068 while let Some(c) = chars.next() {
10069 consumed += c.len_utf8();
10070 // c:utils.c:7194 — `^X` caret notation. A bare `^` (not a backslash
10071 // escape) followed by any char applies the control mask to it, but
10072 // ONLY under GETKEY_CTRL — i.e. `print -b` / bindkey. Plain print
10073 // (GETKEYS_PRINT, no GETKEY_CTRL) keeps `^A` literal.
10074 if c == '^' && !pending_control && (how & GETKEY_CTRL) != 0 {
10075 // A `\M`/`\C` escape after the caret must be handled by the
10076 // modifier arm with control already pending — C reaches it via
10077 // `continue` rather than consuming the base char here. Gated on
10078 // GETKEY_EMACS because that is what enables the modifier arm at
10079 // all (c:7031/7043); without it C's `case 'M'` emits a literal
10080 // backslash instead.
10081 let mut la = chars.clone();
10082 if (how & GETKEY_EMACS) != 0
10083 && la.next() == Some('\\')
10084 && matches!(la.next(), Some('M') | Some('C'))
10085 {
10086 pending_control = true;
10087 continue;
10088 }
10089 if let Some(base) = chars.next() {
10090 consumed += base.len_utf8();
10091 let mut byte = base as u32;
10092 // c:7261-7267 — `^?` → 0x7f (DEL), else clear bits 5-6.
10093 if byte == '?' as u32 {
10094 byte = 0x7f;
10095 } else if byte <= 0xff {
10096 byte &= 0x9f;
10097 }
10098 if byte <= 0xff {
10099 let b_ = byte as u8;
10100 if b_ < 0x80 {
10101 result.push(b_ as char);
10102 } else {
10103 result.push('\u{83}');
10104 result.push(char::from(b_ ^ 32));
10105 }
10106 } else if let Some(ch) = char::from_u32(byte) {
10107 result.push(ch);
10108 }
10109 continue;
10110 }
10111 // c:7194 `s[1]` guard — a trailing `^` with no char is literal.
10112 result.push(c);
10113 continue;
10114 }
10115 if c != '\\' {
10116 result.push(c);
10117 continue;
10118 }
10119 // c is a backslash; record its byte start offset for
10120 // GETKEY_UPDATE_OFFSET (`s - sstart` in C).
10121 let bs_off = consumed - c.len_utf8();
10122 // c:utils.c:6987 — base offset decrement for a collapsed `\<char>`
10123 // escape whose backslash precedes the cursor. C gates on
10124 // `*s == '\\' && s[1]`, so a following char must exist (peek).
10125 let mut miscadded = false;
10126 if update_off && chars.peek().is_some() {
10127 if let Some(m) = misc.as_deref_mut() {
10128 if (bs_off as i32) < *m {
10129 *m -= 1;
10130 miscadded = true;
10131 }
10132 }
10133 }
10134 match chars.next() {
10135 Some('n') => {
10136 result.push('\n');
10137 consumed += 1;
10138 }
10139 Some('t') => {
10140 result.push('\t');
10141 consumed += 1;
10142 }
10143 Some('r') => {
10144 result.push('\r');
10145 consumed += 1;
10146 }
10147 // c:utils.c:7018-7024 — `\E` is EMACS-gated:
10148 // case 'E':
10149 // if (!(how & GETKEY_EMACS)) {
10150 // *t++ = '\\', s--;
10151 // if (miscadded) (*misc)++;
10152 // continue;
10153 // }
10154 // /* FALL THROUGH */
10155 // case 'e':
10156 // *t++ = '\033';
10157 // Without EMACS `\E` stays a literal backslash + `E`; only `\e`
10158 // is unconditional. Folding `E` in with `e` made `${(g::)v}` on
10159 // `\E` yield ESC where zsh yields `\E`. Unlike the octal
10160 // `continue` arm (c:7159), C restores the miscadded decrement
10161 // here, since the backslash is kept.
10162 Some('E') if (how & GETKEY_EMACS) == 0 => {
10163 consumed += 1;
10164 if miscadded {
10165 if let Some(m) = misc.as_deref_mut() {
10166 *m += 1;
10167 }
10168 }
10169 result.push('\\');
10170 result.push('E');
10171 }
10172 Some('e') | Some('E') => {
10173 result.push('\x1b');
10174 consumed += 1;
10175 }
10176 Some('a') => {
10177 result.push('\x07');
10178 consumed += 1;
10179 }
10180 Some('b') => {
10181 result.push('\x08');
10182 consumed += 1;
10183 }
10184 Some('f') => {
10185 result.push('\x0c');
10186 consumed += 1;
10187 }
10188 Some('v') => {
10189 result.push('\x0b');
10190 consumed += 1;
10191 }
10192 // c:utils.c:7140-7152 — `\\` (and `\'` under
10193 // GETKEY_DOLLAR_QUOTE) explicitly emit the trailing byte
10194 // bare. Outside DOLLAR_QUOTE, `\'` FALLTHROUGHs to default.
10195 // `\\` stays special because the default arm (c:7180-7184)
10196 // skips the leading-backslash emission iff `*s == '\\'`,
10197 // so `\\` reduces to `\` regardless of GETKEY_EMACS.
10198 //
10199 // Previous Rust port also had `Some('\'')` and `Some('"')`
10200 // arms that unconditionally dropped the backslash. That
10201 // contradicted C: under GETKEYS_ECHO (no GETKEY_EMACS),
10202 // `\'` must remain `\'` because echo doesn't strip
10203 // unknown escapes. Removed those arms — `\'` and `\"`
10204 // now flow through the default arm and honour GETKEY_EMACS.
10205 //
10206 // Root cause of `echo "${(qq)s}"` for `s="a'b"` emitting
10207 // `'a'''b'` instead of zsh's `'a'\''b'`.
10208 Some('\\') => {
10209 result.push('\\');
10210 consumed += 1;
10211 }
10212 // c:utils.c:7072-7138 — `\u` (4-hex) / `\U` (8-hex)
10213 // Unicode codepoint escapes. Always interpreted; the C
10214 // source's `case 'U':` / `case 'u':` arms have no flag
10215 // gating (only the GETKEY_UPDATE_OFFSET bookkeeping). C
10216 // calls `ucs4tomb(wval, t)` which writes UTF-8 bytes.
10217 // Previously these were absent from `getkeystring_with`,
10218 // so `echo -e "\U00000041"` emitted literal `\U00000041`
10219 // instead of 'A'.
10220 Some('u') => {
10221 consumed += 1;
10222 // c:utils.c:7077-7084 — extra `-6` when the escape precedes
10223 // the cursor (checked at the 'u', i.e. offset bs_off+1).
10224 if update_off {
10225 if let Some(m) = misc.as_deref_mut() {
10226 if ((bs_off + 1) as i32) < *m {
10227 *m -= 6;
10228 }
10229 }
10230 }
10231 let mut hex = String::new();
10232 for _ in 0..4 {
10233 if let Some(&c) = chars.peek() {
10234 if c.is_ascii_hexdigit() {
10235 hex.push(chars.next().unwrap());
10236 consumed += 1;
10237 } else {
10238 break;
10239 }
10240 }
10241 }
10242 // c:utils.c:7085 `wval = 0;` BEFORE the digit loop, and
10243 // c:7087-7095 leaves it untouched when the first char is not
10244 // a hex digit (`s--; break;`). So ZERO digits is not "not an
10245 // escape" — it emits codepoint 0 via `ucs4tomb(wval, t)`
10246 // (c:7101). `\u` → one NUL byte, `\uzz` → NUL + "zz". An Err
10247 // on the empty parse pushed nothing at all.
10248 let val = u32::from_str_radix(&hex, 16).unwrap_or(0);
10249 if let Some(ch) = char::from_u32(val) {
10250 result.push(ch);
10251 // c:utils.c:7123-7124 — add one per output byte,
10252 // checked at the last consumed input byte.
10253 if update_off {
10254 if let Some(m) = misc.as_deref_mut() {
10255 if ((consumed - 1) as i32) < *m {
10256 *m += ch.len_utf8() as i32;
10257 }
10258 }
10259 }
10260 }
10261 }
10262 Some('U') => {
10263 consumed += 1;
10264 // c:utils.c:7073-7084 — `\U` extra `-4` (c:7073) then the
10265 // shared `\u` `-6` (c:7077), both gated at offset bs_off+1.
10266 if update_off {
10267 if let Some(m) = misc.as_deref_mut() {
10268 if ((bs_off + 1) as i32) < *m {
10269 *m -= 10;
10270 }
10271 }
10272 }
10273 let mut hex = String::new();
10274 for _ in 0..8 {
10275 if let Some(&c) = chars.peek() {
10276 if c.is_ascii_hexdigit() {
10277 hex.push(chars.next().unwrap());
10278 consumed += 1;
10279 } else {
10280 break;
10281 }
10282 }
10283 }
10284 // c:utils.c:7085 — same `wval = 0` default as the `\u` arm
10285 // above: `\U` with no hex digits emits codepoint 0, not
10286 // nothing.
10287 let val = u32::from_str_radix(&hex, 16).unwrap_or(0);
10288 if let Some(ch) = char::from_u32(val) {
10289 result.push(ch);
10290 // c:utils.c:7123-7124 — add one per output byte.
10291 if update_off {
10292 if let Some(m) = misc.as_deref_mut() {
10293 if ((consumed - 1) as i32) < *m {
10294 *m += ch.len_utf8() as i32;
10295 }
10296 }
10297 }
10298 }
10299 }
10300 // c:utils.c:7156-7178 — the NUMERIC-ESCAPE branch. C handles
10301 // `\x`, `\NNN` and `\0NNN` in ONE arm, and the shared tail is
10302 // what makes them agree, so this port keeps them together too:
10303 //
10304 // if ((idigit(*s) && *s < '8') || *s == 'x') {
10305 // if (!(how & GETKEY_OCTAL_ESC)) {
10306 // if (*s == '0')
10307 // s++;
10308 // else if (*s != 'x') {
10309 // *t++ = '\\', s--;
10310 // continue;
10311 // }
10312 // }
10313 // if (s[1] && s[2] && s[3]) {
10314 // svchar = s[3]; s[3] = '\0'; u = s;
10315 // }
10316 // *t++ = zstrtol(s + (*s == 'x'), &s,
10317 // (*s == 'x') ? 16 : 8);
10318 // ...
10319 // s--;
10320 // }
10321 //
10322 // Three behaviours here were each missing when these were three
10323 // separate arms:
10324 //
10325 // 1. Without GETKEY_OCTAL_ESC a NON-ZERO digit is not an escape
10326 // at all (c:7159): C emits a literal backslash and backs up so
10327 // the digit is re-read as an ordinary char. This is INSIDE the
10328 // numeric branch, so — unlike the default arm — GETKEY_EMACS
10329 // does not suppress the backslash: `${(g:e:)v}` on `\101` is
10330 // `\101`, not `101`.
10331 // 2. The value goes through `zstrtol`, which SKIPS leading blanks
10332 // and accepts a `+`/`-` sign (c:2444-2450), and whose end
10333 // pointer lands past everything it consumed — blanks included.
10334 // So `\0 1` is byte 0o1, `\x 41` is 0x04 then `1`, `\x y` is
10335 // NUL then `y`, and `\0-1` is 0xff (a NEGATIVE parse truncated
10336 // to a byte). Peeking for "is the next char a digit" stopped at
10337 // the blank and left it in the output.
10338 // 3. The base is chosen from the char AT `s` AFTER the `\0`
10339 // introducer was skipped, so `\0x41` is HEX (`A`), not NUL
10340 // followed by `x41`.
10341 //
10342 // c:7166-7168 windows the parse by NUL-ing `s[3]` when three chars
10343 // follow `s`, capping it at 3 significant chars (2 after an `x`),
10344 // then restores the byte — so `\1234` is 0o123 then `4`.
10345 //
10346 // `*t++ = zstrtol(...)` assigns a zlong to a char, truncating to
10347 // the low byte: `\777` (511) is 0xff, `\400` (256) is NUL. Parsing
10348 // straight into u8 made those an Err and emitted nothing at all.
10349 Some(d) if d.is_digit(8) || d == 'x' => {
10350 consumed += 1;
10351 // c:7157-7161 — the pre-checks that run only without
10352 // GETKEY_OCTAL_ESC. `\0` is the octal introducer (`s++`);
10353 // any other bare digit is not an escape.
10354 let zero_intro = (how & GETKEY_OCTAL_ESC) == 0 && d == '0';
10355 if (how & GETKEY_OCTAL_ESC) == 0 && d != '0' && d != 'x' {
10356 // c:7159 `*t++ = '\\', s--; continue;`
10357 result.push('\\');
10358 result.push(d);
10359 continue;
10360 }
10361 // C's `s` after the pre-checks: the char AFTER the `\0`
10362 // introducer, else the matched char itself.
10363 //
10364 // Four chars of lookahead is all C can read: the window test
10365 // reaches `s[3]`, and the parse never sees past it. Bounding
10366 // the clone keeps this O(1) per escape — collecting the whole
10367 // tail here would make decoding an escape-dense string O(n²).
10368 let rest: Vec<char> = chars.clone().take(4).collect();
10369 let (p, after): (Option<char>, &[char]) = if zero_intro {
10370 (rest.first().copied(), rest.get(1..).unwrap_or(&[]))
10371 } else {
10372 (Some(d), &rest[..])
10373 };
10374 // c:7166 `if (s[1] && s[2] && s[3])` — needs three chars after
10375 // `s`; the window then spans `s` plus two more.
10376 let win: &[char] = if after.len() >= 3 { &after[..2] } else { after };
10377 let mut vis: Vec<char> = Vec::with_capacity(3);
10378 if let Some(pc) = p {
10379 vis.push(pc);
10380 }
10381 vis.extend_from_slice(win);
10382 // c:7170 `zstrtol(s + (*s == 'x'), &s, (*s == 'x') ? 16 : 8)`.
10383 let (base, skip_p) = if p == Some('x') { (16, 1) } else { (8, 0) };
10384 let parse_src: String = vis[skip_p.min(vis.len())..].iter().collect();
10385 let (num, tail) = zstrtol(&parse_src, base);
10386 let used_chars = parse_src[..parse_src.len() - tail.len()].chars().count();
10387 // Advance past what C consumed. `vis[0]` is the already-taken
10388 // match char unless the `\0` introducer moved `s` forward.
10389 let vis_used = skip_p + used_chars;
10390 let advance = if zero_intro { vis_used } else { vis_used.saturating_sub(1) };
10391 for _ in 0..advance {
10392 chars.next();
10393 consumed += 1;
10394 }
10395 let val = (num as u64 & 0xff) as u8;
10396 // c:Src/utils.c — the escape is one raw BYTE; metafy high
10397 // bytes (c:7289-7294) so `\377` unmetafies to a single 0xff,
10398 // not the UTF-8 pair c3 bf.
10399 if val < 0x80 {
10400 result.push(val as char);
10401 } else {
10402 result.push('\u{83}');
10403 result.push(char::from(val ^ 32));
10404 }
10405 // c:7172-7173 — under GETKEY_PRINTF_PERCENT a numeric escape
10406 // producing `%` gets a second `%`.
10407 if (how & crate::ported::zsh_h::GETKEY_PRINTF_PERCENT as u32) != 0 && val == b'%' {
10408 result.push('%');
10409 }
10410 }
10411 // c:utils.c:7029-7052 + c:7255-7275 — `\C` / `\M` set the
10412 // control / meta modifiers (bindkey-style key escapes), an
10413 // optional `-` separator, then the next base char (possibly
10414 // through chained `\C`/`\M`) gets the mask applied:
10415 // control → `& 0x9f` (or `\C-?` → 0x7f), meta → `| 0x80`.
10416 // Gated on GETKEY_EMACS (c:7031/7043 `if (how & GETKEY_EMACS)`):
10417 // print / print -b / $'…' have it, echo does not (so echo keeps
10418 // `\C-a` literal via the default arm). Mirrors the same handler
10419 // already present in `getkeystring` (utils.rs) and
10420 // `getkeystring_dollar_quote` (lex.rs); print's `getkeystring_with`
10421 // path lacked it, so `print "\C-a"` emitted literal `C-a`.
10422 Some(mod_letter @ ('C' | 'M')) if (how & GETKEY_EMACS) != 0 => {
10423 consumed += 1;
10424 // c:7034 `meta = 1 + control; /* preserve the order of ^ and
10425 // meta */` — meta is a COUNT, not a flag: 2 when `\M` was seen
10426 // while control was already pending, which makes c:7261-7264
10427 // apply `|0x80` BEFORE the control mask instead of after. The
10428 // two orders differ for `?`: `\M-\C-?` is 0xff, `\C-\M-?` is
10429 // 0x9f. Booleans could not express that.
10430 // A `^` already seen (c:7194 `control = 1; continue;`) arrives
10431 // here as pending state, so `^\M-a` gives meta = 1 + 1 = 2.
10432 let mut control: u32 = if pending_control {
10433 pending_control = false;
10434 1
10435 } else {
10436 0
10437 };
10438 let mut meta: u32 = 0;
10439 if mod_letter == 'C' {
10440 control = 1; // c:7046
10441 } else {
10442 meta = 1 + control; // c:7034
10443 }
10444 // c:7032-7033 / 7044-7045 — `if (s[1] == '-') s++;` consumes at
10445 // most ONE separator per modifier. Looping over a RUN of dashes
10446 // ate the base character of `\M--` (meta applied to `-`, 0xad),
10447 // leaving an empty binding.
10448 if chars.peek() == Some(&'-') {
10449 chars.next();
10450 consumed += 1;
10451 }
10452 // Chained `\C`/`\M`, and the caret form after a modifier.
10453 loop {
10454 let mut iter_clone = chars.clone();
10455 if iter_clone.next() == Some('\\') {
10456 if let Some(nx) = iter_clone.next() {
10457 if nx == 'C' || nx == 'M' {
10458 chars.next(); // consume '\'
10459 chars.next(); // consume C/M
10460 consumed += 2;
10461 if nx == 'C' {
10462 control = 1; // c:7046
10463 } else {
10464 meta = 1 + control; // c:7034
10465 }
10466 if chars.peek() == Some(&'-') {
10467 chars.next();
10468 consumed += 1;
10469 }
10470 continue;
10471 }
10472 }
10473 }
10474 // c:7194 — `else if (*s == '^' && !control && (how &
10475 // GETKEY_CTRL) && s[1]) { control = 1; continue; }`. C's
10476 // `case 'M'` only sets `meta` and `continue`s, so the MAIN
10477 // loop reaches this `^` handler with meta still pending:
10478 // `\M-^?` is control+meta applied to `?` (0xff). This arm
10479 // consumed the base char itself, so `^` became the base and
10480 // `\M-^?` bound TWO bytes (0xde 0x3f) — breaking the very
10481 // common `bindkey '\M-^?' backward-kill-word`.
10482 if control == 0 && (how & GETKEY_CTRL) != 0 && chars.peek() == Some(&'^') {
10483 let mut it2 = chars.clone();
10484 it2.next();
10485 if it2.next().is_some() {
10486 // c:7194 `s[1]` guard
10487 chars.next();
10488 consumed += 1;
10489 control = 1;
10490 continue;
10491 }
10492 }
10493 break;
10494 }
10495 // Read one base character (allowing nested simple escapes).
10496 let base: Option<char> = if chars.peek() == Some(&'\\') {
10497 chars.next();
10498 consumed += 1;
10499 match chars.next() {
10500 Some('n') => {
10501 consumed += 1;
10502 Some('\n')
10503 }
10504 Some('t') => {
10505 consumed += 1;
10506 Some('\t')
10507 }
10508 Some('r') => {
10509 consumed += 1;
10510 Some('\r')
10511 }
10512 Some('a') => {
10513 consumed += 1;
10514 Some('\x07')
10515 }
10516 Some('b') => {
10517 consumed += 1;
10518 Some('\x08')
10519 }
10520 Some('e') | Some('E') => {
10521 consumed += 1;
10522 Some('\x1b')
10523 }
10524 Some('f') => {
10525 consumed += 1;
10526 Some('\x0c')
10527 }
10528 Some('v') => {
10529 consumed += 1;
10530 Some('\x0b')
10531 }
10532 Some('\\') => {
10533 consumed += 1;
10534 Some('\\')
10535 }
10536 Some('\'') => {
10537 consumed += 1;
10538 Some('\'')
10539 }
10540 Some('"') => {
10541 consumed += 1;
10542 Some('"')
10543 }
10544 Some(other) => {
10545 consumed += 1;
10546 Some(other)
10547 }
10548 None => None,
10549 }
10550 } else {
10551 chars.next().inspect(|c| {
10552 consumed += c.len_utf8();
10553 })
10554 };
10555 if let Some(ch) = base {
10556 let mut byte = ch as u32;
10557 // c:7261-7264 — `if (meta == 2) { t[-1] |= 0x80; meta = 0; }`
10558 // runs BEFORE the control mask: `\M` seen while control was
10559 // already pending sets the high bit first.
10560 if meta == 2 {
10561 byte |= 0x80;
10562 }
10563 // c:7265-7271 — control mask (`\C-?` → 0x7f, else & 0x9f).
10564 if control == 1 {
10565 if byte == '?' as u32 {
10566 byte = 0x7f;
10567 } else {
10568 byte &= 0x9f;
10569 }
10570 }
10571 // c:7272-7275 — `if (meta) { t[-1] |= 0x80; }` — the
10572 // meta==1 order, applied AFTER the mask.
10573 if meta == 1 {
10574 byte |= 0x80;
10575 }
10576 // c:7289-7294 — a masked byte >= 0x80 is metafied so it
10577 // unmetafies back to the single raw byte on output.
10578 if byte <= 0xff {
10579 let b_ = byte as u8;
10580 if b_ < 0x80 {
10581 result.push(b_ as char);
10582 } else {
10583 result.push('\u{83}');
10584 result.push(char::from(b_ ^ 32));
10585 }
10586 } else if let Some(c) = char::from_u32(byte) {
10587 result.push(c);
10588 }
10589 }
10590 }
10591 // c:utils.c:7180-7184 — default arm. With GETKEY_EMACS
10592 // set, drop the backslash; otherwise keep `\<char>`.
10593 Some(c) => {
10594 consumed += 1;
10595 // c:utils.c:7045 — `\c` under GETKEY_BACKSLASH_C
10596 // means TRUNCATE: drop the rest of the input,
10597 // suppress the trailing newline. Used by `echo`
10598 // (and `print` without -r). Set TLS flag so the
10599 // caller can detect + suppress the newline.
10600 if c == 'c' && (how & GETKEY_BACKSLASH_C) != 0 {
10601 GETKEY_TRUNCATED.with(|cell| cell.set(true));
10602 break;
10603 }
10604 if (how & GETKEY_EMACS) == 0 {
10605 // c:utils.c:7181-7183 — backslash kept literal (no EMACS),
10606 // so the string does not collapse here: undo the base
10607 // GETKEY_UPDATE_OFFSET decrement applied above.
10608 if miscadded {
10609 if let Some(m) = misc.as_deref_mut() {
10610 *m += 1;
10611 }
10612 }
10613 result.push('\\');
10614 }
10615 result.push(c);
10616 }
10617 None => {
10618 result.push('\\');
10619 }
10620 }
10621 }
10622 (result, consumed)
10623}
10624
10625thread_local! {
10626 /// Sticky flag set by `getkeystring_with` when a `\c` escape
10627 /// truncates the input under `GETKEY_BACKSLASH_C`. The caller
10628 /// (bin_print / bin_echo) reads + clears via `getkey_truncated_take()`
10629 /// to suppress the trailing newline and skip any subsequent args
10630 /// in the print pass.
10631 pub static GETKEY_TRUNCATED: std::cell::Cell<bool> =
10632 const { std::cell::Cell::new(false) };
10633}
10634
10635/// Read + clear the `\c`-truncated flag set by the previous
10636/// `getkeystring_with` call. Returns true once after a truncation
10637/// fires, false otherwise.
10638pub fn getkey_truncated_take() -> bool {
10639 GETKEY_TRUNCATED.with(|c| {
10640 let v = c.get();
10641 c.set(false);
10642 v
10643 })
10644}
10645
10646/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
10647/// the C expression `fdtable[fd]` (read). Returns `FDT_UNUSED` for
10648/// any fd that has not been explicitly set, matching the C source's
10649/// post-`growfdtable` zero-fill behaviour at Src/utils.c:1979.
10650pub fn fdtable_get(fd: i32) -> i32 {
10651 // c:utils.c:fdtable[fd]
10652 if fd < 0 {
10653 return FDT_UNUSED;
10654 }
10655 let g = fdtable_lock().lock().unwrap();
10656 g.get(fd as usize).copied().unwrap_or(FDT_UNUSED)
10657}
10658
10659/// !!! RUST-ONLY HELPER — see WARNING block above. Equivalent to
10660/// the C statement `fdtable[fd] = kind;`. Inlines the `growfdtable`
10661/// call from Src/utils.c:1965 since C always invokes it immediately
10662/// before the assignment anyway. Also updates `MAX_ZSH_FD` to track
10663/// the highest assigned slot — C does this inside `check_fd_table`
10664/// at `Src/utils.c:1982` (`max_zsh_fd = fd;`). Previously the Rust
10665/// `fdtable_set` skipped this, leaving `max_zsh_fd = 0` after any
10666/// `addlockfd` / `addmodulefd` call, which broke `zcloselockfd`'s
10667/// `if (fd > max_zsh_fd)` guard.
10668pub fn fdtable_set(fd: i32, kind: i32) {
10669 // c:utils.c:fdtable[fd]
10670 if fd < 0 {
10671 return;
10672 }
10673 let mut g = fdtable_lock().lock().unwrap();
10674 if (fd as usize) >= g.len() {
10675 g.resize((fd as usize) + 1, FDT_UNUSED);
10676 }
10677 g[fd as usize] = kind;
10678 // c:1982 — `max_zsh_fd = fd;` (with `if (fd <= max_zsh_fd) return;`
10679 // guard at c:1971). Inline equivalent: bump only when this fd
10680 // exceeds the current max.
10681 let cur = MAX_ZSH_FD.load(Ordering::Relaxed);
10682 if fd > cur {
10683 MAX_ZSH_FD.store(fd, Ordering::Relaxed);
10684 }
10685}
10686
10687/// Port of `imeta()` macro from `Src/ztype.h:60` — `zistype(X, IMETA)`.
10688///
10689/// IMETA is set in the typtab at `Src/utils.c:4195-4201` for:
10690/// - `'\0'` (c:4195)
10691/// - `Meta` (0x83) (c:4196)
10692/// - `Marker` (0xa2) (c:4197)
10693/// - `Pound..=LAST_NORMAL_TOK` = `0x84..=0x9c` (c:4198)
10694/// - `Snull..=Nularg` = `0x9d..=0xa1` (c:4200)
10695///
10696/// The CANONICAL set is `{0x00, 0x83..=0xa2}`. The previous Rust
10697/// port used `b >= 0x83` (every byte 0x83..=0xff) which was WRONG
10698/// for bytes `0xa3..=0xff`: C `imeta()` returns false for those,
10699/// so metafy passes them through unchanged. Rust's broader range
10700/// caused `metafy` to corrupt UTF-8 multibyte content (e.g.,
10701/// `é` = `0xc3 0xa9`: both bytes are NOT IMETA in C, but Rust
10702/// escaped both as `Meta + (byte ^ 32)`, mangling the encoding
10703/// and breaking every downstream consumer that expects the raw
10704/// UTF-8 bytes to round-trip).
10705///
10706/// Use the closed-range form rather than the typtab lookup so
10707/// `metafy` / `imeta_byte` work in test contexts where the typtab
10708/// isn't initialised, AND so the fast-path doesn't go through a
10709/// Mutex lock per byte (`metafy` is called per-character on every
10710/// shell input line).
10711#[inline]
10712pub fn imeta_byte(b: u8) -> bool {
10713 // c:4195-4201 — canonical IMETA range.
10714 b == 0 || (0x83..=0xa2).contains(&b)
10715}
10716
10717// `pub fn convfloat` and `pub fn convfloat_underscore` re-export
10718// wrappers DELETED per PORT.md Rule C. C defines both in
10719// `Src/params.c:5690` and `:5765` — the canonical Rust home is
10720// `src/ported/params.rs` (`convfloat` at line 7356,
10721// `convfloat_underscore` matching). Caller convenience cost is
10722// `use crate::ported::params::convfloat;` instead of `utils::`;
10723// no Rust-side re-export needed.
10724
10725// ===========================================================
10726// Methods moved verbatim from src/ported/vm_helper because their
10727// C counterpart's source file maps 1:1 to this Rust module.
10728// Phase: drift
10729// ===========================================================
10730
10731// BEGIN moved-from-exec-rs
10732// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
10733
10734// END moved-from-exec-rs
10735
10736// ===========================================================
10737// Free ported moved verbatim from src/ported/vm_helper.
10738// ===========================================================
10739// BEGIN moved-from-exec-rs (free ported)
10740pub(crate) fn base64_decode(s: &str) -> Vec<u8> {
10741 let decode_char = |c: u8| -> Option<u8> {
10742 match c {
10743 b'A'..=b'Z' => Some(c - b'A'),
10744 b'a'..=b'z' => Some(c - b'a' + 26),
10745 b'0'..=b'9' => Some(c - b'0' + 52),
10746 b'+' => Some(62),
10747 b'/' => Some(63),
10748 _ => None,
10749 }
10750 };
10751 let bytes = s.as_bytes();
10752 let mut out = Vec::with_capacity(s.len() / 4 * 3);
10753 let mut i = 0;
10754 while i + 4 <= bytes.len() {
10755 let chunk = &bytes[i..i + 4];
10756 let pad = chunk.iter().filter(|&&c| c == b'=').count();
10757 let v0 = decode_char(chunk[0]).unwrap_or(0) as u32;
10758 let v1 = decode_char(chunk[1]).unwrap_or(0) as u32;
10759 let v2 = decode_char(chunk[2]).unwrap_or(0) as u32;
10760 let v3 = decode_char(chunk[3]).unwrap_or(0) as u32;
10761 let n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
10762 out.push(((n >> 16) & 0xff) as u8);
10763 if pad < 2 {
10764 out.push(((n >> 8) & 0xff) as u8);
10765 }
10766 if pad < 1 {
10767 out.push((n & 0xff) as u8);
10768 }
10769 i += 4;
10770 }
10771 out
10772}
10773// END moved-from-exec-rs (free ported)
10774
10775// ===========================================================
10776// Utility helpers moved from src/ported/vm_helper.
10777// All correspond to Src/utils.c logic (path/string/bslashquote helpers).
10778// ===========================================================
10779
10780// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10781// ─── RUST-ONLY ACCESSORS ───
10782//
10783// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10784// RwLock<T>>` globals declared above. C zsh uses direct global
10785// access; Rust needs these wrappers because `OnceLock::get_or_init`
10786// is the only way to lazily construct shared state. These ported sit
10787// here so the body of this file reads in C source order without
10788// the accessor wrappers interleaved between real port ported.
10789// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10790
10791// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10792// ─── RUST-ONLY ACCESSORS ───
10793//
10794// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10795// RwLock<T>>` globals declared above. C zsh uses direct global
10796// access; Rust needs these wrappers because `OnceLock::get_or_init`
10797// is the only way to lazily construct shared state. These ported sit
10798// here so the body of this file reads in C source order without
10799// the accessor wrappers interleaved between real port ported.
10800// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10801
10802// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10803// ─── RUST-ONLY ACCESSORS ───
10804//
10805// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10806// RwLock<T>>` globals declared above. C zsh uses direct global
10807// access; Rust needs these wrappers because `OnceLock::get_or_init`
10808// is the only way to lazily construct shared state. These ported sit
10809// here so the body of this file reads in C source order without
10810// the accessor wrappers interleaved between real port ported.
10811// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10812
10813// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10814// ─── RUST-ONLY ACCESSORS ───
10815//
10816// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
10817// RwLock<T>>` globals declared above. C zsh uses direct global
10818// access; Rust needs these wrappers because `OnceLock::get_or_init`
10819// is the only way to lazily construct shared state. These ported sit
10820// here so the body of this file reads in C source order without
10821// the accessor wrappers interleaved between real port ported.
10822// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10823
10824// WARNING: NOT IN UTILS.C — Rust-only OnceLock get-or-init
10825// helpers. C dereferences each global directly.
10826fn scriptname_lock() -> &'static Mutex<Option<String>> {
10827 SCRIPTNAME.get_or_init(|| Mutex::new(None))
10828}
10829
10830// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10831fn argzero_lock() -> &'static Mutex<Option<String>> {
10832 ARGZERO.get_or_init(|| Mutex::new(None))
10833}
10834
10835// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10836fn scriptfilename_lock() -> &'static Mutex<Option<String>> {
10837 SCRIPTFILENAME.get_or_init(|| Mutex::new(None))
10838}
10839
10840// WARNING: NOT IN UTILS.C — Rust-only OnceLock accessor for `posixzero`.
10841// C's `posixzero` lives in params.c:76; the canonical storage lives
10842// here for OnceLock initialisation parity with `argzero`.
10843fn posixzero_lock() -> &'static Mutex<Option<String>> {
10844 POSIXZERO.get_or_init(|| Mutex::new(None))
10845}
10846
10847// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10848pub fn noerrs_lock() -> &'static Mutex<i32> {
10849 NOERRS.get_or_init(|| Mutex::new(0))
10850}
10851
10852// `locallevel_lock` removed — duplicate of canonical
10853// `LOCALLEVEL` (port of params.c:54). All
10854// accessors here now route through the canonical AtomicI32.
10855// WARNING: NOT IN UTILS.C — see scriptname_lock above.
10856fn lineno_lock() -> &'static Mutex<i32> {
10857 LINENO.get_or_init(|| Mutex::new(0))
10858}
10859
10860// WARNING: NOT IN UTILS.C — Rust-only cache for the SHINSTDIN
10861// option flag so error-emission doesn't pull in the option-table.
10862fn shinstdin_lock() -> &'static Mutex<bool> {
10863 SHINSTDIN_OPT.get_or_init(|| Mutex::new(false))
10864}
10865
10866/// !!! RUST-ONLY HELPER — see WARNING block above. C source uses bare
10867/// `unsigned char *fdtable` global from Src/utils.c:~63.
10868fn fdtable_lock() -> &'static Mutex<Vec<i32>> {
10869 FDTABLE.get_or_init(|| Mutex::new(Vec::new()))
10870}
10871
10872#[cfg(test)]
10873mod tests {
10874 use super::*;
10875
10876 /// checkmailpath port (utils.c:1620). Drives mtime/atime/size through
10877 /// `libc::utimes` so the new-mail condition is deterministic in headless CI.
10878 #[test]
10879 fn checkmailpath_new_mail_conditions() {
10880 use std::io::Write as _;
10881 let _g = crate::test_util::global_state_lock();
10882
10883 // Unique temp file with content (size != 0).
10884 let dir = std::env::temp_dir();
10885 let path = dir.join(format!("zshrs_mailtest_{}", std::process::id()));
10886 {
10887 let mut f = std::fs::File::create(&path).unwrap();
10888 f.write_all(b"new message\n").unwrap();
10889 }
10890 let cpath = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
10891 // atime (times[0]) <= mtime (times[1]); both well past 0.
10892 let set_times = |atime: i64, mtime: i64| {
10893 let tv = [
10894 libc::timeval {
10895 tv_sec: atime as libc::time_t,
10896 tv_usec: 0,
10897 },
10898 libc::timeval {
10899 tv_sec: mtime as libc::time_t,
10900 tv_usec: 0,
10901 },
10902 ];
10903 assert_eq!(unsafe { libc::utimes(cpath.as_ptr(), tv.as_ptr()) }, 0);
10904 };
10905
10906 let saved_shout = *crate::ported::init::shout.lock().unwrap();
10907 let saved_lmc = LAST_MAILCHECK.load(Ordering::Relaxed);
10908 *crate::ported::init::shout.lock().unwrap() = 1; // interactive
10909 LAST_MAILCHECK.store(0, Ordering::Relaxed);
10910
10911 let p = path.to_str().unwrap().to_string();
10912
10913 // c:1671-1675 — unread + newer than last check → "You have new mail."
10914 set_times(1_000_000, 2_000_000);
10915 assert_eq!(
10916 checkmailpath(&[p.clone()]),
10917 vec!["You have new mail.".to_string()]
10918 );
10919
10920 // c:1676-1698 — `PATH?message`: the message (here a literal) is emitted.
10921 assert_eq!(
10922 checkmailpath(&[format!("{}?custom note", p)]),
10923 vec!["custom note".to_string()]
10924 );
10925
10926 // c:1672 — mtime older than the last check → nothing.
10927 LAST_MAILCHECK.store(9_000_000, Ordering::Relaxed);
10928 assert!(checkmailpath(&[p.clone()]).is_empty());
10929
10930 // c:1670 — non-interactive (no shout) → nothing, even when fresh.
10931 LAST_MAILCHECK.store(0, Ordering::Relaxed);
10932 *crate::ported::init::shout.lock().unwrap() = 0;
10933 assert!(checkmailpath(&[p.clone()]).is_empty());
10934
10935 // c:1634-1636 — empty component is reported (stderr), yields no message.
10936 *crate::ported::init::shout.lock().unwrap() = 1;
10937 assert!(checkmailpath(&["?orphan".to_string()]).is_empty());
10938
10939 *crate::ported::init::shout.lock().unwrap() = saved_shout;
10940 LAST_MAILCHECK.store(saved_lmc, Ordering::Relaxed);
10941 let _ = std::fs::remove_file(&path);
10942 }
10943
10944 /// zsh_errno_msg / zerrmsg `%e` rendering (utils.c:348-365). EINTR is the
10945 /// literal "interrupt"; EIO is verbatim strerror (keeps its capital); every
10946 /// other errno lowercases the first letter (`tulower`). The old
10947 /// io::Error-based path kept the capital and appended " (os error N)".
10948 #[test]
10949 fn zsh_errno_msg_matches_c_percent_e() {
10950 // c:355-358 — EINTR → "interrupt".
10951 assert_eq!(zsh_errno_msg(libc::EINTR), "interrupt");
10952
10953 // c:366 — non-EIO errno lowercases the first letter and never carries
10954 // the Rust "(os error N)" suffix.
10955 let acc = zsh_errno_msg(libc::EACCES);
10956 assert!(
10957 acc.chars().next().is_some_and(|c| c.is_lowercase()),
10958 "EACCES first letter must be lowercased: {acc:?}"
10959 );
10960 assert!(
10961 !acc.contains("(os error"),
10962 "must be strerror, not io::Error: {acc:?}"
10963 );
10964
10965 // c:359-364 — EIO is emitted verbatim (its message reads wrong
10966 // lowercased), so its first letter stays capitalized.
10967 let eio = zsh_errno_msg(libc::EIO);
10968 assert!(
10969 eio.chars().next().is_some_and(|c| c.is_uppercase()),
10970 "EIO must be verbatim strerror (capitalized): {eio:?}"
10971 );
10972 }
10973
10974 /// c:351-354 — zerrmsg's `%e` arm sets errflag |= ERRFLAG_ERROR on EINTR.
10975 #[test]
10976 fn zerrmsg_eintr_sets_errflag() {
10977 let _g = crate::test_util::global_state_lock();
10978 let saved = errflag.load(Ordering::Relaxed);
10979 errflag.store(0, Ordering::Relaxed);
10980 zerrmsg("test", Some(libc::EINTR)); // writes to stderr; side-effect is the flag
10981 assert_ne!(
10982 errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
10983 0,
10984 "EINTR must set ERRFLAG_ERROR"
10985 );
10986 // A non-EINTR errno must NOT set the flag.
10987 errflag.store(0, Ordering::Relaxed);
10988 zerrmsg("test", Some(libc::EACCES));
10989 assert_eq!(errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR, 0);
10990 errflag.store(saved, Ordering::Relaxed);
10991 }
10992
10993 /// zjoin port (utils.c:3622). ASCII delimiters join byte-for-byte; a meta
10994 /// delimiter (NUL is `imeta` per inittyptab c:4195) must be written in
10995 /// metafied form (`Meta` + `delim^32`), never as a raw byte.
10996 #[test]
10997 fn zjoin_ascii_and_meta_delims() {
10998 let v = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
10999
11000 // c:3632-3641 — ordinary (non-meta) delimiter: plain interposition.
11001 assert_eq!(zjoin(&v(&["a", "b", "c"]), ' '), "a b c");
11002 assert_eq!(zjoin(&v(&["foo"]), ':'), "foo"); // single elem, no delim
11003 // c:3629-3630 — empty array → "".
11004 assert_eq!(zjoin(&v(&[]), ' '), "");
11005
11006 // c:3634-3637 — NUL is imeta, so the separator is metafied, NOT a raw
11007 // NUL byte (the prior `arr.join("\0")` bug). The Meta byte (0x83) can't
11008 // survive the UTF-8 String boundary, but the raw NUL must be gone.
11009 let joined = zjoin(&v(&["a", "b"]), '\0');
11010 assert!(
11011 !joined.as_bytes().contains(&0u8),
11012 "NUL delim must be metafied, not emitted as a raw NUL byte: {joined:?}"
11013 );
11014 assert_ne!(joined, "a\u{0}b", "must not raw-join on a meta delimiter");
11015 assert!(joined.starts_with('a') && joined.ends_with('b'));
11016 }
11017
11018 /// findsep port (utils.c:3784). Pure function — no global/cwd state.
11019 #[test]
11020 fn findsep_default_ifs_separators() {
11021 inittyptab(); // ISEP bits are set by the type table (c:4155)
11022 // c:3814-3817 — advance to the first ISEP char, return 1.
11023 let mut s = "foo bar".to_string();
11024 let mut pos = 0usize;
11025 assert_eq!(findsep(&mut s, &mut pos, None, false), 1);
11026 assert_eq!(pos, 3); // points at the space
11027 assert_eq!(&s[..pos], "foo");
11028
11029 // c:3821 — already sitting on a separator: return 0, no advance.
11030 let mut s = " foo".to_string();
11031 let mut pos = 0usize;
11032 assert_eq!(findsep(&mut s, &mut pos, None, false), 0);
11033 assert_eq!(pos, 0);
11034 }
11035
11036 #[test]
11037 fn findsep_quote_strips_escaped_separator() {
11038 inittyptab(); // ISEP bits are set by the type table (c:4155)
11039 // c:3795-3804 — `\<sep>` is not a separator; the backslash is
11040 // stripped in place and the bare char is consumed into the word.
11041 let mut s = "foo\\ bar".to_string(); // foo<bslash><space>bar
11042 let mut pos = 0usize;
11043 let r = findsep(&mut s, &mut pos, None, true);
11044 assert_eq!(r, 1);
11045 assert_eq!(s, "foo bar"); // backslash removed
11046 assert_eq!(pos, s.len()); // whole thing is one word, no real sep
11047 }
11048
11049 #[test]
11050 fn findsep_quote_collapses_double_backslash() {
11051 // c:3796-3799 — `\\` collapses to a single literal backslash.
11052 let mut s = "a\\\\b".to_string(); // a <bslash><bslash> b
11053 let mut pos = 0usize;
11054 let r = findsep(&mut s, &mut pos, None, true);
11055 assert_eq!(r, 1);
11056 assert_eq!(s, "a\\b"); // one backslash left
11057 assert_eq!(pos, s.len()); // no separator present
11058 }
11059
11060 #[test]
11061 fn findsep_literal_multichar_separator() {
11062 // c:3836-3841 — explicit multi-byte literal separator.
11063 let mut s = "a::b".to_string();
11064 let mut pos = 0usize;
11065 assert_eq!(findsep(&mut s, &mut pos, Some(b"::"), false), 1);
11066 assert_eq!(pos, 1); // points at the "::"
11067 assert_eq!(&s[..pos], "a");
11068
11069 // c:3844 — separator absent → -1.
11070 let mut s = "abc".to_string();
11071 let mut pos = 0usize;
11072 assert_eq!(findsep(&mut s, &mut pos, Some(b"x"), false), -1);
11073 }
11074
11075 #[test]
11076 fn findsep_empty_separator_advances_one_char() {
11077 // c:3825-3834 — empty sep just steps past one character.
11078 let mut s = "ab".to_string();
11079 let mut pos = 0usize;
11080 assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), 1);
11081 assert_eq!(pos, 1);
11082
11083 let mut s = String::new();
11084 let mut pos = 0usize;
11085 assert_eq!(findsep(&mut s, &mut pos, Some(b""), false), -1); // c:3833
11086 }
11087
11088 /// c:4033 — `subst_string_by_func` returns `getaparam("reply")`
11089 /// after the hook function finishes. The previous Rust port read
11090 /// `env::var("reply")` and split on NUL — wrong because `reply`
11091 /// is a shell-local PM_ARRAY in paramtab, never exported. This
11092 /// pin sets `reply` via `setaparam` (the paramtab write path)
11093 /// and exercises `subst_string_by_hook` end-to-end isn't viable
11094 /// in unit tests without a function-name hook, so we exercise
11095 /// just the getaparam plumbing.
11096 #[test]
11097 fn getaparam_reads_reply_from_paramtab_not_env() {
11098 let _g = crate::test_util::global_state_lock();
11099 // Stash any prior `reply` value.
11100 let saved = getaparam("reply");
11101
11102 // Write a known reply array via the canonical setaparam path.
11103 let payload = vec!["abbreviated".to_string(), "11".to_string()];
11104 let _ = setaparam("reply", payload.clone());
11105
11106 // The reply array must be reachable via getaparam — not env.
11107 // (env::var would return Err because setaparam never exports
11108 // an un-flagged array to env.)
11109 assert_eq!(
11110 getaparam("reply"),
11111 Some(payload),
11112 "getaparam(\"reply\") must return paramtab array"
11113 );
11114
11115 // Restore.
11116 let _ = setaparam("reply", saved.unwrap_or_default());
11117 }
11118
11119 /// c:1133-1134 — `finddir` reads the global `home` variable (the
11120 /// canonical `$HOME` storage, not `getenv("HOME")`). The global
11121 /// is updated by `homesetfn` (Src/params.c:5118) whenever the user
11122 /// assigns `HOME=...` inside the shell.
11123 /// Regression target: a previous Rust port read `env::var("HOME")`
11124 /// so an in-shell `HOME=...` assignment would not retarget
11125 /// `~`-abbreviation until the user re-exported HOME.
11126 #[test]
11127 fn finddir_uses_paramtab_home_not_env() {
11128 let _g = crate::test_util::global_state_lock();
11129 // C's `homesetfn` (params.c:5118) UNUSED(Param pm) — the
11130 // function ignores its param-pointer argument and writes the
11131 // canonical `char *home` global directly. zshrs's port mirrors
11132 // that: `params::homesetfn(_pm, x)` ignores _pm and updates
11133 // `home_lock()`. So we pass a stack-default param; the paramtab
11134 // wiring (PM_SPECIAL dispatch) is not on the test path.
11135 let mut pm = crate::ported::zsh_h::param::default();
11136 let saved = crate::ported::params::homegetfn(&pm);
11137 let sentinel = "/tmp/zshrs-finddir-pin".to_string();
11138 homesetfn(&mut pm, sentinel.clone());
11139
11140 // `/tmp/zshrs-finddir-pin/x` must abbreviate to `~/x`.
11141 let abbrev = finddir(&format!("{}/x", sentinel));
11142 assert_eq!(
11143 abbrev.as_deref(),
11144 Some("~/x"),
11145 "finddir must consult canonical HOME (got {:?})",
11146 abbrev
11147 );
11148
11149 // Restore.
11150 homesetfn(&mut pm, saved);
11151 }
11152
11153 #[test]
11154 fn test_sepsplit() {
11155 let _g = crate::test_util::global_state_lock();
11156 assert_eq!(sepsplit("a:b:c", Some(":"), false), vec!["a", "b", "c"]);
11157 assert_eq!(sepsplit("a::b", Some(":"), false), vec!["a", "b"]);
11158 assert_eq!(sepsplit("a::b", Some(":"), true), vec!["a", "", "b"]);
11159 }
11160
11161 #[test]
11162 fn test_unmetafy_no_meta_byte_passes_through() {
11163 let _g = crate::test_util::global_state_lock();
11164 // No Meta byte → buffer unchanged, length unchanged.
11165 let mut buf = b"hello".to_vec();
11166 let n = unmetafy(&mut buf);
11167 assert_eq!(n, 5);
11168 assert_eq!(&buf, b"hello");
11169 }
11170
11171 #[test]
11172 fn test_unmetafy_collapses_meta_escapes() {
11173 let _g = crate::test_util::global_state_lock();
11174 // C: Meta byte (0x83) followed by `'a' ^ 32` (0x41 = 'A')
11175 // unmetafies to a single byte 'a' (0x61).
11176 // i.e. {0x83, 'a' ^ 32} → {'a'}.
11177 let mut buf = vec![0x83, b'a' ^ 32];
11178 let n = unmetafy(&mut buf);
11179 assert_eq!(n, 1);
11180 assert_eq!(buf, vec![b'a']);
11181 }
11182
11183 #[test]
11184 fn test_unmetafy_mixed_prefix_then_meta() {
11185 let _g = crate::test_util::global_state_lock();
11186 // Plain prefix, then Meta-escaped 0xFF (0x83, 0xFF ^ 32 = 0xDF).
11187 let mut buf = vec![b'X', b'Y', 0x83, 0xFF ^ 32, b'Z'];
11188 let n = unmetafy(&mut buf);
11189 assert_eq!(n, 4);
11190 assert_eq!(buf, vec![b'X', b'Y', 0xFF, b'Z']);
11191 }
11192
11193 #[test]
11194 fn test_unmetafy_returns_self_value() {
11195 let _g = crate::test_util::global_state_lock();
11196 // C returns `s` (the buffer) for chaining; Rust returns
11197 // the new length. Verify length matches a call that
11198 // collapses two Meta-escapes.
11199 let mut buf = vec![
11200 b'A',
11201 0x83,
11202 b'B' ^ 32, // → 'B'
11203 0x83,
11204 b'C' ^ 32, // → 'C'
11205 b'D',
11206 ];
11207 let n = unmetafy(&mut buf);
11208 assert_eq!(n, 4);
11209 assert_eq!(buf, b"ABCD".to_vec());
11210 }
11211
11212 #[test]
11213 fn test_imeta_byte_threshold() {
11214 let _g = crate::test_util::global_state_lock();
11215 // Canonical IMETA per Src/utils.c:4195-4201:
11216 // - 0x00 (c:4195)
11217 // - 0x83..=0xa2 (Meta through Marker — c:4196-4200)
11218 //
11219 // The previous version of this test asserted `imeta_byte(0xFF) == true`
11220 // based on the WRONG `b >= Meta` predicate the Rust port
11221 // originally used. C `imeta()` reads the typtab; 0xa3..=0xff
11222 // have NO typtab assignment so they're NOT IMETA, and
11223 // `metafy` must NOT escape them.
11224 assert!(imeta_byte(0x00), "c:4195 — NUL is IMETA");
11225 assert!(!imeta_byte(0x82), "0x82 is NOT IMETA (below Meta)");
11226 assert!(imeta_byte(Meta), "c:4196 — Meta (0x83) is IMETA");
11227 assert!(imeta_byte(0xa2), "c:4197 — Marker (0xa2) is IMETA");
11228 // c:4198-4200 upper bound — Nularg is the last IMETA byte.
11229 assert!(imeta_byte(0xa1), "c:4200 — Nularg (0xa1) is IMETA");
11230 // 0xa3..=0xff are NOT IMETA — UTF-8 multi-byte content
11231 // must pass through `metafy` unchanged.
11232 assert!(!imeta_byte(0xa3), "0xa3 NOT IMETA (above Marker)");
11233 assert!(!imeta_byte(0xc3), "0xc3 NOT IMETA (UTF-8 'é' lead)");
11234 assert!(!imeta_byte(0xFF), "0xFF NOT IMETA");
11235 }
11236
11237 #[test]
11238 fn test_meta_constant_value() {
11239 let _g = crate::test_util::global_state_lock();
11240 // Locked at 0x83 by Src/zsh.h. If this test fails, zsh
11241 // bumped the Meta sentinel and the encoding mapping needs
11242 // a full audit.
11243 assert_eq!(Meta, 0x83);
11244 }
11245
11246 #[test]
11247 #[cfg(unix)]
11248 fn test_mode_to_octal_canonical_bits() {
11249 let _g = crate::test_util::global_state_lock();
11250 // rwx for owner = 0o700.
11251 let mode = (S_IRUSR | S_IWUSR | S_IXUSR) as u32;
11252 assert_eq!(mode_to_octal(mode), 0o700);
11253 // rwx all = 0o777.
11254 let all = (S_IRUSR | S_IWUSR | S_IXUSR) as u32 * (1 + 8 + 64);
11255 // Use libc constants individually for portability.
11256 let m = (S_IRUSR
11257 | S_IWUSR
11258 | S_IXUSR
11259 | S_IRGRP
11260 | S_IWGRP
11261 | S_IXGRP
11262 | S_IROTH
11263 | S_IWOTH
11264 | S_IXOTH) as u32;
11265 assert_eq!(mode_to_octal(m), 0o777);
11266 let _ = all;
11267 }
11268
11269 #[test]
11270 #[cfg(unix)]
11271 fn test_mode_to_octal_setuid_setgid_sticky() {
11272 let _g = crate::test_util::global_state_lock();
11273 assert_eq!(mode_to_octal(S_ISUID as u32), 0o4000);
11274 assert_eq!(mode_to_octal(S_ISGID as u32), 0o2000);
11275 assert_eq!(mode_to_octal(S_ISVTX as u32), 0o1000);
11276 // All three: 0o7000.
11277 let all = (S_ISUID | S_ISGID | S_ISVTX) as u32;
11278 assert_eq!(mode_to_octal(all), 0o7000);
11279 }
11280
11281 #[test]
11282 #[cfg(unix)]
11283 fn test_mailstat_plain_file_returns_native_stat() {
11284 let _g = crate::test_util::global_state_lock();
11285 // Plain file path → *st fields mirror native stat,
11286 // not the maildir aggregation.
11287 let mut st: libc::stat = unsafe { std::mem::zeroed() };
11288 let rc = mailstat("/etc/hosts", &mut st);
11289 if rc == 0 {
11290 assert_eq!(
11291 st.st_nlink as u64,
11292 fs::metadata("/etc/hosts").unwrap().nlink()
11293 );
11294 }
11295 }
11296
11297 #[test]
11298 fn test_mailstat_nonexistent_returns_neg1() {
11299 let _g = crate::test_util::global_state_lock();
11300 let mut st: libc::stat = unsafe { std::mem::zeroed() };
11301 assert_eq!(mailstat("/nonexistent/path/does/not/exist", &mut st), -1);
11302 }
11303
11304 #[test]
11305 fn test_mailstat_directory_without_maildir_subdirs() {
11306 let _g = crate::test_util::global_state_lock();
11307 // /tmp is a directory but not a maildir (no cur/tmp/new) —
11308 // returns the partial aggregate (top dir's atime/mtime,
11309 // size=0 since cur/ wasn't found before we'd start summing).
11310 let mut st: libc::stat = unsafe { std::mem::zeroed() };
11311 let rc = mailstat("/tmp", &mut st);
11312 assert_eq!(rc, 0);
11313 assert_eq!(st.st_nlink, 1);
11314 assert_eq!(st.st_size, 0);
11315 assert_eq!(st.st_blocks, 0);
11316 // S_IFDIR bit should be cleared, S_IFREG set.
11317 #[cfg(unix)]
11318 {
11319 assert_eq!(st.st_mode & libc::S_IFDIR, 0);
11320 assert_ne!(st.st_mode & libc::S_IFREG, 0);
11321 }
11322 }
11323
11324 #[test]
11325 fn test_dupstrpfx_byte_counted() {
11326 let _g = crate::test_util::global_state_lock(); // c:161
11327 // 5 bytes of ASCII = 5 chars, identical. Canonical
11328 // port lives at `string.rs:161`; pin from utils.rs's test
11329 // module via the qualified path.
11330 assert_eq!(dupstrpfx("hello", 3), "hel"); // c:161
11331 assert_eq!(dupstrpfx("hi", 10), "hi"); // c:161
11332 assert_eq!(dupstrpfx("anything", 0), ""); // c:161
11333 }
11334
11335 #[test]
11336 fn test_metafy_passes_through_ascii() {
11337 let _g = crate::test_util::global_state_lock();
11338 // ASCII bytes (< 0x83) stay untouched.
11339 assert_eq!(metafy("hello"), "hello");
11340 assert_eq!(metafy(""), "");
11341 }
11342
11343 #[test]
11344 fn test_metafy_imeta_predicate_matches_c_macro() {
11345 let _g = crate::test_util::global_state_lock();
11346 // Canonical C IMETA per Src/utils.c:4195-4201:
11347 // - 0x00 (c:4195)
11348 // - 0x83 (Meta, c:4196)
11349 // - 0x84..=0x9c (Pound..LAST_NORMAL_TOK=Bang, c:4198)
11350 // - 0x9d..=0xa1 (Snull..Nularg, c:4200)
11351 // - 0xa2 (Marker, c:4197)
11352 //
11353 // The closed set is {0x00, 0x83..=0xa2}. Every other byte
11354 // (0x01..=0x82, 0xa3..=0xff) is NOT IMETA in C and so must
11355 // NOT be Meta-encoded by `metafy`. The previous Rust
11356 // hardcoded predicate `b >= 0x83` falsely marked
11357 // 0xa3..=0xff as IMETA, corrupting UTF-8 multibyte
11358 // content.
11359
11360 // NUL is IMETA (c:4195).
11361 assert!(imeta_byte(0x00), "c:4195 — '\\0' IS imeta");
11362
11363 // 0x01..=0x82 are NOT IMETA (no typtab assignment for them).
11364 for b in 0x01u8..=0x82 {
11365 assert!(!imeta_byte(b), "byte {:#x} should NOT be imeta", b);
11366 }
11367
11368 // 0x83..=0xa2 ARE IMETA (the canonical full range).
11369 for b in 0x83u8..=0xa2 {
11370 assert!(imeta_byte(b), "c:4196-4200 — byte {:#x} IS imeta", b);
11371 }
11372
11373 // 0xa3..=0xff are NOT IMETA. C `imeta()` returns false
11374 // for these so `metafy` must pass them through unchanged.
11375 // UTF-8 continuation bytes (0x80..=0xbf) and multi-byte
11376 // leads (0xc0..=0xff) live here and must round-trip.
11377 for b in 0xa3u8..=0xff {
11378 assert!(
11379 !imeta_byte(b),
11380 "byte {:#x} should NOT be imeta (no c:4195-4201 assignment)",
11381 b
11382 );
11383 }
11384 }
11385
11386 /// Pin: `metafy` passes UTF-8 multibyte bytes through unchanged
11387 /// when they fall outside the canonical IMETA range. Previously
11388 /// the broader `b >= 0x83` predicate corrupted every UTF-8
11389 /// continuation byte and lead byte that wasn't a token marker.
11390 #[test]
11391 fn metafy_preserves_utf8_high_bytes_outside_imeta_range() {
11392 let _g = crate::test_util::global_state_lock();
11393 // 'é' = U+00E9 = UTF-8 0xc3 0xa9. Both bytes are >= 0x83 BUT
11394 // both are also > 0xa2, so they're NOT IMETA per the typtab.
11395 // C `metafy` passes them through unchanged.
11396 let input = std::str::from_utf8(&[0xC3, 0xA9]).unwrap();
11397 let out = metafy(input);
11398 // Should round-trip the two bytes exactly (no Meta escape).
11399 let out_bytes = out.as_bytes();
11400 assert_eq!(
11401 out_bytes,
11402 &[0xC3, 0xA9],
11403 "c:4196-4200 — UTF-8 bytes 0xc3/0xa9 outside IMETA range must pass through"
11404 );
11405 }
11406
11407 #[test]
11408 fn test_ztrcmp_meta_aware() {
11409 let _g = crate::test_util::global_state_lock();
11410 // Two identical metafied strings → Equal.
11411 assert_eq!(ztrcmp("foo", "foo"), std::cmp::Ordering::Equal);
11412 // "foo" < "foz".
11413 assert_eq!(ztrcmp("foo", "foz"), std::cmp::Ordering::Less);
11414 // Prefix comparison: shorter < longer.
11415 assert_eq!(ztrcmp("foo", "foobar"), std::cmp::Ordering::Less);
11416 // Meta-encoded comparison: {0x83, 'a'^32} should compare as 'a'.
11417 let s_meta = unsafe { std::str::from_utf8_unchecked(&[0x83, b'a' ^ 32]) };
11418 let s_plain = "a";
11419 // Meta-encoded "a" should compare equal to plain "a".
11420 assert_eq!(ztrcmp(s_meta, s_plain), std::cmp::Ordering::Equal);
11421 }
11422
11423 #[test]
11424 fn test_skipwsep_skips_runs() {
11425 let _g = crate::test_util::global_state_lock();
11426 // 3 spaces + 'x' → returns ("x", 3).
11427 let (rest, n) = skipwsep(" x");
11428 assert_eq!(rest, "x");
11429 assert_eq!(n, 3);
11430 // No leading whitespace → 0 skipped.
11431 let (rest, n) = skipwsep("foo");
11432 assert_eq!(rest, "foo");
11433 assert_eq!(n, 0);
11434 // Mix of space/tab/newline.
11435 let (rest, n) = skipwsep(" \t\nbar");
11436 assert_eq!(rest, "bar");
11437 assert_eq!(n, 3);
11438 }
11439
11440 #[test]
11441 fn test_imeta_macro_threshold() {
11442 let _g = crate::test_util::global_state_lock(); // c:60
11443 // `Src/ztype.h:60` `imeta(X) zistype(X, IMETA)` — typtab-driven
11444 // predicate. Per `Src/utils.c:4195-4201`, IMETA is set on:
11445 // NUL, Meta=0x83, Marker=0xa2, and the Pound..Nularg ITOK range
11446 // (0x84..=0xa2). Routes through canonical
11447 // `ztype_h::imeta(u8)`; init the typtab first since the
11448 // canonical port reads through it.
11449 inittyptab(); // c:4148
11450 assert!(imeta(0x00), "c:4195 — NUL is IMETA"); // c:4195
11451 assert!(imeta(Meta), "c:4196 — Meta (0x83) is IMETA"); // c:4196
11452 assert!(imeta(0xa2), "c:4197 — Marker (0xa2) is IMETA"); // c:4197
11453 assert!(
11454 imeta(0x84),
11455 "c:4199-4201 — Pound (0x84) is IMETA via ITOK range"
11456 );
11457 assert!(
11458 imeta(0x9b),
11459 "c:4199-4201 — Dash sentinel within ITOK range is IMETA"
11460 );
11461 assert!(!imeta(0x82), "c:4170 — 0x82 is ICNTRL, not IMETA"); // c:4170
11462 assert!(!imeta(0xa3), "byte 0xa3 is past Marker — NOT IMETA");
11463 assert!(
11464 !imeta(0xff),
11465 "byte 0xff is NOT IMETA (the prior `>= Meta` over-report)"
11466 );
11467 assert!(!imeta(b' '), "space is not IMETA");
11468 assert!(!imeta(b'A'), "'A' is not IMETA");
11469 }
11470
11471 #[test]
11472 fn test_unmeta_routes_through_unmetafy() {
11473 let _g = crate::test_util::global_state_lock();
11474 // unmeta wraps the in-place unmetafy via a byte-vector
11475 // copy; the no-Meta fast path returns the source as-is.
11476 assert_eq!(unmeta("plain"), "plain");
11477 }
11478
11479 #[test]
11480 fn test_iwsep_includes_newline() {
11481 let _g = crate::test_util::global_state_lock(); // c:61
11482 // The previous port omitted '\n' which broke wordcount on
11483 // multi-line input. Routes through canonical
11484 // `ztype_h::iwsep` (`Src/ztype.h:61`).
11485 assert!(iwsep(b'\n')); // c:61
11486 assert!(iwsep(b'\t')); // c:61
11487 assert!(iwsep(b' ')); // c:61
11488 assert!(!iwsep(b'a')); // c:61
11489 }
11490
11491 #[test]
11492 fn test_mailstat_aggregates_maildir() {
11493 let _g = crate::test_util::global_state_lock();
11494 // Create a temp maildir layout with 2 messages in new/ and 1
11495 // in cur/, verify the aggregate.
11496 let tmp = std::env::temp_dir().join(format!("zshrs_mailstat_test_{}", std::process::id()));
11497 let _ = fs::remove_dir_all(&tmp);
11498 fs::create_dir_all(tmp.join("cur")).unwrap();
11499 fs::create_dir_all(tmp.join("new")).unwrap();
11500 fs::create_dir_all(tmp.join("tmp")).unwrap();
11501 let mut f = fs::File::create(tmp.join("new").join("msg1")).unwrap();
11502 f.write_all(b"hello").unwrap();
11503 let mut f = fs::File::create(tmp.join("new").join("msg2")).unwrap();
11504 f.write_all(b"world!").unwrap();
11505 let mut f = fs::File::create(tmp.join("cur").join("msg3")).unwrap();
11506 f.write_all(b"third").unwrap();
11507 let mut st: libc::stat = unsafe { std::mem::zeroed() };
11508 let rc = mailstat(tmp.to_str().unwrap(), &mut st);
11509 assert_eq!(rc, 0, "maildir should stat");
11510 assert_eq!(st.st_blocks, 3, "3 messages total across new/ + cur/");
11511 assert_eq!(st.st_size, 5 + 6 + 5, "5+6+5 bytes total");
11512 let _ = fs::remove_dir_all(&tmp);
11513 }
11514
11515 #[test]
11516 fn test_spacesplit() {
11517 let _g = crate::test_util::global_state_lock();
11518 assert_eq!(spacesplit("a b c", false), vec!["a", "b", "c"]);
11519 assert_eq!(spacesplit("a b", false), vec!["a", "b"]);
11520 }
11521
11522 #[test]
11523 fn test_sepjoin() {
11524 let _g = crate::test_util::global_state_lock();
11525 assert_eq!(
11526 sepjoin(&["a".into(), "b".into(), "c".into()], Some(":")),
11527 "a:b:c"
11528 );
11529 assert_eq!(sepjoin(&["a".into(), "b".into()], None), "a b");
11530 }
11531
11532 #[test]
11533 fn test_isident() {
11534 let _g = crate::test_util::global_state_lock(); // c:1288
11535 // Canonical port lives at `params.rs:2056` (`Src/params.c:1288`).
11536 assert!(isident("foo")); // c:1288
11537 assert!(isident("_bar")); // c:1288
11538 assert!(isident("baz123")); // c:1288
11539 assert!(!isident("123abc")); // c:1288
11540 assert!(!isident("foo-bar")); // c:1288
11541 }
11542
11543 #[test]
11544 fn test_nicechar() {
11545 let _g = crate::test_util::global_state_lock();
11546 assert_eq!(nicechar('\n'), "\\n");
11547 assert_eq!(nicechar('\t'), "\\t");
11548 assert_eq!(nicechar('a'), "a");
11549 }
11550
11551 #[test]
11552 fn test_quotedzputs_single_quote_wrap() {
11553 let _g = crate::test_util::global_state_lock();
11554 assert_eq!(quotedzputs("simple"), "simple");
11555 assert_eq!(quotedzputs("has space"), "'has space'");
11556 assert_eq!(quotedzputs("it's"), "'it'\\''s'");
11557 }
11558
11559 #[test]
11560 fn test_quotestring_backslash() {
11561 let _g = crate::test_util::global_state_lock();
11562 assert_eq!(quotestring("hello", QT_BACKSLASH), "hello");
11563 assert_eq!(quotestring("has space", QT_BACKSLASH), "has\\ space");
11564 assert_eq!(quotestring("$var", QT_BACKSLASH), "\\$var");
11565 }
11566
11567 /// Pin: `ispecial(c)` matches the canonical SPECCHARS set at
11568 /// `Src/zsh.h:228` exactly: `"#$^*()=|{}[]\`<>?~;&\n\t \\'\""`.
11569 /// Previously the Rust local `ispecial` included `!` unconditionally
11570 /// which diverged from C — C only ISPECIAL-tags bangchar (default
11571 /// `!`) under BANGHIST + interactive (per `Src/utils.c:4257-4261`).
11572 ///
11573 /// This test exercises the path indirectly via `quotestring` with
11574 /// `QT_BACKSLASH` (which prepends `\` before every ispecial char).
11575 #[test]
11576 fn quotestring_backslash_only_specchars_no_bang_in_default() {
11577 let _g = crate::test_util::global_state_lock();
11578 // `!` is NOT in canonical SPECCHARS — should NOT be backslashed
11579 // in default non-interactive mode (matches C).
11580 assert_eq!(
11581 quotestring("a!b", QT_BACKSLASH),
11582 "a!b",
11583 "c:228 — `!` is not in SPECCHARS; only bangchar+BANGHIST adds it"
11584 );
11585 // `,` is NOT in canonical SPECCHARS until makecommaspecial(1).
11586 assert_eq!(
11587 quotestring("a,b", QT_BACKSLASH),
11588 "a,b",
11589 "c:228 — `,` not in SPECCHARS until makecommaspecial(1)"
11590 );
11591 // `^` IS in canonical SPECCHARS (per c:228 `"#$^..."`).
11592 assert_eq!(
11593 quotestring("a^b", QT_BACKSLASH),
11594 "a\\^b",
11595 "c:228 — `^` is in SPECCHARS"
11596 );
11597 // Open and close braces are in canonical SPECCHARS.
11598 assert_eq!(
11599 quotestring("a{b", QT_BACKSLASH),
11600 "a\\{b",
11601 "c:228 — open-brace is in SPECCHARS"
11602 );
11603 assert_eq!(
11604 quotestring("a}b", QT_BACKSLASH),
11605 "a\\}b",
11606 "c:228 — close-brace is in SPECCHARS"
11607 );
11608 // `#` is in canonical SPECCHARS (first char of c:228).
11609 assert_eq!(
11610 quotestring("a#b", QT_BACKSLASH),
11611 "a\\#b",
11612 "c:228 — `#` is the first char of SPECCHARS"
11613 );
11614 // `\\` (literal backslash) is in canonical SPECCHARS.
11615 assert_eq!(
11616 quotestring("a\\b", QT_BACKSLASH),
11617 "a\\\\b",
11618 "c:228 — `\\\\` in SPECCHARS"
11619 );
11620 }
11621
11622 #[test]
11623 fn test_quotestring_single() {
11624 let _g = crate::test_util::global_state_lock();
11625 assert_eq!(quotestring("hello", QT_SINGLE), "'hello'");
11626 assert_eq!(quotestring("it's", QT_SINGLE), "'it'\\''s'");
11627 }
11628
11629 #[test]
11630 fn test_quotestring_double() {
11631 let _g = crate::test_util::global_state_lock();
11632 assert_eq!(quotestring("hello", QT_DOUBLE), "\"hello\"");
11633 assert_eq!(quotestring("say \"hi\"", QT_DOUBLE), "\"say \\\"hi\\\"\"");
11634 }
11635
11636 #[test]
11637 fn test_quotestring_dollars() {
11638 let _g = crate::test_util::global_state_lock();
11639 assert_eq!(quotestring("hello", QT_DOLLARS), "$'hello'");
11640 assert_eq!(quotestring("line\nbreak", QT_DOLLARS), "$'line\\nbreak'");
11641 assert_eq!(quotestring("tab\there", QT_DOLLARS), "$'tab\\there'");
11642 }
11643
11644 #[test]
11645 fn test_quotestring_pattern() {
11646 let _g = crate::test_util::global_state_lock();
11647 assert_eq!(quotestring("*.txt", QT_BACKSLASH_PATTERN), "\\*.txt");
11648 assert_eq!(quotestring("file[1]", QT_BACKSLASH_PATTERN), "file\\[1\\]");
11649 }
11650
11651 #[test]
11652 fn test_quotetype_from_q_count() {
11653 let _g = crate::test_util::global_state_lock();
11654 assert_eq!(qflag_quotetype(1), QT_BACKSLASH);
11655 assert_eq!(qflag_quotetype(2), QT_SINGLE);
11656 assert_eq!(qflag_quotetype(3), QT_DOUBLE);
11657 assert_eq!(qflag_quotetype(4), QT_DOLLARS);
11658 }
11659
11660 #[test]
11661 fn test_tulower_tuupper() {
11662 let _g = crate::test_util::global_state_lock();
11663 assert_eq!(tulower('A'), 'a');
11664 assert_eq!(tuupper('a'), 'A');
11665 assert_eq!(tulower('1'), '1');
11666 }
11667
11668 #[test]
11669 fn test_wordcount_ifs_default() {
11670 let _g = crate::test_util::global_state_lock();
11671 // C: wordcount("a b c", NULL, 0) -> 3
11672 assert_eq!(wordcount("a b c", None, 0), 3);
11673 // Leading/trailing whitespace coalesced when mul <= 0.
11674 assert_eq!(wordcount(" a b ", None, 0), 2);
11675 // Empty string with mul == 0 -> 0 words.
11676 assert_eq!(wordcount("", None, 0), 0);
11677 // Single word, no separators.
11678 assert_eq!(wordcount("foo", None, 0), 1);
11679 }
11680
11681 #[test]
11682 fn test_wordcount_with_explicit_sep() {
11683 let _g = crate::test_util::global_state_lock();
11684 // C: wordcount("a:b:c", ":", 0) -> 3 (3 fields, 2 separators)
11685 assert_eq!(wordcount("a:b:c", Some(":"), 0), 3);
11686 // Empty fields counted when mul != 0.
11687 assert_eq!(wordcount("a::b", Some(":"), 1), 3);
11688 // Without mul, consecutive empties collapse: a, b => 2... but
11689 // C's "if ((c || mul) && (sl || *(s+sl)))" — second `:` has
11690 // c=0 and mul=0 so doesn't increment. Result: a, b => 2.
11691 assert_eq!(wordcount("a::b", Some(":"), 0), 2);
11692 }
11693
11694 #[test]
11695 fn test_ucs4tomb_ascii() {
11696 let _g = crate::test_util::global_state_lock();
11697 let mut buf = [0u8; 8];
11698 // 'A' = 0x41, ASCII, single byte in any locale.
11699 let n = ucs4tomb('A' as u32, &mut buf);
11700 // wctomb may return 1 in C/POSIX locale; in UTF-8 locale also 1.
11701 assert_eq!(n, 1);
11702 assert_eq!(buf[0], b'A');
11703 }
11704
11705 #[test]
11706 fn test_is_mb_niceformat_plain_ascii() {
11707 let _g = crate::test_util::global_state_lock();
11708 // Plain printable ASCII — nothing needs nicechar escaping.
11709 assert_eq!(is_mb_niceformat("hello world"), 0);
11710 }
11711
11712 #[test]
11713 fn test_is_mb_niceformat_with_control_char() {
11714 let _g = crate::test_util::global_state_lock();
11715 // Tab is control (< 0x20) — needs nice escaping.
11716 assert_eq!(is_mb_niceformat("a\tb"), 1);
11717 // Bell character.
11718 assert_eq!(is_mb_niceformat("a\x07b"), 1);
11719 }
11720
11721 /// c:4856/4954 — `metafy` + `unmetafy` MUST round-trip for ASCII.
11722 /// A regression in the round-trip corrupts every metafied buffer
11723 /// the lexer/param-subst pipeline produces.
11724 #[test]
11725 fn metafy_unmetafy_round_trips_for_ascii() {
11726 let _g = crate::test_util::global_state_lock();
11727 let s = "hello world";
11728 let m = metafy(s);
11729 let mut buf = m.into_bytes();
11730 unmetafy(&mut buf);
11731 assert_eq!(std::str::from_utf8(&buf).unwrap(), s);
11732 }
11733
11734 /// `ztrlen` counts metafied characters (Meta-pairs as 1).
11735 /// Regression that double-counts Meta-pair bytes would break
11736 /// every fixed-width column path (printf %s).
11737 #[test]
11738 fn ztrlen_counts_ascii_one_per_byte() {
11739 let _g = crate::test_util::global_state_lock();
11740 assert_eq!(ztrlen(""), 0);
11741 assert_eq!(ztrlen("a"), 1);
11742 assert_eq!(ztrlen("hello"), 5);
11743 }
11744
11745 /// `Src/utils.c:5141-5149` — Meta-byte pair counts as 1 char.
11746 /// `*s++ == Meta` advances 2 bytes per iteration but increments
11747 /// `l` only once. Pin: a string with one Meta+X pair counts as 1.
11748 #[test]
11749 fn ztrlen_counts_meta_pair_as_one() {
11750 let _g = crate::test_util::global_state_lock();
11751 // META (0x83) + 0x20 = unmetafies to one '\0' byte (or a NUL).
11752 let meta = char::from_u32(Meta as u32).unwrap();
11753 let s: String = [meta, '\x20'].iter().collect();
11754 assert_eq!(
11755 ztrlen(&s),
11756 1,
11757 "c:5141-5148 — Meta+X pair counts as ONE char, not two"
11758 );
11759 // Mixed: "a" + META + "x" + "b" = 3 unmetafied chars.
11760 let mixed: String = ['a', meta, '\x20', 'b'].iter().collect();
11761 assert_eq!(
11762 ztrlen(&mixed),
11763 3,
11764 "c:5141 — three unmetafied chars from 'a' + Meta+X + 'b'"
11765 );
11766 }
11767
11768 /// `Src/utils.c:2579-2618` — `setblock_fd(turnonblocking, fd, modep)`.
11769 /// Signature pin: previously the Rust port had a 2-arg
11770 /// `(fd, blocking: bool) -> bool` signature, swapping the
11771 /// turnonblocking/fd argument order vs C AND collapsing the
11772 /// 3rd `*modep` out-param entirely. The fix restored canonical
11773 /// C order `(turnonblocking, fd)` with a `(bool, c_long)` tuple
11774 /// return mirroring `int return + long *modep`.
11775 ///
11776 /// Also pin the c:2599 regular-file short-circuit: C only
11777 /// operates on non-regular fds (pipes, sockets, ttys). A regular
11778 /// file returns `(false, -1)` immediately.
11779 #[cfg(unix)]
11780 #[test]
11781 fn setblock_fd_skips_regular_files_per_c_2599() {
11782 let _g = crate::test_util::global_state_lock();
11783 // Open a regular tempfile.
11784 let dir = tempfile::TempDir::new().unwrap();
11785 let f = fs::File::create(dir.path().join("regular")).unwrap();
11786 let fd = f.as_raw_fd();
11787 let (changed, mode) = setblock_fd(true, fd);
11788 assert!(
11789 !changed,
11790 "c:2599 — regular files short-circuit; setblock_fd must NOT report a change"
11791 );
11792 assert_eq!(mode, -1, "c:2614 — `*modep = -1` for regular files");
11793 }
11794
11795 /// `Src/utils.c:2606-2611` — `setblock_fd(turnonblocking=true, fd)`
11796 /// clears O_NONBLOCK on a pipe (non-regular fd). Returns
11797 /// `(true, prior_flags)` if the state was changed.
11798 #[cfg(unix)]
11799 #[test]
11800 fn setblock_fd_clears_o_nonblock_on_pipe() {
11801 let _g = crate::test_util::global_state_lock();
11802 // Create a pipe — non-regular fd.
11803 let mut pipefd: [libc::c_int; 2] = [0; 2];
11804 let r = unsafe { libc::pipe(pipefd.as_mut_ptr()) };
11805 assert_eq!(r, 0, "pipe(2) must succeed");
11806 let read_fd = pipefd[0];
11807 let write_fd = pipefd[1];
11808 // Force NONBLOCK on the read end.
11809 let cur = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11810 unsafe {
11811 libc::fcntl(read_fd, libc::F_SETFL, cur | libc::O_NONBLOCK);
11812 }
11813 // Verify NONBLOCK is set.
11814 let now = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11815 assert_ne!(
11816 now & libc::O_NONBLOCK,
11817 0,
11818 "test setup: NONBLOCK should be set"
11819 );
11820 // Call setblock_fd to ENABLE blocking (clear NONBLOCK).
11821 let (changed, _mode) = setblock_fd(true, read_fd);
11822 assert!(
11823 changed,
11824 "c:2611 — turnonblocking=true on a NONBLOCK pipe must report state change"
11825 );
11826 // Verify NONBLOCK is now cleared.
11827 let after = unsafe { libc::fcntl(read_fd, libc::F_GETFL, 0) };
11828 assert_eq!(
11829 after & libc::O_NONBLOCK,
11830 0,
11831 "c:2611 — O_NONBLOCK must be cleared after turnonblocking=true"
11832 );
11833 // Cleanup.
11834 unsafe {
11835 libc::close(read_fd);
11836 libc::close(write_fd);
11837 }
11838 }
11839
11840 /// `Src/utils.c:2620-2625` — `setblock_stdin()`. C body
11841 /// `setblock_fd(1, 0, &mode)` enables BLOCKING on fd 0 (stdin).
11842 /// Previously the Rust port called `setblock_fd(0, false)`
11843 /// which DISABLES blocking — exact opposite. Pin via real fd
11844 /// inspection.
11845 #[cfg(unix)]
11846 #[test]
11847 fn setblock_stdin_enables_blocking_on_fd_zero() {
11848 let _g = crate::test_util::global_state_lock();
11849 // Set stdin to NONBLOCKING first to verify the function
11850 // ACTUALLY switches it back to blocking. Skip if stdin is
11851 // not a normal fd (some CI configurations).
11852 let cur = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11853 if cur < 0 {
11854 return;
11855 }
11856 // Force NONBLOCK first.
11857 unsafe {
11858 libc::fcntl(0, libc::F_SETFL, cur | libc::O_NONBLOCK);
11859 }
11860 let after_set_nb = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11861 if after_set_nb & libc::O_NONBLOCK == 0 {
11862 // System rejected the change (regular file? CI tty?) — skip.
11863 unsafe {
11864 libc::fcntl(0, libc::F_SETFL, cur);
11865 }
11866 return;
11867 }
11868 // Call setblock_stdin — should CLEAR O_NONBLOCK.
11869 setblock_stdin();
11870 let after_setblock = unsafe { libc::fcntl(0, libc::F_GETFL, 0) };
11871 assert_eq!(
11872 after_setblock & libc::O_NONBLOCK,
11873 0,
11874 "c:2624 — setblock_stdin must CLEAR O_NONBLOCK (enable blocking)"
11875 );
11876 // Restore original flags.
11877 unsafe {
11878 libc::fcntl(0, libc::F_SETFL, cur);
11879 }
11880 }
11881
11882 /// `Src/utils.c:2437-2519` — `zstrtol_underscore(s, base, false)`.
11883 /// Base-10 (explicit) parses canonical decimal.
11884 #[test]
11885 fn zstrtol_underscore_base_10_parses_decimal() {
11886 let _g = crate::test_util::global_state_lock();
11887 let (v, rest) = zstrtol_underscore("12345", 10, false);
11888 assert_eq!(v, 12345, "c:2471 — decimal accumulator");
11889 assert_eq!(rest, "", "rest is empty after full consumption");
11890 // With trailing non-digit.
11891 let (v, rest) = zstrtol_underscore("100abc", 10, false);
11892 assert_eq!(v, 100);
11893 assert_eq!(
11894 rest, "abc",
11895 "c:2467 — loop exits at first non-digit; rest carries on"
11896 );
11897 }
11898
11899 /// c:2452-2461 — base==0 autodetect: `0x`→16, `0b`→2, leading
11900 /// `0`→8 (always, unlike `zstrtoul_underscore` which honors
11901 /// OCTALZEROES). `zstrtol_underscore` does NOT consult
11902 /// OCTALZEROES — pure prefix detection.
11903 #[test]
11904 fn zstrtol_underscore_base_zero_autodetects_prefix() {
11905 let _g = crate::test_util::global_state_lock();
11906 // Hex.
11907 assert_eq!(
11908 zstrtol_underscore("0xff", 0, false).0,
11909 255,
11910 "c:2455 — 0x → base 16"
11911 );
11912 assert_eq!(zstrtol_underscore("0XFF", 0, false).0, 255);
11913 // Binary.
11914 assert_eq!(
11915 zstrtol_underscore("0b1010", 0, false).0,
11916 10,
11917 "c:2457 — 0b → base 2"
11918 );
11919 // Leading 0 → octal (always, no OCTALZEROES gate).
11920 assert_eq!(
11921 zstrtol_underscore("0777", 0, false).0,
11922 511,
11923 "c:2460 — leading 0 → octal (no OCTALZEROES gate for zstrtol)"
11924 );
11925 // Decimal default.
11926 assert_eq!(zstrtol_underscore("12345", 0, false).0, 12345);
11927 }
11928
11929 /// c:2447-2450 — leading `-` and `+` consumed; `-` triggers
11930 /// negation at the end (c:2497-2498).
11931 #[test]
11932 fn zstrtol_underscore_handles_sign_chars() {
11933 let _g = crate::test_util::global_state_lock();
11934 assert_eq!(
11935 zstrtol_underscore("-42", 10, false).0,
11936 -42,
11937 "c:2447 — leading `-` → negate"
11938 );
11939 assert_eq!(
11940 zstrtol_underscore("+42", 10, false).0,
11941 42,
11942 "c:2449 — leading `+` consumed, no negation"
11943 );
11944 // Mixed whitespace + sign.
11945 assert_eq!(
11946 zstrtol_underscore(" -100", 10, false).0,
11947 -100,
11948 "c:2444 — leading whitespace skipped, then sign"
11949 );
11950 }
11951
11952 /// c:2466-2492 — base 16 (hex) accumulator: digits 0-9 + letters
11953 /// a-f / A-F via `idigit(*s)` || `'a' ≤ *s < 'a'+base-10`.
11954 /// Pin both upper- and lower-case.
11955 #[test]
11956 fn zstrtol_underscore_base_16_accepts_letters() {
11957 let _g = crate::test_util::global_state_lock();
11958 assert_eq!(
11959 zstrtol_underscore("ff", 16, false).0,
11960 255,
11961 "c:2485 — base-16 'ff' → 255"
11962 );
11963 assert_eq!(
11964 zstrtol_underscore("FF", 16, false).0,
11965 255,
11966 "c:2485 — base-16 'FF' → 255 (upper case via `*s & 0x1f`)"
11967 );
11968 assert_eq!(
11969 zstrtol_underscore("DEADbeef", 16, false).0,
11970 0xDEADBEEF,
11971 "c:2485 — mixed case 32-bit hex"
11972 );
11973 }
11974
11975 /// c:2468/2482 — `underscore` flag enables digit-separator. With
11976 /// underscore=false, `_` is treated as end-of-digits. With
11977 /// underscore=true, `_` is skipped (c:2469-2470).
11978 #[test]
11979 fn zstrtol_underscore_underscore_flag() {
11980 let _g = crate::test_util::global_state_lock();
11981 // underscore=false: `_` terminates parse.
11982 let (v, rest) = zstrtol_underscore("1_000", 10, false);
11983 assert_eq!(v, 1, "c:2467 — underscore=false: `_` terminates");
11984 assert_eq!(rest, "_000");
11985 // underscore=true: `_` accepted and skipped.
11986 let (v, rest) = zstrtol_underscore("1_000_000", 10, true);
11987 assert_eq!(
11988 v, 1_000_000,
11989 "c:2469-2470 — underscore=true: `_` skipped during accumulation"
11990 );
11991 assert_eq!(rest, "", "fully consumed including `_`s");
11992 }
11993
11994 /// `Src/utils.c:2750-2774` — `timespec_diff_us(t1, t2)` returns
11995 /// the signed microsecond delta `t2 - t1`. Pin both sign
11996 /// conventions: t2 after t1 → positive; t2 before t1 → negative.
11997 #[test]
11998 fn timespec_diff_us_sign_matches_c_t2_minus_t1() {
11999 let _g = crate::test_util::global_state_lock();
12000 let t1 = std::time::Instant::now();
12001 std::thread::sleep(std::time::Duration::from_millis(2));
12002 let t2 = std::time::Instant::now();
12003 // c:2759 — `diff_sec = t2 - t1` for the t2 > t1 path. Result
12004 // positive (microseconds elapsed).
12005 let d = timespec_diff_us(&t1, &t2);
12006 assert!(d > 0, "c:2759 — t2 after t1 → positive delta");
12007 assert!(d >= 1000, "expected >= 1ms (1000us); got {}us", d);
12008 // c:2754 — reversed (t1 > t2): result negative.
12009 let r = timespec_diff_us(&t2, &t1);
12010 assert!(r < 0, "c:2770 — t1 after t2 → negative delta");
12011 assert_eq!(d, -r, "swap of args negates the result");
12012 }
12013
12014 /// c:2752 — `timespec_diff_us(t, t)` is 0 (identical Instants).
12015 #[test]
12016 fn timespec_diff_us_same_instant_returns_zero() {
12017 let _g = crate::test_util::global_state_lock();
12018 let t = std::time::Instant::now();
12019 assert_eq!(
12020 timespec_diff_us(&t, &t),
12021 0,
12022 "c:2752 — identical Instants → 0"
12023 );
12024 }
12025
12026 /// `Src/utils.c:6743-6779` — `ucs4toutf8(dest, wval)`. Encodes
12027 /// a Unicode codepoint to UTF-8. Pin canonical 1-, 2-, 3-, and
12028 /// 4-byte encodings.
12029 #[test]
12030 fn ucs4toutf8_encodes_canonical_lengths() {
12031 let _g = crate::test_util::global_state_lock();
12032 // c:6750 — 1 byte: ASCII range [0, 0x80).
12033 assert_eq!(
12034 ucs4toutf8(0x41),
12035 Some("A".to_string()),
12036 "c:6750 — 0x41 → 'A' (1 byte)"
12037 );
12038 // c:6752 — 2 bytes: [0x80, 0x800). 'é' = U+00E9.
12039 assert_eq!(
12040 ucs4toutf8(0xe9),
12041 Some("é".to_string()),
12042 "c:6752 — U+00E9 → 'é' (2 bytes)"
12043 );
12044 // c:6754 — 3 bytes: [0x800, 0x10000). '字' = U+5B57.
12045 assert_eq!(
12046 ucs4toutf8(0x5B57),
12047 Some("字".to_string()),
12048 "c:6754 — U+5B57 → '字' (3 bytes)"
12049 );
12050 // c:6756 — 4 bytes: [0x10000, 0x200000). '𝄞' = U+1D11E.
12051 assert_eq!(
12052 ucs4toutf8(0x1D11E),
12053 Some("𝄞".to_string()),
12054 "c:6756 — U+1D11E → '𝄞' (4 bytes)"
12055 );
12056 }
12057
12058 /// `Src/utils.c:6762-6764` — values `>= 0x80000000` are
12059 /// out-of-range; C emits `zerr("character not in range")` and
12060 /// returns `-1`. Surrogates (U+D800..U+DFFF) and values in
12061 /// 0x110000..0x80000000 are *encoded as raw bytes* by C
12062 /// (matches `wctomb(3)` extended UCS-4 range).
12063 #[test]
12064 fn ucs4toutf8_rejects_invalid_codepoints() {
12065 let _g = crate::test_util::global_state_lock();
12066 // c:6762-6764 — value >= 0x80000000 → None.
12067 assert_eq!(
12068 ucs4toutf8(0xFFFF_FFFE),
12069 None,
12070 "c:6763 — values >= 0x80000000 → None"
12071 );
12072 assert_eq!(
12073 ucs4toutf8(0x8000_0000),
12074 None,
12075 "c:6760-6764 — exactly 0x80000000 is out of range"
12076 );
12077 // Surrogates encode as 3-byte sequences per C bit-pattern
12078 // (c:6754 → len=3). The C source has no Unicode-validity
12079 // gate; that's a wctomb-only concern in callers.
12080 assert!(
12081 ucs4toutf8(0xD800).is_some(),
12082 "c:6754 — surrogates encode as 3-byte UTF-8 in C (no Unicode validity check)"
12083 );
12084 }
12085
12086 /// `Src/utils.c:6648-6723` — `dquotedztrdup(s)`. Default (non-
12087 /// CSHJUNKIEQUOTES) arm: wrap whole string in `"..."`. Backslash
12088 /// doubles, `"`/`$`/`` ` `` get escaped.
12089 #[test]
12090 fn dquotedztrdup_default_path_wraps_whole_string() {
12091 let _g = crate::test_util::global_state_lock();
12092 if isset(CSHJUNKIEQUOTES) {
12093 return; // Default-only test.
12094 }
12095 // c:6690 — `*p++ = '"';` then iterate.
12096 assert_eq!(
12097 dquotedztrdup("hello"),
12098 "\"hello\"",
12099 "c:6690+6711+6718 — wrap plain text in double quotes"
12100 );
12101 // c:6703-6708 — `$` gets `\$`.
12102 assert_eq!(
12103 dquotedztrdup("$var"),
12104 "\"\\$var\"",
12105 "c:6703-6708 — `$` → `\\$`"
12106 );
12107 // c:6703-6708 — `"` gets `\"`.
12108 assert_eq!(
12109 dquotedztrdup("a\"b"),
12110 "\"a\\\"b\"",
12111 "c:6703-6708 — `\"` → `\\\"`"
12112 );
12113 }
12114
12115 /// c:6697-6701 — backslash handling with `pending` state.
12116 /// Single `\` followed by an ordinary char emits just `\` (no
12117 /// doubling). The "pending" extra `\` fires only when the next
12118 /// char is itself a backslash or one of the escaped specials
12119 /// (`"`/`$`/`` ` ``), OR at end-of-string before the closing `"`.
12120 #[test]
12121 fn dquotedztrdup_pending_backslash_only_doubles_when_needed() {
12122 let _g = crate::test_util::global_state_lock();
12123 if isset(CSHJUNKIEQUOTES) {
12124 return;
12125 }
12126 // Mid-string `\` before ordinary char: stays as single `\`.
12127 assert_eq!(
12128 dquotedztrdup("a\\b"),
12129 "\"a\\b\"",
12130 "c:6711+6712 — `\\b` mid-string: no extra (pending=0 after b)"
12131 );
12132 // Trailing `\` triggers pending quirk: c:6716-6717 emits
12133 // an extra `\` before the closing `"`.
12134 let r = dquotedztrdup("a\\");
12135 assert!(
12136 r.ends_with("\\\\\""),
12137 "c:6716-6717 — trailing `\\` → emit extra `\\` before closing `\"`: got {:?}",
12138 r
12139 );
12140 }
12141
12142 /// `Src/utils.c:6464-6549` — `quotedzputs(s)`. Three branches:
12143 /// - empty → `''`.
12144 /// - has special chars (via `hasspecial`) → wrap in `'...'`
12145 /// with `'\''` escape for embedded apostrophes.
12146 /// - else → return unchanged.
12147 /// Pin all three.
12148 #[test]
12149 fn quotedzputs_empty_string_returns_double_quotes() {
12150 let _g = crate::test_util::global_state_lock();
12151 assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
12152 }
12153
12154 /// c:6514-6543 — needs-quote path: wrap in single quotes,
12155 /// escape embedded `'` as `'\''`.
12156 #[test]
12157 fn quotedzputs_wraps_specials_in_single_quotes() {
12158 let _g = crate::test_util::global_state_lock();
12159 inittyptab();
12160 // Space is special → wrap.
12161 assert_eq!(quotedzputs("hello world"), "'hello world'");
12162 // `=` is special → wrap.
12163 assert_eq!(quotedzputs("foo=bar"), "'foo=bar'");
12164 // Embedded apostrophe — `'\''` escape.
12165 assert_eq!(
12166 quotedzputs("it's"),
12167 "'it'\\''s'",
12168 "c:6517-6519 — apostrophe → '\\''"
12169 );
12170 }
12171
12172 /// c:6512 — no specials → return unchanged.
12173 #[test]
12174 fn quotedzputs_plain_alnum_returns_unchanged() {
12175 let _g = crate::test_util::global_state_lock();
12176 inittyptab();
12177 assert_eq!(
12178 quotedzputs("hello"),
12179 "hello",
12180 "c:6512 — no specials, no wrap"
12181 );
12182 assert_eq!(quotedzputs("abc123"), "abc123");
12183 }
12184
12185 /// c:6578-6637 — Bourne path "avoids empty quoted strings" by
12186 /// tracking `inquote`. Input `'x` should not produce empty `''`
12187 /// at the front; `x'` should not produce trailing `''`.
12188 #[test]
12189 fn quotedzputs_avoids_empty_quoted_runs_on_boundaries() {
12190 let _g = crate::test_util::global_state_lock();
12191 inittyptab();
12192 // Leading `'`: c:6587 with inquote=0 skips the close, emits
12193 // `\'`. Then `x`: c:6604 opens a quote, emits `x`, end emits
12194 // closing `'`. Result: `\''x'` — the visible doubled `''` is
12195 // `\'` (escaped) + `'` (opening single-quote), NOT an empty
12196 // single-quoted string.
12197 assert_eq!(
12198 quotedzputs("'x"),
12199 "\\''x'",
12200 "c:6587-6610 — leading `'` produces `\\'` + opening `'`"
12201 );
12202 // Trailing `'`: open quote at `x`, see `'` with inquote=1 → close
12203 // (`'`), emit `\'`. End-of-string: inquote=0, no trailing `'`.
12204 // Result: `'x'\\'` — NO trailing `''`.
12205 assert_eq!(
12206 quotedzputs("x'"),
12207 "'x'\\'",
12208 "c:6631-6637 — trailing `'` doesn't generate empty `''`"
12209 );
12210 }
12211
12212 /// c:6478-6492 — `is_mb_niceformat` arm of quotedzputs uses
12213 /// mb_niceformat with NICEFLAG_QUOTE so embedded `'` becomes
12214 /// `\'` inside the `$'...'` wrapper (preventing the quote from
12215 /// terminating the wrap). Previously sb_niceformat without
12216 /// NICEFLAG_QUOTE was used, which would have left `'` raw.
12217 #[test]
12218 fn quotedzputs_dollar_quote_escapes_apostrophe_inside_wrap() {
12219 let _g = crate::test_util::global_state_lock();
12220 inittyptab();
12221 // String with embedded `'` AND a control char so the
12222 // is_mb_niceformat arm is taken (controls trigger it).
12223 let r = quotedzputs("a\nb'c");
12224 // Expected: `$'a\nb\'c'` — the embedded `'` is `\'` inside
12225 // dollar-quotes, NOT terminating the wrap.
12226 assert!(
12227 r.starts_with("$'") && r.ends_with("'"),
12228 "c:6488 — must be wrapped in $'...' got {:?}",
12229 r
12230 );
12231 assert!(
12232 r.contains("\\'"),
12233 "c:5413-5414 — embedded `'` must be `\\'` not raw `'`: got {:?}",
12234 r
12235 );
12236 }
12237
12238 /// c:6533-6576 — RCQUOTES branch: wrap everything in `'…'`,
12239 /// double embedded `'` as `''`.
12240 #[test]
12241 fn quotedzputs_rcquotes_doubles_apostrophe() {
12242 let _g = crate::test_util::global_state_lock();
12243 inittyptab();
12244 let prev = crate::ported::options::opt_state_get("rcquotes").unwrap_or(false);
12245 crate::ported::options::opt_state_set("rcquotes", true);
12246 // RCQUOTES: `it's` → `'it''s'` (doubled `'`, NOT `'\''`).
12247 let got = quotedzputs("it's");
12248 crate::ported::options::opt_state_set("rcquotes", prev);
12249 assert_eq!(
12250 got, "'it''s'",
12251 "c:6548-6553 — RCQUOTES: `'` → `''` (doubled)"
12252 );
12253 }
12254
12255 /// `Src/utils.c:2331-2337` — `strucpy(s, t)`. Appends `t` to
12256 /// `*s` and leaves `*s` pointing at the NUL terminator (Rust
12257 /// equivalent: append-in-place; `.len()` gives the new end).
12258 /// Pin: empty input → no-op; non-empty input → append.
12259 #[test]
12260 fn strucpy_appends_t_to_dest() {
12261 let _g = crate::test_util::global_state_lock();
12262 let mut dest = String::from("prefix-");
12263 strucpy(&mut dest, "suffix");
12264 assert_eq!(
12265 dest, "prefix-suffix",
12266 "c:2335 — strucpy appends t (NOT case-changes — c-name 'u' is pointer-walk, not upper)"
12267 );
12268 // Empty t — no change.
12269 let mut dest = String::from("alone");
12270 strucpy(&mut dest, "");
12271 assert_eq!(dest, "alone");
12272 // Empty dest + non-empty t.
12273 let mut dest = String::new();
12274 strucpy(&mut dest, "hello");
12275 assert_eq!(dest, "hello");
12276 }
12277
12278 /// `Src/utils.c:2341-2350` — `struncpy(s, t, n)`. Appends up to
12279 /// `n` bytes of `t` to `*s`. Pin: n=0 no-op; n < len(t) clips;
12280 /// n >= len(t) appends all.
12281 #[test]
12282 fn struncpy_appends_up_to_n_bytes() {
12283 let _g = crate::test_util::global_state_lock();
12284 // n < len(t).
12285 let mut dest = String::from("X");
12286 struncpy(&mut dest, "abcdef", 3);
12287 assert_eq!(dest, "Xabc", "c:2345 — clipped to n=3 bytes of `abcdef`");
12288 // n >= len(t).
12289 let mut dest = String::from("Y");
12290 struncpy(&mut dest, "abc", 100);
12291 assert_eq!(dest, "Yabc", "c:2345 — full copy when n >= len");
12292 // n=0 → no append.
12293 let mut dest = String::from("Z");
12294 struncpy(&mut dest, "abc", 0);
12295 assert_eq!(dest, "Z", "c:2345 — n=0 stops the copy loop immediately");
12296 }
12297
12298 /// `Src/utils.c:2280-2288` — `has_token(s)`. Returns true iff
12299 /// any byte in `s` triggers `itok()`. Pin: ASCII strings →
12300 /// false; strings containing token bytes (Pound 0x84, Bang
12301 /// 0x9c, Nularg 0xa1) → true.
12302 #[test]
12303 fn has_token_detects_typtab_token_bytes() {
12304 let _g = crate::test_util::global_state_lock();
12305 inittyptab();
12306 // c:2285 — pure ASCII has no token bytes.
12307 assert!(!has_token(""), "empty: no tokens");
12308 assert!(
12309 !has_token("hello world"),
12310 "c:2285 — ASCII text has no token bytes"
12311 );
12312 // Pound (0x84) — first token byte.
12313 let s: String = std::iter::once(0x84u8 as char).collect();
12314 assert!(
12315 has_token(&s),
12316 "c:2285 — Pound (0x84) is itok → has_token=true"
12317 );
12318 // Bang (0x9c) — last_normal_tok.
12319 let s: String = std::iter::once(0x9cu8 as char).collect();
12320 assert!(has_token(&s), "c:2285 — Bang (0x9c) is itok");
12321 // Nularg (0xa1) — upper bound.
12322 let s: String = std::iter::once(0xa1u8 as char).collect();
12323 assert!(has_token(&s), "c:2285 — Nularg (0xa1) is itok");
12324 }
12325
12326 /// `Src/utils.c:2280` — Meta byte (0x83) is NOT a token. Pin
12327 /// the regression: previously the Rust port hardcoded `0x83`
12328 /// as a token byte. Now correctly excludes it.
12329 #[test]
12330 fn has_token_excludes_meta_byte() {
12331 let _g = crate::test_util::global_state_lock();
12332 inittyptab();
12333 // 0x83 is Meta, NOT itok. Should return false.
12334 let s: String = std::iter::once(0x83u8 as char).collect();
12335 assert!(
12336 !has_token(&s),
12337 "c:2285 — Meta (0x83) is NOT itok; previous hardcoded 0x83 list misfired"
12338 );
12339 }
12340
12341 /// `Src/utils.c:6082-6124` — `addunprintable(c)`. Renders bytes
12342 /// using shell-compatible C-string escapes (`\a`/`\b`/`\f`/`\n`/
12343 /// `\r`/`\t`/`\v` for the named controls + `\nnn` octal for
12344 /// others + `\0` for NUL). Previously emitted ZLE caret form —
12345 /// wrong convention.
12346 #[test]
12347 fn addunprintable_named_control_escapes() {
12348 let _g = crate::test_util::global_state_lock();
12349 // c:6106-6112 — named-escape per byte.
12350 assert_eq!(addunprintable('\x07'), "\\a", "c:6106 — BEL → \\a");
12351 assert_eq!(addunprintable('\x08'), "\\b", "c:6107 — BS → \\b");
12352 assert_eq!(addunprintable('\x0c'), "\\f", "c:6108 — FF → \\f");
12353 assert_eq!(addunprintable('\n'), "\\n", "c:6109 — LF → \\n");
12354 assert_eq!(addunprintable('\r'), "\\r", "c:6110 — CR → \\r");
12355 assert_eq!(addunprintable('\t'), "\\t", "c:6111 — TAB → \\t");
12356 assert_eq!(addunprintable('\x0b'), "\\v", "c:6112 — VT → \\v");
12357 }
12358
12359 /// c:6097-6103 — NUL renders as `\0`. (C peeks next byte for
12360 /// `\000` disambiguation; Rust port works one char at a time
12361 /// so emits the short `\0` form.)
12362 #[test]
12363 fn addunprintable_nul_renders_as_backslash_zero() {
12364 let _g = crate::test_util::global_state_lock();
12365 assert_eq!(addunprintable('\0'), "\\0", "c:6097-6098 — NUL → \\0");
12366 }
12367
12368 /// c:6114-6119 — `\nnn` 3-digit octal fallback for un-named
12369 /// control bytes. Pin the format: each digit is one octal digit
12370 /// (0-7), zero-padded to 3 positions.
12371 #[test]
12372 fn addunprintable_octal_fallback_for_unnamed_controls() {
12373 let _g = crate::test_util::global_state_lock();
12374 // 0x01 (SOH) = 001 octal.
12375 assert_eq!(addunprintable('\x01'), "\\001", "c:6116-6118 — SOH → \\001");
12376 // 0x1b (ESC) = 033 octal.
12377 assert_eq!(addunprintable('\x1b'), "\\033", "c:6116-6118 — ESC → \\033");
12378 // 0x7f (DEL) = 177 octal.
12379 assert_eq!(addunprintable('\x7f'), "\\177", "c:6116-6118 — DEL → \\177");
12380 // 0xff (high) = 377 octal.
12381 assert_eq!(
12382 addunprintable(char::from_u32(0xff).unwrap()),
12383 "\\377",
12384 "c:6116-6118 — 0xff → \\377"
12385 );
12386 }
12387
12388 /// `Src/utils.c:6072-6082` — `hasspecial(s)`. Pin canonical
12389 /// special chars from SPECCHARS (`Src/zsh.h:228`): `#$^*()=|{}[]
12390 /// \`<>?~;&\\n\\t \\\\\\'\\"`.
12391 #[test]
12392 fn hasspecial_recognises_canonical_special_chars() {
12393 let _g = crate::test_util::global_state_lock();
12394 // typtab access is read-only here (no flag mutation); concurrent
12395 // inittyptab() rebuilds are idempotent for the default flag set.
12396 inittyptab();
12397 // c:6075 — every char in SPECCHARS triggers true.
12398 for c in "#$^*()=|{}[]<>?~;&".chars() {
12399 let s = c.to_string();
12400 assert!(
12401 hasspecial(&s),
12402 "c:6075 — '{}' is in SPECCHARS, must be special",
12403 c
12404 );
12405 }
12406 // Whitespace specials.
12407 assert!(hasspecial(" "), "space in SPECCHARS");
12408 assert!(hasspecial("\t"), "tab in SPECCHARS");
12409 assert!(hasspecial("\n"), "newline in SPECCHARS");
12410 // Non-special chars → false.
12411 assert!(
12412 !hasspecial("hello"),
12413 "c:6075 — plain alphanumerics are NOT special"
12414 );
12415 assert!(!hasspecial("ABC012"));
12416 }
12417
12418 /// `Src/utils.c:6072-6075` — Meta-byte handling: `*s == Meta ?
12419 /// *++s ^ 32 : *s`. Pin Meta+X pair gets decoded before special-
12420 /// check. A Meta+'A' decodes to 'a' (not special), so a string
12421 /// containing only Meta+'A' returns false.
12422 #[test]
12423 fn hasspecial_decodes_meta_byte_before_check() {
12424 let _g = crate::test_util::global_state_lock();
12425 // Same as above — read-only after inittyptab.
12426 inittyptab();
12427 // Meta + 0x41 ('A') → 'a' (0x61) → not special.
12428 let bytes: Vec<u8> = vec![Meta, 0x41u8];
12429 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12430 assert!(
12431 !hasspecial(s),
12432 "c:6075 — Meta+0x41 decodes to 'a' which is NOT special"
12433 );
12434 }
12435
12436 /// `Src/utils.c:5849-5910` — `sb_niceformat(s)`. Pin: ASCII
12437 /// printable passes through unchanged; controls escape.
12438 /// C-equivalent call shape: `char *out; sb_niceformat(s, NULL, &out, 0);`
12439 #[test]
12440 fn sb_niceformat_passes_printable_ascii() {
12441 let _g = crate::test_util::global_state_lock();
12442 let mut out: Option<String> = None;
12443 let l = sb_niceformat("hello", None, Some(&mut out), 0);
12444 assert_eq!(
12445 out.as_deref(),
12446 Some("hello"),
12447 "c:5886 — nicechar_sel passes printable through"
12448 );
12449 assert_eq!(l, 5, "c:5928 — return length equals output bytes");
12450
12451 let mut out: Option<String> = None;
12452 sb_niceformat("", None, Some(&mut out), 0);
12453 assert_eq!(out.as_deref(), Some(""));
12454
12455 let mut out: Option<String> = None;
12456 sb_niceformat("ABC012!?@", None, Some(&mut out), 0);
12457 assert_eq!(out.as_deref(), Some("ABC012!?@"));
12458 }
12459
12460 /// c:5886 — controls get `\n`/`\t`/`^X`/`^?` escapes via nicechar_sel.
12461 #[test]
12462 fn sb_niceformat_escapes_controls() {
12463 let _g = crate::test_util::global_state_lock();
12464 let mut out: Option<String> = None;
12465 sb_niceformat("a\nb", None, Some(&mut out), 0);
12466 assert_eq!(out.as_deref(), Some("a\\nb"), "c:5886 — newline → \\n");
12467
12468 let mut out: Option<String> = None;
12469 sb_niceformat("a\tb", None, Some(&mut out), 0);
12470 assert_eq!(out.as_deref(), Some("a\\tb"));
12471
12472 let mut out: Option<String> = None;
12473 sb_niceformat("\x01", None, Some(&mut out), 0);
12474 assert_eq!(
12475 out.as_deref(),
12476 Some("^A"),
12477 "c:5886 — control char → ^X form"
12478 );
12479
12480 let mut out: Option<String> = None;
12481 sb_niceformat("\x7f", None, Some(&mut out), 0);
12482 assert_eq!(out.as_deref(), Some("^?"));
12483 }
12484
12485 /// `Src/utils.c:5872` — unmetafy step before formatting. Pin:
12486 /// metafied Meta+X pair gets unescaped first, then run through
12487 /// nicechar_sel.
12488 #[test]
12489 fn sb_niceformat_unmetafies_before_formatting() {
12490 let _g = crate::test_util::global_state_lock();
12491 // META + 0x41 ('A') → decodes to 'a' (0x61). 'a' is printable
12492 // → passes through unchanged.
12493 let bytes: Vec<u8> = vec![Meta, 0x41u8];
12494 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12495 let mut out: Option<String> = None;
12496 sb_niceformat(s, None, Some(&mut out), 0);
12497 assert_eq!(
12498 out.as_deref(),
12499 Some("a"),
12500 "c:5872 — unmetafy first: Meta+0x41 → 'a' → printable passthrough"
12501 );
12502 // META + 0x20 → decodes to NUL → "^@" form.
12503 let bytes: Vec<u8> = vec![Meta, 0x20u8];
12504 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12505 let mut out: Option<String> = None;
12506 sb_niceformat(s, None, Some(&mut out), 0);
12507 let r = out.unwrap_or_default();
12508 assert!(
12509 !r.is_empty(),
12510 "c:5872 — Meta+0x20 → NUL → must emit some escape, not empty"
12511 );
12512 }
12513
12514 /// `Src/utils.c:5937-5959` — `is_sb_niceformat(s)`. Returns 1
12515 /// if sb_niceformat would change the input, else 0.
12516 #[test]
12517 fn is_sb_niceformat_true_for_strings_with_controls() {
12518 let _g = crate::test_util::global_state_lock();
12519 assert_eq!(is_sb_niceformat("\n"), 1, "newline is nice");
12520 assert_eq!(is_sb_niceformat("a\tb"), 1);
12521 assert_eq!(is_sb_niceformat("\x01"), 1);
12522 // Non-control ASCII → 0.
12523 assert_eq!(is_sb_niceformat("hello"), 0);
12524 assert_eq!(is_sb_niceformat(""), 0);
12525 }
12526
12527 /// `Src/utils.c:5810-5826` — `metacharlenconv(x, c)`. Returns
12528 /// `(2, decoded)` for Meta+X pair, `(1, byte)` for plain byte.
12529 /// Pin both branches.
12530 #[test]
12531 fn metacharlenconv_plain_ascii_returns_one_byte() {
12532 let _g = crate::test_util::global_state_lock();
12533 // c:5823-5825 — plain byte.
12534 let (n, c) = metacharlenconv("a");
12535 assert_eq!((n, c), (1, Some('a')), "c:5823 — plain byte → (1, byte)");
12536 let (n, c) = metacharlenconv("X");
12537 assert_eq!((n, c), (1, Some('X')));
12538 }
12539
12540 /// c:5818-5821 — Meta+X pair: `*c = x[1] ^ 32; return 2`.
12541 #[test]
12542 fn metacharlenconv_meta_pair_xor_decodes() {
12543 let _g = crate::test_util::global_state_lock();
12544 // Meta + 0x41 ('A') → 'A' ^ 32 = 'a'.
12545 let bytes: Vec<u8> = vec![Meta, 0x41u8];
12546 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12547 let (n, c) = metacharlenconv(s);
12548 assert_eq!(
12549 (n, c),
12550 (2, Some('a')),
12551 "c:5820 — Meta+0x41 → 'a' (XOR 32), consumed 2 bytes"
12552 );
12553 // Empty input.
12554 let (n, c) = metacharlenconv("");
12555 assert_eq!((n, c), (0, None));
12556 }
12557
12558 /// `Src/utils.c:5832-5843` — `charlenconv(x, len, c)`. Returns
12559 /// `(0, NUL)` when len=0, `(1, byte)` otherwise.
12560 #[test]
12561 fn charlenconv_zero_len_returns_zero() {
12562 let _g = crate::test_util::global_state_lock();
12563 // c:5834 — `if (!len) { *c = '\0'; return 0; }`.
12564 let (n, c) = charlenconv("abc", 0);
12565 assert_eq!((n, c), (0, None), "c:5834-5837 — len=0 returns 0");
12566 }
12567
12568 /// c:5840-5842 — non-zero len: read one byte, return (1, byte).
12569 #[test]
12570 fn charlenconv_returns_first_byte_for_nonzero_len() {
12571 let _g = crate::test_util::global_state_lock();
12572 let (n, c) = charlenconv("abc", 3);
12573 assert_eq!((n, c), (1, Some('a')), "c:5841 — *c = *x; return 1");
12574 let (n, c) = charlenconv("xy", 2);
12575 assert_eq!((n, c), (1, Some('x')));
12576 }
12577
12578 /// `Src/utils.c:5474-5524` — `is_mb_niceformat(s)`. Predicate:
12579 /// would mb_niceformat produce a different output? Pin canonical
12580 /// branches:
12581 /// - Pure ASCII printable → false (no escape needed).
12582 /// - Contains control char → true.
12583 /// - Pure UTF-8 wide chars → false under default PRINTEIGHTBIT-off
12584 /// since is_wcs_nicechar treats them as printable.
12585 #[test]
12586 fn is_mb_niceformat_false_for_pure_printable_ascii() {
12587 let _g = crate::test_util::global_state_lock();
12588 assert_eq!(
12589 is_mb_niceformat("hello"),
12590 0,
12591 "c:5509 — printable ASCII needs no nice-format"
12592 );
12593 assert_eq!(
12594 is_mb_niceformat(""),
12595 0,
12596 "c:5486 — empty string has no chars to flag"
12597 );
12598 assert_eq!(is_mb_niceformat("ABC012!?@"), 0);
12599 }
12600
12601 /// c:5509 + `is_wcs_nicechar` — newline/tab/control chars trigger
12602 /// the predicate.
12603 #[test]
12604 fn is_mb_niceformat_true_for_strings_with_controls() {
12605 let _g = crate::test_util::global_state_lock();
12606 assert_eq!(is_mb_niceformat("a\nb"), 1, "c:5509 — newline is nice");
12607 assert_eq!(is_mb_niceformat("\t"), 1);
12608 assert_eq!(is_mb_niceformat("\x01"), 1, "c:5509 — control char is nice");
12609 assert_eq!(is_mb_niceformat("\x7f"), 1, "c:5509 — DEL is nice");
12610 }
12611
12612 /// `Src/utils.c:5366-5460` — `mb_niceformat(s)`. Multibyte-aware
12613 /// nice-formatter. Calls `wcs_nicechar` per char. Test uses
12614 /// C-equivalent call shape: `char *out; mb_niceformat(s, NULL, &out, 0);`
12615 #[test]
12616 fn mb_niceformat_preserves_printable_wide_chars() {
12617 let _g = crate::test_util::global_state_lock();
12618 let mut out: Option<String> = None;
12619 mb_niceformat("hello", None, Some(&mut out), 0);
12620 assert_eq!(
12621 out.as_deref(),
12622 Some("hello"),
12623 "c:5407 — printable ASCII passes through"
12624 );
12625
12626 let mut out: Option<String> = None;
12627 mb_niceformat("café", None, Some(&mut out), 0);
12628 assert_eq!(
12629 out.as_deref(),
12630 Some("café"),
12631 "c:5407 — Latin-1 'é' must NOT byte-mask to \\M-X"
12632 );
12633
12634 let mut out: Option<String> = None;
12635 mb_niceformat("字", None, Some(&mut out), 0);
12636 assert_eq!(
12637 out.as_deref(),
12638 Some("字"),
12639 "c:5407 — CJK printable passes through"
12640 );
12641
12642 let mut out: Option<String> = None;
12643 mb_niceformat("abcéxyz", None, Some(&mut out), 0);
12644 assert_eq!(out.as_deref(), Some("abcéxyz"));
12645 }
12646
12647 /// `Src/utils.c:5407` + `wcs_nicechar` — controls still escape.
12648 #[test]
12649 fn mb_niceformat_escapes_controls() {
12650 let _g = crate::test_util::global_state_lock();
12651 let mut out: Option<String> = None;
12652 mb_niceformat("\n", None, Some(&mut out), 0);
12653 assert_eq!(
12654 out.as_deref(),
12655 Some("\\n"),
12656 "c:5407 → wcs_nicechar c:625 — newline escapes"
12657 );
12658
12659 let mut out: Option<String> = None;
12660 mb_niceformat("\t", None, Some(&mut out), 0);
12661 assert_eq!(
12662 out.as_deref(),
12663 Some("\\t"),
12664 "c:5407 → wcs_nicechar c:628 — tab escapes"
12665 );
12666
12667 let mut out: Option<String> = None;
12668 mb_niceformat("a\nb", None, Some(&mut out), 0);
12669 assert_eq!(out.as_deref(), Some("a\\nb"));
12670 }
12671
12672 /// `Src/utils.c:4971-4983` — `metalen(s, len)`. **Input `len` is
12673 /// the UNMETAFIED char count; output is the METAFIED byte count.**
12674 /// Pin a metafied string with one Meta+X pair: 3 unmetafied chars
12675 /// → 4 metafied bytes.
12676 #[test]
12677 fn metalen_returns_metafied_byte_count() {
12678 let _g = crate::test_util::global_state_lock();
12679 // Pure ASCII: 5 chars → 5 bytes (no Meta encountered).
12680 assert_eq!(
12681 metalen("hello", 5),
12682 5,
12683 "c:4972 — ASCII: metafied bytes == unmetafied chars"
12684 );
12685 // [a, Meta, X, b] = 4 metafied bytes representing 3 chars.
12686 let bytes: Vec<u8> = vec![b'a', Meta, 0x41, b'b'];
12687 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12688 assert_eq!(
12689 metalen(s, 3),
12690 4,
12691 "c:4978 — 3 unmetafied chars + 1 Meta = 4 metafied bytes"
12692 );
12693 // Two Meta pairs: [Meta, X, Meta, Y] = 4 bytes for 2 chars.
12694 let bytes: Vec<u8> = vec![Meta, 0x41, Meta, 0x42];
12695 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12696 assert_eq!(
12697 metalen(s, 2),
12698 4,
12699 "c:4978 — 2 unmetafied chars (both Meta+X) = 4 metafied bytes"
12700 );
12701 }
12702
12703 /// c:4974 — `mlen = len;` is the initial value (no Meta found,
12704 /// returns the input length unchanged). Pin len=0 and pure-ASCII.
12705 #[test]
12706 fn metalen_returns_input_for_no_meta_chars() {
12707 let _g = crate::test_util::global_state_lock();
12708 assert_eq!(metalen("", 0), 0, "c:4974 — empty input returns 0");
12709 assert_eq!(
12710 metalen("abc", 3),
12711 3,
12712 "c:4974 — no Meta chars: output == input len"
12713 );
12714 }
12715
12716 /// `Src/utils.c:5070` — `if (!in || !*in) return 0;`.
12717 /// Empty input returns `('\0', 0)`. Pin: empty &str → 0 bytes consumed.
12718 #[test]
12719 fn unmeta_one_empty_input_returns_zero() {
12720 let _g = crate::test_util::global_state_lock();
12721 let (c, n) = unmeta_one("");
12722 assert_eq!(c, '\0', "c:5070 — empty input returns NUL char");
12723 assert_eq!(n, 0, "c:5070 — empty input consumes 0 bytes");
12724 }
12725
12726 /// `Src/utils.c:5081-5082` — Non-Meta byte: `*sz = 1; wc = byte`.
12727 #[test]
12728 fn unmeta_one_plain_ascii_consumes_one_byte() {
12729 let _g = crate::test_util::global_state_lock();
12730 for c in "aA0!~".chars() {
12731 let s = c.to_string();
12732 let (got, n) = unmeta_one(&s);
12733 assert_eq!(got, c, "c:5082 — '{}' decodes to itself", c);
12734 assert_eq!(n, 1, "c:5081 — non-Meta byte consumes 1");
12735 }
12736 }
12737
12738 /// `Src/utils.c:5077-5079` — Meta byte: `*sz = 2; wc = in[1] ^ 32`.
12739 /// Pin via constructed Meta byte sequence.
12740 #[test]
12741 fn unmeta_one_meta_pair_decodes_xor_32() {
12742 let _g = crate::test_util::global_state_lock();
12743 // META + 0x41 ('A') → 'A' ^ 32 = 0x61 ('a'). 2 bytes consumed.
12744 let bytes: Vec<u8> = vec![Meta, 0x41u8];
12745 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12746 let (got, n) = unmeta_one(s);
12747 assert_eq!(got, 'a', "c:5079 — Meta+0x41 decodes to 0x41^32 = 'a'");
12748 assert_eq!(n, 2, "c:5078 — Meta pair consumes 2 bytes");
12749 // META + 0x20 = 0x20^32 = 0x00 (NUL).
12750 let bytes: Vec<u8> = vec![Meta, 0x20u8];
12751 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12752 let (got, n) = unmeta_one(s);
12753 assert_eq!(
12754 got, '\0',
12755 "c:5079 — Meta+0x20 decodes to NUL (the canonical metafy-NUL pattern)"
12756 );
12757 assert_eq!(n, 2);
12758 }
12759
12760 /// Edge case: bare Meta byte at end with no follower. C source
12761 /// would read past the buffer (UB); Rust port's `bytes.len() > 1`
12762 /// guard handles this by falling through to the non-Meta arm.
12763 #[test]
12764 fn unmeta_one_trailing_meta_byte_falls_through() {
12765 let _g = crate::test_util::global_state_lock();
12766 let bytes: Vec<u8> = vec![Meta];
12767 let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
12768 let (_, n) = unmeta_one(s);
12769 // Defensive: Rust returns the Meta byte itself as char + 1.
12770 assert_eq!(n, 1, "trailing Meta — no panic, no read past buffer");
12771 }
12772
12773 /// `Src/utils.c:5185-5203` — `ztrsub(t, s)`. Count of unmetafied
12774 /// chars between two pointers in the same metafied string.
12775 /// Rust port takes (buf, start, end) byte-offsets since raw
12776 /// pointer subtraction across distinct &str isn't safe.
12777 /// Pin pure-ASCII subrange (no Meta) → returns end-start.
12778 #[test]
12779 fn ztrsub_ascii_no_meta_returns_byte_distance() {
12780 let _g = crate::test_util::global_state_lock();
12781 // "hello world" — substring [0..5) → "hello" → 5 chars.
12782 assert_eq!(
12783 ztrsub("hello world", 0, 5),
12784 5,
12785 "c:5189 — no Meta: returns t-s byte distance"
12786 );
12787 // [6..11) → "world" → 5 chars.
12788 assert_eq!(ztrsub("hello world", 6, 11), 5);
12789 // Empty range.
12790 assert_eq!(ztrsub("hello", 2, 2), 0);
12791 }
12792
12793 /// c:5191-5199 — each Meta+X pair decrements `l` by 1 (since the
12794 /// initial l = byte-distance counts both bytes but the pair is
12795 /// one logical char). Pin via constructed Meta string.
12796 #[test]
12797 fn ztrsub_subtracts_one_per_meta_pair() {
12798 let _g = crate::test_util::global_state_lock();
12799 // Build "a" + Meta + 0x20 + "b" = 4 bytes total.
12800 // Unmetafied: "a" + 1-char-from-meta + "b" = 3 chars.
12801 let meta_byte = Meta;
12802 let buf_bytes: Vec<u8> = vec![b'a', meta_byte, 0x20, b'b'];
12803 let buf = unsafe { std::str::from_utf8_unchecked(&buf_bytes) };
12804 // [0..4) byte distance = 4; Meta pair found → l-- → 3.
12805 assert_eq!(
12806 ztrsub(buf, 0, 4),
12807 3,
12808 "c:5192-5199 — Meta pair decrements distance by 1"
12809 );
12810 }
12811
12812 /// c:5189 — `int l = t - s;` start of count. Pin edge case
12813 /// where start > end (caller bug) doesn't underflow or panic;
12814 /// Rust port clamps via `start.min(end)`.
12815 #[test]
12816 fn ztrsub_clamps_inverted_range_to_zero() {
12817 let _g = crate::test_util::global_state_lock();
12818 assert_eq!(
12819 ztrsub("hello", 4, 2),
12820 0,
12821 "inverted range start>end → 0 (defensive; not in C contract)"
12822 );
12823 // Beyond-buffer end gets clamped.
12824 assert_eq!(
12825 ztrsub("hi", 0, 100),
12826 2,
12827 "end > buf.len() clamps to buffer length"
12828 );
12829 }
12830
12831 /// `Src/utils.c:5141` — trailing Meta byte with no following char
12832 /// is a malformed-string edge case. C has a DPUTS debug-only
12833 /// fprintf for "unexpected end" but otherwise the loop terminates
12834 /// because `*s` is now NUL. Rust port must not panic on this
12835 /// truncated input.
12836 #[test]
12837 fn ztrlen_handles_trailing_meta_byte_without_panic() {
12838 let _g = crate::test_util::global_state_lock();
12839 let meta = char::from_u32(Meta as u32).unwrap();
12840 let trailing: String = ['a', meta].iter().collect();
12841 // c:5141-5149 — increment l, then encounter Meta, then loop
12842 // condition `*s` is empty → terminate. Count: 'a' + Meta = 2
12843 // (Rust port counts Meta as 1 char when no follower).
12844 let r = ztrlen(&trailing);
12845 assert!(r >= 1, "trailing Meta must not panic; got {}", r);
12846 }
12847
12848 /// c:params.c:1288 — `isident` rejects digit-leading names.
12849 /// A regression accepting them lets `typeset 1foo=bar` install
12850 /// a poisoned param that no later expansion can address.
12851 #[test]
12852 fn isident_rejects_digit_leading_names() {
12853 let _g = crate::test_util::global_state_lock(); // c:1288
12854 // c:1315-1319 — C `isident`: if first char is digit, ALL
12855 // chars must be digit (all-digit names are valid positional
12856 // params like $99). So `1foo` is rejected (digit-then-alpha)
12857 // but `99` is accepted as a valid positional-param name.
12858 // The previous utils.rs fake rejected all-digit names too,
12859 // which was non-faithful — the canonical `params::isident`
12860 // at `params.rs:2056` matches the C semantics.
12861 assert!(!isident("1foo")); // c:1318
12862 assert!(
12863 isident("99"), // c:1318
12864 "c:1315-1319 — all-digit names are valid positional params"
12865 );
12866 assert!(!isident("")); // c:1290
12867 }
12868
12869 /// `isident` MUST accept underscore-leading + alpha-leading names —
12870 /// `_foo`, `Foo_BAR_42` are valid POSIX shell idents.
12871 #[test]
12872 fn isident_accepts_underscore_and_alpha_leading() {
12873 let _g = crate::test_util::global_state_lock(); // c:1288
12874 assert!(isident("foo")); // c:1288
12875 assert!(isident("_foo")); // c:1288
12876 assert!(isident("Foo_BAR_42")); // c:1288
12877 assert!(isident("a")); // c:1288
12878 }
12879
12880 /// `isident` rejects whitespace/punctuation/$. A regression
12881 /// accepting them would let assignments install names no
12882 /// subsequent lookup can address (`typeset 'foo bar'=baz`).
12883 #[test]
12884 fn isident_rejects_special_chars() {
12885 let _g = crate::test_util::global_state_lock(); // c:1288
12886 // c:1320-1322 — non-digit-leading names use itype_end with
12887 // INAMESPC bits (alnum + `_` + `.`); other chars terminate.
12888 // The previous utils.rs fake rejected `foo.` outright, which
12889 // is incorrect — C accepts trailing `.` (it's an INAMESPC
12890 // member). Whitespace, `-`, and `$` are NOT INAMESPC chars
12891 // and remain rejected.
12892 assert!(!isident("foo bar")); // c:1322
12893 assert!(!isident("foo-bar")); // c:1322
12894 assert!(!isident("a$b")); // c:1322
12895 }
12896
12897 /// `convbase(0, 10)` MUST produce `"0"`, not `""`. The literal-zero
12898 /// edge case is a real C divergence point — regression here would
12899 /// make `$(( 0 ))` print nothing.
12900 #[test]
12901 fn convbase_zero_renders_as_zero_literal() {
12902 let _g = crate::test_util::global_state_lock();
12903 assert_eq!(convbase(0, 10), "0");
12904 }
12905
12906 /// `convbase` uses zsh's canonical `BASE#NUMBER` syntax for
12907 /// non-decimal output (matches `$(( [#16] 255 ))` → `16#FF`).
12908 /// This is the format `printf "%X"` and `$(( ... ))` both produce.
12909 #[test]
12910 fn convbase_uses_base_prefix_syntax_for_non_decimal() {
12911 let _g = crate::test_util::global_state_lock();
12912 assert_eq!(convbase(255, 16), "16#FF");
12913 assert_eq!(convbase(8, 8), "8#10");
12914 assert_eq!(convbase(5, 2), "2#101");
12915 // Base 10 has no prefix.
12916 assert_eq!(convbase(42, 10), "42");
12917 }
12918
12919 /// Negative number renders with leading `-`. Regression that drops
12920 /// the sign would silently flip arithmetic semantics in `$((...))`.
12921 #[test]
12922 fn convbase_preserves_negative_sign() {
12923 let _g = crate::test_util::global_state_lock();
12924 assert_eq!(convbase(-42, 10), "-42");
12925 }
12926
12927 /// `Src/utils.c:837` — `slashsplit` keeps an EMPTY leading segment
12928 /// when the input has a leading `/`. The caller `xsymlinks`
12929 /// (c:879) reads this empty as the signal "absolute path, start
12930 /// xbuf from /". Previous Rust test asserted filtering of all
12931 /// empties (matching the previous buggy port); that contract was
12932 /// wrong relative to C. Updated to assert the C contract.
12933 #[test]
12934 fn slashsplit_keeps_leading_empty_segment() {
12935 let _g = crate::test_util::global_state_lock();
12936 // c:851 with t==s on first iter → empty prefix segment.
12937 assert_eq!(
12938 slashsplit("/usr/local/bin"),
12939 vec![
12940 "".to_string(),
12941 "usr".to_string(),
12942 "local".to_string(),
12943 "bin".to_string()
12944 ],
12945 "c:851 — leading `/` produces empty first segment"
12946 );
12947 // Single `/`: one empty segment only.
12948 assert_eq!(
12949 slashsplit("/"),
12950 vec!["".to_string()],
12951 "c:854-857 — trailing `/` after first iter returns ['']"
12952 );
12953 // Consecutive slashes collapse (c:852-853 inner while-loop).
12954 assert_eq!(
12955 slashsplit("//foo"),
12956 vec!["".to_string(), "foo".to_string()],
12957 "c:852-853 — consecutive `/` collapse, leading empty kept"
12958 );
12959 // Empty input still empty result.
12960 assert_eq!(
12961 slashsplit(""),
12962 Vec::<String>::new(),
12963 "c:842 — empty input → empty array"
12964 );
12965 }
12966
12967 /// `Src/utils.c:837` — relative paths (no leading `/`) start
12968 /// at the first non-slash; trailing `/` doesn't add a segment
12969 /// (c:854-857 returns before tail emit).
12970 #[test]
12971 fn slashsplit_relative_path_no_trailing_empty() {
12972 let _g = crate::test_util::global_state_lock();
12973 assert_eq!(
12974 slashsplit("a/b/"),
12975 vec!["a".to_string(), "b".to_string()],
12976 "c:854-857 — trailing `/` doesn't add segment"
12977 );
12978 // Mid-slash collapse in relative path.
12979 assert_eq!(
12980 slashsplit("a//b"),
12981 vec!["a".to_string(), "b".to_string()],
12982 "c:852-853 — mid `//` collapses"
12983 );
12984 }
12985
12986 /// `equalsplit("foo=bar")` returns Some(("foo","bar")). Used by
12987 /// `typeset NAME=val` parsing. Regression returning None on a
12988 /// well-formed assignment would silently break every export/typeset.
12989 #[test]
12990 fn equalsplit_returns_first_equals_split() {
12991 let _g = crate::test_util::global_state_lock();
12992 assert_eq!(
12993 equalsplit("foo=bar"),
12994 Some(("foo".to_string(), "bar".to_string()))
12995 );
12996 assert_eq!(
12997 equalsplit("a=b=c"),
12998 Some(("a".to_string(), "b=c".to_string())),
12999 "splits on FIRST `=` only"
13000 );
13001 }
13002
13003 /// `equalsplit("foo")` returns None — no `=` to split on. Catches
13004 /// a regression returning Some(("foo","")) which would let bare
13005 /// names install with empty values silently.
13006 #[test]
13007 fn equalsplit_no_equals_returns_none() {
13008 let _g = crate::test_util::global_state_lock();
13009 assert_eq!(equalsplit("foo"), None);
13010 assert_eq!(equalsplit(""), None);
13011 }
13012
13013 /// c:5106 — `ztrcmp` is the canonical zsh string compare. Same
13014 /// inputs MUST produce same Ordering (deterministic). Regression
13015 /// to a randomised hash-order would mis-sort `${(o)array}` output.
13016 #[test]
13017 fn ztrcmp_deterministic_and_lexicographic() {
13018 let _g = crate::test_util::global_state_lock();
13019 assert_eq!(ztrcmp("abc", "abc"), std::cmp::Ordering::Equal);
13020 assert_eq!(ztrcmp("abc", "abd"), std::cmp::Ordering::Less);
13021 assert_eq!(ztrcmp("abd", "abc"), std::cmp::Ordering::Greater);
13022 // Empty strings.
13023 assert_eq!(ztrcmp("", ""), std::cmp::Ordering::Equal);
13024 assert_eq!(ztrcmp("", "a"), std::cmp::Ordering::Less);
13025 }
13026
13027 /// c:5106 — shorter-as-prefix sorts before longer (like strcmp).
13028 /// Catches a regression where length-then-content would mis-sort.
13029 #[test]
13030 fn ztrcmp_shorter_prefix_is_less() {
13031 let _g = crate::test_util::global_state_lock();
13032 assert_eq!(ztrcmp("a", "ab"), std::cmp::Ordering::Less);
13033 assert_eq!(ztrcmp("foo", "foob"), std::cmp::Ordering::Less);
13034 }
13035
13036 /// `Src/utils.c:5117-5122` — Meta+X pair decodes to `X ^ 32`
13037 /// for comparison purposes. Pin: two strings differing only in
13038 /// the metafied byte's decoded value compare correctly.
13039 #[test]
13040 fn ztrcmp_decodes_meta_byte_for_comparison() {
13041 let _g = crate::test_util::global_state_lock();
13042 // META+0x21 ('!') = NUL? Let me use safer chars.
13043 // META+'A' (0x41) decodes to 'A' ^ 32 = 'a' (0x61).
13044 // So a "META a" pair represents the byte 'a' (0x61).
13045 // Compare "Ma" (M=Meta+a-byte-pair) vs "b" (>a).
13046 let meta_byte = Meta;
13047 let s1_bytes: Vec<u8> = vec![meta_byte, 0x41u8]; // decodes to 0x61 = 'a'
13048 let s2_bytes: Vec<u8> = vec![b'b'];
13049 let s1 = unsafe { std::str::from_utf8_unchecked(&s1_bytes) };
13050 let s2 = unsafe { std::str::from_utf8_unchecked(&s2_bytes) };
13051 // 'a' (0x61) < 'b' (0x62) → Less.
13052 assert_eq!(
13053 ztrcmp(s1, s2),
13054 std::cmp::Ordering::Less,
13055 "c:5117-5118 — Meta+0x41 decodes to 'a' which < 'b'"
13056 );
13057 }
13058
13059 /// `Src/utils.c:531-539` — `is_nicechar`. Returns true for chars
13060 /// needing escape-formatting:
13061 /// - c:534 — printable ASCII (0x20-0x7e) → false.
13062 /// - c:536 — high-bit byte (>= 0x80) → !PRINTEIGHTBIT.
13063 /// - c:538 — DEL/\n/\t/<0x20 → true.
13064 /// Pin the three branches.
13065 #[test]
13066 fn is_nicechar_printable_ascii_is_not_nice() {
13067 let _g = crate::test_util::global_state_lock();
13068 // c:534 — letters, digits, punctuation in 0x20-0x7e all NOT nice.
13069 for c in "abcXYZ012!?@~".chars() {
13070 assert!(
13071 !is_nicechar(c),
13072 "c:534 — '{}' (ASCII printable) must NOT be nice",
13073 c
13074 );
13075 }
13076 // c:534 — space (0x20) is printable.
13077 assert!(!is_nicechar(' '));
13078 }
13079
13080 /// c:538 — control chars (DEL, newline, tab, <0x20) ARE nice.
13081 #[test]
13082 fn is_nicechar_control_chars_are_nice() {
13083 let _g = crate::test_util::global_state_lock();
13084 assert!(is_nicechar('\n'), "c:538 — newline is nice");
13085 assert!(is_nicechar('\t'), "c:538 — tab is nice");
13086 assert!(is_nicechar('\x7f'), "c:538 — DEL is nice");
13087 assert!(is_nicechar('\x00'), "c:538 — NUL is nice (<0x20)");
13088 assert!(is_nicechar('\x07'), "c:538 — BEL is nice (<0x20)");
13089 assert!(is_nicechar('\x1b'), "c:538 — ESC is nice (<0x20)");
13090 assert!(is_nicechar('\x1f'), "c:538 — boundary 0x1f is nice");
13091 }
13092
13093 /// `Src/utils.c:2528-2570` — `zstrtoul_underscore(s)`. Pins
13094 /// the base-prefix dispatch: `0x` → 16, `0b` → 2, leading `0`
13095 /// only when OCTALZEROES is set. Tests the default state.
13096 #[test]
13097 fn zstrtoul_underscore_recognises_hex_binary_decimal() {
13098 let _g = crate::test_util::global_state_lock();
13099 // c:2538 — hex.
13100 assert_eq!(zstrtoul_underscore("0xff"), Some(255));
13101 assert_eq!(zstrtoul_underscore("0XFF"), Some(255));
13102 // c:2540 — binary.
13103 assert_eq!(zstrtoul_underscore("0b1010"), Some(10));
13104 assert_eq!(zstrtoul_underscore("0B11"), Some(3));
13105 // c:2537 — pure decimal (no leading zero).
13106 assert_eq!(zstrtoul_underscore("12345"), Some(12345));
13107 // c:2543 — leading-zero with OCTALZEROES off (default) → decimal.
13108 if !isset(OCTALZEROES) {
13109 assert_eq!(
13110 zstrtoul_underscore("0777"),
13111 Some(777),
13112 "c:2543 — OCTALZEROES off: leading-0 parses as decimal"
13113 );
13114 assert_eq!(zstrtoul_underscore("010"), Some(10));
13115 }
13116 }
13117
13118 /// `Src/utils.c:2547-2548` — underscores are stripped before
13119 /// parsing. C: `if (*s == '_') continue;`. Used to support
13120 /// human-readable big numbers like `1_000_000`.
13121 #[test]
13122 fn zstrtoul_underscore_strips_underscores() {
13123 let _g = crate::test_util::global_state_lock();
13124 assert_eq!(
13125 zstrtoul_underscore("1_000_000"),
13126 Some(1_000_000),
13127 "c:2547-2548 — `_` stripped from numeric input"
13128 );
13129 assert_eq!(zstrtoul_underscore("0xff_ff"), Some(0xffff));
13130 assert_eq!(zstrtoul_underscore("0b1010_1010"), Some(0xaa));
13131 }
13132
13133 /// `Src/utils.c:2533-2534` — leading `+` sign is consumed.
13134 /// `zulong` is unsigned so `-` is not handled here (caller
13135 /// handles negation at `zstrtol_underscore`).
13136 #[test]
13137 fn zstrtoul_underscore_consumes_leading_plus() {
13138 let _g = crate::test_util::global_state_lock();
13139 assert_eq!(zstrtoul_underscore("+42"), Some(42));
13140 assert_eq!(zstrtoul_underscore("+0xff"), Some(255));
13141 }
13142
13143 /// `Src/utils.c:467` — `if (ZISPRINT(c)) goto done;`. Printable
13144 /// ASCII passes through nicechar_sel unchanged. Pin a single-char
13145 /// printable input → single-char output.
13146 #[test]
13147 fn nicechar_sel_passes_printable_ascii_unchanged() {
13148 let _g = crate::test_util::global_state_lock();
13149 for c in "aA0!~".chars() {
13150 assert_eq!(
13151 nicechar_sel(c, false),
13152 c.to_string(),
13153 "c:467 — printable ASCII '{}' passes through",
13154 c
13155 );
13156 }
13157 }
13158
13159 /// `Src/utils.c:487-492` — `\n` becomes `\n` (backslash-n) and
13160 /// `\t` becomes `\t`. Both two-char outputs. Pin the canonical
13161 /// escapes.
13162 #[test]
13163 fn nicechar_sel_escapes_newline_and_tab() {
13164 let _g = crate::test_util::global_state_lock();
13165 assert_eq!(
13166 nicechar_sel('\n', false),
13167 "\\n",
13168 "c:487-489 — newline escape"
13169 );
13170 assert_eq!(nicechar_sel('\t', false), "\\t", "c:490-492 — tab escape");
13171 }
13172
13173 /// `Src/utils.c:493-501` — control chars (<0x20 except \n/\t)
13174 /// get `^X` prefix in non-quotable mode and `\C-X` in quotable.
13175 /// c:500 — `c += 0x40` adds 0x40 to map 0x01 → 0x41 ('A').
13176 #[test]
13177 fn nicechar_sel_control_chars_use_caret_or_c_prefix() {
13178 let _g = crate::test_util::global_state_lock();
13179 // Non-quotable: ^A
13180 assert_eq!(
13181 nicechar_sel('\x01', false),
13182 "^A",
13183 "c:499-500 — \\x01 → ^A non-quotable"
13184 );
13185 // Quotable: \C-A
13186 assert_eq!(
13187 nicechar_sel('\x01', true),
13188 "\\C-A",
13189 "c:495-500 — \\x01 → \\C-A quotable"
13190 );
13191 // ^G (BEL = 0x07 → 0x47 = 'G')
13192 assert_eq!(nicechar_sel('\x07', false), "^G");
13193 // ^[ (ESC = 0x1b → 0x5b = '[')
13194 assert_eq!(nicechar_sel('\x1b', false), "^[");
13195 }
13196
13197 /// `Src/utils.c:479-486` — DEL (0x7f) renders as `^?` or `\C-?`.
13198 #[test]
13199 fn nicechar_sel_del_renders_as_caret_question() {
13200 let _g = crate::test_util::global_state_lock();
13201 assert_eq!(nicechar_sel('\x7f', false), "^?", "c:485-486 — DEL is `^?`");
13202 assert_eq!(
13203 nicechar_sel('\x7f', true),
13204 "\\C-?",
13205 "c:481-486 — DEL is `\\C-?` quotable"
13206 );
13207 }
13208
13209 /// `Src/utils.c:469-478` — high-bit bytes (>= 0x80) under default
13210 /// PRINTEIGHTBIT-off path: write `\M-` then mask to low ASCII.
13211 /// If the masked char is printable, output is `\M-<char>`. Pin
13212 /// the canonical 0xc1 → \M-A and 0xe1 → \M-a cases.
13213 #[test]
13214 fn nicechar_sel_highbit_uses_meta_prefix_when_printeightbit_off() {
13215 let _g = crate::test_util::global_state_lock();
13216 if isset(PRINTEIGHTBIT) {
13217 return; // Test only valid when PRINTEIGHTBIT off (default).
13218 }
13219 // 0xc1 = 'A' + 0x80 → \M-A
13220 let c = char::from_u32(0xc1).unwrap();
13221 assert_eq!(
13222 nicechar_sel(c, false),
13223 "\\M-A",
13224 "c:472-477 — high-bit 0xc1 → \\M-A under PRINTEIGHTBIT-off"
13225 );
13226 // 0xe1 = 'a' + 0x80 → \M-a
13227 let c = char::from_u32(0xe1).unwrap();
13228 assert_eq!(nicechar_sel(c, false), "\\M-a");
13229 }
13230
13231 /// `Src/utils.c:593-705` — `wcs_nicechar_sel`. Wide-char variant
13232 /// of `nicechar_sel`: control chars escape, printable wides
13233 /// emit raw UTF-8, large codepoints get `\u`/`\U` hex escape.
13234 /// Pin every branch. Previously delegated to `nicechar_sel`
13235 /// which byte-masked via `c & 0xff` — every UTF-8 codepoint
13236 /// emerged mangled.
13237 #[test]
13238 fn wcs_nicechar_sel_printable_wide_emits_utf8() {
13239 let _g = crate::test_util::global_state_lock();
13240 // c:644-678 — printable wide char emits raw UTF-8.
13241 assert_eq!(
13242 wcs_nicechar_sel('a', None, None, false),
13243 "a",
13244 "ASCII printable"
13245 );
13246 assert_eq!(
13247 wcs_nicechar_sel('é', None, None, false),
13248 "é",
13249 "Latin-1 printable"
13250 );
13251 assert_eq!(
13252 wcs_nicechar_sel('字', None, None, false),
13253 "字",
13254 "CJK printable"
13255 );
13256 }
13257
13258 /// c:625-630 — `\n` and `\t` escape (same as nicechar_sel for ASCII).
13259 #[test]
13260 fn wcs_nicechar_sel_escapes_newline_and_tab() {
13261 let _g = crate::test_util::global_state_lock();
13262 assert_eq!(wcs_nicechar_sel('\n', None, None, false), "\\n");
13263 assert_eq!(wcs_nicechar_sel('\t', None, None, false), "\\t");
13264 }
13265
13266 /// c:617-623 — DEL (0x7f) renders as `^?` (non-quotable) or
13267 /// `\C-?` (quotable). Same as the byte version.
13268 #[test]
13269 fn wcs_nicechar_sel_del_uses_caret_question() {
13270 let _g = crate::test_util::global_state_lock();
13271 assert_eq!(wcs_nicechar_sel('\x7f', None, None, false), "^?");
13272 assert_eq!(wcs_nicechar_sel('\x7f', None, None, true), "\\C-?");
13273 }
13274
13275 /// c:631-638 — control chars (0x01-0x1f, except \n/\t) emit
13276 /// `^X` or `\C-X` with the +0x40 offset.
13277 #[test]
13278 fn wcs_nicechar_sel_control_chars_use_caret_prefix() {
13279 let _g = crate::test_util::global_state_lock();
13280 assert_eq!(wcs_nicechar_sel('\x01', None, None, false), "^A");
13281 assert_eq!(wcs_nicechar_sel('\x07', None, None, false), "^G");
13282 assert_eq!(wcs_nicechar_sel('\x1b', None, None, false), "^[");
13283 assert_eq!(wcs_nicechar_sel('\x01', None, None, true), "\\C-A");
13284 }
13285
13286 /// `Src/utils.c:656-663` — large non-printable codepoints get
13287 /// hex escape: `\U%.8x` (>= 0x10000), `\u%.4x` (>= 0x100).
13288 /// Both lowercase per C's `%x`. Use a non-printable wide char
13289 /// to test (most BMP chars are printable; pick a control area
13290 /// like U+0085 NEXT LINE).
13291 #[test]
13292 fn wcs_nicechar_sel_large_nonprintable_uses_hex_escape() {
13293 let _g = crate::test_util::global_state_lock();
13294 // U+0085 NEL — control in C1 range, not iswprint.
13295 let nel = char::from_u32(0x85).unwrap();
13296 // Since 0x85 < 0x100, falls into nicechar_sel fallback.
13297 // After u9_iswprint says false, cv < 0x80 is false (cv=0x85),
13298 // PRINTEIGHTBIT off → enters !is_printable && cv < 0x80 branch?
13299 // Actually 0x85 >= 0x80 → !print_eightbit true → enters non-print branch.
13300 // 0x85 not 0x7f, not \n/\t, not <0x20. Falls past all if-elses to
13301 // post-branch logic → !is_printable, cv >= 0x100 false, cv >= 0x100 false
13302 // → falls to nicechar_sel fallback.
13303 let r = wcs_nicechar_sel(nel, None, None, false);
13304 assert!(!r.is_empty(), "must emit something for U+0085");
13305 // U+200B ZERO WIDTH SPACE — non-printable, 0x100-0xffff range.
13306 let zwsp = char::from_u32(0x200B).unwrap();
13307 let r = wcs_nicechar_sel(zwsp, None, None, false);
13308 // u9_iswprint(0x200B) returns true via unicode-width 0-width.
13309 // Per the impl: u9_iswprint true → emit raw. So r should be
13310 // the raw zwsp char.
13311 assert!(!r.is_empty());
13312 }
13313
13314 /// `Src/utils.c:709-714` — `wcs_nicechar(c)` delegates to
13315 /// `wcs_nicechar_sel(c, widthp, swidep, 0)`. Pin the parity.
13316 #[test]
13317 fn wcs_nicechar_matches_wcs_nicechar_sel_with_zero() {
13318 let _g = crate::test_util::global_state_lock();
13319 for c in ['a', '\n', '\t', '\x7f', '\x01', 'é', '字'] {
13320 assert_eq!(
13321 wcs_nicechar(c, None, None),
13322 wcs_nicechar_sel(c, None, None, false),
13323 "c:711 — `wcs_nicechar(c, NULL, NULL)` must equal `wcs_nicechar_sel(c, NULL, NULL, 0)` for {:?}",
13324 c
13325 );
13326 }
13327 }
13328
13329 /// `Src/utils.c:1989-2012` — `movefd(fd)`. Three contracts:
13330 /// 1. fd == -1 → returned as-is (no syscalls).
13331 /// 2. fd >= 10 → returned unchanged (already high).
13332 /// 3. fd < 10 → dupped with F_DUPFD >= 10, original closed
13333 /// unconditionally (even on dup failure).
13334 #[test]
13335 #[cfg(unix)]
13336 fn movefd_returns_minus_one_unchanged() {
13337 let _g = crate::test_util::global_state_lock();
13338 // c:1992 — `if (fd != -1 && fd < 10)` skipped for fd=-1.
13339 assert_eq!(movefd(-1), -1, "fd=-1 must be returned unchanged");
13340 }
13341
13342 /// c:1992 — `fd >= 10` skips the dup-and-close path.
13343 #[test]
13344 #[cfg(unix)]
13345 fn movefd_returns_high_fd_unchanged() {
13346 let _g = crate::test_util::global_state_lock();
13347 // Use a known-open fd. /dev/null open with fd guaranteed to be
13348 // <10 because new fds get the lowest free slot — but we can
13349 // dup it to 10 directly to test the early-return path.
13350 let f = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13351 if f < 0 {
13352 return;
13353 } // Skip on unusual systems.
13354 let high = unsafe { libc::fcntl(f, libc::F_DUPFD, 20) };
13355 unsafe {
13356 libc::close(f);
13357 }
13358 if high < 0 {
13359 return;
13360 }
13361 // movefd(20) must return 20 unchanged (no dup, no close).
13362 let r = movefd(high);
13363 assert_eq!(r, high, "c:1992 — fd >= 10 returned unchanged");
13364 unsafe {
13365 libc::close(high);
13366 }
13367 }
13368
13369 /// `Src/utils.c:1968-1983` — `check_fd_table(fd)`. Grows the
13370 /// fdtable Vec to `fd+1` slots (filling new entries with
13371 /// FDT_UNUSED) and bumps `MAX_ZSH_FD` to `fd`. Pin: a small
13372 /// fd grows the table to `fd+1` length; MAX_ZSH_FD reflects it.
13373 /// fd ≤ cur_max early-returns without growing (c:1971-1972).
13374 #[test]
13375 fn check_fd_table_grows_to_fd_plus_one() {
13376 let _g = crate::test_util::global_state_lock();
13377 // Reset globals so the test is deterministic regardless
13378 // of prior ordering — other tests may have grown the
13379 // table or bumped MAX_ZSH_FD.
13380 MAX_ZSH_FD.store(-1, Ordering::Relaxed);
13381 {
13382 let mut g = fdtable_lock().lock().unwrap();
13383 g.clear();
13384 }
13385
13386 // c:1971-1972 — fd ≤ cur_max is the early-return path.
13387 // With cur_max=-1, fd=-1 hits `fd <= cur_max` → true.
13388 assert!(check_fd_table(-1));
13389 assert_eq!(
13390 MAX_ZSH_FD.load(Ordering::Relaxed),
13391 -1,
13392 "fd ≤ cur_max early-returns; MAX_ZSH_FD untouched"
13393 );
13394
13395 // c:1974-1981 — fd > cur_max grows fdtable to fd+1 slots.
13396 assert!(check_fd_table(7));
13397 assert_eq!(
13398 MAX_ZSH_FD.load(Ordering::Relaxed),
13399 7,
13400 "c:1982 — MAX_ZSH_FD := fd"
13401 );
13402 let len = {
13403 let g = fdtable_lock().lock().unwrap();
13404 g.len()
13405 };
13406 assert!(
13407 len >= 8,
13408 "c:1975-1979 — fdtable grew to ≥ fd+1 = 8 slots, got {}",
13409 len
13410 );
13411
13412 // Idempotence under fd ≤ cur_max.
13413 assert!(check_fd_table(3));
13414 assert_eq!(
13415 MAX_ZSH_FD.load(Ordering::Relaxed),
13416 7,
13417 "fd ≤ cur_max keeps MAX_ZSH_FD at 7"
13418 );
13419 }
13420
13421 /// `Src/utils.c:4364,4367` — `wcsitype(c, IWORD/ISEP)` reads
13422 /// from the `wordchars`/`ifs` GLOBALS (writable by
13423 /// `wordcharssetfn`/`ifssetfn`). Previously read from
13424 /// `std::env::var` — the libc process env — which never
13425 /// reflects runtime `WORDCHARS=…` shell-side assignments.
13426 /// Pin: set wordchars via `wordcharssetfn`, verify `wcsitype`
13427 /// returns true for chars in the new set.
13428 #[test]
13429 fn wcsitype_iword_reads_from_canonical_wordchars_global() {
13430 let _g = crate::test_util::global_state_lock();
13431 // Test only meaningful in MULTIBYTE mode for non-ASCII chars;
13432 // ASCII chars (< 0x80) route through TYPTAB directly.
13433 // Skip if MULTIBYTE off.
13434 if !isset(MULTIBYTE) {
13435 return;
13436 }
13437 // Save and set WORDCHARS to a single non-ASCII char.
13438 // C dispatches `pm->gsu.s->{get,set}fn(pm, val)`; mirror via
13439 // paramtab lookup.
13440 let saved = crate::ported::params::paramtab()
13441 .read()
13442 .ok()
13443 .and_then(|t| {
13444 t.get("WORDCHARS")
13445 .map(|pm| crate::ported::params::wordcharsgetfn(pm))
13446 })
13447 .unwrap_or_default();
13448 // wordcharssetfn ignores its `_pm` arg and re-enters paramtab's
13449 // read lock via inittyptab (sibling of the IFS deadlock above).
13450 // Use a stack-local dummy pm instead of going through paramtab's
13451 // write lock so the test never holds two paramtab locks at once.
13452 let mut dummy = crate::ported::zsh_h::param::default();
13453 let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
13454 wordcharssetfn(dummy, val);
13455 };
13456 do_set(&mut dummy, "é".to_string());
13457 // 'é' is alphanumeric per Unicode → IWORD returns true via
13458 // is_alphanumeric short-circuit at c:4353. So we can't pin
13459 // the WORDCHARS-specific path with 'é'. Use a non-alnum char.
13460 do_set(&mut dummy, ":".to_string());
13461 // ':' is ASCII so wcsitype routes through TYPTAB (which now
13462 // has IWORD on ':' because wordcharssetfn called inittyptab).
13463 assert!(
13464 wcsitype(':', IWORD as u32),
13465 "c:4364 — wordchars membership through canonical global"
13466 );
13467 // Restore.
13468 do_set(&mut dummy, saved);
13469 }
13470
13471 /// `Src/utils.c:2090-2097` — `addmodulefd(fd, fdt)`. Stores the
13472 /// provided fdt in the fdtable slot for the given fd. Previously
13473 /// Rust port hardcoded FDT_MODULE and added CLOEXEC unconditionally.
13474 /// Pin: pass FDT_EXTERNAL, verify slot stores FDT_EXTERNAL (not
13475 /// FDT_MODULE).
13476 #[cfg(unix)]
13477 #[test]
13478 fn addmodulefd_respects_fdt_parameter() {
13479 let _g = crate::test_util::global_state_lock();
13480 // Open a real fd to use.
13481 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13482 if fd < 0 {
13483 return;
13484 }
13485 addmodulefd(fd, FDT_EXTERNAL);
13486 assert_eq!(
13487 fdtable_get(fd),
13488 FDT_EXTERNAL,
13489 "c:2095 — fdt parameter must be stored verbatim"
13490 );
13491 // Switch to FDT_MODULE.
13492 addmodulefd(fd, FDT_MODULE);
13493 assert_eq!(
13494 fdtable_get(fd),
13495 FDT_MODULE,
13496 "c:2095 — second call overwrites with new fdt"
13497 );
13498 // Cleanup.
13499 unsafe {
13500 libc::close(fd);
13501 }
13502 }
13503
13504 /// `Src/utils.c:2093` — `if (fd >= 0)`. Negative fd is silently
13505 /// ignored (no fdtable update). Pin the guard.
13506 #[test]
13507 fn addmodulefd_ignores_negative_fd() {
13508 let _g = crate::test_util::global_state_lock();
13509 // Should not panic, no side effect.
13510 addmodulefd(-1, FDT_MODULE);
13511 // Nothing to assert on side-effect-free path; just verify no panic.
13512 }
13513
13514 /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns -1 for
13515 /// non-lock fds, 0 for FDT_FLOCK/FDT_FLOCK_EXEC. Previously the
13516 /// Rust port always returned 0 — broke `zsystem flock -u <fd>`
13517 /// distinguishing "fd never flocked" from "successfully released".
13518 #[cfg(unix)]
13519 #[test]
13520 fn zcloselockfd_returns_minus_one_for_non_lock_fd() {
13521 let _g = crate::test_util::global_state_lock();
13522 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13523 if fd < 0 {
13524 return;
13525 }
13526 // No addlockfd call → fd is NOT in the flock table.
13527 let r = zcloselockfd(fd);
13528 assert_eq!(r, -1, "c:2160-2161 — non-lock fd must return -1, NOT 0");
13529 unsafe {
13530 libc::close(fd);
13531 }
13532 }
13533
13534 /// `Src/utils.c:2155-2164` — `zcloselockfd(fd)` returns 0 for an
13535 /// fd that was registered via `addlockfd`. Pin the end-to-end
13536 /// round-trip: addlockfd → zcloselockfd returns 0.
13537 #[cfg(unix)]
13538 #[test]
13539 fn zcloselockfd_returns_zero_for_flock_fd_then_closes() {
13540 let _g = crate::test_util::global_state_lock();
13541 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13542 if fd < 0 {
13543 return;
13544 }
13545 addlockfd(fd, true);
13546 let r = zcloselockfd(fd);
13547 assert_eq!(r, 0, "c:2162-2163 — FDT_FLOCK fd → zclose + return 0");
13548 // After zcloselockfd, the underlying close() has fired.
13549 // close() returning -1 with EBADF would confirm that, but
13550 // that's a libc side-effect; just verify the return value.
13551 }
13552
13553 /// `Src/utils.c:1982` — `check_fd_table` sets `max_zsh_fd = fd`
13554 /// when fd is new. `fdtable_set` inlines this to keep the guard
13555 /// in `zcloselockfd` (`if (fd > max_zsh_fd) return -1`) working
13556 /// against fds populated by `addmodulefd`/`addlockfd`. Pin: after
13557 /// `fdtable_set(fd, FDT_FLOCK)`, MAX_ZSH_FD >= fd.
13558 #[cfg(unix)]
13559 #[test]
13560 fn fdtable_set_bumps_max_zsh_fd() {
13561 let _g = crate::test_util::global_state_lock();
13562 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13563 if fd < 0 {
13564 return;
13565 }
13566 fdtable_set(fd, FDT_FLOCK);
13567 let max_fd = MAX_ZSH_FD.load(Ordering::Relaxed);
13568 assert!(
13569 max_fd >= fd,
13570 "c:1982 — max_zsh_fd ({}) must be >= newly-set fd ({})",
13571 max_fd,
13572 fd
13573 );
13574 // Cleanup.
13575 fdtable_set(fd, FDT_UNUSED);
13576 unsafe {
13577 libc::close(fd);
13578 }
13579 }
13580
13581 /// `Src/utils.c:2111-2121` — `addlockfd(fd, cloexec)`. Selects
13582 /// between FDT_FLOCK (when cloexec=true, lock dies on exec) and
13583 /// FDT_FLOCK_EXEC (when cloexec=false, lock survives exec).
13584 /// Previously the Rust port did `fcntl(F_SETFD, CLOEXEC)` —
13585 /// totally wrong semantics. Pin the FDT slot.
13586 #[cfg(unix)]
13587 #[test]
13588 fn addlockfd_selects_flock_category_per_cloexec_flag() {
13589 let _g = crate::test_util::global_state_lock();
13590 let fd = unsafe { libc::open(b"/dev/null\0".as_ptr() as *const _, libc::O_RDONLY) };
13591 if fd < 0 {
13592 return;
13593 }
13594 // cloexec=true → FDT_FLOCK (lock dies on exec).
13595 addlockfd(fd, true);
13596 assert_eq!(
13597 fdtable_get(fd),
13598 FDT_FLOCK,
13599 "c:2117 — cloexec=true → FDT_FLOCK"
13600 );
13601 // cloexec=false → FDT_FLOCK_EXEC (lock survives exec).
13602 addlockfd(fd, false);
13603 assert_eq!(
13604 fdtable_get(fd),
13605 FDT_FLOCK_EXEC,
13606 "c:2119 — cloexec=false → FDT_FLOCK_EXEC"
13607 );
13608 // Cleanup.
13609 unsafe {
13610 libc::close(fd);
13611 }
13612 }
13613
13614 /// `Src/utils.c:1211` — `adduserdir` rejects paths `>= PATH_MAX`
13615 /// by removing the existing entry (treating as "can't use this
13616 /// value as a directory"). Pin with a path constructed to exceed
13617 /// PATH_MAX, verify no insertion happens.
13618 #[cfg(unix)]
13619 #[test]
13620 fn adduserdir_rejects_paths_at_or_above_path_max() {
13621 let _g = crate::test_util::global_state_lock();
13622 if !interact() {
13623 // c:1193 — non-interactive shells skip the table entirely.
13624 // Test inactive in non-interactive contexts.
13625 return;
13626 }
13627 // Ensure the entry doesn't exist beforehand.
13628 let name = "ZSHRS_TEST_PATHMAX_DIR";
13629 let _ = removenameddirnode(name);
13630 // Construct an oversized path.
13631 let mut over: String = "/".to_string();
13632 over.push_str(&"a".repeat(libc::PATH_MAX as usize));
13633 // adduserdir with the oversized path + AUTONAMEDIRS-off path
13634 // via always=true (so the AUTONAMEDIRS guard doesn't reject).
13635 adduserdir(name, &over, 0, true);
13636 // c:1211 — too-long → remove path triggered, no entry added.
13637 let tab = nameddirtab().lock().unwrap();
13638 assert!(
13639 !tab.contains_key(name),
13640 "c:1211 — strlen(t) >= PATH_MAX must NOT insert entry"
13641 );
13642 }
13643
13644 /// `Src/utils.c:760-786` — `pathprog`. Finds any regular file
13645 /// (not directory, no executable-bit check) in `$PATH`.
13646 /// Previously required `mode & 0o111 != 0` — silently missed
13647 /// non-executable autoload-function plaintext scripts. Pin
13648 /// using a known-existing file in /tmp.
13649 #[test]
13650 #[cfg(unix)]
13651 fn pathprog_finds_non_executable_files() {
13652 let _g = crate::test_util::global_state_lock();
13653 // Create a non-executable file in /tmp and add /tmp to PATH.
13654 let test_name = format!("zshrs_test_pathprog_{}", unsafe { libc::getpid() });
13655 let path = PathBuf::from("/tmp").join(&test_name);
13656 // Write content, NO executable bit.
13657 if fs::write(&path, b"plain content").is_err() {
13658 return; // /tmp may be unwritable on some systems.
13659 }
13660 // Verify it's not executable.
13661 let meta = fs::metadata(&path).unwrap();
13662 assert_eq!(
13663 meta.permissions().mode() & 0o111,
13664 0,
13665 "test setup: must be non-executable"
13666 );
13667 // Set PATH to /tmp.
13668 let saved_path = getsparam("PATH");
13669 assignsparam("PATH", "/tmp", 0);
13670 // pathprog should find the file.
13671 let r = pathprog(&test_name);
13672 // Cleanup.
13673 let _ = fs::remove_file(&path);
13674 if let Some(prev) = saved_path {
13675 assignsparam("PATH", &prev, 0);
13676 }
13677 assert_eq!(
13678 r,
13679 Some(path),
13680 "c:776 — pathprog finds non-executable files (only F_OK + !S_ISDIR)"
13681 );
13682 }
13683
13684 /// `Src/utils.c:776-778` — pathprog skips directories. Pin: a
13685 /// directory in $PATH with the queried name must NOT be returned.
13686 #[test]
13687 #[cfg(unix)]
13688 fn pathprog_skips_directories() {
13689 let _g = crate::test_util::global_state_lock();
13690 let test_name = format!("zshrs_test_pathprog_dir_{}", unsafe { libc::getpid() });
13691 let path = PathBuf::from("/tmp").join(&test_name);
13692 if fs::create_dir(&path).is_err() {
13693 return;
13694 }
13695 let saved_path = getsparam("PATH");
13696 assignsparam("PATH", "/tmp", 0);
13697 let r = pathprog(&test_name);
13698 // Cleanup.
13699 let _ = fs::remove_dir(&path);
13700 if let Some(prev) = saved_path {
13701 assignsparam("PATH", &prev, 0);
13702 }
13703 assert!(
13704 r.is_none(),
13705 "c:778 — pathprog must skip directories (!S_ISDIR)"
13706 );
13707 }
13708
13709 /// `Src/utils.c:4367` — `wcsitype(c, ISEP)` reads from canonical
13710 /// `ifs` global. Pin via `ifssetfn`. ASCII path routes through
13711 /// TYPTAB which gets ISEP bit from inittyptab's IFS walk
13712 /// (added earlier this session). End-to-end pin.
13713 #[test]
13714 fn wcsitype_isep_reads_from_canonical_ifs_global() {
13715 let _g = crate::test_util::global_state_lock();
13716 if !isset(MULTIBYTE) {
13717 return;
13718 }
13719 // C: `pm->gsu.s->{get,set}fn(pm, val)`. Mirror via paramtab.
13720 let saved = crate::ported::params::paramtab()
13721 .read()
13722 .ok()
13723 .and_then(|t| t.get("IFS").map(|pm| crate::ported::params::ifsgetfn(pm)))
13724 .unwrap_or_default();
13725 // ifssetfn ignores its `_pm` arg (the C `gsu.s->setfn` receives
13726 // the param but the IFS callback only touches the global `ifs`
13727 // store + calls inittyptab). Calling through paramtab's write
13728 // lock would deadlock because inittyptab() re-takes paramtab's
13729 // read lock for the IFS walk (utils.rs:5028) — pthread rwlock
13730 // forbids same-thread write→read upgrade. Pass a stack-local
13731 // dummy pm instead so we never hold the paramtab lock during
13732 // ifssetfn.
13733 let mut dummy = crate::ported::zsh_h::param::default();
13734 let do_set = |dummy: &mut crate::ported::zsh_h::param, val: String| {
13735 ifssetfn(dummy, val);
13736 };
13737 do_set(&mut dummy, ":".to_string());
13738 assert!(
13739 wcsitype(':', ISEP as u32),
13740 "c:4367 — IFS membership through canonical global"
13741 );
13742 // Restore.
13743 do_set(&mut dummy, saved);
13744 }
13745
13746 /// c:536 — high-bit byte (>= 0x80) is nice when PRINTEIGHTBIT is
13747 /// OFF (the default). Pin the default case; the alternate
13748 /// behavior under PRINTEIGHTBIT-on is harder to exercise from
13749 /// a unit test due to opt-state global mutation.
13750 #[test]
13751 fn is_nicechar_highbit_is_nice_when_printeightbit_off() {
13752 let _g = crate::test_util::global_state_lock();
13753 // Default state: PRINTEIGHTBIT is off → high-bit bytes are nice.
13754 // Use char with high-bit equivalent low byte.
13755 // 0xb5 (Meta+5 territory) — masked to 0xff → still 0xb5.
13756 let c = char::from_u32(0xb5).unwrap();
13757 // ZISPRINT(0xb5) is false (>0x7e). 0xb5 & 0x80 set → check PRINTEIGHTBIT.
13758 // Default PRINTEIGHTBIT off → returns true (nice).
13759 if !isset(PRINTEIGHTBIT) {
13760 assert!(
13761 is_nicechar(c),
13762 "c:536 — high-bit byte 0x{:x} must be nice when PRINTEIGHTBIT off",
13763 0xb5_u32
13764 );
13765 }
13766 }
13767
13768 /// `Src/utils.c:1969-1983` — `check_fd_table(fd)` grows the
13769 /// fdtable so it can index `fd` and bumps `max_zsh_fd`. The
13770 /// previous Rust port was a no-op shim that always returned true;
13771 /// real behavior is "fdtable.len() must be > fd" and
13772 /// "MAX_ZSH_FD >= fd" after the call.
13773 #[test]
13774 fn check_fd_table_grows_fdtable_and_bumps_max_zsh_fd() {
13775 let _g = crate::test_util::global_state_lock();
13776 // Snapshot prior state so we don't poison other tests.
13777 let saved_max = MAX_ZSH_FD.load(Ordering::Relaxed);
13778 // Pick a target fd well above the typical 0/1/2.
13779 let target = saved_max.max(50) + 7;
13780 check_fd_table(target);
13781 let new_max = MAX_ZSH_FD.load(Ordering::Relaxed);
13782 assert!(
13783 new_max >= target,
13784 "c:1982 — max_zsh_fd must be >= target after grow (got {})",
13785 new_max
13786 );
13787 // Calling again with a lower fd MUST NOT shrink max_zsh_fd
13788 // (c:1971-1972 early return).
13789 check_fd_table(target - 3);
13790 assert_eq!(
13791 MAX_ZSH_FD.load(Ordering::Relaxed),
13792 new_max,
13793 "c:1971 — fd <= max_zsh_fd path must not change max"
13794 );
13795 // Restore approximately — best we can do without exposing
13796 // fdtable internals; subsequent fdtable_set callers cope.
13797 MAX_ZSH_FD.store(saved_max, Ordering::Relaxed);
13798 }
13799
13800 /// `Src/utils.c:1971` — `if (fd <= max_zsh_fd) return;` early
13801 /// exit. Calling with a small fd (<= current max) must be a
13802 /// no-op.
13803 #[test]
13804 fn check_fd_table_small_fd_is_noop() {
13805 let _g = crate::test_util::global_state_lock();
13806 // Ensure max_zsh_fd is non-trivial.
13807 let _ = check_fd_table(100);
13808 let max_before = MAX_ZSH_FD.load(Ordering::Relaxed);
13809 check_fd_table(5);
13810 assert_eq!(
13811 MAX_ZSH_FD.load(Ordering::Relaxed),
13812 max_before,
13813 "c:1971 — small fd path must not touch max_zsh_fd"
13814 );
13815 }
13816
13817 /// `Src/utils.c:1969-1983` — defensive: negative fd shouldn't
13818 /// panic. C-side would have indexed past the start of `fdtable`
13819 /// (UB). Rust port should fail-soft.
13820 #[test]
13821 fn check_fd_table_negative_fd_does_not_panic() {
13822 let _g = crate::test_util::global_state_lock();
13823 // Should not panic; behavior beyond "no panic" is unspecified.
13824 let _ = check_fd_table(-1);
13825 let _ = check_fd_table(-100);
13826 }
13827
13828 /// `Src/zsh.h:3274` — under MULTIBYTE_SUPPORT, the C source
13829 /// defines `nicezputs(str, outs)` as a macro:
13830 /// `(void)mb_niceformat((str), (outs), NULL, 0)`.
13831 /// So Rust `nicezputs(s)` must equal `mb_niceformat(s)` for
13832 /// every input. Pins the macro-equivalence after fixing the
13833 /// previous `chars().map(nicechar)` impl (which corrupted
13834 /// non-ASCII multibyte codepoints into `\M-X` mangle).
13835 #[test]
13836 fn nicezputs_matches_mb_niceformat_under_multibyte() {
13837 let _g = crate::test_util::global_state_lock();
13838 // C-equivalent pattern: write to a stream, compare to
13839 // mb_niceformat outstrp-form output. Inline rather than via a
13840 // Rust-only helper.
13841 for input in ["hello", "a\nb", "é", ""] {
13842 let mut nz_buf: Vec<u8> = Vec::new();
13843 let _ = nicezputs(input, &mut nz_buf);
13844 let nz = String::from_utf8(nz_buf).expect("utf8");
13845
13846 let mut mb_out: Option<String> = None;
13847 let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
13848 assert_eq!(
13849 nz,
13850 mb_out.unwrap_or_default(),
13851 "nicezputs and mb_niceformat must agree for {:?}",
13852 input
13853 );
13854 }
13855 // \n must produce literal `\n` (backslash + n).
13856 let mut nz_buf: Vec<u8> = Vec::new();
13857 let _ = nicezputs("a\nb", &mut nz_buf);
13858 let nz = String::from_utf8(nz_buf).expect("utf8");
13859 assert!(nz.contains("\\n"), "nicechar emits `\\n`, not raw 0x0a");
13860 }
13861
13862 /// `Src/utils.c:5530` — under MULTIBYTE_SUPPORT, `nicedup(s, heap)`
13863 /// body is `(void)mb_niceformat(s, NULL, &retstr, …); return retstr;`
13864 /// so the returned string MUST equal mb_niceformat output.
13865 #[test]
13866 fn nicedup_matches_mb_niceformat_under_multibyte() {
13867 let _g = crate::test_util::global_state_lock();
13868 for input in ["hello", "a\nb", "é"] {
13869 let nd = nicedup(input, 0);
13870 let mut mb_out: Option<String> = None;
13871 let _ = mb_niceformat(input, None, Some(&mut mb_out), 0);
13872 assert_eq!(
13873 nd,
13874 mb_out.unwrap_or_default(),
13875 "nicedup must equal mb_niceformat outstrp form for {:?}",
13876 input
13877 );
13878 }
13879 // nicedupstring delegates to nicedup(s, 1).
13880 assert_eq!(nicedupstring("hé\nllo"), nicedup("hé\nllo", 1));
13881 }
13882
13883 /// `Src/utils.c:734-744` — `zwcwidth(wc)` returns 1 when MULTIBYTE
13884 /// option is unset (c:738), regardless of the codepoint's actual
13885 /// display width. The previous Rust port skipped the option gate
13886 /// and always used the Unicode width table. Pin the option-gated
13887 /// behavior end-to-end: unset MULTIBYTE → width 1 for CJK; set →
13888 /// width 2 for CJK.
13889 #[test]
13890 fn zwcwidth_returns_1_when_multibyte_unset() {
13891 let _g = crate::test_util::global_state_lock();
13892 let saved = isset(MULTIBYTE);
13893 // c:738 — unset(MULTIBYTE) path returns 1 for everything.
13894 dosetopt(MULTIBYTE, 0, 1);
13895 assert_eq!(zwcwidth('a'), 1, "c:738 — ASCII width 1 under nomultibyte");
13896 assert_eq!(
13897 zwcwidth('字'),
13898 1,
13899 "c:738 — CJK collapses to 1 under nomultibyte (was 2 via Unicode-width)"
13900 );
13901 assert_eq!(
13902 zwcwidth('\u{200B}'),
13903 1,
13904 "c:738 — zero-width space → 1 under nomultibyte (was 0 via Unicode-width)"
13905 );
13906 // c:740-744 — MULTIBYTE on: Unicode-width table applies.
13907 dosetopt(MULTIBYTE, 1, 1);
13908 assert_eq!(zwcwidth('a'), 1, "ASCII width is 1");
13909 assert_eq!(zwcwidth('字'), 2, "c:740 — CJK width 2 under multibyte");
13910 // Restore prior state.
13911 dosetopt(MULTIBYTE, if saved { 1 } else { 0 }, 1);
13912 }
13913
13914 /// `Src/utils.c:6478-6492` — `quotedzputs(s, NULL)` under
13915 /// MULTIBYTE_SUPPORT detects "needs nice-format" inputs via
13916 /// `is_mb_niceformat(s)` and emits `$'<nice-formatted body>'`
13917 /// (the dollar-quoted form so embedded `\n`/`\t`/escape sequences
13918 /// round-trip through `typeset -p`/`set`/`set -x`). The previous
13919 /// Rust port skipped this branch entirely and single-quoted such
13920 /// strings — POSIX `'…'` is *strong* (no escapes interpreted),
13921 /// breaking round-trip for control bytes.
13922 #[test]
13923 fn quotedzputs_uses_dollar_quotes_for_control_chars() {
13924 let _g = crate::test_util::global_state_lock();
13925 // Ensure typtab is initialised — hasspecial depends on ISPECIAL
13926 // bits which are populated by inittyptab. Without this the
13927 // SPECCHARS arm short-circuits to "no specials".
13928 inittyptab();
13929 // Empty stays `''`.
13930 assert_eq!(quotedzputs(""), "''", "c:6470-6475 — empty input → ''");
13931 // Plain alphanumeric: no specials, no controls → bare.
13932 assert_eq!(
13933 quotedzputs("hello"),
13934 "hello",
13935 "c:6511-6517 — no SPECCHARS member → return unchanged"
13936 );
13937 // Control char `\n` needs `$'…'` to round-trip.
13938 let r = quotedzputs("a\nb");
13939 assert!(
13940 r.starts_with("$'") && r.ends_with('\''),
13941 "c:6488 — control char must use $'…' form (got {:?})",
13942 r
13943 );
13944 // Tab and ESC also force the niceformat arm.
13945 let r = quotedzputs("\t");
13946 assert!(
13947 r.starts_with("$'"),
13948 "c:6488 — TAB forces $'…' arm (got {:?})",
13949 r
13950 );
13951 let r = quotedzputs("\u{1b}[31m");
13952 assert!(
13953 r.starts_with("$'"),
13954 "c:6488 — ESC sequence forces $'…' arm (got {:?})",
13955 r
13956 );
13957 // Single quote forces single-quote arm via SPECCHARS membership
13958 // (Src/zsh.h:228 — SPECCHARS includes `'`). Embedded `'`
13959 // rewrites to `'\''`.
13960 let r = quotedzputs("a'b");
13961 assert!(
13962 r.contains("'\\''"),
13963 "c:6573-6587 — embedded ' → '\\'' (got {:?})",
13964 r
13965 );
13966 }
13967
13968 /// `Src/utils.c:2923-2945` — `read_loop` returns the requested
13969 /// length on full read, or the partial count on EOF. Pin: writing
13970 /// a known buffer to a pipe and reading it back returns the same
13971 /// content + correct length. Drives the no-side-effect path that
13972 /// the diagnostic-message fix doesn't touch.
13973 #[test]
13974 #[cfg(unix)]
13975 fn read_loop_round_trips_pipe_bytes() {
13976 let _g = crate::test_util::global_state_lock();
13977 // Create a pipe; write 16 bytes; read them back.
13978 let mut fds: [libc::c_int; 2] = [0; 2];
13979 unsafe {
13980 assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
13981 }
13982 let payload = b"hello-world-1234";
13983 let written = unsafe {
13984 libc::write(
13985 fds[1],
13986 payload.as_ptr() as *const libc::c_void,
13987 payload.len(),
13988 )
13989 };
13990 assert_eq!(written, payload.len() as isize, "write all 16 bytes");
13991 unsafe {
13992 libc::close(fds[1]);
13993 }
13994 let mut buf = [0u8; 16];
13995 let got = read_loop(fds[0], &mut buf).expect("read_loop ok");
13996 assert_eq!(got, payload.len(), "c:2929 — read_loop returns full length");
13997 assert_eq!(
13998 &buf[..],
13999 &payload[..],
14000 "c:2940-2941 — buffer copied verbatim"
14001 );
14002 unsafe {
14003 libc::close(fds[0]);
14004 }
14005 }
14006
14007 /// `Src/utils.c:2949-2970` — `write_loop` returns the requested
14008 /// length when the kernel accepts all bytes (the common case for
14009 /// a pipe with room). Pin the no-side-effect happy path.
14010 #[test]
14011 #[cfg(unix)]
14012 fn write_loop_writes_all_bytes_to_pipe() {
14013 let _g = crate::test_util::global_state_lock();
14014 let mut fds: [libc::c_int; 2] = [0; 2];
14015 unsafe {
14016 assert_eq!(libc::pipe(fds.as_mut_ptr()), 0, "pipe(2) ok");
14017 }
14018 let payload = b"abcdef";
14019 let got = write_loop(fds[1], payload).expect("write_loop ok");
14020 assert_eq!(
14021 got,
14022 payload.len(),
14023 "c:2955-2956 — write_loop returns full length on accept"
14024 );
14025 // Read back to verify.
14026 unsafe {
14027 libc::close(fds[1]);
14028 }
14029 let mut buf = [0u8; 6];
14030 let _ = unsafe { libc::read(fds[0], buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
14031 assert_eq!(&buf[..], &payload[..]);
14032 unsafe {
14033 libc::close(fds[0]);
14034 }
14035 }
14036
14037 /// `Src/utils.c:2935-2936` — `read_loop` on a closed/invalid fd
14038 /// returns an io::Error (the C path returns `ret` and emits the
14039 /// `zwarn` to stderr). Pin the error propagation; the zwarn
14040 /// emission is a stderr side-effect tested only by inspecting
14041 /// log output (out of scope here).
14042 #[test]
14043 #[cfg(unix)]
14044 fn read_loop_returns_error_on_invalid_fd() {
14045 let _g = crate::test_util::global_state_lock();
14046 let mut buf = [0u8; 4];
14047 // fd 9999 is essentially guaranteed-not-open in a test
14048 // process; the canonical "bad fd" error path.
14049 let r = read_loop(9999, &mut buf);
14050 assert!(
14051 r.is_err(),
14052 "c:2935 — invalid fd → io::Error (and zwarn to stderr)"
14053 );
14054 }
14055
14056 /// `Src/utils.c:2972-2988` — `read1char(echo)` reads from SHTTY,
14057 /// not stdin. With SHTTY uninitialised (default -1 in test
14058 /// environment), the function MUST return -1 immediately rather
14059 /// than blocking on a stdin read. The previous Rust port read
14060 /// from stdin and returned None — which in some test runners
14061 /// would block waiting for input.
14062 #[test]
14063 #[cfg(unix)]
14064 fn read1char_returns_minus_one_when_shtty_unset() {
14065 let _g = crate::test_util::global_state_lock();
14066 // Test environment: SHTTY is -1 (no controlling tty bound
14067 // by the port). C-side would `read(-1, ...)` which fails;
14068 // Rust port should fail-fast with -1.
14069 let saved = SHTTY.load(Ordering::Relaxed);
14070 SHTTY.store(-1, Ordering::Relaxed);
14071 let got = read1char(0); // echo=0
14072 assert_eq!(got, -1, "c:2978 — SHTTY=-1 → read fails → return -1");
14073 // Restore.
14074 SHTTY.store(saved, Ordering::Relaxed);
14075 }
14076
14077 /// `Src/utils.c:1989-2012` — `movefd(fd)` dups fd to >= 10 (so it
14078 /// stays out of the user fd range 0..=9), closes the original,
14079 /// AND marks the new fd as `FDT_INTERNAL` in fdtable. Previous
14080 /// Rust port omitted the fdtable_set call — internal-fd tracking
14081 /// (`closeallelse`, forkexec) silently never saw zshrs-internal
14082 /// fds. Pin: after movefd, fdtable[new_fd] == FDT_INTERNAL.
14083 #[test]
14084 #[cfg(unix)]
14085 fn movefd_marks_fdtable_internal() {
14086 let _g = crate::test_util::global_state_lock();
14087 // Open /dev/null to get a small (< 10) fd...
14088 let dev_null = CString::new("/dev/null").unwrap();
14089 let fd = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14090 assert!(fd >= 0, "open /dev/null returned -1");
14091 let new_fd = movefd(fd);
14092 assert!(
14093 new_fd >= 10,
14094 "c:1992-1994 — movefd dups to fd >= 10 (got {})",
14095 new_fd
14096 );
14097 // c:2009 — fdtable[new_fd] := FDT_INTERNAL.
14098 let entry = fdtable_get(new_fd);
14099 assert_eq!(
14100 entry, FDT_INTERNAL,
14101 "c:2009 — movefd must mark new_fd as FDT_INTERNAL (got {})",
14102 entry
14103 );
14104 unsafe {
14105 libc::close(new_fd);
14106 }
14107 }
14108
14109 /// `Src/utils.c:1989` — `movefd(-1)` is a defensive no-op: the
14110 /// `fd != -1 && fd < 10` gate at c:1992 fails, the c:2007
14111 /// `if (fd != -1)` post-check skips, and -1 is returned
14112 /// unchanged. Pin so a refactor of the early-out doesn't
14113 /// accidentally call fcntl(-1, ...).
14114 #[test]
14115 #[cfg(unix)]
14116 fn movefd_minus_one_returns_minus_one() {
14117 let _g = crate::test_util::global_state_lock();
14118 assert_eq!(
14119 movefd(-1),
14120 -1,
14121 "c:1992 — movefd(-1) bypasses both gates and returns -1"
14122 );
14123 }
14124
14125 /// `Src/utils.c:2021-2068` — `redup(x, y)` after a successful
14126 /// `dup2(x, y)` must:
14127 /// * Copy fdtable[x] to fdtable[y] (c:2054).
14128 /// * Promote FDT_FLOCK/FDT_FLOCK_EXEC to FDT_INTERNAL on the
14129 /// dup target (c:2055-2056) — the lock doesn't transfer.
14130 /// * Then close x (c:2064).
14131 ///
14132 /// Previous Rust port skipped the fdtable updates entirely.
14133 /// Pin the ownership-transfer with two fds the test allocates.
14134 #[test]
14135 #[cfg(unix)]
14136 fn redup_copies_fdtable_ownership_to_target() {
14137 let _g = crate::test_util::global_state_lock();
14138 // Open two distinct fds — both will land in the fdtable.
14139 let dev_null = CString::new("/dev/null").unwrap();
14140 let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14141 let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14142 assert!(x >= 0 && y >= 0, "open /dev/null returned -1");
14143 assert_ne!(x, y);
14144
14145 // Mark x as FDT_INTERNAL so we can observe the copy to y.
14146 check_fd_table(x);
14147 check_fd_table(y);
14148 fdtable_set(x, FDT_INTERNAL);
14149 fdtable_set(y, FDT_UNUSED);
14150
14151 let ret = redup(x, y);
14152 assert_eq!(ret, y, "c:2067 — successful redup returns y");
14153 // c:2054 — fdtable[y] inherited from fdtable[x].
14154 assert_eq!(
14155 fdtable_get(y),
14156 FDT_INTERNAL,
14157 "c:2054 — fdtable[y] = fdtable[x] (FDT_INTERNAL)"
14158 );
14159 // x is closed by c:2064.
14160 unsafe {
14161 libc::close(y);
14162 }
14163 }
14164
14165 /// `Src/utils.c:872-908` — `xsymlinks(path)` resolves `.`/`..`
14166 /// AND follows ONE LEVEL of symlinks via `readlink(2)`. Pin the
14167 /// symlink-following behavior with a temp-dir-managed symlink
14168 /// (the actual bug-fix pin — previous Rust port just normalised
14169 /// `.`/`..` without ever calling readlink).
14170 #[test]
14171 #[cfg(unix)]
14172 fn xsymlinks_follows_one_level_of_symlinks() {
14173 let _g = crate::test_util::global_state_lock();
14174 let tmp = std::env::temp_dir();
14175 let target = tmp.join(format!("zshrs_xsymlinks_target_{}", std::process::id()));
14176 let link = tmp.join(format!("zshrs_xsymlinks_link_{}", std::process::id()));
14177 // Create target as a regular dir to symlink to.
14178 let _ = fs::create_dir(&target);
14179 let _ = fs::remove_file(&link);
14180 std::os::unix::fs::symlink(&target, &link).unwrap();
14181
14182 let got = xsymlinks(link.to_str().unwrap()).unwrap();
14183 assert_eq!(
14184 got,
14185 target.to_string_lossy(),
14186 "c:908 — xsymlinks must follow the symlink to its target"
14187 );
14188
14189 // Cleanup.
14190 let _ = fs::remove_file(&link);
14191 let _ = fs::remove_dir(&target);
14192 }
14193
14194 /// `Src/utils.c:881-882` — `.` components are skipped.
14195 /// `Src/utils.c:883-896` — `..` components walk back one
14196 /// `/`-segment of xbuf (unless xbuf is empty or `/`).
14197 ///
14198 /// Use a temp directory we create ourselves — we can't pin
14199 /// `/tmp/...` literally because the macOS sandbox symlinks
14200 /// `/tmp -> /private/tmp`, which `xsymlinks` correctly follows
14201 /// (proving the bug-fix works). Test with a directory under
14202 /// the env tempdir + a non-existent sub so readlink fails on
14203 /// every component and the test exercises ONLY the c:881-896
14204 /// `.` / `..` paths.
14205 #[test]
14206 #[cfg(unix)]
14207 fn xsymlinks_normalises_dot_and_dotdot() {
14208 let _g = crate::test_util::global_state_lock();
14209 let tmp = fs::canonicalize(std::env::temp_dir()).unwrap();
14210 let base_dir = tmp.join(format!("zshrs_xs_norm_{}", std::process::id()));
14211 let _ = fs::create_dir(&base_dir);
14212 // `<base>/.` should be `<base>` (c:881 — `.` skipped).
14213 let arg = format!("{}/./.", base_dir.display());
14214 let got = xsymlinks(&arg).unwrap();
14215 assert_eq!(
14216 got,
14217 base_dir.to_string_lossy(),
14218 "c:881 — `.` segments collapse"
14219 );
14220 // `<base>/foo/..` should be `<base>` (c:891-895 — `..` walks back).
14221 let arg = format!("{}/foo/..", base_dir.display());
14222 let got = xsymlinks(&arg).unwrap();
14223 assert_eq!(
14224 got,
14225 base_dir.to_string_lossy(),
14226 "c:891-895 — `..` walks back one segment"
14227 );
14228 let _ = fs::remove_dir(&base_dir);
14229 }
14230
14231 /// `Src/utils.c:2055-2056` — when fdtable[x] is FDT_FLOCK or
14232 /// FDT_FLOCK_EXEC, the dup'd fd y gets promoted to FDT_INTERNAL
14233 /// (the dup doesn't carry the flock). Pin the promotion.
14234 ///
14235 /// Note on fdtable_flocks: C's c:2062-2063 decrements the
14236 /// flock count in `redup` BEFORE `zclose(x)`, and zclose ALSO
14237 /// decrements when it sees fdtable[fd] == FDT_FLOCK (c:2135).
14238 /// So C double-decrements for an FDT_FLOCK source fd in redup —
14239 /// the C comment at c:2058-2061 calls this "isn't expected to
14240 /// happen" (FDT_FLOCK fds aren't normally redup'd). We mirror
14241 /// C exactly: test asserts the count drops by 2 (1 → -1) to
14242 /// pin the faithful-to-C behavior. Reporting this as a C bug
14243 /// upstream is the right path; the port preserves it verbatim.
14244 #[test]
14245 #[cfg(unix)]
14246 fn redup_promotes_flock_to_internal_on_target() {
14247 let _g = crate::test_util::global_state_lock();
14248 let dev_null = CString::new("/dev/null").unwrap();
14249 let x = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14250 let y = unsafe { libc::open(dev_null.as_ptr(), libc::O_RDONLY) };
14251 assert!(x >= 0 && y >= 0);
14252 assert_ne!(x, y);
14253
14254 check_fd_table(x);
14255 check_fd_table(y);
14256 // Mark x as FDT_FLOCK.
14257 fdtable_set(x, FDT_FLOCK);
14258 FDTABLE_FLOCKS.store(2, Ordering::SeqCst); // start at 2 so double-decrement lands at 0
14259
14260 let _ = redup(x, y);
14261
14262 // c:2055-2056 — promoted to FDT_INTERNAL on y, NOT carried.
14263 assert_eq!(
14264 fdtable_get(y),
14265 FDT_INTERNAL,
14266 "c:2055-2056 — FDT_FLOCK on x promotes to FDT_INTERNAL on y"
14267 );
14268 // c:2062-2063 + zclose c:2135 — double decrement.
14269 assert_eq!(
14270 FDTABLE_FLOCKS.load(Ordering::SeqCst),
14271 0,
14272 "c:2062-2063 + c:2135 — flock count double-decremented (faithful to C)"
14273 );
14274 unsafe {
14275 libc::close(y);
14276 }
14277 }
14278
14279 /// `Src/utils.c:5217-5240` — `zreaddir(dir, ignoredots)` exposes
14280 /// the dot-filter as a parameter and returns one entry per call.
14281 /// Two paths:
14282 /// * `ignoredots=1` (the common case at c:590/655/1653/2884) —
14283 /// `.` and `..` are filtered out.
14284 /// * `ignoredots=0` (used at c:4648 for spelling correction) —
14285 /// `.` and `..` are RETAINED as valid candidates.
14286 #[test]
14287 #[cfg(unix)]
14288 fn zreaddir_honors_ignoredots_flag() {
14289 let _g = crate::test_util::global_state_lock();
14290 let tmp = std::env::temp_dir();
14291 let base = tmp.join(format!("zshrs_zreaddir_{}", std::process::id()));
14292 let _ = fs::create_dir(&base);
14293 // Create one real entry to make the test non-trivial.
14294 fs::write(base.join("file"), "x").unwrap();
14295
14296 // c:5232 — ignoredots=1: skip `.` and `..`, keep `file`.
14297 let mut dir = fs::read_dir(&base).unwrap();
14298 let mut with_skip: Vec<String> = Vec::new();
14299 while let Some(n) = zreaddir(&mut dir, 1) {
14300 with_skip.push(n);
14301 }
14302 assert!(
14303 with_skip.contains(&"file".to_string()),
14304 "c:5232 — real entry survives ignoredots=1"
14305 );
14306 assert!(
14307 !with_skip.contains(&".".to_string()),
14308 "c:5232 — `.` filtered with ignoredots=1"
14309 );
14310 assert!(
14311 !with_skip.contains(&"..".to_string()),
14312 "c:5232 — `..` filtered with ignoredots=1"
14313 );
14314
14315 // c:4648-equivalent — ignoredots=0: KEEP `.` and `..`.
14316 // (libstd's fs::read_dir filters them before exposing on
14317 // macOS/Linux, so the without_skip set is functionally
14318 // identical here — pin only that the API path doesn't error.)
14319 let mut dir2 = fs::read_dir(&base).unwrap();
14320 let mut without_skip: Vec<String> = Vec::new();
14321 while let Some(n) = zreaddir(&mut dir2, 0) {
14322 without_skip.push(n);
14323 }
14324 assert!(
14325 without_skip.contains(&"file".to_string()),
14326 "ignoredots=0 still yields real entries"
14327 );
14328
14329 let _ = fs::remove_file(base.join("file"));
14330 let _ = fs::remove_dir(&base);
14331 }
14332
14333 /// `Src/utils.c:1075` — `get_username()` uses `ztrdup_metafy` on
14334 /// `pswd->pw_name`. Previous Rust port returned the raw pw_name
14335 /// verbatim — fine for ASCII usernames, broken for high-bit
14336 /// bytes (downstream paramtab consumers assume metafied entries).
14337 /// Pin: ASCII usernames round-trip identically through metafy,
14338 /// AND the result is a non-empty string for the current uid.
14339 /// `Src/utils.c:3445-3460` — ztrftime zsh-specific %K/%L/%f
14340 /// extensions return values WITHOUT leading zeros (vs the
14341 /// strftime %H/%I/%d which pad).
14342 #[test]
14343 #[cfg(unix)]
14344 fn ztrftime_zsh_extensions_no_leading_zero() {
14345 let _g = crate::test_util::global_state_lock();
14346 // Pick a known time: Jan 5 2024 09:07:42.123456789 UTC.
14347 // Use SystemTime + an offset since we want deterministic values
14348 // independent of TZ; we just verify that the format substitutes
14349 // the right NUMBER OF DIGITS for the leading-zero case.
14350 use std::time::Duration;
14351 // 2024-01-05 09:07:42 UTC = 1704445662
14352 let t = UNIX_EPOCH + Duration::new(1704445662, 123_456_789);
14353 // %H gives leading zero "09"; %K should give "9" (in some TZ
14354 // hour will be different but the digit-count rule still holds).
14355 let h_padded = ztrftime("%H", t, false);
14356 let k_unpadded = ztrftime("%K", t, false);
14357 // Both represent the same hour. If h_padded starts with `0`,
14358 // k_unpadded must be 1-char and not start with `0`.
14359 if h_padded.starts_with('0') && h_padded.len() == 2 {
14360 assert!(
14361 !k_unpadded.starts_with('0'),
14362 "c:3445 — %K must strip the leading 0 from %H={}, got %K={}",
14363 h_padded,
14364 k_unpadded
14365 );
14366 assert_eq!(
14367 k_unpadded.len(),
14368 1,
14369 "c:3445 — %K should be 1 digit when hour < 10"
14370 );
14371 }
14372 // %f for day-of-month: t is Jan 5 → in any reasonable TZ
14373 // day is between 4 and 6; format should be 1 digit when day < 10.
14374 let d_padded = ztrftime("%d", t, false);
14375 let f_unpadded = ztrftime("%f", t, false);
14376 if d_padded.starts_with('0') && d_padded.len() == 2 {
14377 assert!(!f_unpadded.starts_with('0'));
14378 assert_eq!(
14379 f_unpadded.len(),
14380 1,
14381 "c:3457 — %f should be 1 digit when day < 10"
14382 );
14383 }
14384 // %3. fractional seconds: input nsec is 123456789 → first 3
14385 // digits should be 123. Note zsh's syntax is `%N.` where N is
14386 // the digit count (c:3374-3384 + c:3409).
14387 let frac = ztrftime("%3.", t, false);
14388 assert_eq!(
14389 frac, "123",
14390 "c:3409-3438 — %3. must emit first 3 digits of nsec"
14391 );
14392 // %. with no digit prefix defaults to 3 digits per c:3409.
14393 let frac_default = ztrftime("%.", t, false);
14394 assert_eq!(frac_default, "123", "c:3409 — %. defaults to 3 digits");
14395 }
14396
14397 #[test]
14398 #[cfg(unix)]
14399 fn get_username_returns_metafied_non_empty_string() {
14400 let _g = crate::test_util::global_state_lock();
14401 let name = get_username();
14402 // CI environments may have a username from `whoami(1)`. In
14403 // weird sandbox-only setups it might be empty, but on every
14404 // standard build it should be set.
14405 if name.is_empty() {
14406 return;
14407 }
14408 // ASCII round-trip via metafy is identity. If the username
14409 // had high-bit bytes (the bug case), the output would be
14410 // Meta-escaped — both forms are non-empty so this pin
14411 // doesn't reject either, just ensures the fn returns
14412 // SOMETHING usable (not an empty string).
14413 assert!(
14414 !name.is_empty(),
14415 "c:1086 — getpwuid result must yield a non-empty username"
14416 );
14417 // Sanity: metafy(name) == name for ASCII input (every byte
14418 // is below Meta range). Confirms the c:1086 step preserves
14419 // ASCII paths byte-for-byte.
14420 if name.bytes().all(|b| b < 0x80) {
14421 assert_eq!(
14422 metafy(&name),
14423 name,
14424 "c:1086 — ASCII metafy is identity (so pin holds for ASCII users)"
14425 );
14426 }
14427 }
14428
14429 // ═══════════════════════════════════════════════════════════════════
14430 // Pure-utility tests — zstrtol/zstrtoul (numeric parse) and
14431 // getkeystring (key-escape decode). Each test pinned against either
14432 // C zsh semantics or a direct zsh shell invocation where applicable.
14433 // ═══════════════════════════════════════════════════════════════════
14434
14435 // ── zstrtol: numeric string → (value, unconsumed_tail) ──────────
14436 /// `zstrtol("42", 10)` → (42, "") — full consumption.
14437 #[test]
14438 fn zstrtol_decimal_full_consumption() {
14439 let (v, t) = zstrtol("42", 10);
14440 assert_eq!(v, 42);
14441 assert_eq!(t, "");
14442 }
14443
14444 /// `zstrtol("42abc", 10)` → (42, "abc") — stops at first non-digit.
14445 #[test]
14446 fn zstrtol_decimal_stops_at_non_digit() {
14447 let (v, t) = zstrtol("42abc", 10);
14448 assert_eq!(v, 42);
14449 assert_eq!(t, "abc");
14450 }
14451
14452 /// `zstrtol("-7", 10)` → (-7, "") — sign handling.
14453 #[test]
14454 fn zstrtol_negative_decimal() {
14455 let (v, t) = zstrtol("-7", 10);
14456 assert_eq!(v, -7);
14457 assert_eq!(t, "");
14458 }
14459
14460 /// `zstrtol("+12", 10)` → (12, "") — explicit `+` sign.
14461 #[test]
14462 fn zstrtol_explicit_plus_sign_consumed() {
14463 let (v, t) = zstrtol("+12", 10);
14464 assert_eq!(v, 12);
14465 assert_eq!(t, "");
14466 }
14467
14468 /// `zstrtol("ff", 16)` → (255, "") — hex base.
14469 #[test]
14470 fn zstrtol_hex_base_16() {
14471 let (v, t) = zstrtol("ff", 16);
14472 assert_eq!(v, 255);
14473 assert_eq!(t, "");
14474 }
14475
14476 /// `zstrtol("FF", 16)` → (255, "") — case-insensitive hex.
14477 #[test]
14478 fn zstrtol_hex_uppercase() {
14479 let (v, t) = zstrtol("FF", 16);
14480 assert_eq!(v, 255);
14481 }
14482
14483 /// `zstrtol("1010", 2)` → (10, "") — binary base.
14484 #[test]
14485 fn zstrtol_binary_base_2() {
14486 let (v, t) = zstrtol("1010", 2);
14487 assert_eq!(v, 10);
14488 assert_eq!(t, "");
14489 }
14490
14491 /// `zstrtol("17", 8)` → (15, "") — octal base.
14492 #[test]
14493 fn zstrtol_octal_base_8() {
14494 let (v, t) = zstrtol("17", 8);
14495 assert_eq!(v, 15);
14496 }
14497
14498 /// `zstrtol("0x1A", 0)` → base-detect picks hex from `0x` prefix → 26.
14499 #[test]
14500 fn zstrtol_base_zero_detects_hex_prefix() {
14501 let (v, _) = zstrtol("0x1A", 0);
14502 assert_eq!(v, 26, "0x1A with base=0 → hex 26");
14503 }
14504
14505 /// `zstrtol("0b101", 0)` → base-detect picks binary → 5.
14506 #[test]
14507 fn zstrtol_base_zero_detects_binary_prefix() {
14508 let (v, _) = zstrtol("0b101", 0);
14509 assert_eq!(v, 5, "0b101 with base=0 → binary 5");
14510 }
14511
14512 /// `zstrtol("017", 0)` → base-detect: leading `0` → octal → 15.
14513 #[test]
14514 fn zstrtol_base_zero_leading_zero_means_octal() {
14515 let (v, _) = zstrtol("017", 0);
14516 assert_eq!(v, 15, "017 with base=0 → octal 15");
14517 }
14518
14519 /// `zstrtol(" 42", 10)` → (42, "") — leading whitespace skipped.
14520 #[test]
14521 fn zstrtol_leading_whitespace_skipped() {
14522 let (v, _) = zstrtol(" 42", 10);
14523 assert_eq!(v, 42);
14524 }
14525
14526 /// `zstrtol("0", 10)` → (0, "") — zero is valid input.
14527 #[test]
14528 fn zstrtol_zero_input() {
14529 let (v, t) = zstrtol("0", 10);
14530 assert_eq!(v, 0);
14531 assert_eq!(t, "");
14532 }
14533
14534 // ── zstrtol_underscore: digit-separator support ─────────────────
14535 /// `zstrtol_underscore("1_000_000", 10, true)` → (1_000_000, "")
14536 /// — underscores skipped during digit accumulation.
14537 #[test]
14538 fn zstrtol_underscore_separator_in_decimal() {
14539 let (v, _) = zstrtol_underscore("1_000_000", 10, true);
14540 assert_eq!(v, 1_000_000);
14541 }
14542
14543 /// Without underscore flag, `_` stops parsing.
14544 #[test]
14545 fn zstrtol_underscore_disabled_stops_at_underscore() {
14546 let (v, t) = zstrtol_underscore("123_456", 10, false);
14547 assert_eq!(v, 123);
14548 assert_eq!(t, "_456");
14549 }
14550
14551 // ── zstrtoul_underscore: unsigned variant ───────────────────────
14552 /// Parses a plain decimal unsigned.
14553 #[test]
14554 fn zstrtoul_underscore_basic_decimal() {
14555 let v = zstrtoul_underscore("12345");
14556 assert_eq!(v, Some(12345));
14557 }
14558
14559 /// Empty string → None (no number to parse).
14560 #[test]
14561 fn zstrtoul_underscore_empty_returns_none() {
14562 let v = zstrtoul_underscore("");
14563 assert_eq!(v, None);
14564 }
14565
14566 // ── getkeystring: shell-escape decode ───────────────────────────
14567 /// `\n` → newline, `\t` → tab, `\r` → CR.
14568 /// Anchor: `print -r -- $'\n\t\r'` produces the three bytes.
14569 #[test]
14570 fn getkeystring_decodes_common_escapes() {
14571 assert_eq!(getkeystring("\\n").0, "\n");
14572 assert_eq!(getkeystring("\\t").0, "\t");
14573 assert_eq!(getkeystring("\\r").0, "\r");
14574 }
14575
14576 /// `\\` → single backslash.
14577 #[test]
14578 fn getkeystring_double_backslash_yields_single() {
14579 assert_eq!(getkeystring("\\\\").0, "\\");
14580 }
14581
14582 /// `\xNN` hex escape → byte value.
14583 /// Anchor: `print -r -- $'\x41'` → "A" (0x41 = 65 = 'A').
14584 #[test]
14585 fn getkeystring_hex_escape_lowercase_x() {
14586 assert_eq!(getkeystring("\\x41").0, "A");
14587 assert_eq!(getkeystring("\\x7E").0, "~");
14588 }
14589
14590 /// `\u{NNNN}` Unicode escape → corresponding char.
14591 /// Anchor: `print -r -- $'é'` → "é".
14592 #[test]
14593 fn getkeystring_unicode_escape_u_four_digits() {
14594 assert_eq!(getkeystring("\\u00E9").0, "é");
14595 }
14596
14597 /// Plain text passes through unchanged.
14598 #[test]
14599 fn getkeystring_plain_text_passes_through() {
14600 assert_eq!(getkeystring("hello").0, "hello");
14601 assert_eq!(getkeystring("").0, "");
14602 }
14603
14604 /// Mixed plain + escapes → both handled.
14605 #[test]
14606 fn getkeystring_mixed_text_and_escapes() {
14607 assert_eq!(getkeystring("a\\nb").0, "a\nb");
14608 assert_eq!(
14609 getkeystring("line1\\nline2\\tindented").0,
14610 "line1\nline2\tindented"
14611 );
14612 }
14613
14614 // ── metafy / unmetafy round-trip on ASCII (identity) ────────────
14615 /// ASCII strings: metafy is identity (no meta-bytes to escape).
14616 #[test]
14617 fn metafy_then_unmetafy_ascii_roundtrips_to_input() {
14618 let s = "hello world";
14619 let m = metafy(s);
14620 assert_eq!(m, s);
14621 let mut bytes = m.into_bytes();
14622 let _len = unmetafy(&mut bytes);
14623 assert_eq!(String::from_utf8(bytes).unwrap(), "hello world");
14624 }
14625
14626 // ═══════════════════════════════════════════════════════════════════
14627 // quotestring — emit a quoted shell-safe form of the input string.
14628 // Each QT_* mode produces a different quoting style. Tests pin
14629 // each mode for both empty and non-empty input.
14630 // ═══════════════════════════════════════════════════════════════════
14631
14632 use crate::zsh_h::{
14633 QT_BACKSLASH, QT_BACKSLASH_PATTERN, QT_BACKSLASH_SHOWNULL, QT_DOLLARS, QT_DOUBLE, QT_NONE,
14634 QT_SINGLE, QT_SINGLE_OPTIONAL,
14635 };
14636
14637 /// QT_NONE: no quoting; passes input through unchanged.
14638 #[test]
14639 fn quotestring_qt_none_passes_through_unchanged() {
14640 let _g = crate::test_util::global_state_lock();
14641 assert_eq!(quotestring("hello world", QT_NONE), "hello world");
14642 assert_eq!(quotestring("", QT_NONE), "");
14643 assert_eq!(quotestring("a*b?c", QT_NONE), "a*b?c");
14644 }
14645
14646 /// QT_NONE empty → empty.
14647 #[test]
14648 fn quotestring_qt_none_empty_is_empty() {
14649 let _g = crate::test_util::global_state_lock();
14650 assert_eq!(quotestring("", QT_NONE), "");
14651 }
14652
14653 /// QT_BACKSLASH on empty → "''" (single-quote pair).
14654 #[test]
14655 fn quotestring_qt_backslash_empty_yields_empty() {
14656 let _g = crate::test_util::global_state_lock();
14657 // c:6194 — shownull is 0 for plain QT_BACKSLASH, so empty → "".
14658 assert_eq!(quotestring("", QT_BACKSLASH), "");
14659 }
14660
14661 /// QT_BACKSLASH_SHOWNULL on empty → "''" too.
14662 #[test]
14663 fn quotestring_qt_backslash_shownull_empty_yields_empty_single_quotes() {
14664 let _g = crate::test_util::global_state_lock();
14665 assert_eq!(quotestring("", QT_BACKSLASH_SHOWNULL), "''");
14666 }
14667
14668 /// QT_SINGLE on empty → "''" (single-quote pair).
14669 #[test]
14670 fn quotestring_qt_single_empty_yields_empty_single_quotes() {
14671 let _g = crate::test_util::global_state_lock();
14672 assert_eq!(quotestring("", QT_SINGLE), "''");
14673 }
14674
14675 /// QT_SINGLE_OPTIONAL on empty → "''" too.
14676 #[test]
14677 fn quotestring_qt_single_optional_empty_yields_empty_single_quotes() {
14678 let _g = crate::test_util::global_state_lock();
14679 assert_eq!(quotestring("", QT_SINGLE_OPTIONAL), "''");
14680 }
14681
14682 /// QT_DOUBLE on empty → "" (empty double-quote pair).
14683 #[test]
14684 fn quotestring_qt_double_empty_yields_empty_double_quotes() {
14685 let _g = crate::test_util::global_state_lock();
14686 assert_eq!(quotestring("", QT_DOUBLE), "\"\"");
14687 }
14688
14689 /// QT_DOLLARS on empty → "$''".
14690 #[test]
14691 fn quotestring_qt_dollars_empty_yields_dollar_quote_pair() {
14692 let _g = crate::test_util::global_state_lock();
14693 assert_eq!(quotestring("", QT_DOLLARS), "$''");
14694 }
14695
14696 /// QT_BACKSLASH_PATTERN escapes only pattern meta-chars.
14697 /// Input "a*b?c[d]" → "a\\*b\\?c\\[d\\]".
14698 #[test]
14699 fn quotestring_qt_backslash_pattern_escapes_glob_metas() {
14700 let _g = crate::test_util::global_state_lock();
14701 let r = quotestring("a*b?c[d]", QT_BACKSLASH_PATTERN);
14702 assert!(
14703 r.contains("\\*") && r.contains("\\?") && r.contains("\\[") && r.contains("\\]"),
14704 "all globs must be escaped; got {r:?}"
14705 );
14706 }
14707
14708 /// QT_BACKSLASH_PATTERN does NOT escape plain ASCII letters/digits.
14709 #[test]
14710 fn quotestring_qt_backslash_pattern_doesnt_escape_plain_chars() {
14711 let _g = crate::test_util::global_state_lock();
14712 let r = quotestring("plain", QT_BACKSLASH_PATTERN);
14713 assert_eq!(r, "plain", "plain text passes through");
14714 }
14715
14716 /// QT_BACKSLASH_PATTERN escapes `<`, `>`, `(`, `)`, `|`, `#`, `^`, `~`.
14717 #[test]
14718 fn quotestring_qt_backslash_pattern_escapes_all_meta_set() {
14719 let _g = crate::test_util::global_state_lock();
14720 for ch in ['<', '>', '(', ')', '|', '#', '^', '~'] {
14721 let s = format!("x{ch}y");
14722 let r = quotestring(&s, QT_BACKSLASH_PATTERN);
14723 assert!(
14724 r.contains(&format!("\\{ch}")),
14725 "{ch:?} must be escaped, got {r:?}"
14726 );
14727 }
14728 }
14729
14730 /// QT_SINGLE on simple input → "'simple'" (wrapped in single quotes).
14731 #[test]
14732 fn quotestring_qt_single_wraps_simple_input() {
14733 let _g = crate::test_util::global_state_lock();
14734 assert_eq!(quotestring("simple", QT_SINGLE), "'simple'");
14735 }
14736
14737 // ═══════════════════════════════════════════════════════════════════
14738 // metafy / unmetafy edge cases — bytes that NEED meta-encoding
14739 // (NUL, Meta byte 0x83, Nularg 0xa1, etc.). Pin that metafy escapes
14740 // them via the Meta + (byte ^ 32) scheme and unmetafy reverses it.
14741 // ═══════════════════════════════════════════════════════════════════
14742
14743 /// `metafy` is identity for plain ASCII (no meta bytes).
14744 #[test]
14745 fn metafy_ascii_is_identity_byte_for_byte() {
14746 let s = "abc 123 XYZ!@#";
14747 assert_eq!(metafy(s), s);
14748 }
14749
14750 /// Empty string round-trips.
14751 #[test]
14752 fn metafy_empty_string_returns_empty() {
14753 assert_eq!(metafy(""), "");
14754 }
14755
14756 /// metafy → unmetafy is round-trip identity for ASCII input.
14757 #[test]
14758 fn metafy_unmetafy_roundtrip_with_punctuation() {
14759 let s = "Hello, World! 123 +-=";
14760 let m = metafy(s);
14761 let mut bytes = m.into_bytes();
14762 let _ = unmetafy(&mut bytes);
14763 let result = String::from_utf8(bytes).expect("valid utf-8");
14764 assert_eq!(result, s);
14765 }
14766
14767 /// Multi-byte UTF-8 chars get meta-encoded then unmetafied. The
14768 /// round-trip MUST preserve the original bytes. Inline the byte-
14769 /// level metafy here — `metafy()`'s String return goes through
14770 /// `from_utf8_lossy` for non-UTF-8 outputs (correct for String
14771 /// consumers like the lexer or pattern compile, but breaks byte-
14772 /// round-trip because U+FFFD inserts EF BF BD where the
14773 /// original Meta-escaped bytes lived). The unmetafy step
14774 /// reverses the Meta-escape to recover the source UTF-8 verbatim.
14775 #[test]
14776 fn metafy_unmetafy_roundtrip_with_utf8_multibyte_anchored() {
14777 let s = "日本語";
14778 // Inline of metafy() byte path (no String materialisation).
14779 let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
14780 for &b in s.as_bytes() {
14781 if imeta_byte(b) {
14782 bytes.push(Meta);
14783 bytes.push(b ^ 32);
14784 } else {
14785 bytes.push(b);
14786 }
14787 }
14788 let _ = unmetafy(&mut bytes);
14789 let result = String::from_utf8(bytes).expect("valid utf-8");
14790 assert_eq!(result, s, "UTF-8 round-trip must preserve content");
14791 }
14792
14793 // ─── zsh-corpus pins for quotestring per quote_type ─────────────
14794
14795 /// `quotestring(QT_NONE)` is identity on any input.
14796 #[test]
14797 fn quotestring_corpus_qt_none_is_identity() {
14798 let s = "anything `goes` $here";
14799 assert_eq!(quotestring(s, QT_NONE), s);
14800 }
14801
14802 /// `quotestring(QT_SINGLE)` on plain word wraps in single quotes.
14803 #[test]
14804 fn quotestring_corpus_qt_single_wraps_plain_word() {
14805 let out = quotestring("hello", QT_SINGLE);
14806 assert!(
14807 out.starts_with('\'') && out.ends_with('\''),
14808 "single-quoted = wraps with ', got {out:?}"
14809 );
14810 assert!(out.contains("hello"), "content preserved");
14811 }
14812
14813 /// `quotestring(QT_DOUBLE)` on plain word wraps in double quotes.
14814 #[test]
14815 fn quotestring_corpus_qt_double_wraps_plain_word() {
14816 let out = quotestring("hello", QT_DOUBLE);
14817 assert!(
14818 out.starts_with('"') && out.ends_with('"'),
14819 "double-quoted = wraps with \", got {out:?}"
14820 );
14821 }
14822
14823 /// `quotestring(QT_SINGLE)` on string with apostrophe escapes the
14824 /// apostrophe by closing+escape+reopen: `it's` → `'it'\''s'`.
14825 #[test]
14826 fn quotestring_corpus_qt_single_escapes_apostrophe() {
14827 let out = quotestring("it's", QT_SINGLE);
14828 assert!(
14829 out.contains("\\'") || out.contains("'\\''"),
14830 "apostrophe gets escaped in single-quote form, got {out:?}",
14831 );
14832 }
14833
14834 /// `quotestring(QT_BACKSLASH)` escapes shell special chars.
14835 /// Pattern chars like `*`, `?`, `$`, `(`, `)` should be backslashed.
14836 #[test]
14837 fn quotestring_corpus_qt_backslash_escapes_glob_chars() {
14838 let out = quotestring("a*b?c", QT_BACKSLASH);
14839 assert!(out.contains("\\*"), "* gets backslashed, got {out:?}");
14840 assert!(out.contains("\\?"), "? gets backslashed, got {out:?}");
14841 }
14842
14843 /// `quotestring("", QT_DOUBLE)` returns `""` literal.
14844 #[test]
14845 fn quotestring_corpus_qt_double_empty_yields_double_quotes() {
14846 let out = quotestring("", QT_DOUBLE);
14847 assert_eq!(out, "\"\"", "empty double-quoted = \"\"");
14848 }
14849
14850 /// `quotestring("", QT_SINGLE)` returns `''` literal.
14851 #[test]
14852 fn quotestring_corpus_qt_single_empty_yields_single_quotes() {
14853 let out = quotestring("", QT_SINGLE);
14854 assert_eq!(out, "''", "empty single-quoted = ''");
14855 }
14856
14857 /// `quotestring(QT_BACKSLASH)` on plain alphanumeric is identity.
14858 /// "abc123" should round-trip with no backslashes added.
14859 #[test]
14860 fn quotestring_corpus_qt_backslash_plain_word_unchanged() {
14861 let out = quotestring("abc123", QT_BACKSLASH);
14862 assert_eq!(
14863 out, "abc123",
14864 "plain alphanumeric needs no escape, got {out:?}"
14865 );
14866 }
14867
14868 /// `ztrlen("hello")` = 5 (no meta chars).
14869 #[test]
14870 fn ztrlen_corpus_plain_ascii_byte_count() {
14871 assert_eq!(ztrlen("hello"), 5);
14872 }
14873
14874 /// `ztrlen("")` = 0.
14875 #[test]
14876 fn ztrlen_corpus_empty_is_zero() {
14877 assert_eq!(ztrlen(""), 0);
14878 }
14879
14880 // ═══════════════════════════════════════════════════════════════════
14881 // itype_end C-parity tests — pin `utils.c:4395-4495` per-itype-flag
14882 // behavior. One assertion per flag/path so a regression in the
14883 // char-class walk points at the exact branch that drifted.
14884 //
14885 // Tests that capture KNOWN ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"]
14886 // so the bug stays surfaced but CI doesn't fail until the fix.
14887 // ═══════════════════════════════════════════════════════════════════
14888
14889 /// `itype_end("abc123", IIDENT, false)` walks the full identifier.
14890 /// C utils.c:4395 — IIDENT matches letters/digits/`_` via TYPTAB.
14891 /// Requires `inittyptab()` for TYPTAB population — call at entry.
14892 #[test]
14893 fn itype_end_ident_walks_full_identifier() {
14894 let _g = crate::test_util::global_state_lock();
14895 inittyptab();
14896 use crate::ported::ztype_h::IIDENT;
14897 assert_eq!(itype_end("abc123", IIDENT as u32, false), 6);
14898 }
14899
14900 /// `itype_end("abc-def", IIDENT, false)` stops at `-` (non-IIDENT).
14901 #[test]
14902 fn itype_end_ident_stops_at_dash() {
14903 let _g = crate::test_util::global_state_lock();
14904 inittyptab();
14905 use crate::ported::ztype_h::IIDENT;
14906 assert_eq!(itype_end("abc-def", IIDENT as u32, false), 3);
14907 }
14908
14909 /// `itype_end("a", IIDENT, true)` with `once=true` stops after 1.
14910 #[test]
14911 fn itype_end_once_stops_after_first_match() {
14912 let _g = crate::test_util::global_state_lock();
14913 inittyptab();
14914 use crate::ported::ztype_h::IIDENT;
14915 assert_eq!(itype_end("abc", IIDENT as u32, true), 1);
14916 }
14917
14918 /// `itype_end("", IIDENT, false)` on empty string returns 0.
14919 #[test]
14920 fn itype_end_empty_string_returns_zero() {
14921 let _g = crate::test_util::global_state_lock();
14922 inittyptab();
14923 use crate::ported::ztype_h::IIDENT;
14924 assert_eq!(itype_end("", IIDENT as u32, false), 0);
14925 }
14926
14927 /// `itype_end(":", IIDENT, false)` on non-IIDENT first char = 0.
14928 #[test]
14929 fn itype_end_non_ident_first_char_returns_zero() {
14930 let _g = crate::test_util::global_state_lock();
14931 inittyptab();
14932 use crate::ported::ztype_h::IIDENT;
14933 assert_eq!(itype_end(":foo", IIDENT as u32, false), 0);
14934 }
14935
14936 /// `itype_end("ns.foo", INAMESPC, false)` — C utils.c:4399-4413
14937 /// INAMESPC special-cases ksh93 namespace `.` separators. With
14938 /// non-POSIXIDENTIFIERS / non-KSH emulation, dotted names should
14939 /// walk through the `.` to consume `ns.foo` as a single name.
14940 /// The Rust port may stop at `.` since dotted-namespace support
14941 /// requires POSIXIDENTIFIERS check + ksh emulation gating that
14942 /// isn't fully wired.
14943 #[test]
14944 fn itype_end_inamespc_walks_through_ksh93_dot() {
14945 let _g = crate::test_util::global_state_lock();
14946 inittyptab();
14947 use crate::ported::ztype_h::INAMESPC;
14948 // Expected: full "ns.foo" = 6 bytes walked.
14949 assert_eq!(itype_end("ns.foo", INAMESPC, false), 6);
14950 }
14951
14952 /// `itype_end("a b c", ISEP, false)` from a space char.
14953 /// ISEP matches IFS chars. Default IFS includes ` \t\n`.
14954 /// Walk should consume only the leading separator(s).
14955 #[test]
14956 fn itype_end_isep_walks_separator_run() {
14957 let _g = crate::test_util::global_state_lock();
14958 inittyptab();
14959 use crate::ported::ztype_h::ISEP;
14960 // " \t " then 'a' — 3 sep bytes.
14961 assert_eq!(itype_end(" \t a", ISEP as u32, false), 3);
14962 }
14963
14964 // ═══════════════════════════════════════════════════════════════════
14965 // unmetafy C-parity tests — pin `utils.c:4954-4983`. Meta-byte
14966 // collapse (0x83 + (b^0x20) → b) must be in-place + return new len.
14967 // ═══════════════════════════════════════════════════════════════════
14968
14969 /// `unmetafy("abc")` is a no-op for non-metafied input.
14970 #[test]
14971 fn unmetafy_pure_ascii_unchanged() {
14972 let _g = crate::test_util::global_state_lock();
14973 let mut v = b"abc".to_vec();
14974 let n = unmetafy(&mut v);
14975 assert_eq!(n, 3);
14976 assert_eq!(v, b"abc");
14977 }
14978
14979 /// `unmetafy("a\x83a")` → "a\x01" because Meta(0x83) + next ^ 0x20
14980 /// = 'a' ^ 0x20 = 0x41 ^ 0x20 = 0x61... wait, 'a'=0x61, 0x61 ^ 0x20
14981 /// = 0x41 = 'A'. So unmetafy("a\x83a") → "aA" (2 bytes).
14982 #[test]
14983 fn unmetafy_meta_pair_collapses_to_xor_byte() {
14984 let _g = crate::test_util::global_state_lock();
14985 // 'a' + Meta(0x83) + 'a' → 'a' + ('a' ^ 0x20) = 'a' + 'A' = "aA"
14986 let mut v: Vec<u8> = vec![b'a', 0x83, b'a'];
14987 let n = unmetafy(&mut v);
14988 assert_eq!(n, 2);
14989 assert_eq!(v, vec![b'a', b'A']);
14990 }
14991
14992 /// `unmetafy("")` returns 0 with empty buffer.
14993 #[test]
14994 fn unmetafy_empty_returns_zero() {
14995 let _g = crate::test_util::global_state_lock();
14996 let mut v: Vec<u8> = Vec::new();
14997 let n = unmetafy(&mut v);
14998 assert_eq!(n, 0);
14999 }
15000
15001 /// `unmetafy` with trailing lone Meta byte. C `unmetafy` checks
15002 /// `p < s.len()` before reading the byte after Meta — trailing
15003 /// Meta at EOF is preserved as a single literal Meta byte.
15004 #[test]
15005 fn unmetafy_trailing_lone_meta_preserved() {
15006 let _g = crate::test_util::global_state_lock();
15007 let mut v: Vec<u8> = vec![b'a', 0x83];
15008 let n = unmetafy(&mut v);
15009 // Rust port copies the Meta byte to t, then loop exits with
15010 // p == s.len() → no second-byte read. Final = "a\x83" (2).
15011 assert_eq!(n, 2);
15012 assert_eq!(v, vec![b'a', 0x83]);
15013 }
15014
15015 // ═══════════════════════════════════════════════════════════════════
15016 // nicechar / wcs_nicechar C-parity tests — pin display-form
15017 // conversion. Required by stradd, prompt expansion, completion etc.
15018 // ═══════════════════════════════════════════════════════════════════
15019
15020 /// `nicechar('a')` returns the printable char unchanged.
15021 #[test]
15022 fn nicechar_printable_ascii_passes_through() {
15023 let _g = crate::test_util::global_state_lock();
15024 assert_eq!(nicechar('a'), "a");
15025 }
15026
15027 /// `nicechar('\n')` returns `\n` escape sequence per C `nicechar`
15028 /// (utils.c:464+ — control chars get `^M` style or `\n` escape).
15029 /// zsh emits `\n` as literal `\n` (two chars) for non-quotable
15030 /// nicechar; the `nicechar_sel(c, false)` form is non-quotable.
15031 #[test]
15032 fn nicechar_newline_emits_escape() {
15033 let _g = crate::test_util::global_state_lock();
15034 let out = nicechar('\n');
15035 assert!(
15036 out == "\\n" || out == "\n",
15037 "newline should be escaped or literal; got {out:?}"
15038 );
15039 }
15040
15041 /// `wcs_nicechar('A', None, None)` returns "A" for printable wide
15042 /// chars per utils.c:689.
15043 #[test]
15044 fn wcs_nicechar_printable_ascii_passes_through() {
15045 let _g = crate::test_util::global_state_lock();
15046 assert_eq!(wcs_nicechar('A', None, None), "A");
15047 }
15048
15049 // ═══════════════════════════════════════════════════════════════════
15050 // Additional C-parity tests for Src/utils.c nicechar dispatch.
15051 // ═══════════════════════════════════════════════════════════════════
15052
15053 /// c:462 — nicechar for printable ASCII passes through.
15054 #[test]
15055 fn nicechar_printable_ascii_passes_through_pin() {
15056 let _g = crate::test_util::global_state_lock();
15057 assert_eq!(nicechar('a'), "a");
15058 assert_eq!(nicechar('Z'), "Z");
15059 assert_eq!(nicechar('5'), "5");
15060 assert_eq!(nicechar(' '), " ");
15061 }
15062
15063 /// c:487 — nicechar('\\n') returns '\\n' (backslash + n).
15064 #[test]
15065 fn nicechar_newline_returns_backslash_n() {
15066 let _g = crate::test_util::global_state_lock();
15067 assert_eq!(nicechar('\n'), "\\n");
15068 }
15069
15070 /// c:490 — nicechar('\\t') returns '\\t' (backslash + t).
15071 #[test]
15072 fn nicechar_tab_returns_backslash_t() {
15073 let _g = crate::test_util::global_state_lock();
15074 assert_eq!(nicechar('\t'), "\\t");
15075 }
15076
15077 /// c:479 — nicechar(0x7f) returns '^?' (DEL → ^?).
15078 #[test]
15079 fn nicechar_del_returns_caret_question() {
15080 let _g = crate::test_util::global_state_lock();
15081 assert_eq!(nicechar('\x7f'), "^?");
15082 }
15083
15084 /// c:493 — nicechar(0x01) returns '^A' (Ctrl-A).
15085 #[test]
15086 fn nicechar_ctrl_a_returns_caret_A() {
15087 let _g = crate::test_util::global_state_lock();
15088 assert_eq!(nicechar('\x01'), "^A");
15089 }
15090
15091 /// c:493 — nicechar(0x1b) returns '^[' (ESC).
15092 #[test]
15093 fn nicechar_esc_returns_caret_bracket() {
15094 let _g = crate::test_util::global_state_lock();
15095 assert_eq!(nicechar('\x1b'), "^[");
15096 }
15097
15098 /// c:462 — nicechar_sel quotable=true uses '\\C-' instead of '^'
15099 /// for control chars.
15100 #[test]
15101 fn nicechar_sel_quotable_uses_backslash_C() {
15102 let _g = crate::test_util::global_state_lock();
15103 let r = nicechar_sel('\x01', true);
15104 assert!(
15105 r.contains("\\C-") || r.contains("^"),
15106 "quotable should emit \\C- form, got {:?}",
15107 r
15108 );
15109 }
15110
15111 /// c:531 — is_nicechar('a') returns FALSE: printable chars don't
15112 /// NEED nice (escape) representation, they pass through verbatim.
15113 /// Per the C comment in utils.c:531: "Return whether the char needs
15114 /// nice representation".
15115 #[test]
15116 fn is_nicechar_printable_returns_false() {
15117 let _g = crate::test_util::global_state_lock();
15118 assert!(!is_nicechar('a'), "printable doesn't NEED nice repr");
15119 assert!(!is_nicechar('Z'));
15120 assert!(!is_nicechar('5'));
15121 }
15122
15123 /// c:531 — is_nicechar for any byte 0..127 is safe (no panic).
15124 #[test]
15125 fn is_nicechar_all_ascii_safe() {
15126 let _g = crate::test_util::global_state_lock();
15127 for c in 0u8..=127 {
15128 let _ = is_nicechar(c as char); // no panic
15129 }
15130 }
15131
15132 /// c:462 — nicechar deterministic.
15133 #[test]
15134 fn nicechar_is_deterministic() {
15135 let _g = crate::test_util::global_state_lock();
15136 for c in ['a', '\n', '\t', '\x7f', '\x01', '\x1b'] {
15137 let first = nicechar(c);
15138 for _ in 0..5 {
15139 assert_eq!(nicechar(c), first, "{:?} must be pure", c);
15140 }
15141 }
15142 }
15143
15144 /// c:706 — `pathprog("")` returns None (empty path invalid).
15145 #[test]
15146 fn pathprog_empty_returns_none() {
15147 let _g = crate::test_util::global_state_lock();
15148 assert!(pathprog("").is_none());
15149 }
15150
15151 /// c:776 — `pathprog("/nonexistent/zshrs_xyz")` returns None.
15152 #[test]
15153 fn pathprog_nonexistent_returns_none() {
15154 let _g = crate::test_util::global_state_lock();
15155 let r = pathprog("/__never_exists_zshrs_pathprog_xyz");
15156 assert!(r.is_none());
15157 }
15158
15159 // ═══════════════════════════════════════════════════════════════════
15160 // Additional C-parity tests for Src/utils.c
15161 // c:99 set_widearray / c:447 nicechar_sel / c:521 is_nicechar /
15162 // c:742 zwcwidth / c:836 findpwd / c:928 slashsplit /
15163 // c:1258 get_username / c:1426 getnameddir / c:1475 dircmp /
15164 // c:1576 callhookfunc
15165 // ═══════════════════════════════════════════════════════════════════
15166
15167 /// c:99 — `set_widearray("")` empty returns empty Vec.
15168 #[test]
15169 fn set_widearray_empty_returns_empty() {
15170 let _g = crate::test_util::global_state_lock();
15171 assert!(set_widearray("").is_empty());
15172 }
15173
15174 /// c:99 — `set_widearray` returns Vec<char>.
15175 #[test]
15176 fn set_widearray_returns_vec_char_type() {
15177 let _g = crate::test_util::global_state_lock();
15178 let _: Vec<char> = set_widearray("a");
15179 }
15180
15181 /// c:447 — `nicechar_sel('a', false)` returns "a" (printable pass-through).
15182 #[test]
15183 fn nicechar_sel_ascii_letter_passthrough() {
15184 assert_eq!(nicechar_sel('a', false), "a", "printable letter as-is");
15185 }
15186
15187 /// c:447 — `nicechar_sel` is pure.
15188 #[test]
15189 fn nicechar_sel_is_pure() {
15190 for c in ['a', '\t', '\n', '\x1b', '\x7f', '日'] {
15191 let first = nicechar_sel(c, false);
15192 for _ in 0..3 {
15193 assert_eq!(
15194 nicechar_sel(c, false),
15195 first,
15196 "nicechar_sel({:?}, false) must be pure",
15197 c
15198 );
15199 }
15200 }
15201 }
15202
15203 /// c:742 — `zwcwidth` returns usize (compile-time type pin).
15204 #[test]
15205 fn zwcwidth_returns_usize_type() {
15206 let _: usize = zwcwidth('a');
15207 }
15208
15209 /// c:742 — `zwcwidth('a')` ASCII letter returns 1.
15210 #[test]
15211 fn zwcwidth_ascii_returns_one() {
15212 assert_eq!(zwcwidth('a'), 1, "ASCII letter width = 1");
15213 }
15214
15215 /// c:836 — `findpwd("")` empty returns Option<String>.
15216 #[test]
15217 fn findpwd_empty_returns_option_string_type() {
15218 let _g = crate::test_util::global_state_lock();
15219 let _: Option<String> = findpwd("");
15220 }
15221
15222 /// c:928 — `slashsplit("")` empty returns empty Vec.
15223 #[test]
15224 fn slashsplit_empty_returns_empty() {
15225 assert!(slashsplit("").is_empty());
15226 }
15227
15228 /// c:928 — `slashsplit("a/b/c")` splits to 3 elements.
15229 #[test]
15230 fn slashsplit_simple_three_components() {
15231 let r = slashsplit("a/b/c");
15232 assert_eq!(r, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
15233 }
15234
15235 /// c:1475 — `dircmp("", "")` both empty returns bool (type pin + safe).
15236 #[test]
15237 fn dircmp_both_empty_returns_bool_type() {
15238 let _g = crate::test_util::global_state_lock();
15239 let _: bool = dircmp("", "");
15240 }
15241
15242 /// c:1426 — `getnameddir("")` empty returns None.
15243 #[test]
15244 fn getnameddir_empty_returns_none() {
15245 let _g = crate::test_util::global_state_lock();
15246 assert!(getnameddir("").is_none(), "empty → None");
15247 }
15248
15249 /// c:1258 — `get_username` returns String (compile-time type pin).
15250 #[test]
15251 fn get_username_returns_string_type() {
15252 let _g = crate::test_util::global_state_lock();
15253 let _: String = get_username();
15254 }
15255
15256 /// c:1921-1935 — adjustwinsize(1) seeds `$LINES`/`$COLUMNS` from the
15257 /// TIOCGWINSZ probe of SHTTY. The port gated the setiparam calls on
15258 /// LINES/COLUMNS being in the OS environment (a misread of C's
15259 /// zgetenv guard, which only covers env re-publication — the C param
15260 /// is always live via its zterm_lines valptr alias), so every
15261 /// interactive shell ran with LINES=0 → `$(( LINES / 3 ))` division
15262 /// by zero in history-search-multi-word pagination (_hsmw_main:81).
15263 #[test]
15264 fn adjustwinsize_seeds_lines_columns_from_shtty() {
15265 let _g = crate::test_util::global_state_lock();
15266 unsafe {
15267 let mut master: libc::c_int = -1;
15268 let mut slave: libc::c_int = -1;
15269 let mut ws: libc::winsize = std::mem::zeroed();
15270 ws.ws_row = 33;
15271 ws.ws_col = 77;
15272 if libc::openpty(
15273 &mut master,
15274 &mut slave,
15275 std::ptr::null_mut(),
15276 std::ptr::null_mut(),
15277 &mut ws,
15278 ) != 0
15279 {
15280 return; // no pty available (sandboxed CI) — skip
15281 }
15282 let saved_shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
15283 crate::ported::init::SHTTY.store(slave, Ordering::Relaxed);
15284 let _ = adjustwinsize(1);
15285 crate::ported::init::SHTTY.store(saved_shtty, Ordering::Relaxed);
15286 libc::close(slave);
15287 libc::close(master);
15288 }
15289 assert_eq!(
15290 crate::ported::params::getiparam("LINES"),
15291 33,
15292 "LINES must reflect the SHTTY winsize probe"
15293 );
15294 assert_eq!(
15295 crate::ported::params::getiparam("COLUMNS"),
15296 77,
15297 "COLUMNS must reflect the SHTTY winsize probe"
15298 );
15299 }
15300}
15301
15302// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
15303// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
15304// pipeline. Extracted body of zerrmsg's `%e` arm (Src/utils.c:355-366):
15305// EINTR → "interrupt", EIO verbatim, else lowercased strerror.
15306// zerrmsg's errno branch routes through this so the prefix
15307// formatting matches C; module callers (zsh/system) embed the
15308// string in their own zwarnnam messages where C uses `%e`.
15309/// Render an errno the way zsh's `%e` warning format does —
15310/// `zerrmsg` case `'e'` at `Src/utils.c:352-368`:
15311///
15312/// - `EINTR` → the literal string `interrupt` (C also sets
15313/// `errflag |= ERRFLAG_ERROR` and truncates the rest of the
15314/// format; every zshrs call site uses `%e` as the final
15315/// conversion so the truncation is a no-op here).
15316/// - `EIO` → `strerror(EIO)` verbatim ("I/O" reads wrong
15317/// lowercased, per the c:361-364 comment).
15318/// - everything else → `strerror(num)` with the first letter
15319/// lowercased (`tulower(errmsg[0])`, c:366).
15320///
15321/// `std::io::Error::from_raw_os_error(n)` Display is NOT this
15322/// format — it appends " (os error N)" and keeps the capital.
15323/// That mismatch was the residual byte-diff in zsh/system's
15324/// `sysopen`/`zsystem flock` warnings (docs/BUGS.md #316).
15325pub fn zsh_errno_msg(num: i32) -> String {
15326 if num == libc::EINTR {
15327 return "interrupt".to_string(); // c:355-358
15328 }
15329 let msg = unsafe {
15330 let p = libc::strerror(num); // c:360
15331 if p.is_null() {
15332 return String::new();
15333 }
15334 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
15335 };
15336 if num == libc::EIO {
15337 return msg; // c:363-364
15338 }
15339 // c:366 — `fputc(tulower(errmsg[0]), file);`
15340 let mut chars = msg.chars();
15341 match chars.next() {
15342 Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
15343 None => msg,
15344 }
15345}
15346
15347// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
15348// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
15349// pipeline. Byte-exact inverse of metafy (Src/utils.c:5022 unmetafy):
15350// decodes Meta (\u{83}) + c^32 pairs back to raw bytes. C
15351// unmetafy works in place on char*; Rust strings are UTF-8 so
15352// the decode returns Vec<u8> for byte-exact writes.
15353/// Decode a char-level metafied String back to RAW bytes for a write
15354/// or exec boundary. c:Src/utils.c:4954 unmetafy — `if (*t++ == Meta
15355/// && *p) t[-1] = *p++ ^ 32;` — transposed onto the char-level
15356/// encoding `meta_encode_byte` produces: U+0083 followed by a scalar
15357/// in U+0080..=U+00FF yields the single raw byte `payload ^ 32`;
15358/// everything else emits its UTF-8 bytes verbatim. A U+0083 followed
15359/// by anything outside that range is not our encoding and passes
15360/// through untouched. Bug #127.
15361pub fn unmetafy_str(s: &str) -> Vec<u8> {
15362 let mut out = Vec::with_capacity(s.len());
15363 let mut it = s.chars().peekable();
15364 let mut buf = [0u8; 4];
15365 while let Some(c) = it.next() {
15366 if c == '\u{83}' {
15367 if let Some(&n) = it.peek() {
15368 let nu = n as u32;
15369 if (0x80..=0xff).contains(&nu) {
15370 out.push((nu as u8) ^ 32);
15371 it.next();
15372 continue;
15373 }
15374 }
15375 }
15376 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
15377 }
15378 out
15379}