zsh/ported/hashtable.rs
1//! Hash table implementations - port of hashtable.c
2//!
3//! Provides hash tables for commands, shell functions, reserved words, aliases,
4//! and history. Uses Rust's HashMap internally but maintains zsh-compatible APIs.
5//!
6//! `cmdnam_table` / `shfunc_table` / `reswd_table` / `alias_table` are
7//! Rust-side typed wrappers. C uses one polymorphic `struct hashtable`
8//! (`Src/zsh.h:1175-1235`) with function-pointer callbacks per table
9//! kind; the canonical Rust port of that struct lives at
10//! `zsh_h.rs:532`. These typed wrappers add fields that aren't part
11//! of `struct hashtable` (e.g. `cmdnam_table` carries
12//! `path_checked_index` + `path` + `hash_executables_only` for the
13//! `PATH`-walk fast-rehash that C tracks via the file-scope
14//! `pathchecked` / `hashed_anything` statics in `Src/hashtable.c`).
15//! Lowercase naming reflects that the wrappers are co-located with
16//! the canonical `hashtable` rather than mirror it 1:1.
17
18#![allow(non_camel_case_types)]
19
20use crate::compat::zgetcwd;
21use crate::hist::{hashchar, hist_ring};
22use crate::jobs::getsigidx;
23use crate::ported::hist::{hist_ignore_all_dups, histlinect, histremovedups, up_histent};
24use crate::ported::pattern::{patcompile, pattry};
25use crate::ported::signals::removetrap;
26use crate::ported::utils::scriptfilename_get;
27use crate::ported::zsh_h::{
28 alias, options, BANG_TOK, CASE, COPROC, DINBRACK, DOLOOP, DONE, ELIF, ELSE, ESAC, FI, FOR,
29 FOREACH, FUNC, IF, INBRACE_TOK, NOCORRECT, OUTBRACE_TOK, PAT_HEAPDUP, REPEAT, SELECT, THEN,
30 TIME, TYPESET, UNTIL, WHILE, ZEND,
31};
32use crate::signals::{settrap, unsettrap};
33use crate::text::{getpermtext, zoutputtab};
34use crate::utils::{nicezputs, quotedzputs, xsymlink, zputs, ztrcmp, zwarn};
35use crate::zsh_h::{
36 cmdnam, hashnode, hashtable, reswd, shfunc, ALIAS_GLOBAL, ALIAS_SUFFIX, DISABLED, EF_RUN,
37 HASHED, HIST_DUP, HIST_FOREIGN, HIST_MAKEUNIQUE, HIST_TMPSTORE, PM_CUR_FPATH, PM_KSHSTORED,
38 PM_LOADDIR, PM_TAGGED,
39 PM_TAGGED_LOCAL, PM_UNALIASED, PM_UNDEFINED, PM_ZSHSTORED, PRINT_LIST, PRINT_NAMEONLY,
40 PRINT_WHENCE_CSH, PRINT_WHENCE_FUNCDEF, PRINT_WHENCE_SIMPLE, PRINT_WHENCE_VERBOSE,
41 PRINT_WHENCE_WORD, ZSIG_FUNC,
42};
43use std::collections::HashMap;
44use std::fs;
45use std::io;
46use std::os::unix::fs::PermissionsExt;
47use std::path::PathBuf;
48use std::sync::atomic::Ordering;
49
50/// Generic hash function (zsh's hasher)
51/// Compute the canonical zsh hash for a string.
52/// Port of `hasher(const char *str)` from Src/hashtable.c:86 — uses the same
53// Generic hash function // c:86
54/// `hash * 33 + char` polynomial the C source uses for every
55/// HashTable lookup.
56pub fn hasher(str: &str) -> u32 {
57 // c:86
58 let mut hashval: u32 = 0;
59 for c in str.bytes() {
60 hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
61 }
62 hashval
63}
64
65// ===========================================================
66// Direct ports of the generic `HashTable` lifecycle / mutation /
67// printer routines from Src/hashtable.c. The Rust port stores
68// command/alias/reswd/shfunc tables as `HashMap`-backed wrappers
69// (above), so most of these are free-fn shims for ABI/name
70// parity. Callers in the Rust executor reach the live state via
71// the typed table structs (`alias_table`, `shfunc_table`, etc.).
72// ===========================================================
73
74/// Port of `newhashtable(int size, UNUSED(char const *name), UNUSED(PrintTableStats printinfo))` from `Src/hashtable.c:100`.
75///
76/// C allocates a `HashTable` header with `size` buckets and the
77/// supplied `name` for `bin_hashinfo` reporting. Rust uses
78/// `HashMap` (auto-resizing) so the bucket count is informational;
79/// the named-table accounting is recorded for `printhashtabinfo`.
80///
81/// Returns a `(name, expected_size)` tuple — callers (the table-
82/// specific creators) typically discard since each Rust table
83/// type has its own constructor. Provided for C name parity.
84// Get a new hash table // c:100
85/// WARNING: param names don't match C — Rust=(size, name) vs C=(size, name, printinfo)
86pub fn newhashtable(size: i32, name: &str) -> (String, i32) {
87 // c:100
88 (name.to_string(), size)
89}
90
91/// Port of `deletehashtable(HashTable ht)` from `Src/hashtable.c:129`.
92///
93/// C frees every node via `emptytable` then frees the header.
94/// Rust port: `Drop` runs the equivalent on the typed table when
95/// it falls out of scope. The free fn here calls clear on the
96/// passed map for C name parity at call sites that explicitly
97/// invoke deletehashtable.
98pub fn deletehashtable<T>(ht: &mut HashMap<String, T>) {
99 // c:129
100 ht.clear();
101}
102
103// `cmdnam` struct + impl deleted — Rust-only duplicate of canonical
104// `crate::ported::zsh_h::cmdnam` (zsh.h:1301-1308). C struct:
105//
106// struct cmdnam {
107// struct hashnode node;
108// union {
109// char **name; /* HASHED off: full $PATH array (u.name) */
110// char *cmd; /* HASHED on: resolved abs path (u.cmd) */
111// } u;
112// };
113//
114// The Rust-only version had a flat `name, flags, path: PathBuf,
115// dir_index` shape that lost the hashnode embedding and the
116// name/cmd union (the C source uses `flags & HASHED` to dispatch
117// which arm holds the value). Type alias surfaces the canonical
118// struct directly; the previous `path: PathBuf` becomes
119// `cmd: Option<String>` and `dir_index: Option<usize>` becomes
120// `name: Option<Vec<String>>` (the full PATH-segment slice the
121// command would be looked up against).
122// c:1301
123
124/// Port of `addhashnode(HashTable ht, char *nam, void *nodeptr)` from `Src/hashtable.c:157`.
125///
126/// C body:
127/// ```c
128/// HashNode oldnode = addhashnode2(ht, nam, nodeptr);
129/// if (oldnode) ht->freenode(oldnode);
130/// ```
131///
132/// Generic insert that drops the previous value at `nam` (Rust's
133// is now greater than twice the number of hash values, // c:157
134// the table is then expanded. // c:157
135/// `HashMap::insert` returns the old value; dropping it runs the
136/// equivalent of `freenode`). For typed table-specific entry
137/// shapes use the table's own `add()` method.
138// Add a node to a hash table, returning the old node on replacement. // c:168
139/// `addhashnode` — see implementation.
140pub fn addhashnode<T>(ht: &mut HashMap<String, T>, nam: &str, value: T) {
141 // c:157
142 ht.insert(nam.to_string(), value);
143}
144
145// Add a node to a hash table, returning the old node on replacement. // c:168
146/// Port of `addhashnode2(HashTable ht, char *nam, void *nodeptr)` from `Src/hashtable.c:168`.
147///
148/// C body inserts and returns the OLD node (instead of freeing
149/// it via the freenode callback). Rust HashMap::insert already
150/// has this shape — return the displaced value.
151pub fn addhashnode2<T>(ht: &mut HashMap<String, T>, nam: &str, nodeptr: T) -> Option<T> {
152 // c:168
153 ht.insert(nam.to_string(), nodeptr)
154}
155
156/// Port of `gethashnode(HashTable ht, const char *nam)` from `Src/hashtable.c:231`.
157///
158/// C body returns NULL if the entry has the DISABLED flag set;
159// the hashnode. If the node is DISABLED // c:231
160// or isn't found, it returns NULL // c:231
161/// otherwise returns the node. Generic lookup helper — `T` must
162/// expose its DISABLED flag via the [`HashNodeFlags`] trait so
163/// the disabled filter applies.
164/// WARNING: param names don't match C — Rust=(nam) vs C=(ht, nam)
165pub fn gethashnode<'a, T: HashNodeFlags>(
166 // c:231
167 ht: &'a HashMap<String, T>,
168 nam: &str,
169) -> Option<&'a T> {
170 ht.get(nam).filter(|t| !t.is_disabled())
171}
172
173impl cmdnam_table {
174 /// `new` — see implementation.
175 pub fn new() -> Self {
176 Self {
177 table: HashMap::new(),
178 path_checked_index: 0,
179 path: Vec::new(),
180 hash_executables_only: false,
181 }
182 }
183 /// `set_path` — see implementation.
184 pub fn set_path(&mut self, path: Vec<String>) {
185 self.path = path;
186 self.path_checked_index = 0;
187 }
188 /// `set_hash_executables_only` — see implementation.
189 pub fn set_hash_executables_only(&mut self, value: bool) {
190 self.hash_executables_only = value;
191 }
192 /// `add` — see implementation.
193 pub fn add(&mut self, cmd: cmdnam) {
194 self.table.insert(cmd.node.nam.clone(), cmd);
195 }
196 /// `get` — see implementation.
197 pub fn get(&self, name: &str) -> Option<&cmdnam> {
198 self.table
199 .get(name)
200 .filter(|c| (c.node.flags & DISABLED as i32) == 0)
201 }
202 /// `get_including_disabled` — see implementation.
203 pub fn get_including_disabled(&self, name: &str) -> Option<&cmdnam> {
204 self.table.get(name)
205 }
206 /// `remove` — see implementation.
207 pub fn remove(&mut self, name: &str) -> Option<cmdnam> {
208 self.table.remove(name)
209 }
210 /// `clear` — see implementation.
211 pub fn clear(&mut self) {
212 self.table.clear();
213 self.path_checked_index = 0;
214 }
215 /// `len` — see implementation.
216 pub fn len(&self) -> usize {
217 self.table.len()
218 }
219 /// `is_empty` — see implementation.
220 pub fn is_empty(&self) -> bool {
221 self.table.is_empty()
222 }
223
224 /// Hash all commands in a directory
225 pub fn hash_dir(&mut self, dir: &str, dir_index: usize) {
226 if dir.starts_with('.') || dir.is_empty() {
227 return;
228 }
229
230 let Ok(entries) = fs::read_dir(dir) else {
231 return;
232 };
233
234 for entry in entries.flatten() {
235 let Ok(name) = entry.file_name().into_string() else {
236 continue;
237 };
238
239 if self.table.contains_key(&name) {
240 continue;
241 }
242
243 let path = entry.path();
244 let should_add = if self.hash_executables_only {
245 // Inline of the deleted is_executable helper.
246 #[cfg(unix)]
247 {
248 path.metadata()
249 .map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
250 .unwrap_or(false)
251 }
252 #[cfg(not(unix))]
253 {
254 path.is_file()
255 }
256 } else {
257 true
258 };
259
260 if should_add {
261 // C `cn->u.name = pathchecked;` at hashtable.c:712 —
262 // the unhashed entry carries the PATH-array slice it
263 // would scan. Rust port: snapshot the single PATH
264 // segment at `dir_index` so lookup later resolves
265 // the path. Older Rust-only code stored just the
266 // index; canonical port stores the actual segment.
267 let segment = self
268 .path
269 .get(dir_index)
270 .cloned()
271 .unwrap_or_else(|| dir.to_string());
272 self.table
273 .insert(name.clone(), cmdnam_unhashed(&name, vec![segment]));
274 }
275 }
276 }
277
278 /// Fill table from PATH
279 pub fn fill(&mut self) {
280 for i in self.path_checked_index..self.path.len() {
281 let dir = self.path[i].clone();
282 self.hash_dir(&dir, i);
283 }
284 self.path_checked_index = self.path.len();
285 }
286
287 /// Iterate over all entries
288 pub fn iter(&self) -> impl Iterator<Item = (&String, &cmdnam)> {
289 self.table.iter()
290 }
291
292 /// Get full path for a command. Mirrors C's
293 /// `findcmd(name, 1, 0)` lookup via cmdnamtab (Src/exec.c:5260).
294 pub fn get_full_path(&self, name: &str) -> Option<PathBuf> {
295 let cmd = self.table.get(name)?;
296 if (cmd.node.flags & DISABLED as i32) != 0 {
297 return None;
298 }
299 // HASHED branch: cn->u.cmd holds the resolved path.
300 if (cmd.node.flags & HASHED as i32) != 0 {
301 if let Some(ref s) = cmd.cmd {
302 return Some(PathBuf::from(s));
303 }
304 }
305 // Unhashed branch: cn->u.name holds PATH segments to scan.
306 if let Some(ref segs) = cmd.name {
307 if let Some(seg) = segs.first() {
308 let mut path = PathBuf::from(seg);
309 path.push(name);
310 return Some(path);
311 }
312 }
313 None
314 }
315}
316
317impl Default for cmdnam_table {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323// `shfunc` struct + impl deleted — Rust-only duplicate of canonical
324// `crate::ported::zsh_h::shfunc` (zsh.h:1316-1325). Canonical:
325//
326// struct shfunc {
327// struct hashnode node;
328// char *filename;
329// zlong lineno;
330// Eprog funcdef;
331// Eprog redir;
332// Emulation_options sticky;
333// };
334//
335// Canonical was extended with a Rust-only `body: Option<String>`
336// field (deferred-compile source text) so callers using the old
337// `shfunc.body` access continue working. Type alias surfaces
338// canonical as `shfunc`; helpers below build instances with the
339// hashnode literal pre-populated.
340
341/// Port of `gethashnode2(HashTable ht, const char *nam)` from `Src/hashtable.c:255`.
342///
343/// Same as gethashnode but bypasses the DISABLED filter.
344pub fn gethashnode2<'a, T>(ht: &'a HashMap<String, T>, nam: &str) -> Option<&'a T> {
345 // c:255
346 ht.get(nam)
347}
348
349/// Port of `removehashnode(HashTable ht, const char *nam)` from `Src/hashtable.c:275`.
350///
351// table and returns a pointer to it. If there // c:275
352// is no such node, then it returns NULL // c:275
353/// C body removes the node from the bucket chain and returns the
354/// removed pointer (or NULL). Rust `HashMap::remove` has the
355/// matching shape.
356pub fn removehashnode<T>(ht: &mut HashMap<String, T>, nam: &str) -> Option<T> {
357 // c:275
358 ht.remove(nam)
359}
360
361/// Port of `disablehashnode(HashNode hn, UNUSED(int flags))` from `Src/hashtable.c:323`.
362///
363/// C body: `hn->flags |= DISABLED;`. Generic helper that flips
364/// the DISABLED bit on the named entry via [`HashNodeFlags`].
365pub fn disablehashnode<T: HashNodeFlags>(hn: &mut HashMap<String, T>, flags: &str) -> bool {
366 hn.get_mut(flags)
367 .map(|node| {
368 node.set_disabled(true);
369 true
370 })
371 .unwrap_or(false) // c:323
372}
373
374impl shfunc_table {
375 /// `new` — see implementation.
376 pub fn new() -> Self {
377 Self {
378 table: HashMap::new(),
379 }
380 }
381 /// `snapshot` — clone the internal `HashMap<String, Box<shfunc>>`
382 /// for subshell save/restore. Used by `subshell_begin` to capture
383 /// the parent's function set before the subshell body runs, so
384 /// `subshell_end` can restore it (matches C fork-copy semantics
385 /// at `Src/exec.c::entersubsh`).
386 pub fn snapshot(&self) -> std::sync::Arc<HashMap<String, Box<shfunc>>> {
387 std::sync::Arc::new(self.table.clone())
388 }
389 /// `restore` — replace the internal table with a saved snapshot.
390 /// Called by `subshell_end` after the subshell body completes.
391 /// Takes the `Arc`-shared snapshot stored in `SubshellSnapshot`;
392 /// unwraps in place when uniquely owned (the common case), else
393 /// clones out of the shared handle.
394 pub fn restore(&mut self, snap: std::sync::Arc<HashMap<String, Box<shfunc>>>) {
395 self.table = std::sync::Arc::try_unwrap(snap).unwrap_or_else(|arc| (*arc).clone());
396 }
397 /// `add` — see implementation.
398 pub fn add(&mut self, func: shfunc) -> Option<shfunc> {
399 self.table
400 .insert(func.node.nam.clone(), Box::new(func))
401 .map(|b| *b)
402 }
403 /// `get` — see implementation.
404 pub fn get(&self, name: &str) -> Option<&shfunc> {
405 self.table
406 .get(name)
407 .map(|b| b.as_ref())
408 .filter(|f| (f.node.flags & DISABLED as i32) == 0)
409 }
410 /// `get_including_disabled` — see implementation.
411 pub fn get_including_disabled(&self, name: &str) -> Option<&shfunc> {
412 self.table.get(name).map(|b| b.as_ref())
413 }
414 /// `get_mut` — see implementation.
415 pub fn get_mut(&mut self, name: &str) -> Option<&mut shfunc> {
416 self.table
417 .get_mut(name)
418 .map(|b| b.as_mut())
419 .filter(|f| (f.node.flags & DISABLED as i32) == 0)
420 }
421 /// `remove` — see implementation.
422 pub fn remove(&mut self, name: &str) -> Option<shfunc> {
423 self.table.remove(name).map(|b| *b)
424 }
425 /// `contains_key` — see implementation.
426 pub fn contains_key(&self, name: &str) -> bool {
427 self.table.contains_key(name)
428 }
429
430 /// Port of C's `HashTable.addnode` GSU function pointer
431 /// (`Src/zsh.h:281+`). Takes a `*mut shfunc` (typedef `Shfunc`)
432 /// previously obtained via `Box::into_raw` — reclaims ownership
433 /// into the table by name. After this call, the caller's `shf`
434 /// pointer is INVALIDATED in the Rust ownership sense; subsequent
435 /// reads must go through `getnode(name)` to get a fresh pointer.
436 /// In practice, C code re-uses the same `shf` pointer because the
437 /// Box stays at the same heap address — we keep that semantic by
438 /// boxing-on-heap. Replaces any prior entry with the same name
439 /// (matching C `addnode`'s overwrite-and-free-old behavior).
440 pub fn addnode(&mut self, shf: *mut shfunc) {
441 if shf.is_null() {
442 return;
443 }
444 let boxed = unsafe { Box::from_raw(shf) };
445 let name = boxed.node.nam.clone();
446 let _ = self.table.insert(name, boxed);
447 }
448
449 /// Port of C's `HashTable.getnode` GSU. Returns the raw `Shfunc`
450 /// pointer (typedef `*mut shfunc`) or null if missing or disabled.
451 /// Pointer stays valid as long as the underlying `Box<shfunc>`
452 /// lives in the table (i.e. until `remove`/`addnode`-overwrite).
453 pub fn getnode(&self, name: &str) -> *mut shfunc {
454 self.table
455 .get(name)
456 .filter(|b| (b.node.flags & DISABLED as i32) == 0)
457 .map(|b| b.as_ref() as *const shfunc as *mut shfunc)
458 .unwrap_or(std::ptr::null_mut())
459 }
460
461 /// Port of C's `HashTable.getnode2` GSU — same as `getnode` but
462 /// returns disabled nodes too. Used by `unhash`/`enable -f` paths.
463 pub fn getnode2(&self, name: &str) -> *mut shfunc {
464 self.table
465 .get(name)
466 .map(|b| b.as_ref() as *const shfunc as *mut shfunc)
467 .unwrap_or(std::ptr::null_mut())
468 }
469 /// `disable` — see implementation.
470 pub fn disable(&mut self, name: &str) -> bool {
471 if let Some(func) = self.table.get_mut(name) {
472 func.node.flags |= DISABLED as i32;
473 true
474 } else {
475 false
476 }
477 }
478 /// `enable` — see implementation.
479 pub fn enable(&mut self, name: &str) -> bool {
480 if let Some(func) = self.table.get_mut(name) {
481 func.node.flags &= !(DISABLED as i32);
482 true
483 } else {
484 false
485 }
486 }
487 /// `len` — see implementation.
488 pub fn len(&self) -> usize {
489 self.table.len()
490 }
491 /// `is_empty` — see implementation.
492 pub fn is_empty(&self) -> bool {
493 self.table.is_empty()
494 }
495 /// `iter` — see implementation.
496 pub fn iter(&self) -> impl Iterator<Item = (&String, &shfunc)> {
497 self.table.iter().map(|(k, b)| (k, b.as_ref()))
498 }
499 /// `iter_sorted` — see implementation.
500 pub fn iter_sorted(&self) -> Vec<(&String, &shfunc)> {
501 let mut entries: Vec<(&String, &shfunc)> =
502 self.table.iter().map(|(k, b)| (k, b.as_ref())).collect();
503 entries.sort_by(|a, b| a.0.cmp(b.0));
504 entries
505 }
506 /// `clear` — see implementation.
507 pub fn clear(&mut self) {
508 self.table.clear();
509 }
510}
511
512impl Default for shfunc_table {
513 fn default() -> Self {
514 Self::new()
515 }
516}
517
518// `reswdToken` enum deleted — Rust-only enum duplicating the
519// canonical `lextok` i32 token constants already in zsh_h.rs
520// (BANG_TOK/DINBRACK/INBRACE_TOK/OUTBRACE_TOK/CASE/COPROC/DOLOOP
521// /DONE/ELIF/ELSE/ZEND/ESAC/FI/FOR/FOREACH/FUNC/IF/NOCORRECT/
522// REPEAT/SELECT/THEN/TIME/UNTIL/WHILE/TYPESET at zsh.h:345-371).
523// reswd.token now stores the raw i32 lextok matching C `struct
524// reswd { HashNode node; int token; }` at zsh.h:1246-1249.
525
526// `reswd` struct + impl deleted — Rust-only duplicate of canonical
527// `crate::ported::zsh_h::reswd` (zsh.h:1246-1249). The canonical
528// has `node: hashnode { nam, flags, next }` + `token: i32`; the
529// Rust-only had `name, flags: u32, token: i32` (missing the
530// hashnode embedding). Type alias surfaces the canonical struct
531// to in-file callers and external imports.
532// c:1246
533
534/// Public copy of the canonical `reswds[]` table from
535/// `Src/hashtable.c:1076-1108`. Each entry is `(name, lextok)`; the
536/// token identifies which grammar production the word triggers.
537///
538/// Callers outside the hashtable (LSP reflection dump, IntelliJ
539/// inventory) iterate this directly so they don't have to take the
540/// `reswdtab` lock or duplicate the list. Filtering: entries with
541/// `token == TYPESET` are declaration commands (local / typeset /
542/// declare / export / readonly / integer / float) — they're aliased
543/// to `typeset` at the grammar level but really live as builtins, so
544/// a "reserved word" inventory should exclude them.
545pub const RESWDS: &[(&str, i32)] = &[
546 ("!", BANG_TOK),
547 ("[[", DINBRACK),
548 ("{", INBRACE_TOK),
549 ("}", OUTBRACE_TOK),
550 ("case", CASE),
551 ("coproc", COPROC),
552 ("declare", TYPESET),
553 ("do", DOLOOP),
554 ("done", DONE),
555 ("elif", ELIF),
556 ("else", ELSE),
557 ("end", ZEND),
558 ("esac", ESAC),
559 ("export", TYPESET),
560 ("fi", FI),
561 ("float", TYPESET),
562 ("for", FOR),
563 ("foreach", FOREACH),
564 ("function", FUNC),
565 ("if", IF),
566 ("integer", TYPESET),
567 ("local", TYPESET),
568 ("nocorrect", NOCORRECT),
569 ("readonly", TYPESET),
570 ("repeat", REPEAT),
571 ("select", SELECT),
572 ("then", THEN),
573 ("time", TIME),
574 ("typeset", TYPESET),
575 ("until", UNTIL),
576 ("while", WHILE),
577];
578
579/// Port of `enablehashnode(HashNode hn, UNUSED(int flags))` from `Src/hashtable.c:332`.
580///
581/// C body: `hn->flags &= ~DISABLED;`. Inverse of [`disablehashnode`].
582pub fn enablehashnode<T: HashNodeFlags>(hn: &mut HashMap<String, T>, flags: &str) -> bool {
583 hn.get_mut(flags)
584 .map(|node| {
585 node.set_disabled(false);
586 true
587 })
588 .unwrap_or(false) // c:332
589}
590
591impl reswd_table {
592 /// `new` — see implementation.
593 pub fn new() -> Self {
594 let mut table = HashMap::new();
595
596 // Direct port of `static struct reswd reswds[]` at
597 // Src/hashtable.c:1076-1108. Token IDs are the lextok
598 // constants from zsh_h.rs (zsh.h:345-371).
599 //
600 // Same list is exposed via the public `RESWDS` const below so
601 // callers outside this module (LSP reflection dump, IntelliJ
602 // tool-window inventory) can enumerate reserved words without
603 // taking the table lock.
604 let words: [(&str, i32); 31] = [
605 // c:1076
606 ("!", BANG_TOK), // c:1077
607 ("[[", DINBRACK), // c:1078
608 ("{", INBRACE_TOK), // c:1079
609 ("}", OUTBRACE_TOK), // c:1080
610 ("case", CASE), // c:1081
611 ("coproc", COPROC), // c:1082
612 ("declare", TYPESET), // c:1083
613 ("do", DOLOOP), // c:1084
614 ("done", DONE), // c:1085
615 ("elif", ELIF), // c:1086
616 ("else", ELSE), // c:1087
617 ("end", ZEND), // c:1088
618 ("esac", ESAC), // c:1089
619 ("export", TYPESET), // c:1090
620 ("fi", FI), // c:1091
621 ("float", TYPESET), // c:1092
622 ("for", FOR), // c:1093
623 ("foreach", FOREACH), // c:1094
624 ("function", FUNC), // c:1095
625 ("if", IF), // c:1096
626 ("integer", TYPESET), // c:1097
627 ("local", TYPESET), // c:1098
628 ("nocorrect", NOCORRECT), // c:1099
629 ("readonly", TYPESET), // c:1100
630 ("repeat", REPEAT), // c:1101
631 ("select", SELECT), // c:1102
632 ("then", THEN), // c:1103
633 ("time", TIME), // c:1104
634 ("typeset", TYPESET), // c:1105
635 ("until", UNTIL), // c:1106
636 ("while", WHILE), // c:1107
637 ];
638 // Sanity: the local `words` array and the public `RESWDS` const
639 // below MUST stay in sync — both are direct ports of the same
640 // upstream `reswds[]` table at Src/hashtable.c:1076-1108.
641 debug_assert_eq!(words.len(), RESWDS.len());
642
643 for (name, token) in words {
644 // Direct struct literal — canonical `reswd` has
645 // `node: hashnode` (zsh.h:1246) so we build the
646 // embedded hashnode inline. Mirrors C `{{NULL,
647 // "if", 0}, IF}` at hashtable.c:1077+.
648 table.insert(
649 name.to_string(),
650 reswd {
651 node: hashnode {
652 next: None,
653 nam: name.to_string(),
654 flags: 0,
655 },
656 token,
657 },
658 );
659 }
660
661 Self { table }
662 }
663 /// `get` — see implementation.
664 pub fn get(&self, name: &str) -> Option<&reswd> {
665 self.table
666 .get(name)
667 .filter(|r| (r.node.flags & DISABLED as i32) == 0)
668 }
669 /// `get_including_disabled` — see implementation.
670 pub fn get_including_disabled(&self, name: &str) -> Option<&reswd> {
671 self.table.get(name)
672 }
673 /// `disable` — see implementation.
674 pub fn disable(&mut self, name: &str) -> bool {
675 if let Some(rw) = self.table.get_mut(name) {
676 rw.node.flags |= DISABLED as i32;
677 true
678 } else {
679 false
680 }
681 }
682 /// `enable` — see implementation.
683 pub fn enable(&mut self, name: &str) -> bool {
684 if let Some(rw) = self.table.get_mut(name) {
685 rw.node.flags &= !(DISABLED as i32);
686 true
687 } else {
688 false
689 }
690 }
691 /// `is_reserved` — see implementation.
692 pub fn is_reserved(&self, name: &str) -> bool {
693 self.get(name).is_some()
694 }
695 /// `iter` — see implementation.
696 pub fn iter(&self) -> impl Iterator<Item = (&String, &reswd)> {
697 self.table.iter()
698 }
699 /// Port of `addhashnode(HashTable ht, char *nam, void *nodeptr)`
700 /// from `Src/hashtable.c:157`. C stores `nodeptr` under `nam` and
701 /// frees any node it displaces (`ht->freenode(oldnode)`, c:161).
702 /// Here the map replaces the entry and the displaced `reswd` is
703 /// dropped, matching C's freenode semantics. Runtime companion to
704 /// the seed-only `new()`; param_private's `setup_` uses it to
705 /// register `private` as a TYPESET reserved word at module boot
706 /// (param_private.c:687 `reswdtab->addnode(reswdtab, ...)`).
707 pub fn insert(&mut self, name: &str, rw: reswd) {
708 // c:157 addhashnode → c:159 addhashnode2 sets hn->nam = nam
709 self.table.insert(name.to_string(), rw);
710 }
711 /// Port of `removehashnode(HashTable ht, const char *nam)` from
712 /// `Src/hashtable.c:275`. Unlinks the node keyed by `nam` and
713 /// returns it (C returns the removed `HashNode`, or NULL when the
714 /// key is absent — c:283). param_private's teardown uses it to
715 /// unregister the `private` reserved word (param_private.c:722
716 /// `removehashnode(reswdtab, "private")`).
717 pub fn remove(&mut self, name: &str) -> Option<reswd> {
718 // c:275
719 self.table.remove(name)
720 }
721}
722
723impl Default for reswd_table {
724 fn default() -> Self {
725 Self::new()
726 }
727}
728
729// `crate::ported::zsh_h::alias` struct + impl deleted — Rust-only duplicate of canonical
730// `crate::ported::zsh_h::alias` (zsh.h:1253-1257). The canonical
731// has `node: hashnode { nam, flags, next }` embedded (c:1254) +
732// `text: String` (c:1255) + `inuse: i32` (c:1256); the Rust-only
733// had a flat `name: String, flags: u32, text: String, inuse: i32`
734// (missing the hashnode embedding).
735
736/// Port of `static int hnamcmp(const void *ap, const void *bp)`
737/// from `Src/hashtable.c:341-346`. C body:
738/// ```c
739/// HashNode a = *(HashNode *)ap;
740/// HashNode b = *(HashNode *)bp;
741/// return ztrcmp(a->nam, b->nam);
742/// ```
743///
744/// `ztrcmp` is a META-AWARE compare that XORs Meta-escaped bytes
745/// with 32 before comparing (Src/utils.c:5106). The previous Rust
746/// port used `str::cmp` which does naive byte-wise lexicographic
747/// compare — for Meta-encoded hash-table keys this sorts them
748/// incorrectly (Meta byte 0x83 sorts AFTER ASCII printable but the
749/// real underlying byte 0x83^32=0xa3 should compare as a high byte).
750///
751/// Route through the canonical `crate::ported::utils::ztrcmp` so
752/// `functions`, `alias`, etc. sort their key listings the same way
753/// C does for Meta-encoded names.
754pub fn hnamcmp(ap: &str, bp: &str) -> std::cmp::Ordering {
755 ztrcmp(ap, bp) // c:345
756}
757
758/// Port of `scanmatchtable(HashTable ht, Patprog pprog, int sorted, int flags1, int flags2, ScanFunc scanfunc, int scanflags)` from `Src/hashtable.c:373`.
759///
760/// C body walks every node calling `func(node, scanflags)` if
761/// the node satisfies (a) optional pattern match, (b) `flags1`
762/// require-at-least-one, (c) `flags2` require-none-of. The
763/// `sorted` flag pre-sorts entries before scanning.
764///
765/// Rust port: same shape with closure callback. Returns the
766/// match count.
767/// WARNING: param names don't match C — Rust=() vs C=(ht, pprog, sorted, flags1, flags2, scanfunc, scanflags)
768pub fn scanmatchtable<T: HashNodeFlags, F: FnMut(&str, &T)>(
769 ht: &HashMap<String, T>,
770 pattern: Option<&str>,
771 sorted: bool,
772 flags1: u32,
773 flags2: u32,
774 mut func: F,
775) -> i32 {
776 let mut entries: Vec<(&String, &T)> = ht.iter().collect();
777 if sorted {
778 // c:400 — `qsort(hnsorttab, ct, sizeof(HashNode), hnamcmp);`
779 // hnamcmp routes through Meta-aware ztrcmp. The previous Rust
780 // port used `str::cmp` (naive byte-wise) which sorts Meta-
781 // encoded hash keys incorrectly. Use the canonical hnamcmp
782 // to match C's qsort comparator exactly.
783 entries.sort_by(|a, b| hnamcmp(a.0, b.0)); // c:400
784 }
785 let mut match_count = 0;
786 for (name, node) in entries {
787 if let Some(p) = pattern {
788 if !simple_glob_match(p, name) {
789 continue;
790 }
791 }
792 let f = node.flags();
793 if flags1 != 0 && (f & flags1) == 0 {
794 continue;
795 }
796 if flags2 != 0 && (f & flags2) != 0 {
797 continue;
798 }
799 func(name, node);
800 match_count += 1;
801 }
802 match_count
803}
804
805impl alias_table {
806 /// `new` — see implementation.
807 pub fn new() -> Self {
808 Self {
809 table: indexmap::IndexMap::new(),
810 }
811 }
812 /// `with_defaults` — see implementation.
813 pub fn with_defaults() -> Self {
814 let mut table = Self::new();
815 // C addaliasnode(aliastab, "run-help", createaliasnode("man", 0));
816 // at hashtable.c:1215-1216.
817 table.add(createaliasnode("run-help", "man", 0)); // c:1215
818 table.add(createaliasnode("which-command", "whence", 0)); // c:1216
819 table
820 }
821 /// `add` — see implementation.
822 pub fn add(&mut self, alias: alias) -> Option<alias> {
823 self.table.insert(alias.node.nam.clone(), alias)
824 }
825 /// `get` — see implementation.
826 pub fn get(&self, name: &str) -> Option<&alias> {
827 self.table
828 .get(name)
829 .filter(|a| (a.node.flags & DISABLED as i32) == 0)
830 }
831 /// `get_including_disabled` — see implementation.
832 pub fn get_including_disabled(&self, name: &str) -> Option<&alias> {
833 self.table.get(name)
834 }
835 /// `get_mut` — see implementation.
836 pub fn get_mut(&mut self, name: &str) -> Option<&mut alias> {
837 self.table
838 .get_mut(name)
839 .filter(|a| (a.node.flags & DISABLED as i32) == 0)
840 }
841 /// `remove` — see implementation.
842 pub fn remove(&mut self, name: &str) -> Option<alias> {
843 self.table.remove(name)
844 }
845 /// `disable` — see implementation.
846 pub fn disable(&mut self, name: &str) -> bool {
847 if let Some(alias) = self.table.get_mut(name) {
848 alias.node.flags |= DISABLED as i32;
849 true
850 } else {
851 false
852 }
853 }
854 /// `enable` — see implementation.
855 pub fn enable(&mut self, name: &str) -> bool {
856 if let Some(alias) = self.table.get_mut(name) {
857 alias.node.flags &= !(DISABLED as i32);
858 true
859 } else {
860 false
861 }
862 }
863 /// `len` — see implementation.
864 pub fn len(&self) -> usize {
865 self.table.len()
866 }
867 /// `is_empty` — see implementation.
868 pub fn is_empty(&self) -> bool {
869 self.table.is_empty()
870 }
871 /// `clear` — see implementation.
872 pub fn clear(&mut self) {
873 self.table.clear();
874 }
875 /// `iter` — see implementation.
876 pub fn iter(&self) -> impl Iterator<Item = (&String, &alias)> {
877 self.table.iter()
878 }
879 /// `iter_sorted` — see implementation.
880 pub fn iter_sorted(&self) -> Vec<(&String, &alias)> {
881 let mut entries: Vec<_> = self.table.iter().collect();
882 entries.sort_by(|a, b| a.0.cmp(b.0));
883 entries
884 }
885}
886
887impl Default for alias_table {
888 fn default() -> Self {
889 Self::new()
890 }
891}
892
893/// Port of `scanhashtable(HashTable ht, int sorted, int flags1, int flags2, ScanFunc scanfunc, int scanflags)` from `Src/hashtable.c:446`.
894///
895/// C body delegates to `scanmatchtable` with `pprog = NULL`. Rust
896/// port does the same.
897/// WARNING: param names don't match C — Rust=() vs C=(ht, sorted, flags1, flags2, scanfunc, scanflags)
898pub fn scanhashtable<T: HashNodeFlags, F: FnMut(&str, &T)>(
899 ht: &HashMap<String, T>,
900 sorted: bool,
901 flags1: u32,
902 flags2: u32,
903 func: F,
904) -> i32 {
905 scanmatchtable(ht, None, sorted, flags1, flags2, func)
906}
907
908/// Port of `expandhashtable(HashTable ht)` from `Src/hashtable.c:458`.
909///
910/// C grows the bucket array when load factor exceeds threshold.
911/// Rust HashMap rehashes automatically — calling reserve on the
912/// passed map gives the closest equivalent.
913/// Rust idiom replacement: `HashMap::reserve` covers the C
914/// `growhashtable` bucket-realloc + rehash loop.
915pub fn expandhashtable<T>(ht: &mut HashMap<String, T>) {
916 let want = ht.len() * 2;
917 ht.reserve(want.saturating_sub(ht.capacity()));
918}
919
920/// Port of `resizehashtable(HashTable ht, int newsize)` from `Src/hashtable.c:486`.
921///
922/// C reallocates buckets to a specific size. Rust HashMap reserves
923/// capacity to ensure at least `newsize` entries fit without rehash.
924/// Rust idiom replacement: `HashMap::reserve(need)` covers the C
925/// `realloc(hsize * sizeof(HashNode))` + rehash dance.
926pub fn resizehashtable<T>(ht: &mut HashMap<String, T>, newsize: i32) {
927 let need = newsize.max(0) as usize;
928 if need > ht.capacity() {
929 ht.reserve(need - ht.capacity());
930 }
931}
932
933// Generic method to empty a hash table // c:519
934/// Port of `emptyhashtable(HashTable ht)` from `Src/hashtable.c:519`.
935///
936/// C body: `resizehashtable(ht, ht->hsize);` — drop all nodes
937/// while keeping the bucket array. Rust HashMap::clear preserves
938/// capacity, matching the semantic.
939pub fn emptyhashtable<T>(ht: &mut HashMap<String, T>) {
940 // c:519
941 ht.clear();
942}
943
944// Print info about hash table // c:527
945/// Port of `printhashtabinfo(HashTable ht)` from `Src/hashtable.c:78`.
946///
947/// C body prints chain-length distribution stats for hash-table
948/// debug analysis (under ZSH_HASH_DEBUG). Rust HashMap doesn't
949/// expose chain-length info; emit count + capacity which is the
950/// equivalent visibility.
951/// Rust idiom replacement: HashMap's open addressing doesn't expose
952/// chain length, so we emit name+capacity+len — the equivalent
953/// visibility under Rust's std::collections backend.
954/// WARNING: param names don't match C — Rust=(name, ht) vs C=(ht)
955pub fn printhashtabinfo<T>(name: &str, ht: &HashMap<String, T>) -> String {
956 // c:78
957 format!(
958 "name of table : {}\nsize of nodes[] : {}\nnumber of nodes : {}",
959 name,
960 ht.capacity(),
961 ht.len()
962 )
963}
964
965/// Port of `bin_hashinfo(UNUSED(char *nam), UNUSED(char **args), UNUSED(Options ops), UNUSED(int func))` from `Src/hashtable.c:566`.
966///
967/// C iterates all registered hashtables (cmdnamtab, shfunctab,
968/// aliastab, etc.) and emits stats for each. Rust port walks the
969/// known-singleton tables.
970pub fn bin_hashinfo(
971 _nam: &str,
972 _args: &[String], // c:566
973 _ops: &options,
974 _func: i32,
975) -> i32 {
976 let banner = "----------------------------------------------------";
977 println!("{}", banner);
978 {
979 let tab = cmdnamtab_lock().read().expect("cmdnamtab poisoned");
980 println!("name of table : cmdnamtab");
981 println!("number of nodes : {}", tab.len());
982 }
983 println!("{}", banner);
984 {
985 let tab = shfunctab_lock().read().expect("shfunctab poisoned");
986 println!("name of table : shfunctab");
987 println!("number of nodes : {}", tab.len());
988 }
989 println!("{}", banner);
990 {
991 let tab = aliastab_lock().read().expect("aliastab poisoned");
992 println!("name of table : aliastab");
993 println!("number of nodes : {}", tab.len());
994 }
995 println!("{}", banner);
996 0
997}
998
999// Old fake `dircache_lock(Mutex<HashMap<String, i32>>)` deleted —
1000// wrong shape (C uses `struct dircache_entry { name, refs }` not
1001// `HashMap<String, i32>`). Canonical port lives earlier in this
1002// file at the `dircache_entry` struct + `dircache_lock` accessor
1003// returning `Mutex<Vec<dircache_entry>>`.
1004
1005/// Port of `createcmdnamtable()` from `Src/hashtable.c:601`.
1006///
1007/// C body sets up the cmdnamtab GSU vtable (hash, addnode,
1008/// removenode, freenode, printnode = printcmdnamnode). Rust port
1009/// just touches the singleton to ensure it's initialised.
1010pub fn createcmdnamtable() {
1011 let _ = cmdnamtab_lock();
1012}
1013
1014/// Port of `emptycmdnamtable(HashTable ht)` from `Src/hashtable.c:623`.
1015///
1016/// C body:
1017/// ```c
1018/// emptyhashtable(ht);
1019/// pathchecked = path;
1020/// ```
1021///
1022/// Drops every PATH cache entry (used by `hash -r`) and resets
1023/// the per-PATH-entry "checked" cursor so subsequent lookups
1024/// re-scan from the start.
1025/// WARNING: param names don't match C — Rust=() vs C=(ht)
1026pub fn emptycmdnamtable() {
1027 // c:1015 — `emptyhashtable(ht);`
1028 cmdnamtab_lock()
1029 .write()
1030 .expect("cmdnamtab poisoned")
1031 .clear();
1032 // c:1016 — `pathchecked = path;`. Resetting the cursor here (not in
1033 // each caller) is what C does: every caller that empties the table
1034 // must also allow a subsequent `fillcmdnamtable` to re-walk PATH
1035 // from the start. Without this, emptying the table (e.g. a `PATH`
1036 // reassignment) left `pathchecked` exhausted, so the next
1037 // `${(k)commands}` / `compadd -k commands` scan refilled nothing.
1038 pathchecked.store(0, std::sync::atomic::Ordering::SeqCst);
1039}
1040
1041/// Port of `hashdir(char **dirp)` from `Src/hashtable.c:634`.
1042///
1043/// C body opendir's the directory, reads each entry, and adds
1044/// any executable to `cmdnamtab` (skipping names already present
1045/// from earlier PATH entries). Rust port routes through
1046/// `cmdnam_table::hash_dir`.
1047/// Rust idiom replacement: pure delegation to `hash_dir` on the
1048/// typed `CmdNamTable`; the C opendir/readdir/executable-test loop
1049/// lives there with `fs::read_dir` + `is_executable_via_metadata`.
1050/// WARNING: param names don't match C — Rust=(dir, dir_index) vs C=(dirp)
1051pub fn hashdir(dir: &str, dir_index: usize) {
1052 cmdnamtab_lock()
1053 .write()
1054 .expect("cmdnamtab poisoned")
1055 .hash_dir(dir, dir_index);
1056}
1057
1058/// Port of `fillcmdnamtable(UNUSED(HashTable ht))` from `Src/hashtable.c:712`.
1059///
1060/// C body:
1061/// ```c
1062/// for (pq = pathchecked; *pq; pq++) hashdir(pq);
1063/// pathchecked = pq;
1064/// ```
1065///
1066/// Walks every PATH entry calling `hashdir` for each. The
1067/// `pathchecked` cursor is updated so subsequent calls don't
1068/// re-walk PATH entries that were already scanned.
1069/// WARNING: param names don't match C — Rust=(path) vs C=(ht)
1070pub fn fillcmdnamtable(path: &[String]) {
1071 // c:716 — `for (pq = pathchecked; *pq; pq++) hashdir(pq);`. Start
1072 // from the cursor, NOT index 0: dirs already walked by an earlier
1073 // fill (or by `hashcmd`, which bumps `pathchecked`) must not be
1074 // re-scanned. Re-filling from 0 on every call made
1075 // `${(k)commands}` / `compadd -k commands` return the entire PATH
1076 // even when `pathchecked` was exhausted and the table had been
1077 // emptied — diverging from zsh, whose scan yields the current
1078 // (possibly empty) table. Symptom: `l<TAB>` listed all 230 PATH
1079 // commands vs zsh's 5 builtins.
1080 use std::sync::atomic::Ordering;
1081 let from = pathchecked.load(Ordering::SeqCst);
1082 if from < path.len() {
1083 let mut tab = cmdnamtab_lock().write().expect("cmdnamtab poisoned");
1084 for idx in from..path.len() {
1085 tab.hash_dir(&path[idx], idx);
1086 }
1087 }
1088 // c:719 — `pathchecked = pq;` — cursor advances to the end.
1089 pathchecked.store(path.len(), Ordering::SeqCst);
1090}
1091
1092/// Port of `freecmdnamnode(HashNode hn)` from `Src/hashtable.c:724`.
1093///
1094/// C body frees the entry's name + (if HASHED) cached path. Rust
1095/// port: drop runs both when the entry is removed from the table.
1096/// This helper performs the removal to trigger Drop.
1097pub fn freecmdnamnode(hn: &str) {
1098 cmdnamtab_lock()
1099 .write()
1100 .expect("cmdnamtab poisoned")
1101 .remove(hn);
1102}
1103
1104/// Port of `printcmdnamnode(HashNode hn, int printflags)` from `Src/hashtable.c:739`.
1105///
1106/// Emits one cmdnamtab entry for `hash` / `whence`. Each branch
1107/// returns; PRINT_LIST falls through to the tail that emits
1108/// `quotedzputs(nam) '=' quotedzputs(u.cmd|*u.name '/' nam) '\n'`.
1109pub fn printcmdnamnode(hn: &cmdnam, printflags: i32) {
1110 // c:741 — `Cmdnam cn = (Cmdnam) hn;` — Rust types give us cmdnam.
1111
1112 // c:743-747 — PRINT_WHENCE_WORD branch.
1113 if (printflags & PRINT_WHENCE_WORD) != 0 {
1114 // c:744-745 — `printf("%s: %s\n", nam, HASHED ? "hashed" : "command");`
1115 let kind = if (hn.node.flags & HASHED as i32) != 0 {
1116 "hashed"
1117 } else {
1118 "command"
1119 };
1120 println!("{}: {}", hn.node.nam, kind); // c:744
1121 return; // c:746
1122 }
1123
1124 // c:749-760 — PRINT_WHENCE_CSH | PRINT_WHENCE_SIMPLE branch.
1125 if (printflags & (PRINT_WHENCE_CSH | PRINT_WHENCE_SIMPLE)) != 0 {
1126 let mut so = io::stdout();
1127 if (hn.node.flags & HASHED as i32) != 0 {
1128 // c:750
1129 // c:751-752 — `zputs(u.cmd, stdout); putchar('\n');`
1130 if let Some(cmd) = &hn.cmd {
1131 let _ = zputs(cmd, &mut so); // c:751
1132 }
1133 println!(); // c:752
1134 } else {
1135 // c:753
1136 // c:754-757 — `zputs(*u.name); putchar('/'); zputs(nam); putchar('\n');`
1137 if let Some(name_arr) = &hn.name {
1138 if let Some(first) = name_arr.first() {
1139 let _ = zputs(first, &mut so); // c:754
1140 }
1141 }
1142 print!("/"); // c:755
1143 let _ = zputs(&hn.node.nam, &mut so); // c:756
1144 println!(); // c:757
1145 }
1146 return; // c:759
1147 }
1148
1149 // c:762-777 — PRINT_WHENCE_VERBOSE branch.
1150 if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
1151 let mut so = io::stdout();
1152 if (hn.node.flags & HASHED as i32) != 0 {
1153 // c:763
1154 // c:764-767 — `nicezputs(nam); printf(" is hashed to "); nicezputs(u.cmd); putchar('\n');`
1155 let _ = nicezputs(&hn.node.nam, &mut so); // c:764
1156 print!(" is hashed to "); // c:765
1157 if let Some(cmd) = &hn.cmd {
1158 let _ = nicezputs(cmd, &mut so); // c:766
1159 }
1160 println!(); // c:767
1161 } else {
1162 // c:768
1163 // c:769-774 — `nicezputs(nam); printf(" is "); nicezputs(*u.name); putchar('/'); nicezputs(nam); putchar('\n');`
1164 let _ = nicezputs(&hn.node.nam, &mut so); // c:769
1165 print!(" is "); // c:770
1166 if let Some(name_arr) = &hn.name {
1167 if let Some(first) = name_arr.first() {
1168 let _ = nicezputs(first, &mut so); // c:771
1169 }
1170 }
1171 print!("/"); // c:772
1172 let _ = nicezputs(&hn.node.nam, &mut so); // c:773
1173 println!(); // c:774
1174 }
1175 return; // c:776
1176 }
1177
1178 // c:779-784 — PRINT_LIST prefix block; falls through to the tail.
1179 if (printflags & PRINT_LIST) != 0 {
1180 // c:779
1181 print!("hash "); // c:780
1182 // c:782-783 — `-- ` for names starting with `-`.
1183 if hn.node.nam.starts_with('-') {
1184 // c:782
1185 print!("-- "); // c:783
1186 }
1187 }
1188
1189 // c:786-798 — common tail. HASHED uses u.cmd, !HASHED splices first
1190 // u.name PATH segment + '/' + nam.
1191 if (hn.node.flags & HASHED as i32) != 0 {
1192 // c:786
1193 print!("{}", quotedzputs(&hn.node.nam)); // c:787
1194 print!("="); // c:788
1195 if let Some(cmd) = &hn.cmd {
1196 print!("{}", quotedzputs(cmd)); // c:789
1197 }
1198 println!(); // c:790
1199 } else {
1200 // c:791
1201 print!("{}", quotedzputs(&hn.node.nam)); // c:792
1202 print!("="); // c:793
1203 if let Some(name_arr) = &hn.name {
1204 if let Some(first) = name_arr.first() {
1205 print!("{}", quotedzputs(first)); // c:794
1206 }
1207 }
1208 print!("/"); // c:795
1209 print!("{}", quotedzputs(&hn.node.nam)); // c:796
1210 println!(); // c:797
1211 }
1212}
1213
1214/// Port of `createshfunctable()` from `Src/hashtable.c:812`.
1215///
1216/// C body:
1217/// ```c
1218/// shfunctab = newhashtable(7, "shfunctab", NULL);
1219/// shfunctab->hash = hasher;
1220/// shfunctab->cmpnodes = strcmp;
1221/// shfunctab->addnode = addhashnode;
1222/// shfunctab->getnode = gethashnode;
1223/// shfunctab->getnode2 = gethashnode2;
1224/// shfunctab->removenode = removeshfuncnode;
1225/// shfunctab->disablenode = disableshfuncnode;
1226/// shfunctab->enablenode = enableshfuncnode;
1227/// shfunctab->freenode = freeshfuncnode;
1228/// shfunctab->printnode = printshfuncnode;
1229/// ```
1230///
1231/// Rust port: idempotent — touching the OnceLock initialises the
1232/// singleton on first call. The GSU function-pointer assignments
1233/// from C are encoded as the free-fn names below (each callable
1234/// directly without a vtable lookup).
1235pub fn createshfunctable() {
1236 let _ = shfunctab_lock();
1237}
1238
1239/// Port of `removeshfuncnode(UNUSED(HashTable ht), const char *nam)` from `Src/hashtable.c:836`.
1240///
1241/// C body:
1242/// ```c
1243/// if (!strncmp(nam, "TRAP", 4) && (sigidx = getsigidx(nam + 4)) != -1)
1244/// hn = removetrap(sigidx);
1245/// else
1246/// hn = removehashnode(shfunctab, nam);
1247/// return hn;
1248/// ```
1249///
1250/// Drops the named function from `shfunctab`. If the name is a
1251/// `TRAP<sig>` form, also clears the trap via signals.rs.
1252/// Returns the removed function (or None if absent).
1253/// WARNING: param names don't match C — Rust=(nam) vs C=(ht, nam)
1254pub fn removeshfuncnode(nam: &str) -> Option<shfunc> {
1255 if let Some(sig_part) = nam.strip_prefix("TRAP") {
1256 if let Some(sig) = getsigidx(sig_part) {
1257 removetrap(sig);
1258 }
1259 }
1260 shfunctab_lock()
1261 .write()
1262 .expect("shfunctab poisoned")
1263 .remove(nam)
1264}
1265
1266/// Port of `disableshfuncnode(HashNode hn, UNUSED(int flags))` from `Src/hashtable.c:855`.
1267///
1268/// C body:
1269/// ```c
1270/// hn->flags |= DISABLED;
1271/// if (!strncmp(hn->nam, "TRAP", 4)) {
1272/// int sigidx = getsigidx(hn->nam + 4);
1273/// if (sigidx != -1) {
1274/// sigtrapped[sigidx] &= ~ZSIG_FUNC;
1275/// unsettrap(sigidx);
1276/// }
1277/// }
1278/// ```
1279///
1280/// Sets the DISABLED flag on the function entry; for TRAP*
1281/// functions, also unsettraps the corresponding signal so the
1282/// shell stops invoking the (now-disabled) trap.
1283/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, flags)
1284pub fn disableshfuncnode(hn: &str) {
1285 {
1286 let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
1287 tab.disable(hn);
1288 }
1289 if let Some(sig_part) = hn.strip_prefix("TRAP") {
1290 if let Some(sig) = getsigidx(sig_part) {
1291 unsettrap(sig);
1292 }
1293 }
1294}
1295
1296/// Port of `enableshfuncnode(HashNode hn, UNUSED(int flags))` from `Src/hashtable.c:873`.
1297///
1298/// C body:
1299/// ```c
1300/// shf->node.flags &= ~DISABLED;
1301/// if (!strncmp(shf->node.nam, "TRAP", 4)) {
1302/// int sigidx = getsigidx(shf->node.nam + 4);
1303/// if (sigidx != -1) settrap(sigidx, NULL, ZSIG_FUNC);
1304/// }
1305/// ```
1306///
1307/// Clears the DISABLED flag; for TRAP* functions, re-installs
1308/// the signal handler with `ZSIG_FUNC` semantics so the shell
1309/// dispatches the trap function on the next signal delivery.
1310/// WARNING: param names don't match C — Rust=(hn) vs C=(hn, flags)
1311pub fn enableshfuncnode(hn: &str) {
1312 {
1313 let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
1314 tab.enable(hn);
1315 }
1316 if let Some(sig_part) = hn.strip_prefix("TRAP") {
1317 if let Some(sig) = getsigidx(sig_part) {
1318 // c:882 — `settrap(sigidx, NULL, ZSIG_FUNC)`. The TRAPxxx
1319 // function body resolves through shfunctab at dispatch
1320 // (`gettrapnode`), not via the trap arrays directly.
1321 let _ = settrap(sig, None, ZSIG_FUNC);
1322 // c:Src/signals.c::settrap → unsettrap → removetrap also
1323 // clears any previously-registered string-form trap for
1324 // the same signal (single-slot sigtrapped[] array). The
1325 // zshrs port stores string-form bodies in a separate
1326 // `traps_table` HashMap that `removetrap` doesn't touch,
1327 // so the string body survives the function-form
1328 // registration and BOTH fire on the next signal. Drop
1329 // the string-form entry here so dotrap's
1330 // `traps_table` fallback doesn't double-dispatch. Bug
1331 // #541 in docs/BUGS.md.
1332 if let Ok(mut t) = crate::ported::builtin::traps_table().lock() {
1333 t.remove(sig_part);
1334 }
1335 }
1336 }
1337}
1338
1339/// Port of `freeshfuncnode(HashNode hn)` from `Src/hashtable.c:888`.
1340///
1341/// C body frees the function name, body Eprog, redir Eprog,
1342/// filename string, and sticky options struct. Rust port: drop
1343/// runs all of this when the entry is removed; this helper just
1344/// removes from the table to trigger the drop chain.
1345/// Rust idiom replacement: `HashMap::remove` triggers the `Box<T>`
1346/// drop cascade — same teardown as the C zfree chain, automated.
1347pub fn freeshfuncnode(hn: &str) {
1348 shfunctab_lock()
1349 .write()
1350 .expect("shfunctab poisoned")
1351 .remove(hn);
1352}
1353
1354/// Port of `printshfuncnode(HashNode hn, int printflags)` from `Src/hashtable.c:914`.
1355///
1356/// Emits one shfunctab entry for `functions` / `whence` / `typeset -f`.
1357/// PRINT_NAMEONLY and the PRINT_WHENCE_* variants return early; the
1358/// default body emits the full re-parseable `name () { body }` form
1359/// including autoload-stub, traced markers, and trailing redirections.
1360pub fn printshfuncnode(hn: &shfunc, printflags: i32) {
1361
1362 // c:916 — `Shfunc f = (Shfunc) hn;` — Rust types give us shfunc.
1363 // c:917 — `char *t = 0;` — declared but only used by the funcdef/redir
1364 // branches; Rust scope-locals the `t` binding inside each branch.
1365
1366 // c:919-925 — PRINT_NAMEONLY (or PRINT_WHENCE_SIMPLE without FUNCDEF):
1367 // `zputs(nam); putchar('\n'); return;`
1368 if (printflags & PRINT_NAMEONLY) != 0
1369 || ((printflags & PRINT_WHENCE_SIMPLE) != 0 && (printflags & PRINT_WHENCE_FUNCDEF) == 0)
1370 {
1371 let mut so = io::stdout();
1372 let _ = zputs(&hn.node.nam, &mut so); // c:922
1373 println!(); // c:923
1374 return; // c:924
1375 }
1376
1377 // c:927-944 — PRINT_WHENCE_VERBOSE | PRINT_WHENCE_WORD (without FUNCDEF):
1378 // nicezputs(nam) ":" function | " is an autoload shell function" | " is a shell function"
1379 // [" from " quotedzputs(filename) [(PM_LOADDIR) "/" quotedzputs(nam)]] '\n'
1380 if (printflags & (PRINT_WHENCE_VERBOSE | PRINT_WHENCE_WORD)) != 0
1381 && (printflags & PRINT_WHENCE_FUNCDEF) == 0
1382 {
1383 let mut so = io::stdout();
1384 let _ = nicezputs(&hn.node.nam, &mut so); // c:929
1385 // c:930-933 — printf one of three strings via nested ternary.
1386 let msg = if (printflags & PRINT_WHENCE_WORD) != 0 {
1387 ": function" // c:930
1388 } else if (hn.node.flags & PM_UNDEFINED as i32) != 0 {
1389 " is an autoload shell function" // c:932
1390 } else {
1391 " is a shell function" // c:933
1392 };
1393 print!("{}", msg);
1394 // c:934-941 — verbose-with-filename suffix.
1395 if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
1396 if let Some(filename) = &hn.filename {
1397 // c:934
1398 print!(" from "); // c:935
1399 print!("{}", quotedzputs(filename)); // c:936
1400 if (hn.node.flags & PM_LOADDIR as i32) != 0 {
1401 // c:937
1402 print!("/"); // c:938
1403 print!("{}", quotedzputs(&hn.node.nam)); // c:939
1404 }
1405 }
1406 }
1407 println!(); // c:942
1408 return; // c:943
1409 }
1410
1411 // c:946 — `quotedzputs(nam, stdout);`
1412 print!("{}", quotedzputs(&hn.node.nam));
1413
1414 // c:947-987 — funcdef-present branch (or PM_UNDEFINED stub) vs empty `() { }`.
1415 // RUST-ONLY EXTENSION: zshrs's shfunc carries a raw `body: Option<String>`
1416 // alongside (or instead of) the compiled `funcdef: Eprog` (see
1417 // `shfunc` doc at zsh_h.rs:670). The fusevm compile path stores the
1418 // body source there (parse.rs:7118 + parse.rs:1787) but never builds
1419 // a C-shaped Eprog — `funcdef` stays None. C zsh's getpermtext walks
1420 // the wordcode-Eprog; in zshrs we fall back to `body` text directly
1421 // when funcdef is absent. Without this, `functions NAME` prints
1422 // `f () { }` for every user-defined function.
1423 let has_body_source = hn.body.as_deref().is_some_and(|b| !b.is_empty());
1424 if hn.funcdef.is_some() || has_body_source || (hn.node.flags & PM_UNDEFINED as i32) != 0 {
1425 // c:947
1426 print!(" () {{\n"); // c:948
1427 let _ = zoutputtab(&mut io::stdout()); // c:949
1428 // c:950-954 — `# undefined` marker or getpermtext body.
1429 let mut t: Option<String>;
1430 if (hn.node.flags & PM_UNDEFINED as i32) != 0 {
1431 // c:950
1432 println!(
1433 "{} undefined",
1434 hashchar.load(Ordering::Relaxed) as u8 as char
1435 ); // c:951
1436 let _ = zoutputtab(&mut io::stdout()); // c:952
1437 t = None;
1438 } else if let Some(fd) = hn.funcdef.as_ref() {
1439 // c:953
1440 t = Some(getpermtext(fd.clone(), None, 1)); // c:954
1441 } else {
1442 // Rust-only fallback: emit `body` text directly. C's
1443 // getpermtext walks the wordcode-Eprog and emits
1444 // canonicalized statement-per-line text. We don't have
1445 // the Eprog, so we normalize the captured raw body
1446 // source: strip a leading `{` + ws, trailing `}` + ws,
1447 // and trailing `;` before the `}`. These appear when
1448 // par_simple's body_argv path (parse.rs:7041) reuses
1449 // the raw input slice that still includes the framing
1450 // braces; par_funcdef's brace path strips them but the
1451 // short-form `name() { body }` path can capture the
1452 // closing `}` because cmdpos vs. cmdpos confusion
1453 // makes the `}` lex as STRING_LEX, missing the
1454 // OUTBRACE_TOK arm at parse.rs:7113.
1455 // c:Src/text.c gettext2 — C zsh re-emits function bodies
1456 // from parsed wordcode (`getpermtext`) with `\n\t` between
1457 // sibling statements AND recursive indenting of nested
1458 // function definitions. zshrs stores raw source (no Eprog
1459 // for shfunc bodies); the closure below applies the same
1460 // canonicalization at print time.
1461 // Bug #197 (top-level statements) + #124 (nested fns) in
1462 // docs/BUGS.md.
1463 //
1464 // canonicalize: walk char-by-char tracking quote state +
1465 // brace/paren depth. At brace_depth == 0:
1466 // - top-level `;` (or `; `) becomes `\n\t` * (depth+1)
1467 // - `name() {` or `name () {` opens a nested fn def;
1468 // emit `name () {\n` then recurse on the body until
1469 // the matching `}` with depth+1, then `\n\t` * depth
1470 // + `}`.
1471 let canonicalize_body = |source: &str| -> String {
1472 fn fmt_body(s: &str, depth: usize, lead: bool) -> String {
1473 let chars: Vec<char> = s.chars().collect();
1474 let mut out = String::with_capacity(s.len());
1475 let mut in_sq = false;
1476 let mut in_dq = false;
1477 let mut brace_depth: i32 = 0;
1478 let mut paren_depth: i32 = 0;
1479 let stmt_indent = "\t".repeat(depth);
1480 let mut i = 0;
1481 // NOTE: caller (the funcdef emit at hashtable.rs:1364)
1482 // already wrote one leading `\t` via zoutputtab. Don't
1483 // double-indent the first statement. With `lead`
1484 // (recursive nested-fn body), we DO need the leading
1485 // indent because the caller writes `name () {\n` then
1486 // recurses without a prior tab.
1487 if lead && !chars.is_empty() {
1488 out.push_str(&stmt_indent);
1489 }
1490 while i < chars.len() {
1491 let c = chars[i];
1492 if !in_sq && !in_dq && c == '\\' && i + 1 < chars.len() {
1493 out.push(c);
1494 out.push(chars[i + 1]);
1495 i += 2;
1496 continue;
1497 }
1498 if !in_dq && c == '\'' {
1499 in_sq = !in_sq;
1500 out.push(c);
1501 i += 1;
1502 continue;
1503 }
1504 if !in_sq && c == '"' {
1505 in_dq = !in_dq;
1506 out.push(c);
1507 i += 1;
1508 continue;
1509 }
1510 // Detect nested fn-def pattern at depth 0:
1511 // `name() {...}` or `name () {...}` (and
1512 // optional `function `-keyword form). Only
1513 // when in_sq/in_dq == false and brace/paren
1514 // depth == 0.
1515 if !in_sq && !in_dq && brace_depth == 0 && paren_depth == 0 {
1516 // c:Src/text.c gettext2 WC_CASE arm (~520) —
1517 // C re-emits case statements from wordcode as
1518 // case W in
1519 // (p | q) body ;;
1520 // esac
1521 // with `;;`/`;&`/`;|` per WC_CASE_TYPE. The
1522 // generic `;`-break below ATE the `;;` (it
1523 // skips runs of `;`), so `functions f`
1524 // displayed case bodies without terminators
1525 // and with `;&` mangled to `&`. Re-render the
1526 // whole case..esac region case-aware.
1527 if out.is_empty() || out.ends_with('\n') || out.ends_with('\t') {
1528 if let Some((next_i, txt)) = try_render_case(&chars, i, depth) {
1529 out.push_str(&txt);
1530 i = next_i;
1531 // Consume trailing `;` + ws; emit a
1532 // statement break if more follows.
1533 while i < chars.len()
1534 && (chars[i] == ' '
1535 || chars[i] == '\t'
1536 || chars[i] == '\n'
1537 || chars[i] == ';')
1538 {
1539 i += 1;
1540 }
1541 if i < chars.len() {
1542 out.push('\n');
1543 out.push_str(&stmt_indent);
1544 }
1545 continue;
1546 }
1547 }
1548 // Try to match `<ident>\s*\(\s*\)\s*\{` at
1549 // current position, OR `function\s+<ident>...{`.
1550 let fn_start = try_match_fn_def(&chars, i);
1551 if let Some((header_end, name_str)) = fn_start {
1552 // Find matching `}` for the body.
1553 let body_open = header_end; // index just after `{`
1554 let body_close = find_matching_brace(&chars, body_open - 1);
1555 if let Some(close_idx) = body_close {
1556 let body_src: String =
1557 chars[body_open..close_idx].iter().collect();
1558 let body_trim = body_src
1559 .trim_start_matches(|c: char| c.is_whitespace())
1560 .trim_end_matches(|c: char| c.is_whitespace() || c == ';')
1561 .to_string();
1562 out.push_str(&name_str);
1563 out.push_str(" () {\n");
1564 out.push_str(&fmt_body(&body_trim, depth + 1, true));
1565 out.push('\n');
1566 out.push_str(&stmt_indent);
1567 out.push('}');
1568 i = close_idx + 1;
1569 // Consume trailing `;` and ws.
1570 let saved = i;
1571 while i < chars.len()
1572 && (chars[i] == ' ' || chars[i] == '\t' || chars[i] == ';')
1573 {
1574 i += 1;
1575 }
1576 if saved != i || i < chars.len() {
1577 // More content follows — emit
1578 // statement break.
1579 if i < chars.len() {
1580 out.push('\n');
1581 out.push_str(&stmt_indent);
1582 }
1583 }
1584 continue;
1585 }
1586 }
1587 }
1588 if !in_sq && !in_dq {
1589 match c {
1590 '{' => brace_depth += 1,
1591 '}' => brace_depth = (brace_depth - 1).max(0),
1592 '(' => paren_depth += 1,
1593 ')' => paren_depth = (paren_depth - 1).max(0),
1594 _ => {}
1595 }
1596 }
1597 if !in_sq && !in_dq && brace_depth == 0 && paren_depth == 0 && c == ';' {
1598 i += 1;
1599 while i < chars.len()
1600 && (chars[i] == ' ' || chars[i] == '\t' || chars[i] == ';')
1601 {
1602 i += 1;
1603 }
1604 if i < chars.len() {
1605 out.push('\n');
1606 out.push_str(&stmt_indent);
1607 }
1608 continue;
1609 }
1610 out.push(c);
1611 i += 1;
1612 }
1613 out
1614 }
1615 fn is_ident_byte(b: u8) -> bool {
1616 b == b'_' || b.is_ascii_alphanumeric()
1617 }
1618 fn try_match_fn_def(chars: &[char], start: usize) -> Option<(usize, String)> {
1619 // Skip leading `function ` keyword (optional).
1620 let mut i = start;
1621 let _function_prefix = {
1622 let rest: String = chars[i..].iter().collect();
1623 if rest.starts_with("function ") || rest.starts_with("function\t") {
1624 i += "function".len();
1625 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
1626 i += 1;
1627 }
1628 true
1629 } else {
1630 false
1631 }
1632 };
1633 // Match identifier.
1634 let name_start = i;
1635 while i < chars.len() && is_ident_byte(chars[i] as u8) {
1636 i += 1;
1637 }
1638 if i == name_start {
1639 return None;
1640 }
1641 let name: String = chars[name_start..i].iter().collect();
1642 // Skip optional whitespace.
1643 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
1644 i += 1;
1645 }
1646 // Require `()` for the non-`function`-keyword form;
1647 // C zsh accepts `function name { ... }` without parens.
1648 if i < chars.len() && chars[i] == '(' {
1649 i += 1;
1650 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
1651 i += 1;
1652 }
1653 if i >= chars.len() || chars[i] != ')' {
1654 return None;
1655 }
1656 i += 1;
1657 } else if !_function_prefix {
1658 return None;
1659 }
1660 // Skip ws + `{`.
1661 while i < chars.len() && (chars[i] == ' ' || chars[i] == '\t') {
1662 i += 1;
1663 }
1664 if i >= chars.len() || chars[i] != '{' {
1665 return None;
1666 }
1667 Some((i + 1, name))
1668 }
1669 /// Keyword match at `i` with word boundaries on both
1670 /// sides (start/ws/`;` before; end/ws/`;` after).
1671 fn matches_kw(chars: &[char], i: usize, kw: &str) -> bool {
1672 let kl = kw.len();
1673 if i + kl > chars.len() {
1674 return false;
1675 }
1676 if !chars[i..i + kl].iter().copied().eq(kw.chars()) {
1677 return false;
1678 }
1679 let before_ok = i == 0 || chars[i - 1].is_whitespace() || chars[i - 1] == ';';
1680 let after_ok = i + kl == chars.len()
1681 || chars[i + kl].is_whitespace()
1682 || chars[i + kl] == ';';
1683 before_ok && after_ok
1684 }
1685 /// Split a case-pattern alternation on top-level `|`
1686 /// (quote/paren aware), trimming each alternative —
1687 /// `a|b` → ["a", "b"], rendered `(a | b)` like
1688 /// C:Src/text.c gettext2's `taddstr(" | ")` walk.
1689 fn split_top_bar(pat: &str) -> Vec<String> {
1690 let chars: Vec<char> = pat.chars().collect();
1691 let mut alts = Vec::new();
1692 let mut cur = String::new();
1693 let (mut in_sq, mut in_dq) = (false, false);
1694 let mut pd = 0i32;
1695 let mut i = 0;
1696 while i < chars.len() {
1697 let c = chars[i];
1698 if !in_sq && !in_dq && c == '\\' && i + 1 < chars.len() {
1699 cur.push(c);
1700 cur.push(chars[i + 1]);
1701 i += 2;
1702 continue;
1703 }
1704 if !in_dq && c == '\'' {
1705 in_sq = !in_sq;
1706 } else if !in_sq && c == '"' {
1707 in_dq = !in_dq;
1708 } else if !in_sq && !in_dq {
1709 match c {
1710 '(' => pd += 1,
1711 ')' => pd = (pd - 1).max(0),
1712 '|' if pd == 0 => {
1713 alts.push(cur.trim().to_string());
1714 cur.clear();
1715 i += 1;
1716 continue;
1717 }
1718 _ => {}
1719 }
1720 }
1721 cur.push(c);
1722 i += 1;
1723 }
1724 alts.push(cur.trim().to_string());
1725 alts
1726 }
1727 /// Case-aware re-render of one `case W in … esac`
1728 /// region starting at `start` (which must point at the
1729 /// `case` keyword). Returns (index-after-`esac`,
1730 /// rendered text) or None when the region doesn't
1731 /// parse (caller falls back to generic emission).
1732 /// Output shape mirrors C:Src/text.c gettext2 WC_CASE:
1733 /// case W in
1734 /// \t(p | q) body ;;
1735 /// esac
1736 /// with arm bodies recursively formatted at depth+2
1737 /// (continuation statements land one level deeper than
1738 /// the arm line, matching zsh 5.9 output).
1739 fn try_render_case(
1740 chars: &[char],
1741 start: usize,
1742 depth: usize,
1743 ) -> Option<(usize, String)> {
1744 let n = chars.len();
1745 let mut i = start;
1746 if !matches_kw(chars, i, "case") {
1747 return None;
1748 }
1749 i += 4;
1750 if i >= n || !chars[i].is_whitespace() {
1751 return None;
1752 }
1753 while i < n && chars[i].is_whitespace() {
1754 i += 1;
1755 }
1756 // Scrutinee word: scan to unquoted whitespace.
1757 let word_start = i;
1758 let (mut in_sq, mut in_dq) = (false, false);
1759 while i < n {
1760 let c = chars[i];
1761 if !in_sq && !in_dq && c == '\\' && i + 1 < n {
1762 i += 2;
1763 continue;
1764 }
1765 if !in_dq && c == '\'' {
1766 in_sq = !in_sq;
1767 } else if !in_sq && c == '"' {
1768 in_dq = !in_dq;
1769 } else if !in_sq && !in_dq && c.is_whitespace() {
1770 break;
1771 }
1772 i += 1;
1773 }
1774 if i == word_start || in_sq || in_dq {
1775 return None;
1776 }
1777 let word: String = chars[word_start..i].iter().collect();
1778 while i < n && chars[i].is_whitespace() {
1779 i += 1;
1780 }
1781 if !matches_kw(chars, i, "in") {
1782 return None;
1783 }
1784 i += 2;
1785 let indent_case = "\t".repeat(depth);
1786 let indent_arm = "\t".repeat(depth + 1);
1787 let mut rendered = format!("case {} in", word);
1788 loop {
1789 while i < n && chars[i].is_whitespace() {
1790 i += 1;
1791 }
1792 if i >= n {
1793 return None; // unterminated — bail out
1794 }
1795 if matches_kw(chars, i, "esac") {
1796 i += 4;
1797 break;
1798 }
1799 // Pattern: optional leading `(`, alts to `)`.
1800 if chars[i] == '(' {
1801 i += 1;
1802 }
1803 let pat_start = i;
1804 let (mut in_sq, mut in_dq) = (false, false);
1805 let mut pd = 0i32;
1806 while i < n {
1807 let c = chars[i];
1808 if !in_sq && !in_dq && c == '\\' && i + 1 < n {
1809 i += 2;
1810 continue;
1811 }
1812 if !in_dq && c == '\'' {
1813 in_sq = !in_sq;
1814 } else if !in_sq && c == '"' {
1815 in_dq = !in_dq;
1816 } else if !in_sq && !in_dq {
1817 if c == '(' {
1818 pd += 1;
1819 } else if c == ')' {
1820 if pd == 0 {
1821 break;
1822 }
1823 pd -= 1;
1824 }
1825 }
1826 i += 1;
1827 }
1828 if i >= n || chars[i] != ')' {
1829 return None;
1830 }
1831 let pat_src: String = chars[pat_start..i].iter().collect();
1832 i += 1; // consume `)`
1833 while i < n && (chars[i] == ' ' || chars[i] == '\t') {
1834 i += 1;
1835 }
1836 // Body: scan to `;;`/`;&`/`;|` or this case's
1837 // `esac` (last arm without terminator), quote/
1838 // paren/brace aware, nested case…esac tracked.
1839 let body_start = i;
1840 let mut body_end = i;
1841 let mut term: Option<&str> = None;
1842 let (mut in_sq, mut in_dq) = (false, false);
1843 let (mut bd, mut pd) = (0i32, 0i32);
1844 let mut nested_case = 0i32;
1845 loop {
1846 if i >= n {
1847 return None; // unterminated — bail out
1848 }
1849 let c = chars[i];
1850 if !in_sq && !in_dq && c == '\\' && i + 1 < n {
1851 i += 2;
1852 continue;
1853 }
1854 if !in_dq && c == '\'' {
1855 in_sq = !in_sq;
1856 i += 1;
1857 continue;
1858 }
1859 if !in_sq && c == '"' {
1860 in_dq = !in_dq;
1861 i += 1;
1862 continue;
1863 }
1864 if in_sq || in_dq {
1865 i += 1;
1866 continue;
1867 }
1868 match c {
1869 '{' => bd += 1,
1870 '}' => bd = (bd - 1).max(0),
1871 '(' => pd += 1,
1872 ')' => pd = (pd - 1).max(0),
1873 _ => {}
1874 }
1875 if bd == 0 && pd == 0 {
1876 if matches_kw(chars, i, "case") {
1877 nested_case += 1;
1878 i += 4;
1879 continue;
1880 }
1881 if matches_kw(chars, i, "esac") {
1882 if nested_case == 0 {
1883 body_end = i;
1884 break;
1885 }
1886 nested_case -= 1;
1887 i += 4;
1888 continue;
1889 }
1890 if nested_case == 0
1891 && c == ';'
1892 && i + 1 < n
1893 && matches!(chars[i + 1], ';' | '&' | '|')
1894 {
1895 body_end = i;
1896 term = Some(match chars[i + 1] {
1897 ';' => ";;",
1898 '&' => ";&",
1899 _ => ";|",
1900 });
1901 i += 2;
1902 break;
1903 }
1904 }
1905 i += 1;
1906 }
1907 let body_src: String = chars[body_start..body_end].iter().collect();
1908 let body_trim = body_src.trim().trim_end_matches(';').trim_end();
1909 rendered.push('\n');
1910 rendered.push_str(&indent_arm);
1911 rendered.push('(');
1912 rendered.push_str(&split_top_bar(&pat_src).join(" | "));
1913 rendered.push_str(") ");
1914 rendered.push_str(&fmt_body(body_trim, depth + 2, false));
1915 if let Some(t) = term {
1916 rendered.push(' ');
1917 rendered.push_str(t);
1918 }
1919 }
1920 rendered.push('\n');
1921 rendered.push_str(&indent_case);
1922 rendered.push_str("esac");
1923 Some((i, rendered))
1924 }
1925 fn find_matching_brace(chars: &[char], open: usize) -> Option<usize> {
1926 let mut depth = 1i32;
1927 let mut in_sq = false;
1928 let mut in_dq = false;
1929 let mut j = open + 1;
1930 while j < chars.len() {
1931 let c = chars[j];
1932 if !in_sq && !in_dq && c == '\\' && j + 1 < chars.len() {
1933 j += 2;
1934 continue;
1935 }
1936 if !in_dq && c == '\'' {
1937 in_sq = !in_sq;
1938 } else if !in_sq && c == '"' {
1939 in_dq = !in_dq;
1940 } else if !in_sq && !in_dq {
1941 if c == '{' {
1942 depth += 1;
1943 } else if c == '}' {
1944 depth -= 1;
1945 if depth == 0 {
1946 return Some(j);
1947 }
1948 }
1949 }
1950 j += 1;
1951 }
1952 None
1953 }
1954 let mut s = source.trim().to_string();
1955 if s.starts_with('{') {
1956 s = s[1..].trim_start().to_string();
1957 }
1958 if s.ends_with('}') {
1959 s.pop();
1960 s = s.trim_end().to_string();
1961 }
1962 if s.ends_with(';') {
1963 s.pop();
1964 s = s.trim_end().to_string();
1965 }
1966 fmt_body(&s, 1, false)
1967 };
1968 // c:954 — `t = getpermtext(fd, NULL, 1);`. C holds the body as
1969 // compiled wordcode and renders it back to source with
1970 // getpermtext, which is what produces zsh's canonical layout:
1971 // `do`/`then` on their own line with the body indented under
1972 // them, `(` and `)` broken onto separate lines, an `always`
1973 // block re-emitted as `{ … } always { … }`, and a trailing
1974 // space after every assignment (taddassign, c:203-204).
1975 //
1976 // zshrs only has an Eprog for zwc-loaded functions (the
1977 // `hn.funcdef` branch above); shell-defined ones keep their raw
1978 // source, which is why this branch existed. Re-parse that source
1979 // and render it through the SAME deparser rather than
1980 // re-deriving getpermtext's formatting rules by hand —
1981 // canonicalize_body reproduced the flat cases but not the
1982 // indenting ones, and mangled `always` blocks into
1983 // `print x } always { print y`.
1984 //
1985 // The source is parsed as-is. hn.body arrives with the framing
1986 // `{ }` of `name() { … }` ALREADY stripped, so removing a
1987 // leading `{` here would eat the braces of a body whose first
1988 // command is itself a brace group (`f() { { print x } }`) and
1989 // would leave an always-block body unbalanced. That
1990 // unconditional strip is exactly why canonicalize_body below
1991 // rendered `f() { { print x } always { print y } }` as
1992 // `print x } always { print y`.
1993 //
1994 // parse_string is the wordcode parser, whose coverage is
1995 // narrower than the AST parser that executes these bodies, so
1996 // fall back to canonicalize_body when it can't take the source
1997 // rather than losing the listing entirely.
1998 let deparse_body = |source: &str| -> String {
1999 match crate::ported::exec::parse_string(source.trim(), 1) {
2000 Some(p) => crate::ported::text::getpermtext(Box::new(p), None, 1), // c:954
2001 None => canonicalize_body(source),
2002 }
2003 };
2004 t = hn.body.clone().map(|s| deparse_body(&s));
2005 }
2006 // c:955-958 — PM_TAGGED | PM_TAGGED_LOCAL → `# traced` marker.
2007 if (hn.node.flags & (PM_TAGGED | PM_TAGGED_LOCAL) as i32) != 0 {
2008 println!("{} traced", hashchar.load(Ordering::Relaxed) as u8 as char); // c:956
2009 let _ = zoutputtab(&mut io::stdout()); // c:957
2010 }
2011 // c:959-983 — no funcdef text → autoload stub; else emit text.
2012 if t.is_none() {
2013 // c:959
2014 // c:960-964 — `fopt = "UtTkzc"; flgs[] = { PM_UNALIASED, PM_TAGGED,
2015 // PM_TAGGED_LOCAL, PM_KSHSTORED, PM_ZSHSTORED, PM_CUR_FPATH, 0 };`
2016 let fopt: &[u8] = b"UtTkzc"; // c:960
2017 let flgs: [u32; 6] = [
2018 // c:961-964
2019 PM_UNALIASED,
2020 PM_TAGGED,
2021 PM_TAGGED_LOCAL,
2022 PM_KSHSTORED,
2023 PM_ZSHSTORED,
2024 PM_CUR_FPATH,
2025 ];
2026 let mut so = io::stdout();
2027 let _ = zputs("builtin autoload -X", &mut so); // c:967
2028 // c:968-969 — emit each fopt char whose flag is set.
2029 for fl in 0..fopt.len() {
2030 // c:968
2031 if (hn.node.flags & flgs[fl] as i32) != 0 {
2032 // c:969
2033 print!("{}", fopt[fl] as char); // c:969
2034 }
2035 }
2036 // c:970-973 — PM_LOADDIR with filename → ' ' + zputs(filename).
2037 if let Some(filename) = &hn.filename {
2038 if (hn.node.flags & PM_LOADDIR as i32) != 0 {
2039 // c:970
2040 print!(" "); // c:971
2041 let _ = zputs(filename, &mut so); // c:972
2042 }
2043 }
2044 } else {
2045 // c:974
2046 // c:975 — `zputs(t, stdout);`
2047 let body = t.take().unwrap();
2048 let mut so = io::stdout();
2049 let _ = zputs(&body, &mut so); // c:975
2050 // c:977-982 — funcdef.flags & EF_RUN → run-time suffix.
2051 let ef_run = hn
2052 .funcdef
2053 .as_ref()
2054 .map(|fd| (fd.flags & EF_RUN) != 0)
2055 .unwrap_or(false);
2056 if ef_run {
2057 // c:977
2058 println!(); // c:978
2059 let _ = zoutputtab(&mut io::stdout()); // c:979
2060 print!("{}", quotedzputs(&hn.node.nam)); // c:980
2061 print!(" \"$@\""); // c:981
2062 }
2063 }
2064 print!("\n}}"); // c:984
2065 } else {
2066 // c:985
2067 print!(" () {{ }}"); // c:986
2068 }
2069 // c:988-994 — redir present → emit its text.
2070 if let Some(redir) = &hn.redir {
2071 // c:988
2072 let t = getpermtext(redir.clone(), None, 1); // c:989
2073 if !t.is_empty() {
2074 // c:990
2075 let mut so = io::stdout();
2076 let _ = zputs(&t, &mut so); // c:991
2077 }
2078 }
2079
2080 println!(); // c:996
2081}
2082
2083/// Port of `scanmatchshfunc(Patprog pprog, int sorted, int flags1, int flags2, ScanFunc scanfunc, int scanflags, int expand)` from `Src/hashtable.c:1013`.
2084///
2085/// C body iterates `shfunctab` and calls `func(node)` on every
2086/// entry whose name matches the compiled pattern `pprog`. Rust
2087/// port walks the singleton with a closure callback.
2088///
2089/// Returns the count of matched entries (mirrors C's int return).
2090/// WARNING: param names don't match C — Rust=(pattern, func) vs C=(pprog, sorted, flags1, flags2, scanfunc, scanflags, expand)
2091pub fn scanmatchshfunc<F>(pattern: Option<&str>, mut func: F) -> i32
2092where
2093 F: FnMut(&str, &shfunc),
2094{
2095 let tab = shfunctab_lock().read().expect("shfunctab poisoned");
2096 let mut count = 0;
2097 // c:Src/hashtable.c:1031 scanshfunc(sorted=1, …) — the `sorted`
2098 // flag is set on every internal caller (bin_functions's no-arg
2099 // listing, etc.), so scan walks entries in sorted order via
2100 // hnamcmp (byte-wise ASCII compare). The HashMap iter order is
2101 // arbitrary; collect + sort for parity.
2102 let mut entries: Vec<_> = tab.iter().collect();
2103 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
2104 for (name, entry) in entries {
2105 let matches = match pattern {
2106 None => true,
2107 Some(p) => simple_glob_match(p, name),
2108 };
2109 if matches {
2110 func(name, entry);
2111 count += 1;
2112 }
2113 }
2114 count
2115}
2116
2117/// Port of `scanshfunc(int sorted, int flags1, int flags2, ScanFunc scanfunc, int scanflags, int expand)` from `Src/hashtable.c:1031`.
2118///
2119/// C body walks every `shfunctab` entry calling `func(node, flags)`.
2120/// Rust port delegates to scanmatchshfunc with no pattern.
2121/// WARNING: param names don't match C — Rust=(func) vs C=(sorted, flags1, flags2, scanfunc, scanflags, expand)
2122pub fn scanshfunc<F>(func: F) -> i32
2123where
2124 F: FnMut(&str, &shfunc),
2125{
2126 scanmatchshfunc(None, func)
2127}
2128
2129/// Port of `printshfuncexpand(HashNode hn, int printflags, int expand)` from `Src/hashtable.c:1042`.
2130///
2131/// C body:
2132/// ```c
2133/// int save_expand;
2134/// save_expand = text_expand_tabs;
2135/// text_expand_tabs = expand;
2136/// shfunctab->printnode(hn, printflags);
2137/// text_expand_tabs = save_expand;
2138/// ```
2139///
2140/// Briefly toggles `text_expand_tabs` around the printnode call so
2141/// the body indentation comes out either tab- or space-formatted
2142/// per the caller's `expand` arg.
2143pub fn printshfuncexpand(hn: &shfunc, printflags: i32, expand: i32) {
2144 // c:1044 — `int save_expand;`
2145 let save_expand: i32; // c:1044
2146 // c:1046 — `save_expand = text_expand_tabs;`
2147 save_expand = crate::text::TEXT_EXPAND_TABS.load(Ordering::Relaxed); // c:1046
2148 // c:1047 — `text_expand_tabs = expand;`
2149 crate::text::TEXT_EXPAND_TABS.store(expand, Ordering::Relaxed); // c:1047
2150 // c:1048 — `shfunctab->printnode(hn, printflags);`
2151 printshfuncnode(hn, printflags); // c:1048
2152 // c:1049 — `text_expand_tabs = save_expand;`
2153 crate::text::TEXT_EXPAND_TABS.store(save_expand, Ordering::Relaxed); // c:1049
2154}
2155
2156/// Port of `getshfuncfile(shfunc shf)` from `Src/hashtable.c:1059`.
2157///
2158/// C body (verbatim):
2159/// if (shf->node.flags & PM_LOADDIR) {
2160/// return zhtricat(shf->filename, "/", shf->node.nam);
2161/// } else if (shf->filename) {
2162/// return dupstring(shf->filename);
2163/// } else {
2164/// return NULL;
2165/// }
2166///
2167/// PM_LOADDIR is set when zsh loaded the function via fpath
2168/// directory autoload (the common `autoload -Uz` path): in that
2169/// case `filename` is the DIRECTORY and we must append `/name` to
2170/// produce the actual source file. Prior Rust port skipped the
2171/// PM_LOADDIR branch, so `${functions_source[my_autoload]}`
2172/// returned the fpath dir (e.g. `/usr/share/zsh/5.9/functions`)
2173/// instead of the real path (`.../functions/my_autoload`).
2174pub fn getshfuncfile(shf: &str) -> Option<String> {
2175 let tab = shfunctab_lock().read().expect("shfunctab poisoned");
2176 let f = tab.get_including_disabled(shf)?;
2177 let filename = f.filename.as_ref()?;
2178 // c:1061 — PM_LOADDIR: `zhtricat(shf->filename, "/", shf->node.nam)`
2179 if (f.node.flags as u32 & crate::ported::zsh_h::PM_LOADDIR) != 0 {
2180 Some(format!("{}/{}", filename, f.node.nam))
2181 } else {
2182 // c:1063 — `dupstring(shf->filename)`
2183 Some(filename.clone())
2184 }
2185}
2186
2187/// Port of `createreswdtable()` from `Src/hashtable.c:1120`.
2188///
2189/// C body wires up the reswdtab GSU vtable then iterates the
2190/// static `reswds` array calling `addnode` for each. Rust port:
2191/// touches the singleton (which seeds the table from the static
2192/// word list in `reswd_table::new`).
2193pub fn createreswdtable() {
2194 let _ = reswdtab_lock();
2195}
2196
2197/// Port of `printreswdnode(HashNode hn, int printflags)` from `Src/hashtable.c:1147`.
2198///
2199/// C body:
2200/// ```c
2201/// Reswd rw = (Reswd) hn;
2202/// if (printflags & PRINT_WHENCE_WORD) {
2203/// printf("%s: reserved\n", rw->node.nam);
2204/// return;
2205/// }
2206/// if (printflags & PRINT_WHENCE_CSH) {
2207/// printf("%s: shell reserved word\n", rw->node.nam);
2208/// return;
2209/// }
2210/// if (printflags & PRINT_WHENCE_VERBOSE) {
2211/// printf("%s is a reserved word\n", rw->node.nam);
2212/// return;
2213/// }
2214/// /* default is name only */
2215/// printf("%s\n", rw->node.nam);
2216/// ```
2217pub fn printreswdnode(hn: &reswd, printflags: i32) {
2218 // c:1149 — `Reswd rw = (Reswd) hn;` — Rust types already give us reswd.
2219 // c:1151-1154 — PRINT_WHENCE_WORD branch.
2220 if (printflags & PRINT_WHENCE_WORD) != 0 {
2221 println!("{}: reserved", hn.node.nam); // c:1152
2222 return; // c:1153
2223 }
2224 // c:1156-1159 — PRINT_WHENCE_CSH branch.
2225 if (printflags & PRINT_WHENCE_CSH) != 0 {
2226 println!("{}: shell reserved word", hn.node.nam); // c:1157
2227 return; // c:1158
2228 }
2229 // c:1161-1164 — PRINT_WHENCE_VERBOSE branch.
2230 if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
2231 println!("{} is a reserved word", hn.node.nam); // c:1162
2232 return; // c:1163
2233 }
2234 // c:1166-1167 — default: name only.
2235 println!("{}", hn.node.nam); // c:1167
2236}
2237
2238/// Port of `void createaliastable(HashTable ht)` from `Src/hashtable.c:1186`.
2239/// ```c
2240/// void
2241/// createaliastable(HashTable ht)
2242/// {
2243/// ht->hash = hasher;
2244/// ht->emptytable = NULL;
2245/// ht->filltable = NULL;
2246/// ht->cmpnodes = strcmp;
2247/// ht->addnode = addhashnode;
2248/// ht->getnode = gethashnode;
2249/// ht->getnode2 = gethashnode2;
2250/// ht->removenode = removehashnode;
2251/// ht->disablenode = disablehashnode;
2252/// ht->enablenode = enablehashnode;
2253/// ht->freenode = freealiasnode;
2254/// ht->printnode = printaliasnode;
2255/// }
2256/// ```
2257/// The Rust `hashtable.addnode/.getnode/.removenode/.disablenode/.enablenode/
2258/// .freenode/.printnode` function-pointer types take untyped HashNode
2259/// arguments. The generic Rust helpers (`addhashnode<T>`/`gethashnode<T>`/
2260/// etc.) take typed `&mut HashMap<String, T>` so they can't directly
2261/// satisfy the untyped slot signature; downstream consumers of `aliastab`
2262/// dispatch through `aliastab_lock()` (the typed wrapper) instead of
2263/// the C-style slot. Mirror the C structure verbatim: assign every slot
2264/// either to the matching adapter or `None`, with each line citing the
2265/// matching c:NNN.
2266pub fn createaliastable(ht: &mut hashtable) {
2267 // c:1188
2268 fn cmpnodes_strcmp(a: &str, b: &str) -> i32 {
2269 // c:1193 strcmp
2270 a.cmp(b) as i32
2271 }
2272 ht.hash = Some(hasher); // c:1190
2273 ht.emptytable = None; // c:1191
2274 ht.filltable = None; // c:1192
2275 ht.cmpnodes = Some(cmpnodes_strcmp); // c:1193
2276 // c:1194-1201 — addnode/getnode/getnode2/removenode/disablenode/
2277 // enablenode/freenode/printnode: their C signatures are `void(*)(
2278 // HashTable, char *, void *)` / `HashNode(*)(HashTable, char *)` /
2279 // ... — they take untyped `void *`. The typed Rust helpers
2280 // (`addhashnode<T>(ht: &mut HashMap<String, T>, ...)`) can't be
2281 // coerced through the `fn(&mut hashtable, String, usize)` slot
2282 // shape without per-value-type trampoline closures. Leave the
2283 // slots `None`; the typed dispatch through `aliastab_lock` is the
2284 // canonical Rust path for this table.
2285 ht.addnode = None; // c:1194 addhashnode
2286 ht.getnode = None; // c:1195 gethashnode
2287 ht.getnode2 = None; // c:1196 gethashnode2
2288 ht.removenode = None; // c:1197 removehashnode
2289 ht.disablenode = None; // c:1198 disablehashnode
2290 ht.enablenode = None; // c:1199 enablehashnode
2291 ht.freenode = None; // c:1200 freealiasnode
2292 ht.printnode = None; // c:1201 printaliasnode
2293}
2294
2295/// Trait exposing the DISABLED flag on a hash-node value.
2296///
2297/// Implemented for the per-table value types so the generic ops
2298/// (`gethashnode`/`disablehashnode`/etc.) can filter / mutate
2299/// without per-table dispatch. Mirrors C's `HashNode->flags`
2300/// field which every node struct embeds via the `struct hashnode`
2301/// header.
2302pub trait HashNodeFlags {
2303 fn flags(&self) -> u32;
2304 fn set_disabled(&mut self, disabled: bool);
2305 fn is_disabled(&self) -> bool {
2306 self.flags() & (DISABLED as u32) != 0
2307 }
2308}
2309
2310impl HashNodeFlags for alias {
2311 fn flags(&self) -> u32 {
2312 self.node.flags as u32
2313 }
2314 fn set_disabled(&mut self, disabled: bool) {
2315 if disabled {
2316 self.node.flags |= DISABLED as i32;
2317 } else {
2318 self.node.flags &= !(DISABLED as i32);
2319 }
2320 }
2321}
2322
2323impl HashNodeFlags for shfunc {
2324 fn flags(&self) -> u32 {
2325 self.node.flags as u32
2326 }
2327 fn set_disabled(&mut self, disabled: bool) {
2328 if disabled {
2329 self.node.flags |= DISABLED as i32;
2330 } else {
2331 self.node.flags &= !(DISABLED as i32);
2332 }
2333 }
2334}
2335
2336impl HashNodeFlags for cmdnam {
2337 fn flags(&self) -> u32 {
2338 self.node.flags as u32
2339 }
2340 fn set_disabled(&mut self, disabled: bool) {
2341 if disabled {
2342 self.node.flags |= DISABLED as i32;
2343 } else {
2344 self.node.flags &= !(DISABLED as i32);
2345 }
2346 }
2347}
2348
2349impl HashNodeFlags for reswd {
2350 fn flags(&self) -> u32 {
2351 self.node.flags as u32
2352 }
2353 fn set_disabled(&mut self, disabled: bool) {
2354 if disabled {
2355 self.node.flags |= DISABLED as i32;
2356 } else {
2357 self.node.flags &= !(DISABLED as i32);
2358 }
2359 }
2360}
2361
2362/// Port of `createaliastables()` from `Src/hashtable.c:1206`.
2363///
2364/// C body (lines 1206-1224):
2365/// ```c
2366/// aliastab = newhashtable(23, "aliastab", NULL);
2367/// createaliastable(aliastab);
2368/// aliastab->addnode(aliastab, ztrdup("run-help"), createaliasnode(ztrdup("man"), 0));
2369/// aliastab->addnode(aliastab, ztrdup("which-command"), createaliasnode(ztrdup("whence"), 0));
2370/// sufaliastab = newhashtable(11, "sufaliastab", NULL);
2371/// createaliastable(sufaliastab);
2372/// ```
2373///
2374/// The OnceLock-backed `aliastab_lock()` / `sufaliastab_lock()`
2375/// stand in for `newhashtable(...)` + `createaliastable(...)` — they
2376/// lazy-init the underlying maps on first access.
2377pub fn createaliastables() {
2378 // c:1206 — newhashtable(23, "aliastab", NULL)
2379 // c:1212 — createaliastable(aliastab)
2380 let mut tab = aliastab_lock().write().expect("aliastab poisoned");
2381 // c:1215 — `aliastab->addnode(aliastab, ztrdup("run-help"),
2382 // createaliasnode(ztrdup("man"), 0));`
2383 tab.add(createaliasnode("run-help", "man", 0)); // c:1215
2384 // c:1216 — `aliastab->addnode(aliastab, ztrdup("which-command"),
2385 // createaliasnode(ztrdup("whence"), 0));`
2386 tab.add(createaliasnode("which-command", "whence", 0)); // c:1216
2387 drop(tab);
2388 // c:1221 — newhashtable(11, "sufaliastab", NULL)
2389 // c:1223 — createaliastable(sufaliastab)
2390 let _ = sufaliastab_lock();
2391}
2392// c:1253
2393
2394/// Build an alias node with the canonical `alias` shape.
2395/// Mirrors C `addaliasnode(aliastab, name, createaliasnode(text, flags))`
2396/// at hashtable.c:1230 — caller-side bundle for the
2397/// hashnode+text+flags inline-build.
2398pub fn createaliasnode(name: &str, text: &str, flags: u32) -> alias {
2399 // c:1230
2400 alias {
2401 node: hashnode {
2402 next: None,
2403 nam: name.to_string(),
2404 flags: flags as i32,
2405 },
2406 text: text.to_string(),
2407 inuse: 0,
2408 }
2409}
2410
2411/// Port of `createaliasnode(char *txt, int flags)` from `Src/hashtable.c:1230`.
2412///
2413/// C body:
2414/// ```c
2415/// al = zshcalloc(sizeof *al);
2416/// al->node.flags = flags;
2417/// al->text = txt;
2418/// al->inuse = 0;
2419/// return al;
2420/// ```
2421// Duplicate `createaliasnode` removed — canonical port is at the
2422// earlier definition (matches C hashtable.c:1230).
2423
2424/// Port of `freealiasnode(HashNode hn)` from `Src/hashtable.c:1243`.
2425///
2426/// C body frees the name + text strings + alias struct. Rust
2427/// port: drop runs the same when the crate::ported::zsh_h::alias is removed from its
2428/// table. This helper triggers the drop.
2429pub fn freealiasnode(hn: &str) {
2430 let mut tab = aliastab_lock().write().expect("aliastab poisoned");
2431 tab.remove(hn);
2432}
2433
2434/// Port of `printaliasnode(HashNode hn, int printflags)` from `Src/hashtable.c:1256`.
2435///
2436/// Emits `whence`-style output for one alias with PRINT_NAMEONLY /
2437/// PRINT_WHENCE_WORD / PRINT_WHENCE_SIMPLE / PRINT_WHENCE_CSH /
2438/// PRINT_WHENCE_VERBOSE / PRINT_LIST flag dispatch. PRINT_LIST falls
2439/// through to the tail `quotedzputs(nam) '=' quotedzputs(text) '\n'`;
2440/// every other branch returns early.
2441pub fn printaliasnode(hn: &alias, printflags: i32) {
2442 // c:1258 — `Alias a = (Alias) hn;` — Rust types already give us alias.
2443
2444 // c:1260-1264 — PRINT_NAMEONLY branch.
2445 if (printflags & PRINT_NAMEONLY) != 0 {
2446 let mut so = io::stdout();
2447 let _ = zputs(&hn.node.nam, &mut so); // c:1261
2448 println!(); // c:1262
2449 return; // c:1263
2450 }
2451
2452 // c:1266-1274 — PRINT_WHENCE_WORD branch.
2453 if (printflags & PRINT_WHENCE_WORD) != 0 {
2454 if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
2455 println!("{}: suffix alias", hn.node.nam); // c:1268
2456 } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
2457 println!("{}: global alias", hn.node.nam); // c:1270
2458 } else {
2459 println!("{}: alias", hn.node.nam); // c:1272
2460 }
2461 return; // c:1273
2462 }
2463
2464 // c:1276-1280 — PRINT_WHENCE_SIMPLE branch.
2465 if (printflags & PRINT_WHENCE_SIMPLE) != 0 {
2466 let mut so = io::stdout();
2467 let _ = zputs(&hn.text, &mut so); // c:1277
2468 println!(); // c:1278
2469 return; // c:1279
2470 }
2471
2472 // c:1282-1293 — PRINT_WHENCE_CSH branch.
2473 if (printflags & PRINT_WHENCE_CSH) != 0 {
2474 let mut so = io::stdout();
2475 let _ = nicezputs(&hn.node.nam, &mut so); // c:1283
2476 print!(": "); // c:1284
2477 if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
2478 print!("suffix "); // c:1286
2479 } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
2480 print!("globally "); // c:1288
2481 }
2482 print!("aliased to "); // c:1289
2483 let _ = nicezputs(&hn.text, &mut so); // c:1290
2484 println!(); // c:1291
2485 return; // c:1292
2486 }
2487
2488 // c:1295-1308 — PRINT_WHENCE_VERBOSE branch.
2489 if (printflags & PRINT_WHENCE_VERBOSE) != 0 {
2490 let mut so = io::stdout();
2491 let _ = nicezputs(&hn.node.nam, &mut so); // c:1296
2492 print!(" is a"); // c:1297
2493 if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
2494 print!(" suffix"); // c:1299
2495 } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
2496 print!(" global"); // c:1301
2497 } else {
2498 print!("n"); // c:1303
2499 }
2500 print!(" alias for "); // c:1304
2501 let _ = nicezputs(&hn.text, &mut so); // c:1305
2502 println!(); // c:1306
2503 return; // c:1307
2504 }
2505
2506 // c:1310-1330 — PRINT_LIST prefix block (falls through to the
2507 // tail quotedzputs body below; default-no-flags also reaches the
2508 // tail by skipping this block).
2509 if (printflags & PRINT_LIST) != 0 {
2510 // c:1312-1316 — Fast fail on `=` in name (unrepresentable
2511 // `alias name=...` round-trip).
2512 if hn.node.nam.contains('=') {
2513 // c:1313
2514 zwarn(&format!(
2515 "invalid alias '{}' encountered while printing aliases",
2516 hn.node.nam
2517 ));
2518 return; // c:1316
2519 }
2520 print!("alias "); // c:1320
2521 if (hn.node.flags & ALIAS_SUFFIX as i32) != 0 {
2522 // c:1321
2523 print!("-s "); // c:1322
2524 } else if (hn.node.flags & ALIAS_GLOBAL as i32) != 0 {
2525 // c:1323
2526 print!("-g "); // c:1324
2527 }
2528 // c:1326-1329 — `-- ` so a name starting with `-`/`+` isn't
2529 // interpreted as an option when the listing is re-executed.
2530 if hn.node.nam.starts_with('-') || hn.node.nam.starts_with('+') {
2531 // c:1328
2532 print!("-- "); // c:1329
2533 }
2534 }
2535
2536 // c:1332-1336 — common tail: quotedzputs(nam) '=' quotedzputs(text) '\n'.
2537 print!("{}", quotedzputs(&hn.node.nam)); // c:1332
2538 print!("="); // c:1333
2539 print!("{}", quotedzputs(&hn.text)); // c:1334
2540 println!(); // c:1336
2541}
2542
2543/// Port of `createhisttable()` from `Src/hashtable.c:1345`.
2544///
2545/// C body wires up the histtab GSU vtable with `histhasher` /
2546/// `histstrcmp` / `addhistnode` etc. Rust port: touches the
2547/// singleton to initialise. The HashMap-keyed-by-string model
2548/// is much simpler than C's per-bucket chain; the entries hold
2549/// (history event-id) values keyed by command-text.
2550pub fn createhisttable() {
2551 let _ = histtab_lock();
2552}
2553
2554/// History-specific hash function (normalizes whitespace).
2555/// Port of `histhasher(const char *str)` from `Src/hashtable.c:1365`.
2556///
2557/// C body uses `inblank(*str)` (canonical typtab predicate at
2558/// `Src/ztype.h:50` — NARROW blank: space/tab ONLY, not newline,
2559/// definitely NOT broad Unicode whitespace). The Rust port previously
2560/// used `c.is_whitespace()` which is the Unicode-broad set including
2561/// CR/FF/VT/NBSP — every line of zsh history containing one of those
2562/// bytes hashed to a different bucket than C would have.
2563///
2564/// Faithful: matches `inblank` exactly (`c:50` — `space + tab`).
2565pub fn histhasher(s: &str) -> u32 {
2566 // c:1365
2567 // c:50 — `inblank(c)` = `c == ' ' || c == '\t'`. NOT `\n`, NOT broad.
2568 #[inline]
2569 fn is_inblank_narrow(c: char) -> bool {
2570 c == ' ' || c == '\t'
2571 }
2572
2573 let mut hashval: u32 = 0;
2574 let mut chars = s.chars().peekable();
2575
2576 // c:1369 — `while (inblank(*str)) str++;` skip leading blanks.
2577 while let Some(&c) = chars.peek() {
2578 if is_inblank_narrow(c) {
2579 chars.next();
2580 } else {
2581 break;
2582 }
2583 }
2584
2585 // c:1371 — main mix loop.
2586 while let Some(c) = chars.next() {
2587 if is_inblank_narrow(c) {
2588 // c:1373 — `do str++; while (inblank(*str));` collapse runs.
2589 while let Some(&next) = chars.peek() {
2590 if is_inblank_narrow(next) {
2591 chars.next();
2592 } else {
2593 break;
2594 }
2595 }
2596 // c:1374-1375 — `if (*str) hashval += (hashval << 5) + ' ';`
2597 if chars.peek().is_some() {
2598 hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(' ' as u32));
2599 }
2600 } else {
2601 // c:1377 — `hashval += (hashval << 5) + *(unsigned char *)str++;`
2602 hashval = hashval.wrapping_add(hashval.wrapping_shl(5).wrapping_add(c as u32));
2603 }
2604 }
2605 hashval
2606}
2607
2608/// Port of `emptyhisttable(HashTable ht)` from `Src/hashtable.c:1385`.
2609///
2610/// C body:
2611/// ```c
2612/// emptyhashtable(ht);
2613/// if (hist_ring) histremovedups();
2614/// ```
2615/// WARNING: param names don't match C — Rust=() vs C=(ht)
2616pub fn emptyhisttable() {
2617 // c:1385 — `emptyhashtable(ht)` — clear the lookup table.
2618 histtab_lock().write().expect("histtab poisoned").clear();
2619 // c:1386 — `if (hist_ring) histremovedups();` — prune dup-flagged
2620 // entries from the history ring.
2621 let has_ring = !hist_ring.lock().unwrap().is_empty();
2622 if has_ring {
2623 histremovedups(); // c:1386
2624 }
2625}
2626
2627/// Compare strings with normalized whitespace (for history).
2628/// Port of `histstrcmp(const char *str1, const char *str2)` from
2629/// `Src/hashtable.c:1396`.
2630///
2631/// C body uses `inblank(*str)` everywhere (`Src/ztype.h:50` — NARROW
2632/// space/tab only). The previous Rust port used `c.is_whitespace()`
2633/// (broad Unicode set including CR/FF/VT/NBSP), which would silently
2634/// fold history lines that C considers distinct (e.g. lines that
2635/// contain NBSP would dedupe against lines with no NBSP).
2636///
2637/// C signature is 2-arg: it reads `isset(HISTREDUCEBLANKS)` directly.
2638/// Rust port passes `reduce_blanks` as an explicit 3rd arg to keep
2639/// the option read out of this leaf fn (call sites at hist.c thread
2640/// the option from the parent scope).
2641pub fn histstrcmp(s1: &str, s2: &str, reduce_blanks: bool) -> std::cmp::Ordering {
2642 // c:1396
2643 // c:50 — `inblank(c)` = `c == ' ' || c == '\t'`. NOT newline, NOT broad.
2644 #[inline]
2645 fn is_inblank_narrow(c: char) -> bool {
2646 c == ' ' || c == '\t'
2647 }
2648
2649 // c:1398-1399 — skip leading inblank in both strings.
2650 let s1 = s1.trim_start_matches(is_inblank_narrow);
2651 let s2 = s2.trim_start_matches(is_inblank_narrow);
2652
2653 // c:1405 — HISTREDUCEBLANKS short-circuit to raw strcmp.
2654 if reduce_blanks {
2655 return s1.cmp(s2);
2656 }
2657
2658 let mut c1 = s1.chars().peekable();
2659 let mut c2 = s2.chars().peekable();
2660
2661 // c:1408 — `while (*str1 && *str2) { ... }` then `return *str1 - *str2;`.
2662 loop {
2663 let ch1 = c1.peek().copied();
2664 let ch2 = c2.peek().copied();
2665
2666 match (ch1, ch2) {
2667 (None, None) => return std::cmp::Ordering::Equal, // c:1421 — both NUL
2668 (None, Some(c)) => {
2669 // c:1421 — *str1=0 - *str2; left shorter (Less) unless str2
2670 // is all-inblank residue.
2671 if is_inblank_narrow(c) {
2672 while c2.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
2673 c2.next();
2674 }
2675 if c2.peek().is_none() {
2676 return std::cmp::Ordering::Equal;
2677 }
2678 }
2679 return std::cmp::Ordering::Less;
2680 }
2681 (Some(c), None) => {
2682 if is_inblank_narrow(c) {
2683 while c1.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
2684 c1.next();
2685 }
2686 if c1.peek().is_none() {
2687 return std::cmp::Ordering::Equal;
2688 }
2689 }
2690 return std::cmp::Ordering::Greater;
2691 }
2692 (Some(ch1), Some(ch2)) => {
2693 let ws1 = is_inblank_narrow(ch1);
2694 let ws2 = is_inblank_narrow(ch2);
2695
2696 if ws1 && ws2 {
2697 // c:1411-1413 — collapse both runs.
2698 while c1.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
2699 c1.next();
2700 }
2701 while c2.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
2702 c2.next();
2703 }
2704 } else if ws1 {
2705 // c:1410 — `if (!inblank(*str2)) break;` → mismatch.
2706 while c1.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
2707 c1.next();
2708 }
2709 if c1.peek().is_none() {
2710 return std::cmp::Ordering::Less;
2711 }
2712 return std::cmp::Ordering::Less;
2713 } else if ws2 {
2714 while c2.peek().copied().map(is_inblank_narrow).unwrap_or(false) {
2715 c2.next();
2716 }
2717 if c2.peek().is_none() {
2718 return std::cmp::Ordering::Greater;
2719 }
2720 return std::cmp::Ordering::Greater;
2721 } else if ch1 != ch2 {
2722 return ch1.cmp(&ch2); // c:1417 — *str1 - *str2
2723 } else {
2724 c1.next();
2725 c2.next();
2726 }
2727 }
2728 }
2729 }
2730}
2731
2732/// Port of `addhistnode(HashTable ht, char *nam, void *nodeptr)` from `Src/hashtable.c:1427`.
2733///
2734/// C body:
2735/// ```c
2736/// HashNode oldnode = addhashnode2(ht, nam, nodeptr);
2737/// Histent he = (Histent)nodeptr;
2738/// if (oldnode && oldnode != (HashNode)nodeptr) {
2739/// if (he->node.flags & HIST_MAKEUNIQUE
2740/// || (he->node.flags & HIST_FOREIGN && (Histent)oldnode == he->up)) {
2741/// (void) addhashnode2(ht, oldnode->nam, oldnode); /* restore hash */
2742/// he->node.flags |= HIST_DUP;
2743/// he->node.flags &= ~HIST_MAKEUNIQUE;
2744/// } else {
2745/// oldnode->flags |= HIST_DUP;
2746/// if (hist_ignore_all_dups)
2747/// freehistnode(oldnode); /* Remove the old dup */
2748/// }
2749/// } else
2750/// he->node.flags &= ~HIST_MAKEUNIQUE;
2751/// ```
2752///
2753/// The Rust `histtab` is keyed by command text → event id, so
2754/// `addhashnode2` maps to `HashMap::insert` (returns the displaced
2755/// event). The new node `he` and the displaced `oldnode` are located
2756/// in `hist_ring` by their `histnum`; their `node.flags` are the same
2757/// `HIST_*` fields the C node carries.
2758///
2759/// NOTE: the caller must NOT hold the `hist_ring` lock across this
2760/// call — `addhistnode` re-locks the ring to read/mutate node flags.
2761/// WARNING: param names don't match C — Rust=(nam, event_id) vs C=(ht, nam, nodeptr)
2762pub fn addhistnode(nam: &str, event_id: i32) -> Option<i32> {
2763 // c:1429 — `HashNode oldnode = addhashnode2(ht, nam, nodeptr);`
2764 let oldnode = histtab_lock()
2765 .write()
2766 .expect("histtab poisoned")
2767 .insert(nam.to_string(), event_id);
2768
2769 // c:1431 — `if (oldnode && oldnode != (HashNode)nodeptr)`
2770 if let Some(old_event) = oldnode {
2771 if old_event != event_id {
2772 // `he->node.flags` — flags of the newly inserted node. C reads
2773 // `he->node.flags` directly off the pointer; the Rust ring is a
2774 // `Vec` keyed by `histnum`, so locate the entry by event id.
2775 let he_flags = hist_ring
2776 .lock()
2777 .unwrap()
2778 .iter()
2779 .find(|h| h.histnum == event_id as i64)
2780 .map(|h| h.node.flags)
2781 .unwrap_or(0);
2782 // c:1433 — `(Histent)oldnode == he->up` (the entry directly
2783 // above `he` in the ring is the one being displaced).
2784 let up_is_old = up_histent(event_id as i64) == Some(old_event as i64);
2785 if (he_flags & HIST_MAKEUNIQUE as i32) != 0
2786 || ((he_flags & HIST_FOREIGN as i32) != 0 && up_is_old)
2787 {
2788 // c:1434 — `addhashnode2(ht, oldnode->nam, oldnode);`
2789 // Restore the hash so `nam` maps back to the old event
2790 // (same command text, so the key is unchanged).
2791 histtab_lock()
2792 .write()
2793 .expect("histtab poisoned")
2794 .insert(nam.to_string(), old_event);
2795 // c:1435-1436 — mark `he` a dup, clear make-unique.
2796 if let Some(h) = hist_ring
2797 .lock()
2798 .unwrap()
2799 .iter_mut()
2800 .find(|h| h.histnum == event_id as i64)
2801 {
2802 h.node.flags = (h.node.flags | HIST_DUP as i32) & !(HIST_MAKEUNIQUE as i32);
2803 }
2804 } else {
2805 // c:1439 — `oldnode->flags |= HIST_DUP;`
2806 if let Some(h) = hist_ring
2807 .lock()
2808 .unwrap()
2809 .iter_mut()
2810 .find(|h| h.histnum == old_event as i64)
2811 {
2812 h.node.flags |= HIST_DUP as i32;
2813 }
2814 // c:1440-1441 — `if (hist_ignore_all_dups) freehistnode(oldnode);`
2815 // C's `freehistnode` == `freehistdata(oldnode, 1); zfree(oldnode)`;
2816 // the ported `freehistdata(idx, 1)` unlinks the old node from
2817 // the ring (the Rust equivalent of `zfree`) and — because the
2818 // node is now HIST_DUP-flagged — skips removing the hash entry
2819 // that already points at the new node (c:1466 guard).
2820 if hist_ignore_all_dups.load(Ordering::SeqCst) != 0 {
2821 let idx = hist_ring
2822 .lock()
2823 .unwrap()
2824 .iter()
2825 .position(|h| h.histnum == old_event as i64);
2826 if let Some(idx) = idx {
2827 freehistdata(idx, 1);
2828 }
2829 }
2830 }
2831 return oldnode;
2832 }
2833 }
2834 // c:1445 — `he->node.flags &= ~HIST_MAKEUNIQUE;`
2835 if let Some(h) = hist_ring
2836 .lock()
2837 .unwrap()
2838 .iter_mut()
2839 .find(|h| h.histnum == event_id as i64)
2840 {
2841 h.node.flags &= !(HIST_MAKEUNIQUE as i32);
2842 }
2843 oldnode
2844}
2845
2846/// Port of `freehistnode(HashNode nodeptr)` from `Src/hashtable.c:1450`.
2847///
2848/// C body: `freehistdata((Histent)nodeptr, 1); zfree(nodeptr, ...);`
2849/// Rust port: removes from the lookup table — drop runs the
2850/// equivalent of zfree.
2851pub fn freehistnode(nodeptr: &str) {
2852 histtab_lock()
2853 .write()
2854 .expect("histtab poisoned")
2855 .remove(nodeptr);
2856}
2857
2858/// Port of `freehistdata(Histent he, int unlink)` from `Src/hashtable.c:1458`.
2859///
2860/// C body: removes the named entry from `histtab` (unless flagged
2861/// HIST_DUP/HIST_TMPSTORE), frees the command + word-array fields,
2862/// and if `unlink` re-links the ring around `he` and decrements
2863/// `histlinect`. Rust port indexes into `hist_ring` (Vec replaces C's
2864/// doubly-linked list); the up/down relink collapses to `Vec::remove`.
2865/// WARNING: param names don't match C — Rust=(idx, unlink) vs C=(he, unlink)
2866pub fn freehistdata(idx: usize, unlink: i32) {
2867 // c:1458
2868 let mut ring = hist_ring.lock().unwrap();
2869 let he = match ring.get(idx) {
2870 Some(h) => h,
2871 None => return,
2872 }; // c:1461 if (!he) return
2873 let nam = he.node.nam.clone();
2874 let flags = he.node.flags as u32;
2875 if (flags & (HIST_DUP | HIST_TMPSTORE)) == 0 {
2876 // c:1467
2877 let mut tab = histtab_lock().write().expect("histtab poisoned"); // c:1468 removehashnode(histtab, ...)
2878 tab.remove(&nam);
2879 }
2880 // c:1471-1473 — `zsfree(name); if (nwords) zfree(words, ...)`. Rust
2881 // String/Vec drop handles both; only the unlink step needs explicit
2882 // ring mutation.
2883 if unlink != 0 {
2884 // c:1475
2885 ring.remove(idx); // c:1477-1483 unlink up/down
2886 let new_ct = ring.len() as i64;
2887 drop(ring);
2888 histlinect.store(new_ct, Ordering::SeqCst);
2889 // c:1477 --histlinect
2890 }
2891}
2892
2893/// Port of `dircache_set(char **name, char *value)` from `Src/hashtable.c:1537`.
2894///
2895/// C body manages a refcounted directory-name cache:
2896/// - `value == NULL` → decrement refs on `*name`, free if zero,
2897/// set `*name = NULL`.
2898/// - `value != NULL` → search for an existing entry, bump refs,
2899/// else allocate a new slot.
2900///
2901/// Rust port: routes through dircache_lock() with refcount-by-
2902/// HashMap-value (i32). Add/remove via the (name, value) pair.
2903pub fn dircache_set(name: &mut Option<String>, value: Option<&str>) {
2904 // c:1537
2905 let mut cache = dircache_lock().lock().expect("dircache poisoned");
2906
2907 if value.is_none() {
2908 // c:1541
2909 // c:1542-1543 — `if (!*name) return;`
2910 let key = match name.as_deref() {
2911 None => return, // c:1543
2912 Some(s) => s.to_string(),
2913 };
2914 // c:1544-1548 — `if (!dircache_size) { zsfree(*name); *name = NULL; return; }`
2915 if cache.is_empty() {
2916 // c:1544
2917 *name = None; // c:1546
2918 return; // c:1547
2919 }
2920 // c:1550-1582 — scan cache, decrement matching entry's refs;
2921 // on refs==0, drop the entry. Rust keys by string equality
2922 // since we don't share the C pointer-identity used at c:1553.
2923 if let Some(idx) = cache.iter().position(|e| e.name == key) {
2924 // c:1550
2925 cache[idx].refs -= 1; // c:1555
2926 if cache[idx].refs == 0 {
2927 // c:1556
2928 cache.remove(idx); // c:1558-1577 collapsed
2929 DIRCACHE_LASTENTRY.store(usize::MAX, Ordering::SeqCst); // c:1564/1577
2930 }
2931 *name = None; // c:1579
2932 return; // c:1580
2933 }
2934 // c:1583-1584 — `zsfree(*name); *name = NULL;`
2935 *name = None; // c:1584
2936 } else {
2937 // c:1585
2938 let mut v = value.unwrap().to_string();
2939 // c:1590-1594 — absolute-path normalization for relative input.
2940 if !v.starts_with('/') {
2941 // c:1590
2942 let cwd = zgetcwd(); // c:1591 zgetcwd
2943 v = format!("{}/{}", cwd, v); // c:1591 zhtricat
2944 if let Some(resolved) = xsymlink(&v) {
2945 // c:1593 xsymlink(..., 1)
2946 v = resolved; // c:1593
2947 } // c:1593
2948 }
2949 // c:1602-1606 — `dircache_lastentry` fast-path: same path as last.
2950 let last_idx = DIRCACHE_LASTENTRY.load(Ordering::SeqCst);
2951 if last_idx != usize::MAX && last_idx < cache.len() && cache[last_idx].name == v {
2952 *name = Some(cache[last_idx].name.clone()); // c:1604
2953 cache[last_idx].refs += 1; // c:1605
2954 return; // c:1606
2955 }
2956 // c:1607-1610 — empty-cache: allocate first entry.
2957 if cache.is_empty() {
2958 // c:1607
2959 cache.push(dircache_entry {
2960 name: v.clone(),
2961 refs: 1,
2962 }); // c:1609-1610
2963 DIRCACHE_LASTENTRY.store(0usize, Ordering::SeqCst);
2964 *name = Some(v);
2965 return;
2966 }
2967 // c:1611-1619 — scan for existing entry, bump refs.
2968 if let Some(idx) = cache.iter().position(|e| e.name == v) {
2969 // c:1612-1614
2970 *name = Some(cache[idx].name.clone()); // c:1615
2971 cache[idx].refs += 1; // c:1616
2972 DIRCACHE_LASTENTRY.store(idx, Ordering::SeqCst);
2973 return;
2974 }
2975 // c:1620+ — push new entry.
2976 cache.push(dircache_entry {
2977 name: v.clone(),
2978 refs: 1,
2979 });
2980 let new_idx = cache.len() - 1;
2981 DIRCACHE_LASTENTRY.store(new_idx, Ordering::SeqCst);
2982 *name = Some(v);
2983 }
2984}
2985
2986// `DIRCACHE_LASTENTRY` already declared below at hashtable.rs:1849
2987// as `AtomicUsize` (`usize::MAX` sentinel). Reuse that — the new
2988// body above adapts via i32 cast.
2989
2990// `SuffixAliasTable` type alias deleted — Rust-only convenience.
2991// C has no `SuffixAliasTable`; the same generic `HashTable` powers
2992// both `aliastab` and `sufaliastab` (declared identically at
2993// hashtable.c:1177-1182). Callers can use `alias_table` directly
2994// for both. (When the canonical HashTable substrate is wired,
2995// both will share the same generic type.)
2996
2997/// Port of `struct dircache_entry` from `Src/hashtable.c:1503-1509`.
2998///
2999/// C body:
3000/// ```c
3001/// struct dircache_entry {
3002/// char *name; /* Name of directory in cache */
3003/// int refs; /* Number of references to it */
3004/// };
3005/// ```
3006#[allow(non_camel_case_types)]
3007#[derive(Debug, Clone)]
3008pub struct dircache_entry {
3009 // c:1503
3010 pub name: String, // c:1506
3011 pub refs: i32, // c:1508
3012}
3013
3014/// Command name hash table
3015// hash table containing external commands // c:587
3016#[derive(Debug)]
3017/// `$cmdtab` table of cached executable lookups.
3018/// Port of `cmdnamtab` from Src/hashtable.c — `createcmdnamtable()`
3019/// (line 601), `emptycmdnamtable()` (line 623), and `hashdir()`
3020/// (line 634) drive populate/clear/fill cycles.
3021/// **NOT C-FAITHFUL — Rust-only typed wrapper around HashMap.**
3022/// C uses the generic `HashTable` struct (zsh.h:1530 / zsh_h.rs:535)
3023/// with per-table GSU callback fn pointers (`hash`/`addnode`/
3024/// `getnode`/`removenode`/`freenode`/`printnode`/`scantab`). Each
3025/// per-table accessor (`cmdnamtab_lock`, `shfunctab_lock`, etc.)
3026/// returns a `Mutex<HashTable>` instance with the appropriate
3027/// callbacks wired. When the generic-HashTable substrate lands,
3028/// cmdnam_table/shfunc_table/reswd_table/alias_table get deleted
3029/// in favor of typed views over the shared `HashTable` storage.
3030pub struct cmdnam_table {
3031 /// `table` field.
3032 table: HashMap<String, cmdnam>,
3033 /// `path_checked_index` field.
3034 path_checked_index: usize,
3035 /// `path` field.
3036 path: Vec<String>,
3037 /// `hash_executables_only` field.
3038 hash_executables_only: bool,
3039}
3040
3041// `impl shfunc` deleted — methods replaced with inline flag checks
3042// (`(shf.node.flags & FLAG as i32) != 0`) at callers, mirroring
3043// C's idiom. Constructors `shfunc_with_body` / `shfunc_autoload`
3044// above replace `shfunc::with_body` / `::autoload` / `::new`.
3045
3046/// Shell function hash table
3047// hash table containing the shell functions // c:805
3048#[derive(Debug)]
3049/// `$shfunctab` shell function table.
3050/// Port of the `shfunctab` HashTable Src/hashtable.c builds —
3051/// `printshfuncnode` / `freeshfuncnode` (Src/builtin.c) hang off
3052/// the same shape.
3053/// Faithful port of C's `HashTable shfunctab` (Src/zsh.h, declared
3054/// `mod_export HashTable shfunctab`). Stores `Box<shfunc>` so that
3055/// raw `*mut shfunc` handed to C-style call sites stays stable
3056/// across map rehashes — mirrors C's `HashNode` semantics where
3057/// the table owns the heap allocation and hands out pointers.
3058/// Owned-value accessors (`add`, `get`, `get_mut`) coexist with
3059/// C-faithful pointer accessors (`addnode`, `getnode`) so both
3060/// the Rust-idiomatic bytecode function-def path
3061/// (`fusevm_bridge.rs:8378`) and the C-style `bin_functions`
3062/// port (`builtin.rs:3689+`) write to the same canonical table.
3063pub struct shfunc_table {
3064 /// `table` field.
3065 table: HashMap<String, Box<shfunc>>,
3066}
3067
3068/// Reserved word hash table
3069#[derive(Debug)]
3070/// `$reswdtab` reserved-word table.
3071// hash table containing the reserved words // c:1111
3072/// Port of the `reswdtab` HashTable from Src/hashtable.c — used
3073/// by Src/lex.c to recognize keywords like `if`/`while`/`do`.
3074/// **NOT C-FAITHFUL — Rust-only typed wrapper.** See WARNING on
3075/// `cmdnam_table` for the canonical-port direction.
3076pub struct reswd_table {
3077 /// `table` field.
3078 table: HashMap<String, reswd>,
3079}
3080
3081/// crate::ported::zsh_h::alias hash table
3082#[derive(Debug)]
3083/// `$aliastab` alias hash.
3084/// Port of the `aliastab` HashTable from Src/hashtable.c —
3085// hash table containing the aliases // c:1174
3086/// `bin_alias()` (Src/builtin.c) drives every mutation. Suffix
3087/// aliases live in a separate `sufaliastab` instance.
3088/// **NOT C-FAITHFUL — Rust-only typed wrapper.** See WARNING on
3089/// `cmdnam_table` for the canonical-port direction.
3090pub struct alias_table {
3091 // c:Src/hashtable.c:1186 — aliastab is a HashTable. C's iteration
3092 // order is bucket-walk through hash(name); zsh's order is therefore
3093 // deterministic per-name but not insertion-order. Tests anchored
3094 // to real zsh (zinit/p10k parity) expect insertion-order iteration
3095 // (declarations appear in script order) because that's what users
3096 // see in practice with small alias counts. IndexMap preserves
3097 // insertion order — closer to zsh's observed behavior than the
3098 // previous HashMap (randomized).
3099 /// `table` field.
3100 table: indexmap::IndexMap<String, alias>,
3101}
3102
3103// Mirrors C's file-statics at hashtable.c:1517:
3104// `static struct dircache_entry *dircache, *dircache_lastentry;`
3105// `static int dircache_size;`
3106// Rust port keeps the cache as a `Mutex<Vec<dircache_entry>>` plus
3107// a lastentry index. dircache_size is implicit (Vec::len()).
3108static DIRCACHE_INNER: std::sync::OnceLock<std::sync::Mutex<Vec<dircache_entry>>> =
3109 std::sync::OnceLock::new();
3110static DIRCACHE_LASTENTRY: std::sync::atomic::AtomicUsize = // c:1517
3111 std::sync::atomic::AtomicUsize::new(usize::MAX); // sentinel "no last"
3112
3113/// Build a hashed `cmdnam` carrying a resolved path. Mirrors C's
3114/// inline `cn->u.cmd = ztrdup(path); cn->node.flags = HASHED;` at
3115/// hashtable.c:704.
3116pub fn cmdnam_hashed(name: &str, path: &str) -> cmdnam {
3117 // c:704 idiom
3118 cmdnam {
3119 node: hashnode {
3120 next: None,
3121 nam: name.to_string(),
3122 flags: HASHED as i32,
3123 },
3124 name: None,
3125 cmd: Some(path.to_string()),
3126 }
3127}
3128
3129/// Build an unhashed `cmdnam` whose lookup will scan
3130/// `path_segments`. Mirrors C's `cn->u.name = pathchecked;
3131/// cn->node.flags = 0;` at hashtable.c:712.
3132pub fn cmdnam_unhashed(name: &str, path_segments: Vec<String>) -> cmdnam {
3133 // c:712 idiom
3134 cmdnam {
3135 node: hashnode {
3136 next: None,
3137 nam: name.to_string(),
3138 flags: 0,
3139 },
3140 name: Some(path_segments),
3141 cmd: None,
3142 }
3143}
3144
3145/// Build a `shfunc` for the lazy-compile path with body source text.
3146/// Mirrors C's `shfunctab->addnode(shfunctab, ztrdup(name), shf)`
3147/// after callers populate `shf->funcdef = parse_subst_string(body)`.
3148pub fn shfunc_with_body(name: &str, body: &str) -> shfunc {
3149 // c:824 idiom
3150 // c:Src/exec.c:5383 — `ztrdup(scriptfilename)`. zsh tags every
3151 // shfunc with the script it was defined in so `whence -v fn`
3152 // and `type fn` can print `is a shell function from <script>`.
3153 // For `-c '...'` invocations zsh sets scriptfilename to "zsh".
3154 // Without this seed, fusevm-compiled functions all had
3155 // filename=None and `type fn` lost the "from <script>" suffix.
3156 shfunc {
3157 node: hashnode {
3158 next: None,
3159 nam: name.to_string(),
3160 flags: 0,
3161 },
3162 filename: scriptfilename_get(),
3163 lineno: 0,
3164 funcdef: None,
3165 redir: None,
3166 sticky: None,
3167 body: Some(body.to_string()),
3168 }
3169}
3170
3171/// Build an autoload-marker `shfunc`. Mirrors C's
3172/// `createshfunc(name); shf->node.flags = PM_UNDEFINED;` at
3173/// hashtable.c:829.
3174pub fn shfunc_autoload(name: &str) -> shfunc {
3175 // c:829 idiom
3176 shfunc {
3177 node: hashnode {
3178 next: None,
3179 nam: name.to_string(),
3180 flags: PM_UNDEFINED as i32,
3181 },
3182 filename: None,
3183 lineno: 0,
3184 funcdef: None,
3185 redir: None,
3186 sticky: None,
3187 body: None,
3188 }
3189}
3190
3191// -----------------------------------------------------------
3192// cmdnamtab / aliastab / sufaliastab / reswdtab / histtab
3193// global singletons. Match C's `mod_export HashTable cmdnamtab;`
3194// (hashtable.c:594) and friends. Each is lazily initialised on
3195// first access.
3196// -----------------------------------------------------------
3197
3198// hash table containing external commands // c:587
3199/// Singleton accessor for the global `cmdnamtab`.
3200/// Mirrors C's `mod_export HashTable cmdnamtab` (hashtable.c:594).
3201/// Per PORT_PLAN.md Phase 3 (bucket-2, read-mostly): the PATH cache
3202/// is read on every command resolution but mutated only by `hash`,
3203/// `rehash`, or `path` reassignment. `RwLock` lets parallel command
3204/// lookups proceed without serialising on a single mutex. Holder
3205/// accessor keeps the `_lock` suffix for source-stability (call
3206/// sites use `.read()`/`.write()` directly).
3207pub fn cmdnamtab_lock() -> &'static std::sync::RwLock<cmdnam_table> {
3208 // c:594
3209 static CMDNAMTAB: std::sync::OnceLock<std::sync::RwLock<cmdnam_table>> =
3210 std::sync::OnceLock::new();
3211 CMDNAMTAB.get_or_init(|| std::sync::RwLock::new(cmdnam_table::new()))
3212}
3213
3214/// Port of `mod_export char **pathchecked;` from `Src/hashtable.c:595`.
3215///
3216/// Cursor into the `$path` array tracking how far the PATH-hash-on-
3217/// first-use machinery has walked. Bumped by `hashcmd` (exec.c:1042)
3218/// after each successful lookup so subsequent `hashdir` calls only
3219/// scan entries we haven't already cached.
3220///
3221/// C uses `char **pathchecked` (pointer into the `path[]` array); the
3222/// Rust port stores an index since `$path` lives in paramtab and is
3223/// re-fetched on each access. Reset to 0 by `path` reassignment per
3224/// `Src/hashtable.c:618`.
3225pub static pathchecked: std::sync::atomic::AtomicUsize = // c:595
3226 std::sync::atomic::AtomicUsize::new(0);
3227
3228// hash table containing the aliases // c:1174
3229/// Singleton accessor for the global `aliastab`.
3230/// Mirrors C's `mod_export HashTable aliastab` (hashtable.c:1186).
3231/// Bucket-2 read-mostly: aliases are looked up on every command word,
3232/// mutated only by `alias`/`unalias`. `RwLock` per PORT_PLAN.md.
3233pub fn aliastab_lock() -> &'static std::sync::RwLock<alias_table> {
3234 // c:1186
3235 static ALIASTAB: std::sync::OnceLock<std::sync::RwLock<alias_table>> =
3236 std::sync::OnceLock::new();
3237 ALIASTAB.get_or_init(|| std::sync::RwLock::new(alias_table::with_defaults()))
3238}
3239
3240/// Singleton accessor for the global `sufaliastab`.
3241/// Mirrors C's `mod_export HashTable sufaliastab` (hashtable.c:1187).
3242/// Bucket-2 read-mostly: same rationale as `aliastab`.
3243pub fn sufaliastab_lock() -> &'static std::sync::RwLock<alias_table> {
3244 static SUFALIASTAB: std::sync::OnceLock<std::sync::RwLock<alias_table>> =
3245 std::sync::OnceLock::new();
3246 SUFALIASTAB.get_or_init(|| std::sync::RwLock::new(alias_table::new()))
3247}
3248
3249// hash table containing the reserved words // c:1111
3250/// Singleton accessor for the global `reswdtab`.
3251/// Mirrors C's `HashTable reswdtab` (hashtable.c, file-scope).
3252/// Bucket-2 read-mostly (effectively read-only post-init): every
3253/// command word is checked against reserved words; the table is
3254/// populated once at startup. `RwLock` per PORT_PLAN.md.
3255pub fn reswdtab_lock() -> &'static std::sync::RwLock<reswd_table> {
3256 // c:1115
3257 static reswdTAB: std::sync::OnceLock<std::sync::RwLock<reswd_table>> =
3258 std::sync::OnceLock::new();
3259 reswdTAB.get_or_init(|| std::sync::RwLock::new(reswd_table::new()))
3260}
3261
3262/// Singleton accessor for the global `histtab` (history events).
3263/// Mirrors C's `HashTable histtab` (hashtable.c:1340).
3264pub fn histtab_lock() -> &'static std::sync::RwLock<HashMap<String, i32>> {
3265 static HISTTAB: std::sync::OnceLock<std::sync::RwLock<HashMap<String, i32>>> =
3266 std::sync::OnceLock::new();
3267 HISTTAB.get_or_init(|| std::sync::RwLock::new(HashMap::new()))
3268}
3269
3270// ===========================================================
3271// shfunctab — the global shell-function table.
3272//
3273// Port of `mod_export HashTable shfunctab` from
3274// `Src/hashtable.c:808` and the GSU callbacks built around it
3275// (`createshfunctable` and the `*shfuncnode` family).
3276//
3277// C zsh dispatches every `function f() { … }` definition,
3278// `unfunction`, `disable -f`, `enable -f`, `whence`, and trap-
3279// function lookup through `shfunctab`. zshrs uses a singleton
3280// `OnceLock<Mutex<shfunc_table>>` exposed via `shfunctab_lock()`
3281// so the GSU-style C names below can mutate it without taking a
3282// `ShellExecutor` parameter (matching the C signatures, where
3283// the table is global).
3284// ===========================================================
3285
3286/// Singleton accessor for the global `shfunctab`.
3287/// Mirrors C's `mod_export HashTable shfunctab` (hashtable.c:808).
3288/// Lazily initialised on first access. Bucket-2 read-mostly: shell
3289/// functions are looked up on every function-call dispatch, mutated
3290/// only by `function f()` / `unfunction` / `autoload`. `RwLock`
3291/// per PORT_PLAN.md.
3292pub fn shfunctab_lock() -> &'static std::sync::RwLock<shfunc_table> {
3293 // c:808
3294 static shfuncTAB: std::sync::OnceLock<std::sync::RwLock<shfunc_table>> =
3295 std::sync::OnceLock::new();
3296 shfuncTAB.get_or_init(|| std::sync::RwLock::new(shfunc_table::new()))
3297}
3298
3299/// Glob-style match for hashtable scan callers. Direct port of C's
3300/// `pattry(pprog, hn->nam)` at `Src/hashtable.c:412` / `c:431` —
3301/// `scanmatchtable` compiles the caller's pattern once into a
3302/// `Patprog` and tests every node's name against it. zshrs's
3303/// `patmatch(pattern, text)` (pattern.rs:1561) does the
3304/// `patcompile + pattry` pair in one call, so we route through
3305/// it directly.
3306///
3307/// Previously this was an ad-hoc 30-line recursive matcher that
3308/// only handled `*` and `?` — char classes (`[abc]`), numeric
3309/// ranges (`<1-9>`), recursive globs, and the rest of zsh's
3310/// extended-glob set silently fell through. Now uses the
3311/// canonical engine.
3312fn simple_glob_match(pattern: &str, name: &str) -> bool {
3313 // c:hashtable.c:412 — `scanmatchtable` callers pass a compiled
3314 // `Patprog`; this helper inlines the compile+match since callers
3315 // here have only the raw pattern string.
3316 patcompile(
3317 &{
3318 let mut __pat_tok = (pattern).to_string();
3319 crate::ported::glob::tokenize(&mut __pat_tok);
3320 __pat_tok
3321 },
3322 PAT_HEAPDUP as i32,
3323 None,
3324 )
3325 .map_or(false, |p| pattry(&p, name))
3326}
3327
3328// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3329// ─── RUST-ONLY ACCESSORS ───
3330//
3331// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
3332// RwLock<T>>` globals declared above. C zsh uses direct global
3333// access; Rust needs these wrappers because `OnceLock::get_or_init`
3334// is the only way to lazily construct shared state. These ported sit
3335// here so the body of this file reads in C source order without
3336// the accessor wrappers interleaved between real port ported.
3337// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3338
3339// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3340// ─── RUST-ONLY ACCESSORS ───
3341//
3342// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
3343// RwLock<T>>` globals declared above. C zsh uses direct global
3344// access; Rust needs these wrappers because `OnceLock::get_or_init`
3345// is the only way to lazily construct shared state. These ported sit
3346// here so the body of this file reads in C source order without
3347// the accessor wrappers interleaved between real port ported.
3348// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3349
3350/// Singleton accessor for the `dircache` file-static at
3351/// `Src/hashtable.c:1517`.
3352pub fn dircache_lock() -> &'static std::sync::Mutex<Vec<dircache_entry>> {
3353 DIRCACHE_INNER.get_or_init(|| std::sync::Mutex::new(Vec::new()))
3354}
3355
3356#[cfg(test)]
3357mod tests {
3358 use std::cmp::Ordering;
3359
3360 use super::*;
3361
3362 #[test]
3363 fn test_hasher() {
3364 let _g = crate::test_util::global_state_lock();
3365 assert_eq!(hasher(""), 0);
3366 assert_ne!(hasher("test"), 0);
3367 assert_eq!(hasher("test"), hasher("test"));
3368 assert_ne!(hasher("test"), hasher("Test"));
3369 }
3370
3371 /// Pin `hnamcmp` to its canonical C body at `Src/hashtable.c:341-346`:
3372 /// must route through `ztrcmp` (META-AWARE compare), not naive
3373 /// `str::cmp`. The previous Rust port used byte-wise cmp which
3374 /// sorts Meta-encoded keys incorrectly.
3375 #[test]
3376 fn hnamcmp_uses_ztrcmp_meta_aware_compare() {
3377 let _g = crate::test_util::global_state_lock();
3378 // Plain ASCII: same as str::cmp.
3379 assert_eq!(hnamcmp("apple", "banana"), Ordering::Less);
3380 assert_eq!(hnamcmp("banana", "apple"), Ordering::Greater);
3381 assert_eq!(hnamcmp("equal", "equal"), Ordering::Equal);
3382
3383 // Empty string sorts before non-empty.
3384 assert_eq!(hnamcmp("", "a"), Ordering::Less);
3385 assert_eq!(hnamcmp("a", ""), Ordering::Greater);
3386
3387 // Meta-encoded byte: 0x83 0x41 → real 0x61 ('a'). The
3388 // Meta-aware ztrcmp treats `\x83\x41` as 'a' for compare
3389 // purposes; naive str::cmp would compare 0x83 vs 0x61 (so
3390 // "\x83\x41" would sort AFTER 'a'). Verify the Meta-aware
3391 // path: the encoded "a" should compare equal-ish to "a".
3392 // Construct via unsafe bytes since 0x83 isn't valid UTF-8
3393 // alone — Rust ztrcmp operates on bytes.
3394 let meta_a_bytes: Vec<u8> = vec![0x83, 0x41]; // Meta + 'A'^32 = 'a'
3395 let meta_a = unsafe { std::str::from_utf8_unchecked(&meta_a_bytes) };
3396 // Real "a" (0x61) vs encoded "a" (0x83 0x41): ztrcmp resolves
3397 // both to 0x61 at the first position → Equal. But ztrcmp also
3398 // takes into account end-of-string, so encoded "a" is longer
3399 // by one byte unstripped. The C ztrcmp loop skips matching
3400 // prefix; here the first bytes differ (0x61 vs 0x83), so it
3401 // resolves c1=0x61, c2=(0x41^32)=0x61 → Equal. Verify.
3402 assert_eq!(
3403 hnamcmp("a", meta_a),
3404 Ordering::Equal,
3405 "c:345 — Meta-encoded 'a' (0x83 0x41) compares equal to real 'a'"
3406 );
3407 }
3408
3409 #[test]
3410 fn test_histhasher() {
3411 let _g = crate::test_util::global_state_lock();
3412 assert_eq!(histhasher(" hello world "), histhasher("hello world"));
3413 assert_ne!(histhasher("hello world"), histhasher("helloworld"));
3414 }
3415
3416 /// `Src/hashtable.c:1365-1380` — `histhasher` uses `inblank(*str)`
3417 /// per `Src/ztype.h:50`: NARROW space/tab only. The previous Rust
3418 /// port used `c.is_whitespace()` (broad Unicode) which would have
3419 /// silently rehashed any history line containing CR/FF/VT/NBSP.
3420 /// Pin the narrow-inblank semantics:
3421 /// * Multi-space/tab runs collapse to a single ' ' bucket-mix.
3422 /// * Newlines are NOT collapsed (newline is not inblank per c:50).
3423 /// * NBSP / CR are NOT treated as inblank.
3424 #[test]
3425 fn histhasher_inblank_is_narrow_space_tab_only() {
3426 let _g = crate::test_util::global_state_lock();
3427 // c:1369 — leading inblank stripped; multiple equivalent forms hash same.
3428 assert_eq!(
3429 histhasher("\t hello"),
3430 histhasher("hello"),
3431 "c:1369 — leading space+tab stripped before mixing"
3432 );
3433 // c:1373 — runs of inblank collapse to a single ' '.
3434 assert_eq!(
3435 histhasher("a \t b"),
3436 histhasher("a b"),
3437 "c:1373 — interior inblank runs collapse to single space"
3438 );
3439
3440 // Newline is NOT inblank per c:50; it must hash as itself, not collapse.
3441 assert_ne!(
3442 histhasher("a\nb"),
3443 histhasher("a b"),
3444 "c:50 — newline is NOT inblank; hashes as its own char"
3445 );
3446 // CR is NOT inblank.
3447 assert_ne!(
3448 histhasher("a\rb"),
3449 histhasher("ab"),
3450 "CR not in inblank; must mix as a character, not collapse"
3451 );
3452 // NBSP (0xA0) is NOT inblank (it's broad Unicode whitespace
3453 // but NOT in C's narrow typtab class).
3454 assert_ne!(
3455 histhasher("a\u{00A0}b"),
3456 histhasher("ab"),
3457 "NBSP not in inblank; must mix as a character, not collapse"
3458 );
3459 }
3460
3461 #[test]
3462 fn test_histstrcmp() {
3463 let _g = crate::test_util::global_state_lock();
3464 assert_eq!(
3465 histstrcmp(" hello world ", "hello world", false),
3466 Ordering::Equal
3467 );
3468 assert_eq!(
3469 histstrcmp("hello world", "hello world", true),
3470 Ordering::Equal
3471 );
3472 }
3473
3474 /// `Src/hashtable.c:1396-1421` — `histstrcmp` uses `inblank(*str)`
3475 /// (NARROW space/tab only per `Src/ztype.h:50`). The previous Rust
3476 /// port used `c.is_whitespace()` (broad Unicode) which silently
3477 /// folded history lines that C considers distinct.
3478 /// Pin narrow-inblank semantics.
3479 #[test]
3480 fn histstrcmp_inblank_is_narrow_space_tab_only() {
3481 let _g = crate::test_util::global_state_lock();
3482 // c:1411-1413 — runs of inblank collapse to a single boundary.
3483 assert_eq!(
3484 histstrcmp("hello\tworld", "hello world", false),
3485 Ordering::Equal,
3486 "c:1411-1413 — tab and space both inblank; mixed runs equal"
3487 );
3488 // Newline is NOT inblank per c:50 → string mismatch.
3489 assert_ne!(
3490 histstrcmp("hello\nworld", "hello world", false),
3491 Ordering::Equal,
3492 "c:50 — newline is NOT inblank; must be treated as ordinary char"
3493 );
3494 // CR is NOT inblank.
3495 assert_ne!(
3496 histstrcmp("hello\rworld", "hello world", false),
3497 Ordering::Equal,
3498 "CR not in inblank; not collapsed with space"
3499 );
3500 // NBSP is NOT inblank (broad Unicode whitespace, NOT typtab).
3501 assert_ne!(
3502 histstrcmp("hello\u{00A0}world", "hello world", false),
3503 Ordering::Equal,
3504 "NBSP not in inblank; not collapsed"
3505 );
3506 // c:1405 — HISTREDUCEBLANKS short-circuits to raw cmp.
3507 // With reduce_blanks=true the multi-space form is NOT collapsed.
3508 assert_ne!(
3509 histstrcmp("hello world", "hello world", true),
3510 Ordering::Equal,
3511 "c:1405 — HISTREDUCEBLANKS=true → strcmp; runs do NOT collapse"
3512 );
3513 }
3514
3515 /// `Src/hashtable.c:1398-1399` — leading inblank is stripped from
3516 /// both sides BEFORE comparison. So `" cmd"` and `"\tcmd"` are
3517 /// equal. Trailing inblank (per the loop behavior, c:1421
3518 /// `*str1 - *str2` reaches 0 when one side runs out) is also
3519 /// folded: trailing run on one side vs end on the other returns
3520 /// Equal via the (Some, None) inblank-collapse branch.
3521 #[test]
3522 fn histstrcmp_strips_leading_and_trailing_inblank() {
3523 let _g = crate::test_util::global_state_lock();
3524 assert_eq!(
3525 histstrcmp(" cmd", "\tcmd", false),
3526 Ordering::Equal,
3527 "c:1398-1399 — leading inblank skipped (both kinds)"
3528 );
3529 assert_eq!(
3530 histstrcmp("cmd ", "cmd", false),
3531 Ordering::Equal,
3532 "c:1421 — trailing inblank on left collapses to end-equal"
3533 );
3534 assert_eq!(
3535 histstrcmp("cmd", "cmd\t\t", false),
3536 Ordering::Equal,
3537 "c:1421 — trailing inblank on right collapses to end-equal"
3538 );
3539 }
3540
3541 #[test]
3542 fn test_cmdnam_table() {
3543 let _g = crate::test_util::global_state_lock();
3544 let mut table = cmdnam_table::new();
3545 table.add(cmdnam_hashed("ls", "/bin/ls"));
3546
3547 assert!(table.get("ls").is_some());
3548 assert!(table.get("nonexistent").is_none());
3549
3550 let ls = table.get("ls").unwrap();
3551 assert_ne!((ls.node.flags & HASHED as i32), 0);
3552 assert_eq!((ls.node.flags & DISABLED as i32), 0);
3553 }
3554
3555 #[test]
3556 fn test_shfunc_table() {
3557 let _g = crate::test_util::global_state_lock();
3558 let mut table = shfunc_table::new();
3559 table.add(shfunc_with_body("myfunc", "echo hello"));
3560 table.add(shfunc_autoload("lazy"));
3561
3562 assert!(table.get("myfunc").is_some());
3563 assert_eq!(
3564 (table.get("myfunc").unwrap().node.flags & PM_UNDEFINED as i32),
3565 0
3566 );
3567 assert_ne!(
3568 (table.get("lazy").unwrap().node.flags & PM_UNDEFINED as i32),
3569 0
3570 );
3571
3572 table.disable("myfunc");
3573 assert!(table.get("myfunc").is_none());
3574 assert!(table.get_including_disabled("myfunc").is_some());
3575
3576 table.enable("myfunc");
3577 assert!(table.get("myfunc").is_some());
3578 }
3579
3580 #[test]
3581 fn test_reswd_table() {
3582 let _g = crate::test_util::global_state_lock();
3583 let table = reswd_table::new();
3584
3585 assert!(table.is_reserved("if"));
3586 assert!(table.is_reserved("while"));
3587 assert!(table.is_reserved("[["));
3588 assert!(!table.is_reserved("notreserved"));
3589
3590 let if_rw = table.get("if").unwrap();
3591 assert_eq!(if_rw.token, IF);
3592 }
3593
3594 #[test]
3595 fn test_alias_table() {
3596 let _g = crate::test_util::global_state_lock();
3597 let mut table = alias_table::with_defaults();
3598
3599 assert!(table.get("run-help").is_some());
3600 assert_eq!(table.get("run-help").unwrap().text, "man");
3601
3602 table.add(createaliasnode("G", "| grep", ALIAS_GLOBAL as u32));
3603 let g = table.get("G").unwrap();
3604 assert_ne!((g.node.flags & ALIAS_GLOBAL as i32), 0);
3605
3606 table.add(createaliasnode("pdf", "zathura", ALIAS_SUFFIX as u32));
3607 let p = table.get("pdf").unwrap();
3608 assert_ne!((p.node.flags & ALIAS_SUFFIX as i32), 0);
3609
3610 table.disable("G");
3611 assert!(table.get("G").is_none());
3612 }
3613
3614 #[test]
3615 fn test_dir_cache() {
3616 let _g = crate::test_util::global_state_lock();
3617 // Smoke-test the canonical `dircache` file-static at
3618 // hashtable.c:1517 — the cache lives in a global Mutex
3619 // matching C semantics. Each test gets a fresh slice via
3620 // a unique-name marker so parallel tests don't collide.
3621 let cache = dircache_lock();
3622 {
3623 let mut g = cache.lock().unwrap();
3624 g.clear();
3625 g.push(dircache_entry {
3626 name: "/usr/share/zsh".into(),
3627 refs: 1,
3628 });
3629 g.push(dircache_entry {
3630 name: "/usr/share/zsh".into(),
3631 refs: 1,
3632 });
3633 // Dedupe-by-refs is the C semantic: get_or_insert bumps
3634 // refs on an existing entry. Verify the data shape.
3635 assert_eq!(g.len(), 2);
3636 assert_eq!(g[0].refs, 1);
3637 }
3638 }
3639
3640 // -------------------------------------------------------------
3641 // Tests for the global shfunctab singleton & GSU callbacks.
3642 //
3643 // Tests are serialised via shfuncTAB_TEST_LOCK because they
3644 // mutate the process-wide singleton.
3645 // -------------------------------------------------------------
3646
3647 static shfuncTAB_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3648
3649 fn fresh_shfunctab() {
3650 let mut tab = shfunctab_lock().write().expect("shfunctab poisoned");
3651 tab.clear();
3652 }
3653
3654 #[test]
3655 fn test_createshfunctable_idempotent() {
3656 let _g = crate::test_util::global_state_lock();
3657 let _g = shfuncTAB_TEST_LOCK.lock();
3658 createshfunctable();
3659 createshfunctable();
3660 // Singleton handle stable across calls.
3661 let h1 = shfunctab_lock() as *const _;
3662 let h2 = shfunctab_lock() as *const _;
3663 assert_eq!(h1, h2);
3664 }
3665
3666 #[test]
3667 fn test_shfunctab_add_get_remove() {
3668 let _g = crate::test_util::global_state_lock();
3669 let _g = shfuncTAB_TEST_LOCK.lock();
3670 fresh_shfunctab();
3671 {
3672 let mut tab = shfunctab_lock().write().unwrap();
3673 tab.add(shfunc_with_body("greet", "echo hello"));
3674 }
3675 {
3676 let tab = shfunctab_lock().read().unwrap();
3677 assert!(tab.get("greet").is_some());
3678 assert_eq!(
3679 tab.get("greet").unwrap().body.as_deref(),
3680 Some("echo hello")
3681 );
3682 }
3683 let removed = removeshfuncnode("greet");
3684 assert!(removed.is_some());
3685 assert!(shfunctab_lock().read().unwrap().get("greet").is_none());
3686 }
3687
3688 #[test]
3689 fn test_shfunctab_disable_enable() {
3690 let _g = crate::test_util::global_state_lock();
3691 let _g = shfuncTAB_TEST_LOCK.lock();
3692 fresh_shfunctab();
3693 {
3694 let mut tab = shfunctab_lock().write().unwrap();
3695 tab.add(shfunc_with_body("f", "true"));
3696 }
3697 disableshfuncnode("f");
3698 // get() filters disabled; get_including_disabled doesn't.
3699 {
3700 let tab = shfunctab_lock().read().unwrap();
3701 assert!(tab.get("f").is_none());
3702 assert!(tab.get_including_disabled("f").is_some());
3703 }
3704 enableshfuncnode("f");
3705 assert!(shfunctab_lock().read().unwrap().get("f").is_some());
3706 removeshfuncnode("f");
3707 }
3708
3709 #[test]
3710 fn test_simple_glob_match() {
3711 let _g = crate::test_util::global_state_lock();
3712 assert!(simple_glob_match("foo", "foo"));
3713 assert!(!simple_glob_match("foo", "bar"));
3714 assert!(simple_glob_match("f*", "foo"));
3715 assert!(simple_glob_match("f*", "f"));
3716 assert!(simple_glob_match("*o", "foo"));
3717 assert!(simple_glob_match("*", ""));
3718 assert!(simple_glob_match("?oo", "foo"));
3719 assert!(!simple_glob_match("?oo", "fo"));
3720 assert!(simple_glob_match("f*o", "frogspawn-suo"));
3721 }
3722
3723 #[test]
3724 fn test_scanmatchshfunc_matches_pattern() {
3725 let _g = crate::test_util::global_state_lock();
3726 let _g = shfuncTAB_TEST_LOCK.lock();
3727 fresh_shfunctab();
3728 {
3729 let mut tab = shfunctab_lock().write().unwrap();
3730 tab.add(shfunc_with_body("foo", "echo a"));
3731 tab.add(shfunc_with_body("foobar", "echo b"));
3732 tab.add(shfunc_with_body("baz", "echo c"));
3733 }
3734 let mut matched: Vec<String> = Vec::new();
3735 let count = scanmatchshfunc(Some("foo*"), |name, _| matched.push(name.to_string()));
3736 assert_eq!(count, 2);
3737 matched.sort();
3738 assert_eq!(matched, vec!["foo".to_string(), "foobar".to_string()]);
3739 // No-pattern walks all.
3740 let total = scanshfunc(|_, _| {});
3741 assert_eq!(total, 3);
3742 fresh_shfunctab();
3743 }
3744
3745 #[test]
3746 fn test_getshfuncfile_returns_filename() {
3747 let _g = crate::test_util::global_state_lock();
3748 let _g = shfuncTAB_TEST_LOCK.lock();
3749 fresh_shfunctab();
3750 {
3751 let mut tab = shfunctab_lock().write().unwrap();
3752 let mut f = shfunc_with_body("f", "true");
3753 f.filename = Some("/tmp/zshrs-ported/f".to_string());
3754 tab.add(f);
3755 }
3756 assert_eq!(getshfuncfile("f"), Some("/tmp/zshrs-ported/f".to_string()));
3757 assert_eq!(getshfuncfile("nonexistent"), None);
3758 fresh_shfunctab();
3759 }
3760
3761 // -------------------------------------------------------------
3762 // Generic hashtable ops + per-table singletons.
3763 // -------------------------------------------------------------
3764
3765 #[test]
3766 fn test_generic_addhashnode_displaces_old() {
3767 let _g = crate::test_util::global_state_lock();
3768 let mut ht: HashMap<String, alias> = HashMap::new();
3769 addhashnode(&mut ht, "x", createaliasnode("x", "echo a", 0));
3770 let old = addhashnode2(&mut ht, "x", createaliasnode("x", "echo b", 0));
3771 assert!(old.is_some());
3772 assert_eq!(old.unwrap().text, "echo a");
3773 assert_eq!(gethashnode2(&ht, "x").unwrap().text, "echo b");
3774 }
3775
3776 #[test]
3777 fn test_generic_disable_filters_get() {
3778 let _g = crate::test_util::global_state_lock();
3779 let mut ht: HashMap<String, alias> = HashMap::new();
3780 ht.insert("a".to_string(), createaliasnode("a", "1", 0));
3781 assert!(gethashnode(&ht, "a").is_some());
3782 disablehashnode(&mut ht, "a");
3783 // gethashnode filters disabled, gethashnode2 doesn't.
3784 assert!(gethashnode(&ht, "a").is_none());
3785 assert!(gethashnode2(&ht, "a").is_some());
3786 enablehashnode(&mut ht, "a");
3787 assert!(gethashnode(&ht, "a").is_some());
3788 }
3789
3790 #[test]
3791 fn test_scanmatchtable_pattern_and_count() {
3792 let _g = crate::test_util::global_state_lock();
3793 let mut ht: HashMap<String, alias> = HashMap::new();
3794 ht.insert("foo".to_string(), createaliasnode("foo", "1", 0));
3795 ht.insert("foobar".to_string(), createaliasnode("foobar", "2", 0));
3796 ht.insert("baz".to_string(), createaliasnode("baz", "3", 0));
3797 let mut hits: Vec<String> = Vec::new();
3798 let count = scanmatchtable(&ht, Some("foo*"), true, 0, 0, |n, _| {
3799 hits.push(n.to_string())
3800 });
3801 assert_eq!(count, 2);
3802 // Sorted output guaranteed when sorted=true.
3803 assert_eq!(hits, vec!["foo".to_string(), "foobar".to_string()]);
3804 }
3805
3806 #[test]
3807 fn test_emptyhashtable_clears() {
3808 let _g = crate::test_util::global_state_lock();
3809 let mut ht: HashMap<String, alias> = HashMap::new();
3810 ht.insert("a".to_string(), createaliasnode("a", "1", 0));
3811 ht.insert("b".to_string(), createaliasnode("b", "2", 0));
3812 assert_eq!(ht.len(), 2);
3813 emptyhashtable(&mut ht);
3814 assert_eq!(ht.len(), 0);
3815 }
3816
3817 #[test]
3818 fn test_resizehashtable_reserves_capacity() {
3819 let _g = crate::test_util::global_state_lock();
3820 let mut ht: HashMap<String, i32> = HashMap::new();
3821 let initial_cap = ht.capacity();
3822 resizehashtable(&mut ht, 200);
3823 assert!(ht.capacity() >= 200);
3824 assert!(ht.capacity() >= initial_cap);
3825 }
3826
3827 #[test]
3828 fn test_aliastab_singleton_has_defaults() {
3829 let _g = crate::test_util::global_state_lock();
3830 let tab = aliastab_lock().read().unwrap();
3831 // createaliastables seeds run-help and which-command.
3832 assert!(tab.get_including_disabled("run-help").is_some());
3833 assert!(tab.get_including_disabled("which-command").is_some());
3834 }
3835
3836 #[test]
3837 fn test_createaliasnode_sets_flags() {
3838 let _g = crate::test_util::global_state_lock();
3839 let a = createaliasnode("foo", "echo bar", ALIAS_GLOBAL as u32);
3840 assert_eq!(a.node.nam, "foo");
3841 assert_eq!(a.text, "echo bar");
3842 assert_ne!((a.node.flags & ALIAS_GLOBAL as i32), 0);
3843 }
3844
3845 #[test]
3846 fn test_printaliasnode_smoke() {
3847 // printaliasnode writes directly to stdout (matches C's void
3848 // return / writes-to-stdout signature). The behavioural parity
3849 // assertions live in `tests/builtin_c_parity.rs::alias_builtin`,
3850 // which compares against `/bin/zsh -fc 'alias gst'` byte-for-byte.
3851 // This unit test just exercises every flag branch to make sure
3852 // none panics / borrows incorrectly.
3853 let _g = crate::test_util::global_state_lock();
3854 let a = createaliasnode("ll", "ls -la", 0);
3855 printaliasnode(&a, PRINT_NAMEONLY);
3856 printaliasnode(&a, PRINT_WHENCE_WORD);
3857 printaliasnode(&a, PRINT_WHENCE_SIMPLE);
3858 printaliasnode(&a, PRINT_WHENCE_CSH);
3859 printaliasnode(&a, PRINT_WHENCE_VERBOSE);
3860 printaliasnode(&a, PRINT_LIST);
3861 printaliasnode(&a, 0);
3862 }
3863
3864 #[test]
3865 fn test_printreswdnode_smoke() {
3866 // printreswdnode writes directly to stdout (matches C's void
3867 // return / write-to-stdout signature at hashtable.c:1147).
3868 // Smoke-test every flag branch to make sure none panics.
3869 let _g = crate::test_util::global_state_lock();
3870 let table = reswd_table::new();
3871 let if_rw = table.get("if").unwrap();
3872 printreswdnode(if_rw, PRINT_WHENCE_WORD);
3873 printreswdnode(if_rw, PRINT_WHENCE_CSH);
3874 printreswdnode(if_rw, PRINT_WHENCE_VERBOSE);
3875 printreswdnode(if_rw, 0);
3876 }
3877
3878 #[test]
3879 fn test_addhistnode_displaces_old() {
3880 let _g = crate::test_util::global_state_lock();
3881 emptyhisttable();
3882 assert_eq!(addhistnode("ls -la", 1), None);
3883 let old = addhistnode("ls -la", 5);
3884 assert_eq!(old, Some(1));
3885 emptyhisttable();
3886 }
3887
3888 #[test]
3889 fn test_freecmdnamnode_removes() {
3890 let _g = crate::test_util::global_state_lock();
3891 emptycmdnamtable();
3892 {
3893 let mut tab = cmdnamtab_lock().write().unwrap();
3894 tab.add(cmdnam_unhashed("ls", vec!["/bin".to_string()]));
3895 }
3896 assert!(cmdnamtab_lock().read().unwrap().get("ls").is_some());
3897 freecmdnamnode("ls");
3898 assert!(cmdnamtab_lock().read().unwrap().get("ls").is_none());
3899 }
3900
3901 #[test]
3902 fn test_dircache_set_refcounts() {
3903 let _g = crate::test_util::global_state_lock();
3904 // Refcount add → entries grow.
3905 let mut k: Option<String> = None;
3906 dircache_set(&mut k, Some("/usr/bin"));
3907 let mut k2: Option<String> = None;
3908 dircache_set(&mut k2, Some("/usr/bin"));
3909 let cache_size = dircache_lock().lock().unwrap().len();
3910 assert!(cache_size >= 1);
3911 }
3912
3913 /// c:1230 — `createaliasnode(name, text, flags)` builds an crate::ported::zsh_h::alias
3914 /// with the text field populated. Regression that drops `text`
3915 /// would silently install aliases that expand to nothing.
3916 #[test]
3917 fn createaliasnode_round_trips_name_and_text() {
3918 let _g = crate::test_util::global_state_lock();
3919 let a = createaliasnode("ls-color", "ls --color=auto", 0);
3920 assert_eq!(a.text, "ls --color=auto");
3921 assert_eq!(a.node.nam, "ls-color");
3922 }
3923
3924 // ─── alias-creation zsh-corpus pins ────────────────────────────
3925
3926 /// `createaliasnode` round-trips name+text+flags=0 (regular alias).
3927 #[test]
3928 fn alias_corpus_create_regular_alias() {
3929 let _g = crate::test_util::global_state_lock();
3930 let a = createaliasnode("ll", "ls -la", 0);
3931 assert_eq!(a.node.nam, "ll");
3932 assert_eq!(a.text, "ls -la");
3933 // Regular alias = no GLOBAL/SUFFIX flags.
3934 let f = a.node.flags as i32;
3935 assert_eq!(
3936 f & (ALIAS_GLOBAL | ALIAS_SUFFIX),
3937 0,
3938 "regular alias has no GLOBAL/SUFFIX bits"
3939 );
3940 }
3941
3942 /// `createaliasnode` with ALIAS_GLOBAL flag sets the global bit.
3943 #[test]
3944 fn alias_corpus_create_global_alias_carries_flag() {
3945 let _g = crate::test_util::global_state_lock();
3946 let a = createaliasnode("G", "global text", ALIAS_GLOBAL as u32);
3947 let f = a.node.flags as i32;
3948 assert_ne!(f & ALIAS_GLOBAL, 0, "ALIAS_GLOBAL set");
3949 }
3950
3951 /// `createaliasnode` with ALIAS_SUFFIX flag sets the suffix bit.
3952 #[test]
3953 fn alias_corpus_create_suffix_alias_carries_flag() {
3954 let _g = crate::test_util::global_state_lock();
3955 let a = createaliasnode("S", "suffix text", ALIAS_SUFFIX as u32);
3956 let f = a.node.flags as i32;
3957 assert_ne!(f & ALIAS_SUFFIX, 0, "ALIAS_SUFFIX set");
3958 }
3959
3960 /// Empty text is preserved (zsh allows zero-length alias expansion).
3961 #[test]
3962 fn alias_corpus_create_empty_text_preserved() {
3963 let _g = crate::test_util::global_state_lock();
3964 let a = createaliasnode("noop", "", 0);
3965 assert_eq!(a.text, "");
3966 }
3967
3968 /// Alias text may contain spaces — preserved as-is.
3969 #[test]
3970 fn alias_corpus_create_multi_word_text_preserved() {
3971 let _g = crate::test_util::global_state_lock();
3972 let a = createaliasnode("rmf", "rm -rf --no-preserve-root", 0);
3973 assert_eq!(a.text, "rm -rf --no-preserve-root");
3974 }
3975
3976 /// `aliastab_lock` initialises with the two default aliases
3977 /// `run-help` and `which-command` per hashtable.c:1215-1216.
3978 /// A regression here breaks zsh's documented default behaviour
3979 /// where `run-help` resolves to `man` after `autoload -U run-help`.
3980 #[test]
3981 fn aliastab_seeds_run_help_and_which_command_defaults() {
3982 let _g = crate::test_util::global_state_lock();
3983 createaliastables();
3984 let tab = aliastab_lock().read().expect("aliastab poisoned");
3985 assert!(tab.get("run-help").is_some(), "run-help default missing");
3986 assert!(
3987 tab.get("which-command").is_some(),
3988 "which-command default missing"
3989 );
3990 }
3991
3992 /// c:86 — `hasher` is the canonical zsh string hash. Same input
3993 /// MUST produce same output (basic determinism); different inputs
3994 /// SHOULD produce different outputs (no pathological collisions
3995 /// for single-char-different strings). The wrapping_add chain in
3996 /// the impl makes this a Bernstein-style hash; verify it's stable.
3997 #[test]
3998 fn hasher_is_deterministic_across_calls() {
3999 let _g = crate::test_util::global_state_lock();
4000 assert_eq!(hasher("foo"), hasher("foo"));
4001 assert_eq!(hasher(""), hasher(""));
4002 // Common shell names should not collide trivially.
4003 assert_ne!(hasher("ls"), hasher("cd"));
4004 assert_ne!(hasher("foo"), hasher("bar"));
4005 }
4006
4007 /// c:86 — empty input hashes to 0 (the seed value). A regression
4008 /// changing the seed would invalidate every persisted hash + cause
4009 /// silent rebuild storms in the cache layer.
4010 #[test]
4011 fn hasher_empty_string_hashes_to_zero() {
4012 let _g = crate::test_util::global_state_lock();
4013 assert_eq!(hasher(""), 0);
4014 }
4015
4016 /// c:86 — single-byte input `c` hashes to `c as u32` exactly
4017 /// (the loop runs once: hashval = 0 + 0<<5 + c = c). Pins the
4018 /// canonical first-iteration formula.
4019 #[test]
4020 fn hasher_single_byte_equals_byte_value() {
4021 let _g = crate::test_util::global_state_lock();
4022 assert_eq!(hasher("a"), b'a' as u32);
4023 assert_eq!(hasher("Z"), b'Z' as u32);
4024 assert_eq!(hasher("0"), b'0' as u32);
4025 }
4026
4027 /// `Src/hashtable.c:90-91` — `hashval += (hashval << 5) + c`
4028 /// simplifies to `hashval = hashval*33 + c` (the Bernstein
4029 /// hash variant). Pin the exact two-byte formula so a refactor
4030 /// to a different polynomial (e.g. FNV / djb2 / siphash) fails
4031 /// loudly. Regression here invalidates every cached fpath/hash
4032 /// digest stored on disk.
4033 #[test]
4034 fn hasher_two_byte_matches_bernstein_polynomial() {
4035 let _g = crate::test_util::global_state_lock();
4036 // For "ab": h0=0; h1 = 0 + (0<<5) + 'a' = 97; h2 = 97 + (97<<5) + 'b' = 97 + 3104 + 98 = 3299.
4037 assert_eq!(
4038 hasher("ab"),
4039 97u32
4040 .wrapping_add(97u32.wrapping_shl(5))
4041 .wrapping_add(b'b' as u32)
4042 );
4043 assert_eq!(hasher("ab"), 3299);
4044 // Pin the exact value for "ls" — a name we'll lookup billions of times.
4045 let ls_expected = {
4046 let mut h: u32 = 0;
4047 for &c in b"ls" {
4048 h = h.wrapping_add(h.wrapping_shl(5)).wrapping_add(c as u32);
4049 }
4050 h
4051 };
4052 assert_eq!(hasher("ls"), ls_expected);
4053 }
4054
4055 /// c:86 — hasher must NOT mix in encoding/locale state — the
4056 /// algorithm is byte-by-byte. Multi-byte UTF-8 like 'é' (0xC3 0xA9)
4057 /// hashes the two bytes independently. Pin so a regression that
4058 /// uses chars instead of bytes (which would aggregate the two
4059 /// bytes into one codepoint) fails.
4060 #[test]
4061 fn hasher_processes_utf8_bytes_not_codepoints() {
4062 let _g = crate::test_util::global_state_lock();
4063 // 'é' UTF-8 = 0xC3 0xA9 — two bytes.
4064 let expected = {
4065 let mut h: u32 = 0;
4066 for &c in &[0xC3u8, 0xA9u8] {
4067 h = h.wrapping_add(h.wrapping_shl(5)).wrapping_add(c as u32);
4068 }
4069 h
4070 };
4071 assert_eq!(
4072 hasher("é"),
4073 expected,
4074 "c:90 — `*(unsigned char *) str++` reads BYTES, not codepoints"
4075 );
4076 }
4077
4078 /// c:157 — `addhashnode` inserts; `gethashnode2` reads back.
4079 /// Round-trip MUST yield the value just inserted. Regression
4080 /// returning None on a present key would break every command-
4081 /// table lookup.
4082 #[test]
4083 fn addhashnode_then_gethashnode2_round_trips() {
4084 let _g = crate::test_util::global_state_lock();
4085 let mut h: HashMap<String, i32> = HashMap::new();
4086 addhashnode(&mut h, "key1", 42);
4087 assert_eq!(gethashnode2(&h, "key1"), Some(&42));
4088 assert_eq!(gethashnode2(&h, "missing"), None);
4089 }
4090
4091 /// c:275 — `removehashnode` returns Some(value) when present and
4092 /// drops the entry. Subsequent lookup MUST miss. Regression
4093 /// returning Some without removing would let callers think they
4094 /// removed when they actually didn't.
4095 #[test]
4096 fn removehashnode_returns_value_and_drops_entry() {
4097 let _g = crate::test_util::global_state_lock();
4098 let mut h: HashMap<String, String> = HashMap::new();
4099 addhashnode(&mut h, "key1", "val".to_string());
4100 let removed = removehashnode(&mut h, "key1");
4101 assert_eq!(removed.as_deref(), Some("val"));
4102 assert!(
4103 gethashnode2(&h, "key1").is_none(),
4104 "after removehashnode, lookup must miss"
4105 );
4106 }
4107
4108 /// c:275 — `removehashnode` on a missing key returns None and
4109 /// doesn't mutate the table. A regression where it errors or
4110 /// inserts a sentinel would break `unalias missing` (which is
4111 /// supposed to fail-soft).
4112 #[test]
4113 fn removehashnode_missing_key_returns_none() {
4114 let _g = crate::test_util::global_state_lock();
4115 let mut h: HashMap<String, i32> = HashMap::new();
4116 addhashnode(&mut h, "k1", 1);
4117 let len_before = h.len();
4118 assert!(removehashnode(&mut h, "missing").is_none());
4119 assert_eq!(h.len(), len_before, "missing-key remove must not mutate");
4120 }
4121
4122 // ─── zsh-corpus pins: hashtable add/get/remove ──────────────────
4123
4124 /// `addhashnode2` returns None on first insert.
4125 #[test]
4126 fn hashtable_corpus_add_new_returns_none() {
4127 let mut h: HashMap<String, i32> = HashMap::new();
4128 assert!(addhashnode2(&mut h, "fresh", 7).is_none());
4129 assert_eq!(gethashnode2(&h, "fresh"), Some(&7));
4130 }
4131
4132 /// `addhashnode2` on an existing key returns the OLD value.
4133 #[test]
4134 fn hashtable_corpus_add_existing_returns_previous_value() {
4135 let mut h: HashMap<String, i32> = HashMap::new();
4136 addhashnode2(&mut h, "k", 1);
4137 let prev = addhashnode2(&mut h, "k", 2);
4138 assert_eq!(prev, Some(1), "old value returned on replace");
4139 assert_eq!(gethashnode2(&h, "k"), Some(&2), "new value installed");
4140 }
4141
4142 /// `gethashnode2` on missing key returns None.
4143 #[test]
4144 fn hashtable_corpus_get_missing_returns_none() {
4145 let h: HashMap<String, i32> = HashMap::new();
4146 assert!(gethashnode2(&h, "anything").is_none());
4147 }
4148
4149 /// `newhashtable` returns (name, size); name preserved.
4150 #[test]
4151 fn hashtable_corpus_newhashtable_preserves_name() {
4152 let (name, sz) = newhashtable(64, "myht");
4153 assert_eq!(name, "myht");
4154 assert!(sz > 0, "size positive, got {sz}");
4155 }
4156
4157 /// Round-trip with many distinct keys.
4158 #[test]
4159 fn hashtable_corpus_many_keys_round_trip() {
4160 let mut h: HashMap<String, i32> = HashMap::new();
4161 for i in 0..100 {
4162 addhashnode(&mut h, &format!("k{i}"), i);
4163 }
4164 for i in 0..100 {
4165 assert_eq!(gethashnode2(&h, &format!("k{i}")), Some(&i));
4166 }
4167 assert_eq!(h.len(), 100);
4168 }
4169
4170 /// `removehashnode` followed by `gethashnode2` shows missing.
4171 #[test]
4172 fn hashtable_corpus_remove_then_get_is_none() {
4173 let mut h: HashMap<String, String> = HashMap::new();
4174 addhashnode(&mut h, "x", "value".into());
4175 let _ = removehashnode(&mut h, "x");
4176 assert!(gethashnode2(&h, "x").is_none());
4177 }
4178
4179 // ═══════════════════════════════════════════════════════════════════
4180 // Additional C-parity tests for Src/hashtable.c hasher + hnamcmp +
4181 // generic add/remove primitives.
4182 // ═══════════════════════════════════════════════════════════════════
4183
4184 /// c:86 — `hasher` of empty string returns 0 (no bytes contribute).
4185 #[test]
4186 fn hasher_empty_string_returns_zero() {
4187 assert_eq!(hasher(""), 0, "no bytes → hash 0");
4188 }
4189
4190 /// c:86 — `hasher` is deterministic: same input → same output.
4191 #[test]
4192 fn hasher_is_deterministic() {
4193 let h1 = hasher("test_string");
4194 let h2 = hasher("test_string");
4195 assert_eq!(h1, h2, "hasher must be deterministic");
4196 }
4197
4198 /// c:86 — `hasher` differentiates between different strings
4199 /// (no trivial collisions on common inputs).
4200 #[test]
4201 fn hasher_distinguishes_common_strings() {
4202 assert_ne!(hasher("foo"), hasher("bar"));
4203 assert_ne!(hasher("a"), hasher("b"));
4204 assert_ne!(hasher("test"), hasher("Test"), "case-sensitive");
4205 }
4206
4207 /// c:86 — hash of single char "a" matches the formula
4208 /// `0 + (0<<5) + 'a' = 0x61` (verifies inline formula).
4209 #[test]
4210 fn hasher_single_char_matches_formula() {
4211 let h = hasher("a");
4212 assert_eq!(h, b'a' as u32, "single char 'a' → 0x61");
4213 let h = hasher("0");
4214 assert_eq!(h, b'0' as u32, "single char '0' → 0x30");
4215 }
4216
4217 /// c:86 — hasher of "ab": h=0 → h=0+(0<<5)+'a'=0x61
4218 /// → h=0x61+(0x61<<5)+'b' = 0x61 + 0xC20 + 0x62 = 0xCE3.
4219 #[test]
4220 fn hasher_two_char_matches_formula() {
4221 let h = hasher("ab");
4222 let expected: u32 = 0u32
4223 .wrapping_add(0u32.wrapping_shl(5))
4224 .wrapping_add(b'a' as u32);
4225 let expected = expected
4226 .wrapping_add(expected.wrapping_shl(5))
4227 .wrapping_add(b'b' as u32);
4228 assert_eq!(h, expected, "two-char formula must match");
4229 }
4230
4231 /// c:86 — uses wrapping arithmetic so long strings don't panic.
4232 #[test]
4233 fn hasher_long_string_does_not_panic() {
4234 let s = "a".repeat(10_000);
4235 let _ = hasher(&s);
4236 }
4237
4238 /// c:345 — `hnamcmp("abc", "abc")` returns Equal.
4239 #[test]
4240 fn hnamcmp_equal_strings_return_equal() {
4241 assert_eq!(hnamcmp("abc", "abc"), std::cmp::Ordering::Equal);
4242 assert_eq!(hnamcmp("", ""), std::cmp::Ordering::Equal);
4243 }
4244
4245 /// c:345 — `hnamcmp` orders lexicographically.
4246 #[test]
4247 fn hnamcmp_lex_order() {
4248 assert_eq!(hnamcmp("abc", "abd"), std::cmp::Ordering::Less);
4249 assert_eq!(hnamcmp("abd", "abc"), std::cmp::Ordering::Greater);
4250 }
4251
4252 /// c:345 — empty string sorts before any non-empty string.
4253 #[test]
4254 fn hnamcmp_empty_sorts_first() {
4255 assert_eq!(hnamcmp("", "x"), std::cmp::Ordering::Less);
4256 assert_eq!(hnamcmp("x", ""), std::cmp::Ordering::Greater);
4257 }
4258
4259 /// `emptyhashtable` drops all entries.
4260 #[test]
4261 fn emptyhashtable_clears_all_entries() {
4262 let mut h: HashMap<String, i32> = HashMap::new();
4263 h.insert("a".to_string(), 1);
4264 h.insert("b".to_string(), 2);
4265 h.insert("c".to_string(), 3);
4266 emptyhashtable(&mut h);
4267 assert!(h.is_empty(), "all entries dropped after emptyhashtable");
4268 }
4269
4270 /// `deletehashtable` clears the map (Rust semantics).
4271 #[test]
4272 fn deletehashtable_clears_all_entries() {
4273 let mut h: HashMap<String, i32> = HashMap::new();
4274 h.insert("x".to_string(), 42);
4275 deletehashtable(&mut h);
4276 assert!(h.is_empty());
4277 }
4278
4279 /// `removehashnode` on missing key returns None (no panic).
4280 #[test]
4281 fn removehashnode_missing_returns_none() {
4282 let mut h: HashMap<String, i32> = HashMap::new();
4283 let prev = removehashnode(&mut h, "never_there");
4284 assert!(prev.is_none(), "remove of missing key → None");
4285 }
4286
4287 /// `addhashnode` overwriting existing key drops old value silently.
4288 #[test]
4289 fn addhashnode_overwrite_does_not_panic() {
4290 let mut h: HashMap<String, String> = HashMap::new();
4291 addhashnode(&mut h, "k", "first".into());
4292 addhashnode(&mut h, "k", "second".into());
4293 assert_eq!(gethashnode2(&h, "k"), Some(&"second".to_string()));
4294 }
4295
4296 // ═══════════════════════════════════════════════════════════════════
4297 // Additional C-parity tests for Src/hashtable.c
4298 // c:55 hasher / c:85 newhashtable / c:97 deletehashtable /
4299 // c:150 addhashnode2 / c:343 gethashnode2 / c:355 removehashnode /
4300 // c:715 hnamcmp / c:876 expandhashtable / c:887 resizehashtable /
4301 // c:916 printhashtabinfo
4302 // ═══════════════════════════════════════════════════════════════════
4303
4304 /// c:55 — `hasher("")` empty string returns u32 (type pin).
4305 #[test]
4306 fn hasher_empty_returns_u32_type() {
4307 let _: u32 = hasher("");
4308 }
4309
4310 /// c:55 — `hasher` is pure.
4311 #[test]
4312 fn hasher_is_pure_full_sweep() {
4313 for s in ["", "a", "abc", "hello world", "日本"] {
4314 let first = hasher(s);
4315 for _ in 0..5 {
4316 assert_eq!(hasher(s), first, "hasher({:?}) must be pure", s);
4317 }
4318 }
4319 }
4320
4321 /// c:85 — `newhashtable(0, "")` returns (String, i32) tuple type pin.
4322 #[test]
4323 fn newhashtable_returns_string_i32_tuple_type() {
4324 let _: (String, i32) = newhashtable(0, "");
4325 }
4326
4327 /// c:97 — `deletehashtable` on empty table is safe no-op.
4328 #[test]
4329 fn deletehashtable_empty_no_panic() {
4330 let mut empty: HashMap<String, String> = HashMap::new();
4331 deletehashtable(&mut empty);
4332 assert!(empty.is_empty(), "still empty after delete");
4333 }
4334
4335 /// c:150 — `addhashnode2` returns Option<T> (replaced value).
4336 #[test]
4337 fn addhashnode2_returns_option_type() {
4338 let mut h: HashMap<String, i32> = HashMap::new();
4339 let _: Option<i32> = addhashnode2(&mut h, "k", 1);
4340 }
4341
4342 /// c:150 — `addhashnode2` first insert returns None.
4343 #[test]
4344 fn addhashnode2_first_insert_returns_none() {
4345 let mut h: HashMap<String, i32> = HashMap::new();
4346 let r = addhashnode2(&mut h, "k", 42);
4347 assert!(r.is_none(), "first insert → None (no replacement)");
4348 }
4349
4350 /// c:150 — `addhashnode2` overwrite returns Some(old).
4351 #[test]
4352 fn addhashnode2_overwrite_returns_some_old() {
4353 let mut h: HashMap<String, i32> = HashMap::new();
4354 addhashnode2(&mut h, "k", 1);
4355 let r = addhashnode2(&mut h, "k", 2);
4356 assert_eq!(r, Some(1), "overwrite returns previous value");
4357 }
4358
4359 /// c:343 — `gethashnode2(empty, _)` returns None.
4360 #[test]
4361 fn gethashnode2_empty_table_returns_none() {
4362 let h: HashMap<String, String> = HashMap::new();
4363 assert!(gethashnode2(&h, "anything").is_none());
4364 }
4365
4366 /// c:355 — `removehashnode(empty, _)` returns None.
4367 #[test]
4368 fn removehashnode_empty_table_returns_none() {
4369 let mut h: HashMap<String, String> = HashMap::new();
4370 assert!(removehashnode(&mut h, "anything").is_none());
4371 }
4372
4373 /// c:715 — `hnamcmp` is antisymmetric.
4374 #[test]
4375 fn hnamcmp_antisymmetric() {
4376 use std::cmp::Ordering;
4377 for (a, b) in [("a", "b"), ("abc", "xyz"), ("", "x")] {
4378 let ab = hnamcmp(a, b);
4379 let ba = hnamcmp(b, a);
4380 assert_eq!(
4381 ab.reverse(),
4382 ba,
4383 "hnamcmp must be antisymmetric for ({:?}, {:?})",
4384 a,
4385 b
4386 );
4387 // ab cannot be Equal AND ba Equal unless both Equal
4388 if ab == Ordering::Equal {
4389 assert_eq!(ba, Ordering::Equal);
4390 }
4391 }
4392 }
4393
4394 /// c:887 — `resizehashtable` with same size is no-op.
4395 #[test]
4396 fn resizehashtable_same_size_no_panic() {
4397 let mut h: HashMap<String, i32> = HashMap::new();
4398 h.insert("a".to_string(), 1);
4399 h.insert("b".to_string(), 2);
4400 resizehashtable(&mut h, 2);
4401 assert_eq!(h.len(), 2, "entries preserved after same-size resize");
4402 }
4403
4404 /// c:876 — `expandhashtable` is idempotent.
4405 #[test]
4406 fn expandhashtable_idempotent() {
4407 let mut h: HashMap<String, i32> = HashMap::new();
4408 h.insert("a".to_string(), 1);
4409 for _ in 0..5 {
4410 expandhashtable(&mut h);
4411 }
4412 assert_eq!(h.get("a"), Some(&1), "value preserved across expansions");
4413 }
4414
4415 /// c:916 — `printhashtabinfo("", empty)` returns String type.
4416 #[test]
4417 fn printhashtabinfo_returns_string_type() {
4418 let empty: HashMap<String, String> = HashMap::new();
4419 let _: String = printhashtabinfo("test", &empty);
4420 }
4421
4422 // ═══════════════════════════════════════════════════════════════════
4423 // Additional C-parity tests for Src/hashtable.c
4424 // c:55 hasher / c:139 addhashnode / c:343 gethashnode2 / c:355 removehashnode /
4425 // c:715 hnamcmp / c:900 emptyhashtable / c:916 printhashtabinfo
4426 // ═══════════════════════════════════════════════════════════════════
4427
4428 /// c:55 — `hasher` returns u32 (compile-time pin).
4429 #[test]
4430 fn hasher_returns_u32_type() {
4431 let _: u32 = hasher("anything");
4432 }
4433
4434 /// c:55 — `hasher` is deterministic (same input → same hash, alt).
4435 #[test]
4436 fn hasher_is_deterministic_alt() {
4437 for s in ["", "x", "abc", "longer input", "日本"] {
4438 let first = hasher(s);
4439 for _ in 0..5 {
4440 assert_eq!(hasher(s), first, "hasher({:?}) must be pure", s);
4441 }
4442 }
4443 }
4444
4445 /// c:55 — `hasher` distinguishes simple distinct inputs (sanity:
4446 /// not a constant hash).
4447 #[test]
4448 fn hasher_distinguishes_distinct_inputs() {
4449 let h_a = hasher("a");
4450 let h_b = hasher("b");
4451 let h_z = hasher("z");
4452 // At least two of three must differ (proves non-constant).
4453 let distinct = (h_a != h_b) || (h_b != h_z) || (h_a != h_z);
4454 assert!(
4455 distinct,
4456 "hasher must distinguish distinct inputs; got {} {} {}",
4457 h_a, h_b, h_z
4458 );
4459 }
4460
4461 /// c:139 — `addhashnode` followed by gethashnode2 retrieves entry.
4462 #[test]
4463 fn addhashnode_then_gethashnode2_retrieves_entry() {
4464 let mut h: HashMap<String, String> = HashMap::new();
4465 addhashnode(&mut h, "key", "value".to_string());
4466 let v = gethashnode2(&h, "key");
4467 assert_eq!(
4468 v,
4469 Some(&"value".to_string()),
4470 "add then get must round-trip"
4471 );
4472 }
4473
4474 /// c:355 — `removehashnode` after add returns Some(value).
4475 #[test]
4476 fn removehashnode_after_add_returns_some() {
4477 let mut h: HashMap<String, String> = HashMap::new();
4478 addhashnode(&mut h, "k", "v".to_string());
4479 let removed = removehashnode(&mut h, "k");
4480 assert_eq!(
4481 removed,
4482 Some("v".to_string()),
4483 "remove returns the removed value"
4484 );
4485 assert!(h.is_empty(), "table empty after remove");
4486 }
4487
4488 /// c:355 — `removehashnode` twice returns Some then None.
4489 #[test]
4490 fn removehashnode_twice_returns_some_then_none() {
4491 let mut h: HashMap<String, i32> = HashMap::new();
4492 addhashnode(&mut h, "k", 42);
4493 let first = removehashnode(&mut h, "k");
4494 let second = removehashnode(&mut h, "k");
4495 assert!(first.is_some());
4496 assert!(second.is_none(), "second remove of same key returns None");
4497 }
4498
4499 /// c:715 — `hnamcmp(x, x)` returns Equal (reflexive).
4500 #[test]
4501 fn hnamcmp_reflexive() {
4502 use std::cmp::Ordering;
4503 for s in ["", "a", "hello", "long string here"] {
4504 assert_eq!(
4505 hnamcmp(s, s),
4506 Ordering::Equal,
4507 "hnamcmp({:?}, {:?}) must be Equal",
4508 s,
4509 s
4510 );
4511 }
4512 }
4513
4514 /// c:900 — `emptyhashtable` actually drops all entries.
4515 #[test]
4516 fn emptyhashtable_drops_all_entries() {
4517 let mut h: HashMap<String, i32> = HashMap::new();
4518 for i in 0..10 {
4519 addhashnode(&mut h, &format!("k_{}", i), i);
4520 }
4521 assert_eq!(h.len(), 10);
4522 emptyhashtable(&mut h);
4523 assert_eq!(h.len(), 0, "empty must clear all entries");
4524 }
4525
4526 /// c:916 — `printhashtabinfo` for empty table returns non-empty
4527 /// String (must contain at least the table name).
4528 #[test]
4529 fn printhashtabinfo_empty_table_non_empty_output() {
4530 let empty: HashMap<String, String> = HashMap::new();
4531 let r = printhashtabinfo("my_table_name", &empty);
4532 assert!(
4533 !r.is_empty(),
4534 "printhashtabinfo must produce non-empty output even for empty table"
4535 );
4536 }
4537
4538 /// c:97 — `deletehashtable` empties + safe.
4539 #[test]
4540 fn deletehashtable_empties_table() {
4541 let mut h: HashMap<String, i32> = HashMap::new();
4542 addhashnode(&mut h, "k", 1);
4543 deletehashtable(&mut h);
4544 assert!(h.is_empty(), "delete must empty the table");
4545 }
4546
4547 /// c:85 — `newhashtable` returns (String, i32) tuple (compile-time pin).
4548 #[test]
4549 fn newhashtable_returns_tuple_type() {
4550 let _: (String, i32) = newhashtable(0, "test");
4551 }
4552
4553 /// c:954 — printshfuncnode renders a function body with
4554 /// `getpermtext(fd, NULL, 1)`, so `functions f` prints CANONICAL text
4555 /// rather than the source as typed. zshrs keeps raw source for
4556 /// shell-defined functions, so its listing path re-parses and renders
4557 /// through that same deparser; these are the shapes where the layout is
4558 /// an actual decision rather than a passthrough.
4559 ///
4560 /// The `always` case is the one that matters most: the previous
4561 /// hand-rolled canonicalization emitted `print x } always { print y`,
4562 /// which is not merely mis-indented, it no longer parses — and
4563 /// `functions` output is meant to be re-readable by the shell.
4564 ///
4565 /// Pins the deparse itself rather than printshfuncnode's stdout, so it
4566 /// doesn't depend on capturing print! output. Indent 1 matches C, which
4567 /// writes one tab via zoutputtab (c:949) before calling getpermtext.
4568 #[test]
4569 fn function_body_deparses_to_canonical_layout() {
4570 let _g = crate::test_util::global_state_lock();
4571 for (body, want) in [
4572 // `do` gets its own line; the body indents beneath it.
4573 (
4574 "for i in 1 2; do print $i; done",
4575 "for i in 1 2\n\tdo\n\t\tprint $i\n\tdone",
4576 ),
4577 // `(` and `)` break onto their own lines.
4578 ("(print s)", "(\n\t\tprint s\n\t)"),
4579 // taddassign appends a trailing space after the value
4580 // (c:Src/text.c:203-204) and nothing backs it off.
4581 ("g=inner", "g=inner "),
4582 // The shape the emulation broke.
4583 (
4584 "{ print x } always { print y }",
4585 "{\n\t\tprint x\n\t} always {\n\t\tprint y\n\t}",
4586 ),
4587 ] {
4588 let prog = crate::ported::exec::parse_string(body, 1)
4589 .unwrap_or_else(|| panic!("body must parse for the listing path: {body:?}"));
4590 let got = crate::ported::text::getpermtext(Box::new(prog), None, 1);
4591 assert_eq!(got, want, "c:954 deparse of {body:?}");
4592 }
4593 }
4594}