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