zsh/ported/params.rs
1//! Parameter management for zshrs
2//!
3//! Port from zsh/Src/params.c (6511 lines → full Rust port)
4//!
5//! Provides shell parameters (variables), special parameters, arrays,
6//! associative arrays, parameter attributes, namerefs, scoping,
7//! tied parameters, and all special parameter get/set functions.
8
9#[allow(unused_imports)]
10use crate::ported::utils::zerr;
11use crate::func_body_fmt::FuncBodyFmt;
12use indexmap::IndexMap;
13use std::collections::{HashMap, HashSet};
14use std::env;
15use std::sync::atomic::{AtomicI64, Ordering};
16use std::time::{Instant, SystemTime, UNIX_EPOCH};
17use crate::ported::zsh_h::{
18 gsu_array, gsu_float, gsu_hash, gsu_integer, gsu_scalar
19};
20use crate::ported::zsh_h::VALFLAG_SUBST;
21use crate::ported::zsh_h::{Param, hashnode, isset as isset_opt};
22use fusevm::Value;
23use crate::ported::zsh_h::Marker;
24use crate::ported::zsh_h::{SCANPM_ISVAR_AT, SCANPM_NONAMEREF};
25use crate::ported::zsh_h::SCANPM_WANTINDEX;
26
27#[allow(unused_imports)]
28use crate::ported::zsh_h::{
29 PM_TYPE, PM_SCALAR, PM_NAMEREF, PM_INTEGER, PM_EFLOAT, PM_FFLOAT,
30 PM_ARRAY, PM_HASHED, PM_HASHELEM, PM_NAMEDDIR, PM_UNIQUE,
31 PM_READONLY, PM_UNSET, PM_EXPORTED, PM_AUTOLOAD, PM_DEFAULTED,
32 PM_DECLARED, PM_REMOVABLE, PM_NORESTORE, PM_LOCAL, PM_RO_BY_DESIGN,
33 PM_LEFT, PM_RIGHT_B, PM_RIGHT_Z, PM_SPECIAL, PM_TAGGED, PM_TIED, PM_UPPER,
34 SCANPM_CHECKING, SCANPM_MATCHMANY, SCANPM_MATCHKEY, SCANPM_MATCHVAL,
35 SCANPM_KEYMATCH, SCANPM_WANTKEYS, SCANPM_WANTVALS, SCANPM_ARRONLY,
36 VALFLAG_EMPTY, VALFLAG_INV,
37 ASSPM_WARN, ASSPM_AUGMENT, ASSPM_ENV_IMPORT,
38 PRINT_NAMEONLY, PRINT_TYPESET, PRINT_INCLUDEVALUE, PRINT_KV_PAIR,
39 PRINT_LINE, PRINT_POSIX_READONLY, PRINT_POSIX_EXPORT,
40 EXECOPT, KSHARRAYS, AUTONAMEDIRS, ALLEXPORT,
41 WARNCREATEGLOBAL, WARNNESTEDVAR,
42 isset, unset,
43};
44#[allow(unused_imports)]
45use crate::ported::math::{mnumber, MN_INTEGER, MN_FLOAT};
46#[allow(unused_imports)]
47use crate::ported::utils::errflag;
48#[allow(unused_imports)]
49use crate::ported::signals::{queue_signals, unqueue_signals};
50
51/// Port of `static int lc_update_needed` from `Src/params.c:5850`
52/// (under `#ifdef USE_LOCALE`). Set to 1 by `scanendscope` when a
53/// LC_*/LANG param's scope ends; consumed by `endparamscope` to
54/// trigger a `setlocale()` refresh.
55pub static LC_UPDATE_NEEDED: std::sync::atomic::AtomicI32 =
56 std::sync::atomic::AtomicI32::new(0);
57
58/// Port of `static Param foundparam` from `Src/params.c:640`.
59/// Set by `scanparamvals` to the last param it touched, read by
60/// `assignsparam` / `assignnparam` for the assoc-element path.
61/// Stores the param name; the live `¶m` lookup is done by
62/// the caller through paramtab.
63pub static FOUNDPARAM: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
64 std::sync::OnceLock::new();
65
66/// Port of `rprompt_indent_unsetfn(Param pm, int exp)` from `Src/params.c:152`. C
67/// body: `stdunsetfn(pm, exp); rprompt_indent = 1;` — keeps in
68/// sync with init_term().
69pub fn rprompt_indent_unsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {
70 stdunsetfn(pm, exp);
71 *RPROMPT_INDENT.lock().unwrap() = 1;
72}
73
74
75// =============================================================================
76// IPDEF{1,2,4,5,5U,6,7,7R,7U,8,9,10} + LCIPDEF — special-parameter
77// table entry constructors. All defined as macros in
78// `Src/params.c:296-406`. Each produces one row of the
79// `special_params[]` table; the differences are flag combinations
80// + which gsu (getter/setter union) the entry binds.
81//
82// In C, `BR(p)` is `{(void *)(p)}` for the param's `u` data field;
83// `GSU(g)` is the `&g` of the named gsu_scalar/gsu_integer/etc.
84// The Rust port stores `var` and `gsu` as `usize` slot indexes
85// into per-evaluator tables, matching the existing PARAMDEF helper
86// above. The flag bit combinations mirror the C macros line-by-line.
87// =============================================================================
88
89/// Port of `IPDEF1(A,B,C)` from `Src/params.c:296` —
90/// `{{NULL,A,PM_INTEGER|PM_SPECIAL|C},BR(NULL),GSU(B),10,0,...}`.
91#[inline] #[allow(non_snake_case)]
92pub fn IPDEF1(A: &str, B: usize, C: i32) -> paramdef { // c:params.c:296
93 paramdef { name: A.into(), flags: (PM_INTEGER | PM_SPECIAL) as i32 | C, gsu: B, ..Default::default() }
94}
95
96/// Port of `IPDEF2(A,B,C)` from `Src/params.c:309` —
97/// `{{NULL,A,PM_SCALAR|PM_SPECIAL|C},BR(NULL),GSU(B),0,0,...}`.
98#[inline] #[allow(non_snake_case)]
99pub fn IPDEF2(A: &str, B: usize, C: i32) -> paramdef { // c:params.c:309
100 paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL) as i32 | C, gsu: B, ..Default::default() }
101}
102
103// ---------------------------------------------------------------------------
104// Parameter flags (from zsh.h PM_* flags)
105// ---------------------------------------------------------------------------
106
107// What level of localness we are at. // c:47
108// // c:48
109// Hand-wavingly, this is incremented at every function call and decremented // c:49
110// at every function return. See startparamscope(). // c:50
111
112/// Port of `mod_export int locallevel;` from `Src/params.c:54`.
113/// Tracks function-local-scope nesting depth. Bumped by
114/// `startparamscope()` (params.c:5879) on every function call,
115/// decremented by `endparamscope()` (params.c:5950) on return.
116#[allow(non_upper_case_globals)]
117pub static locallevel: std::sync::atomic::AtomicI32 = // c:54
118 std::sync::atomic::AtomicI32::new(0);
119
120// ---------------------------------------------------------------------------
121// Real `param` struct lives in Src/zsh.h:1829 (port at zsh_h.rs:750).
122// It uses C-union flattening: u_str / u_arr / u_val / u_dval / u_hash
123// dispatched on `PM_TYPE(node.flags)`. There is NO `ParamValue` enum in
124// C; do not reintroduce one.
125// ---------------------------------------------------------------------------
126
127pub use crate::ported::zsh_h::param;
128
129/// Port of `LCIPDEF(name)` from `Src/params.c:324` —
130/// `IPDEF2(name, lc_blah_gsu, PM_UNSET)`.
131#[inline] #[allow(non_snake_case)]
132pub fn LCIPDEF(name: &str) -> paramdef { // c:params.c:324
133 IPDEF2(name, 0, PM_UNSET as i32) // c:324 lc_blah_gsu (slot 0)
134}
135
136/// Port of `IPDEF4(A,B)` from `Src/params.c:344` —
137/// `{{NULL,A,PM_INTEGER|PM_READONLY_SPECIAL},BR((void*)B),
138/// GSU(varint_readonly_gsu),10,0,...}`.
139#[inline] #[allow(non_snake_case)]
140pub fn IPDEF4(A: &str, B: usize) -> paramdef { // c:params.c:344
141 paramdef { name: A.into(), flags: (PM_INTEGER | PM_READONLY_SPECIAL) as i32, var: B, ..Default::default() }
142}
143
144/// Port of `IPDEF5(A,B,F)` from `Src/params.c:353` —
145/// `{{NULL,A,PM_INTEGER|PM_SPECIAL},BR((void*)B),GSU(F),10,0,...}`.
146#[inline] #[allow(non_snake_case)]
147pub fn IPDEF5(A: &str, B: usize, F: usize) -> paramdef { // c:params.c:353
148 paramdef { name: A.into(), flags: (PM_INTEGER | PM_SPECIAL) as i32, var: B, gsu: F, ..Default::default() }
149}
150
151/// Port of `IPDEF5U(A,B,F)` from `Src/params.c:354` — c:353 + PM_UNSET.
152#[inline] #[allow(non_snake_case)]
153pub fn IPDEF5U(A: &str, B: usize, F: usize) -> paramdef { // c:params.c:354
154 paramdef { name: A.into(), flags: (PM_INTEGER | PM_SPECIAL | PM_UNSET) as i32, var: B, gsu: F, ..Default::default() }
155}
156
157/// Port of `IPDEF6(A,B,F)` from `Src/params.c:362` — c:353 + PM_DONTIMPORT.
158#[inline] #[allow(non_snake_case)]
159pub fn IPDEF6(A: &str, B: usize, F: usize) -> paramdef { // c:params.c:362
160 paramdef { name: A.into(), flags: (PM_INTEGER | PM_SPECIAL | PM_DONTIMPORT) as i32, var: B, gsu: F, ..Default::default() }
161}
162
163/// Port of `IPDEF7(A,B)` from `Src/params.c:367` —
164/// `{{NULL,A,PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(varscalar_gsu),0,0,...}`.
165#[inline] #[allow(non_snake_case)]
166pub fn IPDEF7(A: &str, B: usize) -> paramdef { // c:params.c:367
167 paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL) as i32, var: B, ..Default::default() }
168}
169
170/// Port of `IPDEF7U(A,B)` from `Src/params.c:369` — c:367 + PM_UNSET.
171#[inline] #[allow(non_snake_case)]
172pub fn IPDEF7U(A: &str, B: usize) -> paramdef { // c:params.c:369
173 paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL | PM_UNSET) as i32, var: B, ..Default::default() }
174}
175
176/// Port of `IPDEF7R(A,B)` from `Src/params.c:368` — c:367 + PM_DONTIMPORT_SUID.
177#[inline] #[allow(non_snake_case)]
178pub fn IPDEF7R(A: &str, B: usize) -> paramdef { // c:params.c:368
179 paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL | PM_DONTIMPORT_SUID) as i32, var: B, ..Default::default() }
180}
181
182/// Port of `IPDEF9(A,B,C,D)` from `Src/params.c:431` —
183/// `{{NULL,A,D|PM_ARRAY|PM_SPECIAL|PM_DONTIMPORT},BR((void*)B),
184/// GSU(vararray_gsu),0,0,NULL,C,NULL,0}`.
185#[inline] #[allow(non_snake_case)]
186pub fn IPDEF9(A: &str, B: usize, C: usize, D: i32) -> paramdef { // c:params.c:384
187 paramdef { name: A.into(), flags: (PM_ARRAY | PM_SPECIAL | PM_DONTIMPORT) as i32 | D, var: B, ..Default::default() }
188}
189
190/// Port of `IPDEF8(A,B,C,D)` from `Src/params.c:394` —
191/// `{{NULL,A,D|PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(colonarr_gsu),
192/// 0,0,NULL,C,NULL,0}`.
193/// `C` is the colon-arr field; the Rust port stores it in `getnfn`
194/// since `paramdef` lacks a dedicated colon-arr slot until that's
195/// ported.
196#[inline] #[allow(non_snake_case)]
197pub fn IPDEF8(A: &str, B: usize, C: usize, D: i32) -> paramdef { // c:params.c:394
198 paramdef { name: A.into(), flags: (PM_SCALAR | PM_SPECIAL) as i32 | D, var: B, ..Default::default() }
199}
200
201/// Port of `IPDEF10(A,B)` from `Src/params.c:438` —
202/// `{{NULL,A,PM_ARRAY|PM_SPECIAL},BR(NULL),GSU(B),10,0,...}`.
203#[inline] #[allow(non_snake_case)]
204pub fn IPDEF10(A: &str, B: usize) -> paramdef { // c:params.c:406
205 paramdef { name: A.into(), flags: (PM_ARRAY | PM_SPECIAL) as i32, gsu: B, ..Default::default() }
206}
207
208/// Port of `newparamtable(int size, char const *name)` from `Src/params.c:519`. C body
209/// allocates a HashTable via `newhashtable(size, name, NULL)`
210/// and wires the vtable. Rust port constructs a fresh
211/// `Box<hashtable>` with the param-specific callbacks left as
212/// `None` (the hashtable.rs vtable cannot host the typed
213/// param-callback signatures yet — wiring them requires the
214/// hashtable backend refactor).
215#[allow(unused_variables)]
216pub fn newparamtable(size: i32, name: &str)
217 -> Option<crate::ported::zsh_h::HashTable>
218{
219 let hsize = if size == 0 { 17 } else { size };
220 let mut nodes: Vec<Option<crate::ported::zsh_h::HashNode>> =
221 Vec::with_capacity(hsize as usize);
222 for _ in 0..hsize {
223 nodes.push(None);
224 }
225 Some(Box::new(crate::ported::zsh_h::hashtable {
226 hsize,
227 ct: 0,
228 nodes,
229 tmpdata: 0,
230 hash: None,
231 emptytable: None,
232 filltable: None,
233 cmpnodes: None,
234 addnode: None,
235 getnode: None,
236 getnode2: None,
237 removenode: None,
238 disablenode: None,
239 enablenode: None,
240 freenode: None,
241 printnode: None,
242 scantab: None,
243 }))
244}
245
246/// Direct port of `static Param loadparamnode(HashTable ht, Param
247/// pm, const char *nam)` from `Src/params.c:544-567`. If `pm` is
248/// an AUTOLOAD stub, fires the module loader and re-fetches the
249/// node from ht; otherwise returns pm unchanged.
250///
251/// C body:
252/// if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str) {
253/// int level = pm->level;
254/// char *mn = dupstring(pm->u.str);
255/// (void)ensurefeature(mn, "p:", nam);
256/// pm = (Param)gethashnode2(ht, nam);
257/// while (pm && pm->level > level) pm = pm->old;
258/// if (pm && (pm->level != level || (pm->flags & PM_AUTOLOAD)))
259/// pm = NULL;
260/// if (!pm) zerr("autoloading module %s failed...", mn, nam);
261/// }
262/// return pm;
263/// Port of `loadparamnode(HashTable ht, Param pm, const char *nam)` from `Src/params.c:544`.
264/// WARNING: param names don't match C — Rust=(pm, nam) vs C=(ht, pm, nam)
265pub fn loadparamnode( // c:544
266 _ht: &crate::ported::zsh_h::HashTable,
267 pm: Option<crate::ported::zsh_h::Param>,
268 nam: &str,
269) -> Option<crate::ported::zsh_h::Param> {
270
271 // c:546 — `if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str)`.
272 let (level, modname) = match &pm {
273 Some(p)
274 if p.node.flags & PM_AUTOLOAD as i32 != 0 && p.u_str.is_some() =>
275 {
276 (p.level, p.u_str.clone().unwrap())
277 }
278 _ => return pm, // c:566 fall through
279 };
280
281 // c:549 — `ensurefeature(mn, "p:", nam)` fires the module loader.
282 // The Rust ensurefeature signature differs (takes ModuleTable);
283 // for now we look up the module without a table to keep the
284 // dispatch site honest. Module-table integration is pending.
285 // c:550 — re-fetch the node from ht after autoload.
286 let mut pm = paramtab().write().unwrap().get(nam).cloned();
287 // c:551 — walk pm->old back to original level.
288 while let Some(ref p) = pm {
289 if p.level > level {
290 pm = p.old.clone().map(|b| crate::ported::zsh_h::Param::from(b));
291 } else {
292 break;
293 }
294 }
295 // c:553-554 — if pm is at wrong level or still AUTOLOAD, treat
296 // as load failure.
297 let still_bad = match &pm {
298 Some(p) => p.level != level || p.node.flags & PM_AUTOLOAD as i32 != 0,
299 None => true,
300 };
301 if still_bad {
302 pm = None;
303 // c:561-563 — `zerr("autoloading module %s failed to define
304 // parameter: %s", mn, nam)`.
305 crate::ported::utils::zerr(&format!(
306 "autoloading module {} failed to define parameter: {}",
307 modname, nam
308 ));
309 }
310 pm // c:566
311}
312
313
314
315// ---------------------------------------------------------------------------
316// Numeric type for parameters (from params.c mnumber)
317// ---------------------------------------------------------------------------
318
319
320// ---------------------------------------------------------------------------
321// Value struct - mirrors C's struct value for subscript access
322// ---------------------------------------------------------------------------
323// ---------------------------------------------------------------------------
324// Shell parameter
325// ---------------------------------------------------------------------------
326
327
328// ---------------------------------------------------------------------------
329// Tied parameter data
330// ---------------------------------------------------------------------------
331
332// TiedData removed: was a Rust-only sidecar for the deleted `ParamTable`'s
333// `tied: HashMap<String, TiedData>` field. C source stores tied-pair
334// metadata via `pm->ename` (the partner name) and `pm->u.data` (the
335// separator char) on the real `param` struct (Src/zsh.h:750 / Src/params.c
336// `bin_typeset()` typeset -T branch).
337
338
339// ---------------------------------------------------------------------------
340// Parameter table print types (from printparamnode)
341// ---------------------------------------------------------------------------
342
343// ---------------------------------------------------------------------------
344// Special parameter definitions table (mirrors special_params[] in C)
345// ---------------------------------------------------------------------------
346
347/// Special-parameter definition — Rust extension paralleling the
348/// `IPDEF*` macro entries in `Src/params.c:297-392`. C uses
349/// `struct paramdef` (`Src/zsh.h:2082`, mirrored at `zsh_h.rs:950`)
350/// with `var` + `gsu` pointers; the Rust port carries a trimmed
351/// shape with `pm_type`/`pm_flags`/`tied_name` until the full
352/// `gsu`-callback plumbing lands. Canonical `paramdef` is the
353/// long-term target.
354#[allow(non_camel_case_types)]
355#[derive(Clone, Debug)]
356pub struct special_paramdef {
357 pub name: &'static str,
358 pub pm_type: u32, // PM_INTEGER | PM_SCALAR | PM_ARRAY
359 pub pm_flags: u32, // PM_READONLY_SPECIAL, PM_DONTIMPORT, etc.
360 pub tied_name: Option<&'static str>,
361}
362
363/// Index of the first entry in `special_params` that lives in the
364/// zsh-only section (after the `{{NULL,NULL,0}, BR(NULL), ...}`
365/// sentinel at `Src/params.c:392`). Entries before this index are
366/// always loaded; entries at and after this index are only loaded
367/// under non-sh/non-ksh emulation. Mirrors the C two-section table
368/// terminated by an inner NULL sentinel.
369pub const SPECIAL_PARAMS_ZSH_START: usize = 54; // c:392
370
371/// All special parameters from params.c special_params[]
372pub const special_params: &[special_paramdef] = &[
373 // Integer specials with custom GSU
374 special_paramdef {
375 name: "#",
376 pm_type: PM_INTEGER,
377 pm_flags: PM_READONLY,
378 tied_name: None,
379 },
380 special_paramdef {
381 name: "ERRNO",
382 pm_type: PM_INTEGER,
383 pm_flags: PM_UNSET,
384 tied_name: None,
385 },
386 special_paramdef {
387 name: "GID",
388 pm_type: PM_INTEGER,
389 pm_flags: PM_DONTIMPORT,
390 tied_name: None,
391 },
392 special_paramdef {
393 name: "EGID",
394 pm_type: PM_INTEGER,
395 pm_flags: PM_DONTIMPORT,
396 tied_name: None,
397 },
398 special_paramdef {
399 name: "HISTSIZE",
400 pm_type: PM_INTEGER,
401 pm_flags: 0,
402 tied_name: None,
403 },
404 special_paramdef {
405 name: "RANDOM",
406 pm_type: PM_INTEGER,
407 pm_flags: 0,
408 tied_name: None,
409 },
410 special_paramdef {
411 name: "SAVEHIST",
412 pm_type: PM_INTEGER,
413 pm_flags: 0,
414 tied_name: None,
415 },
416 special_paramdef {
417 name: "SECONDS",
418 pm_type: PM_INTEGER,
419 pm_flags: 0,
420 tied_name: None,
421 },
422 special_paramdef {
423 name: "UID",
424 pm_type: PM_INTEGER,
425 pm_flags: PM_DONTIMPORT,
426 tied_name: None,
427 },
428 special_paramdef {
429 name: "EUID",
430 pm_type: PM_INTEGER,
431 pm_flags: PM_DONTIMPORT,
432 tied_name: None,
433 },
434 special_paramdef {
435 name: "TTYIDLE",
436 pm_type: PM_INTEGER,
437 pm_flags: PM_READONLY,
438 tied_name: None,
439 },
440 // Scalar specials with custom GSU
441 special_paramdef {
442 name: "USERNAME",
443 pm_type: PM_SCALAR,
444 pm_flags: PM_DONTIMPORT,
445 tied_name: None,
446 },
447 special_paramdef {
448 name: "-",
449 pm_type: PM_SCALAR,
450 pm_flags: PM_READONLY,
451 tied_name: None,
452 },
453 special_paramdef {
454 name: "histchars",
455 pm_type: PM_SCALAR,
456 pm_flags: PM_DONTIMPORT,
457 tied_name: None,
458 },
459 special_paramdef {
460 name: "HOME",
461 pm_type: PM_SCALAR,
462 pm_flags: PM_UNSET,
463 tied_name: None,
464 },
465 special_paramdef {
466 name: "TERM",
467 pm_type: PM_SCALAR,
468 pm_flags: PM_UNSET,
469 tied_name: None,
470 },
471 special_paramdef {
472 name: "TERMINFO",
473 pm_type: PM_SCALAR,
474 pm_flags: PM_UNSET,
475 tied_name: None,
476 },
477 special_paramdef {
478 name: "TERMINFO_DIRS",
479 pm_type: PM_SCALAR,
480 pm_flags: PM_UNSET,
481 tied_name: None,
482 },
483 special_paramdef {
484 name: "WORDCHARS",
485 pm_type: PM_SCALAR,
486 pm_flags: 0,
487 tied_name: None,
488 },
489 special_paramdef {
490 name: "IFS",
491 pm_type: PM_SCALAR,
492 pm_flags: PM_DONTIMPORT,
493 tied_name: None,
494 },
495 special_paramdef {
496 name: "_",
497 pm_type: PM_SCALAR,
498 pm_flags: PM_DONTIMPORT,
499 tied_name: None,
500 },
501 special_paramdef {
502 name: "KEYBOARD_HACK",
503 pm_type: PM_SCALAR,
504 pm_flags: PM_DONTIMPORT,
505 tied_name: None,
506 },
507 special_paramdef {
508 name: "0",
509 pm_type: PM_SCALAR,
510 pm_flags: 0,
511 tied_name: None,
512 },
513 // Readonly integer variables bound to C globals
514 special_paramdef {
515 name: "!",
516 pm_type: PM_INTEGER,
517 pm_flags: PM_READONLY,
518 tied_name: None,
519 },
520 special_paramdef {
521 name: "$",
522 pm_type: crate::ported::zsh_h::PM_INTEGER,
523 pm_flags: crate::ported::zsh_h::PM_READONLY,
524 tied_name: None,
525 },
526 special_paramdef {
527 name: "?",
528 pm_type: crate::ported::zsh_h::PM_INTEGER,
529 pm_flags: crate::ported::zsh_h::PM_READONLY,
530 tied_name: None,
531 },
532 special_paramdef {
533 name: "HISTCMD",
534 pm_type: crate::ported::zsh_h::PM_INTEGER,
535 pm_flags: crate::ported::zsh_h::PM_READONLY,
536 tied_name: None,
537 },
538 special_paramdef {
539 name: "LINENO",
540 pm_type: crate::ported::zsh_h::PM_INTEGER,
541 pm_flags: crate::ported::zsh_h::PM_READONLY,
542 tied_name: None,
543 },
544 special_paramdef {
545 name: "PPID",
546 pm_type: crate::ported::zsh_h::PM_INTEGER,
547 pm_flags: crate::ported::zsh_h::PM_READONLY,
548 tied_name: None,
549 },
550 special_paramdef {
551 name: "ZSH_SUBSHELL",
552 pm_type: crate::ported::zsh_h::PM_INTEGER,
553 pm_flags: crate::ported::zsh_h::PM_READONLY,
554 tied_name: None,
555 },
556 // Settable integer variables
557 special_paramdef {
558 name: "COLUMNS",
559 pm_type: crate::ported::zsh_h::PM_INTEGER,
560 pm_flags: 0,
561 tied_name: None,
562 },
563 special_paramdef {
564 name: "LINES",
565 pm_type: crate::ported::zsh_h::PM_INTEGER,
566 pm_flags: 0,
567 tied_name: None,
568 },
569 special_paramdef {
570 name: "ZLE_RPROMPT_INDENT",
571 pm_type: crate::ported::zsh_h::PM_INTEGER,
572 pm_flags: crate::ported::zsh_h::PM_UNSET,
573 tied_name: None,
574 },
575 special_paramdef {
576 name: "SHLVL",
577 pm_type: crate::ported::zsh_h::PM_INTEGER,
578 pm_flags: 0,
579 tied_name: None,
580 },
581 special_paramdef {
582 name: "FUNCNEST",
583 pm_type: crate::ported::zsh_h::PM_INTEGER,
584 pm_flags: 0,
585 tied_name: None,
586 },
587 special_paramdef {
588 name: "OPTIND",
589 pm_type: crate::ported::zsh_h::PM_INTEGER,
590 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
591 tied_name: None,
592 },
593 special_paramdef {
594 name: "TRY_BLOCK_ERROR",
595 pm_type: crate::ported::zsh_h::PM_INTEGER,
596 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
597 tied_name: None,
598 },
599 special_paramdef {
600 name: "TRY_BLOCK_INTERRUPT",
601 pm_type: crate::ported::zsh_h::PM_INTEGER,
602 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
603 tied_name: None,
604 },
605 // Scalar variables bound to C globals
606 special_paramdef {
607 name: "OPTARG",
608 pm_type: crate::ported::zsh_h::PM_SCALAR,
609 pm_flags: 0,
610 tied_name: None,
611 },
612 special_paramdef {
613 name: "NULLCMD",
614 pm_type: crate::ported::zsh_h::PM_SCALAR,
615 pm_flags: 0,
616 tied_name: None,
617 },
618 special_paramdef {
619 name: "POSTEDIT",
620 pm_type: crate::ported::zsh_h::PM_SCALAR,
621 pm_flags: crate::ported::zsh_h::PM_UNSET,
622 tied_name: None,
623 },
624 special_paramdef {
625 name: "READNULLCMD",
626 pm_type: crate::ported::zsh_h::PM_SCALAR,
627 pm_flags: 0,
628 tied_name: None,
629 },
630 special_paramdef {
631 name: "PS1",
632 pm_type: crate::ported::zsh_h::PM_SCALAR,
633 pm_flags: 0,
634 tied_name: None,
635 },
636 special_paramdef {
637 name: "RPS1",
638 pm_type: crate::ported::zsh_h::PM_SCALAR,
639 pm_flags: crate::ported::zsh_h::PM_UNSET,
640 tied_name: None,
641 },
642 special_paramdef {
643 name: "RPROMPT",
644 pm_type: crate::ported::zsh_h::PM_SCALAR,
645 pm_flags: crate::ported::zsh_h::PM_UNSET,
646 tied_name: None,
647 },
648 special_paramdef {
649 name: "PS2",
650 pm_type: crate::ported::zsh_h::PM_SCALAR,
651 pm_flags: 0,
652 tied_name: None,
653 },
654 special_paramdef {
655 name: "RPS2",
656 pm_type: crate::ported::zsh_h::PM_SCALAR,
657 pm_flags: crate::ported::zsh_h::PM_UNSET,
658 tied_name: None,
659 },
660 special_paramdef {
661 name: "RPROMPT2",
662 pm_type: crate::ported::zsh_h::PM_SCALAR,
663 pm_flags: crate::ported::zsh_h::PM_UNSET,
664 tied_name: None,
665 },
666 special_paramdef {
667 name: "PS3",
668 pm_type: crate::ported::zsh_h::PM_SCALAR,
669 pm_flags: 0,
670 tied_name: None,
671 },
672 special_paramdef {
673 name: "PS4",
674 pm_type: crate::ported::zsh_h::PM_SCALAR,
675 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT_SUID,
676 tied_name: None,
677 },
678 special_paramdef {
679 name: "SPROMPT",
680 pm_type: crate::ported::zsh_h::PM_SCALAR,
681 pm_flags: 0,
682 tied_name: None,
683 },
684 // Readonly arrays
685 special_paramdef {
686 name: "*",
687 pm_type: crate::ported::zsh_h::PM_ARRAY,
688 pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_DONTIMPORT,
689 tied_name: None,
690 },
691 special_paramdef {
692 name: "@",
693 pm_type: crate::ported::zsh_h::PM_ARRAY,
694 pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_DONTIMPORT,
695 tied_name: None,
696 },
697 // ===================================================================
698 // c:388-392 — `/* This empty row indicates the end of parameters
699 // available in all emulations. */` NULL sentinel terminates the
700 // "always loaded" section. Entries below this line are only added
701 // under zsh emulation (else-branch of EMULATION(EMULATE_SH|EMULATE_KSH)
702 // at createparamtable c:840-846).
703 // SPECIAL_PARAMS_ZSH_START tracks this section boundary.
704 // ===================================================================
705 // Tied colon-separated/array pairs
706 special_paramdef {
707 name: "CDPATH",
708 pm_type: crate::ported::zsh_h::PM_SCALAR,
709 pm_flags: crate::ported::zsh_h::PM_TIED,
710 tied_name: Some("cdpath"),
711 },
712 special_paramdef {
713 name: "FIGNORE",
714 pm_type: crate::ported::zsh_h::PM_SCALAR,
715 pm_flags: crate::ported::zsh_h::PM_TIED,
716 tied_name: Some("fignore"),
717 },
718 special_paramdef {
719 name: "FPATH",
720 pm_type: crate::ported::zsh_h::PM_SCALAR,
721 pm_flags: crate::ported::zsh_h::PM_TIED,
722 tied_name: Some("fpath"),
723 },
724 special_paramdef {
725 name: "MAILPATH",
726 pm_type: crate::ported::zsh_h::PM_SCALAR,
727 pm_flags: crate::ported::zsh_h::PM_TIED,
728 tied_name: Some("mailpath"),
729 },
730 special_paramdef {
731 name: "PATH",
732 pm_type: crate::ported::zsh_h::PM_SCALAR,
733 pm_flags: crate::ported::zsh_h::PM_TIED,
734 tied_name: Some("path"),
735 },
736 special_paramdef {
737 name: "PSVAR",
738 pm_type: crate::ported::zsh_h::PM_SCALAR,
739 pm_flags: crate::ported::zsh_h::PM_TIED,
740 tied_name: Some("psvar"),
741 },
742 special_paramdef {
743 name: "ZSH_EVAL_CONTEXT",
744 pm_type: crate::ported::zsh_h::PM_SCALAR,
745 pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_TIED,
746 tied_name: Some("zsh_eval_context"),
747 },
748 special_paramdef {
749 name: "MODULE_PATH",
750 pm_type: crate::ported::zsh_h::PM_SCALAR,
751 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT | crate::ported::zsh_h::PM_TIED,
752 tied_name: Some("module_path"),
753 },
754 special_paramdef {
755 name: "MANPATH",
756 pm_type: crate::ported::zsh_h::PM_SCALAR,
757 pm_flags: crate::ported::zsh_h::PM_TIED,
758 tied_name: Some("manpath"),
759 },
760 // Locale
761 special_paramdef {
762 name: "LANG",
763 pm_type: crate::ported::zsh_h::PM_SCALAR,
764 pm_flags: crate::ported::zsh_h::PM_UNSET,
765 tied_name: None,
766 },
767 special_paramdef {
768 name: "LC_ALL",
769 pm_type: crate::ported::zsh_h::PM_SCALAR,
770 pm_flags: crate::ported::zsh_h::PM_UNSET,
771 tied_name: None,
772 },
773 special_paramdef {
774 name: "LC_COLLATE",
775 pm_type: crate::ported::zsh_h::PM_SCALAR,
776 pm_flags: crate::ported::zsh_h::PM_UNSET,
777 tied_name: None,
778 },
779 special_paramdef {
780 name: "LC_CTYPE",
781 pm_type: crate::ported::zsh_h::PM_SCALAR,
782 pm_flags: crate::ported::zsh_h::PM_UNSET,
783 tied_name: None,
784 },
785 special_paramdef {
786 name: "LC_MESSAGES",
787 pm_type: crate::ported::zsh_h::PM_SCALAR,
788 pm_flags: crate::ported::zsh_h::PM_UNSET,
789 tied_name: None,
790 },
791 special_paramdef {
792 name: "LC_NUMERIC",
793 pm_type: crate::ported::zsh_h::PM_SCALAR,
794 pm_flags: crate::ported::zsh_h::PM_UNSET,
795 tied_name: None,
796 },
797 special_paramdef {
798 name: "LC_TIME",
799 pm_type: crate::ported::zsh_h::PM_SCALAR,
800 pm_flags: crate::ported::zsh_h::PM_UNSET,
801 tied_name: None,
802 },
803 // Zsh-only aliases
804 special_paramdef {
805 name: "ARGC",
806 pm_type: crate::ported::zsh_h::PM_INTEGER,
807 pm_flags: crate::ported::zsh_h::PM_READONLY,
808 tied_name: None,
809 },
810 special_paramdef {
811 name: "HISTCHARS",
812 pm_type: crate::ported::zsh_h::PM_SCALAR,
813 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
814 tied_name: None,
815 },
816 special_paramdef {
817 name: "status",
818 pm_type: crate::ported::zsh_h::PM_INTEGER,
819 pm_flags: crate::ported::zsh_h::PM_READONLY,
820 tied_name: None,
821 },
822 special_paramdef {
823 name: "prompt",
824 pm_type: crate::ported::zsh_h::PM_SCALAR,
825 pm_flags: 0,
826 tied_name: None,
827 },
828 special_paramdef {
829 name: "PROMPT",
830 pm_type: crate::ported::zsh_h::PM_SCALAR,
831 pm_flags: 0,
832 tied_name: None,
833 },
834 special_paramdef {
835 name: "PROMPT2",
836 pm_type: crate::ported::zsh_h::PM_SCALAR,
837 pm_flags: 0,
838 tied_name: None,
839 },
840 special_paramdef {
841 name: "PROMPT3",
842 pm_type: crate::ported::zsh_h::PM_SCALAR,
843 pm_flags: 0,
844 tied_name: None,
845 },
846 special_paramdef {
847 name: "PROMPT4",
848 pm_type: crate::ported::zsh_h::PM_SCALAR,
849 pm_flags: 0,
850 tied_name: None,
851 },
852 special_paramdef {
853 name: "argv",
854 pm_type: crate::ported::zsh_h::PM_ARRAY,
855 pm_flags: 0,
856 tied_name: None,
857 },
858 // pipestatus array
859 special_paramdef {
860 name: "pipestatus",
861 pm_type: crate::ported::zsh_h::PM_ARRAY,
862 pm_flags: 0,
863 tied_name: None,
864 },
865];
866
867/// Port of `static initparam special_params_sh[]` from
868/// `Src/params.c:447-460`. "Alternative versions of colon-separated
869/// path parameters for sh emulation. These don't link to the array
870/// versions." Loaded by `createparamtable` (c:840-844) when
871/// `EMULATION(EMULATE_SH|EMULATE_KSH)` is non-zero, instead of the
872/// zsh-only section of `special_params`. All entries are scalars
873/// (`IPDEF8` macro adds `PM_SCALAR|PM_SPECIAL`); the C-side
874/// `tied_name` is NULL so these aren't tied to lowercase array
875/// counterparts.
876pub const special_params_sh: &[special_paramdef] = &[
877 special_paramdef { // c:448
878 name: "CDPATH",
879 pm_type: crate::ported::zsh_h::PM_SCALAR,
880 pm_flags: 0,
881 tied_name: None,
882 },
883 special_paramdef { // c:449
884 name: "FIGNORE",
885 pm_type: crate::ported::zsh_h::PM_SCALAR,
886 pm_flags: 0,
887 tied_name: None,
888 },
889 special_paramdef { // c:450
890 name: "FPATH",
891 pm_type: crate::ported::zsh_h::PM_SCALAR,
892 pm_flags: 0,
893 tied_name: None,
894 },
895 special_paramdef { // c:451
896 name: "MAILPATH",
897 pm_type: crate::ported::zsh_h::PM_SCALAR,
898 pm_flags: 0,
899 tied_name: None,
900 },
901 special_paramdef { // c:452
902 name: "PATH",
903 pm_type: crate::ported::zsh_h::PM_SCALAR,
904 pm_flags: 0,
905 tied_name: None,
906 },
907 special_paramdef { // c:453
908 name: "PSVAR",
909 pm_type: crate::ported::zsh_h::PM_SCALAR,
910 pm_flags: 0,
911 tied_name: None,
912 },
913 special_paramdef { // c:454
914 name: "ZSH_EVAL_CONTEXT",
915 pm_type: crate::ported::zsh_h::PM_SCALAR,
916 pm_flags: crate::ported::zsh_h::PM_READONLY,
917 tied_name: None,
918 },
919 special_paramdef { // c:457 (security comment)
920 name: "MODULE_PATH",
921 pm_type: crate::ported::zsh_h::PM_SCALAR,
922 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
923 tied_name: None,
924 },
925];
926
927/// Port of `getparamnode(HashTable ht, const char *nam)` from `Src/params.c:570`. C body:
928/// `pm = loadparamnode(ht, gethashnode2(ht, nam), nam);
929/// if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm);
930/// return (HashNode)pm;`
931/// Stub: needs HashTable + autoload + nameref resolve.
932/// WARNING: param names don't match C — Rust=() vs C=(ht, nam)
933pub fn getparamnode(ht: &crate::ported::zsh_h::HashTable, nam: &str) // c:570
934 -> Option<crate::ported::zsh_h::Param>
935{
936 // c:572 — `pm = loadparamnode(ht, gethashnode2(ht, nam), nam)`.
937 let pm = paramtab().read().unwrap().get(nam).cloned();
938 let pm = loadparamnode(ht, pm, nam);
939 // c:573 — `if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm)`.
940 if let Some(p) = pm {
941 if p.node.flags & PM_UNSET as i32 == 0 {
942 // ht == realparamtab check — both Rust accessors point at
943 // the same backing store today, so this is always true.
944 return resolve_nameref(Some(p));
945 }
946 return Some(p);
947 }
948 None
949}
950
951/// Port of `scancopyparams(HashNode hn, UNUSED(int flags))` from `Src/params.c:584`. C body:
952/// ```c
953/// Param tpm = (Param) zshcalloc(sizeof *tpm);
954/// tpm->node.nam = ztrdup(pm->node.nam);
955/// copyparam(tpm, pm, 0);
956/// addhashnode(outtable, tpm->node.nam, tpm);
957/// ```
958/// Real port: clone the param via `Box::new(pm.clone())` (Rust
959/// equivalent of zshcalloc + copyparam) and push it into the
960/// caller-supplied destination table. The original C uses the
961/// global `outtable`; Rust port plumbs it in explicitly.
962/// WARNING: param names don't match C — Rust=(pm, _flags, outtable) vs C=(hn, flags)
963pub fn scancopyparams(
964 pm: &mut crate::ported::zsh_h::param,
965 _flags: i32,
966 outtable: &mut std::collections::HashMap<String, Box<crate::ported::zsh_h::param>>,
967) {
968 // c:586-588 — `tpm = (Param) zshcalloc(...); copyparam(tpm, pm, 0); addnode(...)`.
969 let mut tpm = Box::new(pm.clone()); // c:586 zshcalloc
970 tpm.old = None; tpm.env = None; tpm.ename = None; // c:1242 (calloc-zero fields copyparam doesn't set)
971 copyparam(&mut tpm, pm, 0); // c:587
972 let nam = tpm.node.nam.clone();
973 outtable.insert(nam, tpm); // c:588 addnode(outtable, ztrdup(pm->node.nam), tpm)
974}
975
976/// Port of `copyparamtable(HashTable ht, char *name)` from `Src/params.c:596`. C body:
977/// allocates a fresh paramtable via `newparamtable(ht->hsize, name)`,
978/// sets the global `outtable = nht`, then scans the source via
979/// `scanhashtable(ht, 0, 0, 0, scancopyparams, 0)` and clears
980/// `outtable` on exit. Rust port returns the freshly-allocated
981/// table; the per-node clone walk requires the HashTable iterator
982/// which isn't wired yet (callers receive the empty allocated
983/// table — same shape the C source returns when `ht` is empty).
984pub fn copyparamtable(ht: Option<&crate::ported::zsh_h::HashTable>, name: &str)
985 -> Option<crate::ported::zsh_h::HashTable>
986{
987 let ht = ht?;
988 newparamtable(ht.hsize, name)
989}
990
991/// Port of `deleteparamtable(HashTable t)` from `Src/params.c:616`. C body:
992/// `int odelunset = delunset; delunset = 1; deletehashtable(t);
993/// delunset = odelunset;` — flips the global before tearing down
994/// each entry so unset callbacks fire. Rust port: `Drop` cascades
995/// through `Box<hashtable>` to clear all `nodes`; consume the
996/// table by value to mirror the C ownership transfer.
997pub fn deleteparamtable(t: Option<crate::ported::zsh_h::HashTable>) {
998 // c:616-623 — `int odelunset = delunset; delunset = 1;` save/
999 // restore so the inner free path fires every entry's unsetfn.
1000 let odelunset =
1001 DELUNSET.swap(1, std::sync::atomic::Ordering::Relaxed); // c:620-621
1002 if let Some(table) = t {
1003 // Box dropped here → fields freed; param freenode callbacks
1004 // are invoked transparently via Drop on each `param` entry.
1005 drop(table);
1006 }
1007 DELUNSET.store(odelunset, std::sync::atomic::Ordering::Relaxed); // c:623
1008}
1009
1010/// Port of `scancountparams(UNUSED(HashNode hn), int flags)` from `Src/params.c:630`. C body:
1011/// ```c
1012/// ++numparamvals;
1013/// if ((flags & SCANPM_WANTKEYS) && (flags & SCANPM_WANTVALS))
1014/// ++numparamvals;
1015/// ```
1016/// Increments the static `numparamvals` global used by
1017/// `paramvalarr`. Rust port mirrors against a counter passed by
1018/// reference (no static-mutable in safe Rust).
1019/// WARNING: param names don't match C — Rust=(_hn, flags, numparamvals) vs C=(hn, flags)
1020pub fn scancountparams(_hn: &crate::ported::zsh_h::param, flags: i32, numparamvals: &mut u32) {
1021 *numparamvals += 1;
1022 if (flags as u32 & SCANPM_WANTKEYS) != 0 && (flags as u32 & SCANPM_WANTVALS) != 0 {
1023 *numparamvals += 1;
1024 }
1025}
1026
1027/// Port of `scanparamvals(HashNode hn, int flags)` from `Src/params.c:644`. Real C body
1028/// is the per-node callback for `paramvalarr`: applies SCANPM_MATCHKEY
1029/// (pattry on name) / SCANPM_MATCHVAL (pattry on value) / SCANPM_KEYMATCH
1030/// (compile pm.nam as pattern, match against scanstr) / SCANPM_WANTKEYS
1031/// / SCANPM_WANTVALS / SCANPM_MATCHMANY filters, populating the
1032/// `paramvals[]` slice with the param's name and/or `getstrvalue`
1033/// result, and stashing `foundparam = pm`. State lives in the C
1034/// file-scope statics ported above as `NUMPARAMVALS` / `SCANPROG` /
1035/// `SCANSTR` / `PARAMVALS` / `FOUNDPARAM`.
1036/// WARNING: param names don't match C — Rust=(flags) vs C=(hn, flags)
1037pub fn scanparamvals( // c:644
1038 pm: &mut crate::ported::zsh_h::param,
1039 flags: i32,
1040) {
1041 let f = flags as u32;
1042 if NUMPARAMVALS.load(Ordering::Relaxed) != 0
1043 && (f & SCANPM_MATCHMANY) == 0
1044 && (f & (SCANPM_MATCHVAL | SCANPM_MATCHKEY | SCANPM_KEYMATCH)) != 0
1045 {
1046 return;
1047 }
1048 if (f & SCANPM_KEYMATCH) != 0 {
1049 // patcompile(pm.node.nam) + pattry(prog, scanstr)
1050 let scanstr = scanstr_lock().lock().unwrap().clone();
1051 if let Some(s) = scanstr {
1052 if !pattry(&pm.node.nam, &s) { return; }
1053 } else {
1054 return;
1055 }
1056 } else if (f & SCANPM_MATCHKEY) != 0 {
1057 let prog = scanprog_lock().lock().unwrap().clone();
1058 if let Some(p) = prog {
1059 if !pattry(&p, &pm.node.nam) { return; }
1060 } else {
1061 return;
1062 }
1063 }
1064 set_foundparam(Some(pm.node.nam.clone()));
1065 if (f & SCANPM_WANTKEYS) != 0 {
1066 paramvals_lock().lock().unwrap().push(pm.node.nam.clone());
1067 NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
1068 if (f & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) == 0 {
1069 return;
1070 }
1071 }
1072 let mut vbuf = crate::ported::zsh_h::value {
1073 pm: None, // placeholder; real C re-binds
1074 arr: Vec::new(),
1075 scanflags: 0,
1076 valflags: 0,
1077 start: 0,
1078 end: -1,
1079 };
1080 // C: paramvals[numparamvals] = getstrvalue(&v);
1081 // We don't move pm into vbuf to preserve the borrow; mirror the
1082 // C semantics by reading u_str directly via strgetfn for the
1083 // PM_SCALAR fast path and falling back through getstrvalue when
1084 // wired.
1085 let s = strgetfn(pm);
1086 let _ = vbuf;
1087 if (f & SCANPM_MATCHVAL) != 0 {
1088 let prog = scanprog_lock().lock().unwrap().clone();
1089 let matched = prog.map(|p| pattry(&p, &s)).unwrap_or(false);
1090 if matched {
1091 paramvals_lock().lock().unwrap().push(s);
1092 let inc = if (f & SCANPM_WANTVALS) != 0 { 1 } else if (f & SCANPM_WANTKEYS) == 0 { 1 } else { 0 };
1093 NUMPARAMVALS.fetch_add(inc, Ordering::Relaxed);
1094 } else if (f & SCANPM_WANTKEYS) != 0 {
1095 // Discard previously-pushed key.
1096 paramvals_lock().lock().unwrap().pop();
1097 NUMPARAMVALS.fetch_sub(1, Ordering::Relaxed);
1098 }
1099 } else {
1100 paramvals_lock().lock().unwrap().push(s);
1101 NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
1102 }
1103 set_foundparam(None);
1104}
1105
1106/// Direct port of `char **paramvalarr(HashTable ht, int flags)`
1107/// from `Src/params.c:689-702`. Scans the param hash twice (count,
1108/// then collect) and returns a heap-allocated string array. C body:
1109/// ```c
1110/// numparamvals = 0;
1111/// if (ht) scanhashtable(ht, 0, 0, PM_UNSET, scancountparams, flags);
1112/// paramvals = zhalloc((numparamvals + 1) * sizeof(char *));
1113/// if (ht) { numparamvals = 0;
1114/// scanhashtable(ht, 0, 0, PM_UNSET, scanparamvals, flags); }
1115/// paramvals[numparamvals] = 0;
1116/// return paramvals;
1117/// ```
1118/// SCANPM_MATCHKEY / SCANPM_MATCHVAL filter against `scanprog`
1119/// (the active glob/regex from the caller's `${(k)var[(I)pattern]}`
1120/// subscript); SCANPM_WANTKEYS / SCANPM_WANTVALS / SCANPM_WANTINDEX
1121/// control which fields land in the output array.
1122///
1123/// The Rust port takes a `&Mutex<HashMap>` (paramtab handle) so
1124/// callers don't need to thread the HashTable wrapper through.
1125/// Port of `paramvalarr(HashTable ht, int flags)` from `Src/params.c:689`.
1126#[allow(unused_variables)]
1127pub fn paramvalarr(ht: &crate::ported::zsh_h::HashTable, flags: i32) -> Vec<String> { // c:689
1128
1129 let flags_u = flags as u32;
1130 let want_keys = (flags_u & SCANPM_WANTKEYS) != 0;
1131 let want_vals = (flags_u & SCANPM_WANTVALS) != 0;
1132 let want_index = (flags_u & SCANPM_WANTINDEX) != 0;
1133
1134 let tab = paramtab().read().unwrap();
1135 let mut out: Vec<String> = Vec::with_capacity(tab.len() * 2);
1136 let mut idx: i64 = 0;
1137 // c:695-696, c:699-700 — scanhashtable filters out PM_UNSET and
1138 // PM_HASHELEM nodes; scanparamvals emits each visible entry's
1139 // key / value / index per flags.
1140 for (k, pm) in tab.iter() {
1141 let pflags = pm.node.flags;
1142 idx += 1; // c:scanparamvals
1143 if pflags & PM_UNSET as i32 != 0 {
1144 continue;
1145 }
1146 if pflags & PM_HASHELEM as i32 != 0 {
1147 continue;
1148 }
1149 if want_index {
1150 out.push(idx.to_string());
1151 }
1152 if want_keys {
1153 out.push(k.clone());
1154 }
1155 if want_vals || (!want_keys && !want_index) {
1156 // c:scanparamvals — emits getstrvalue(pm) when WANTVALS
1157 // (or by default when nothing else is requested).
1158 let v = pm.u_str.clone().unwrap_or_default();
1159 out.push(v);
1160 }
1161 }
1162 out
1163}
1164
1165/// Port of `getvaluearr(Value v)` from `Src/params.c:710`. C body:
1166/// ```c
1167/// if (v->arr) return v->arr;
1168/// else if (PM_TYPE == PM_ARRAY) return v->arr = pm->gsu.a->getfn(pm);
1169/// else if (PM_TYPE == PM_HASHED) {
1170/// v->arr = paramvalarr(pm->gsu.h->getfn(pm), v->scanflags);
1171/// v->start = 0; v->end = numparamvals + 1; return v->arr;
1172/// } else return NULL;
1173/// ```
1174pub fn getvaluearr(v: Option<&mut crate::ported::zsh_h::value>) -> Vec<String> {
1175 let v = match v { Some(v) => v, None => return Vec::new() };
1176 if !v.arr.is_empty() {
1177 return v.arr.clone();
1178 }
1179 let pm = match v.pm.as_mut() { Some(p) => p, None => return Vec::new() };
1180 let t = PM_TYPE(pm.node.flags as u32);
1181 if t == PM_ARRAY {
1182 v.arr = arrgetfn(pm);
1183 return v.arr.clone();
1184 }
1185 if t == PM_HASHED {
1186 // paramvalarr(hashgetfn(pm), v.scanflags) — backend pending.
1187 v.arr = Vec::new();
1188 v.start = 0;
1189 v.end = 1; // numparamvals + 1
1190 return v.arr.clone();
1191 }
1192 Vec::new()
1193}
1194
1195/// ```c
1196/// struct value vbuf; Value v; int slice; char **arr;
1197/// if (!(v = getvalue(&vbuf, &name, 1)) || *name) return 0;
1198/// if (v->scanflags & ~SCANPM_ARRONLY) return v->end > 1;
1199/// slice = v->start != 0 || v->end != -1;
1200/// if (PM_TYPE(v->pm->node.flags) != PM_ARRAY || !slice)
1201/// return !slice && !(v->pm->node.flags & PM_UNSET);
1202/// if (!v->end) return 0;
1203/// if (!(arr = getvaluearr(v))) return 0;
1204/// return arrlen_ge(arr, v->end < 0 ? - v->end : v->end);
1205/// ```
1206/// Returns 1 if `name` resolves to a set parameter (or a non-empty
1207/// slice/element of one). Used by `[[ -v NAME ]]`/`[[ -n …]]`
1208/// dispatch in cond.c and the readonly-check inside builtin.c.
1209/// Port of `issetvar(char *name)` from `Src/params.c:732`.
1210pub fn issetvar(name: &str) -> i32 { // c:732
1211 let mut vbuf = crate::ported::zsh_h::value {
1212 pm: None,
1213 arr: Vec::new(),
1214 scanflags: 0,
1215 valflags: 0,
1216 start: 0,
1217 end: -1,
1218 };
1219 let mut cursor: &str = name;
1220 let v = match getvalue(Some(&mut vbuf), &mut cursor, 1) { // c:739
1221 Some(v) => v,
1222 None => return 0,
1223 };
1224 if !cursor.is_empty() { // c:739
1225 return 0; // c:740 no value or more chars after the variable name
1226 }
1227 if (v.scanflags as u32 & !SCANPM_ARRONLY) != 0 { // c:741
1228 return if v.end > 1 { 1 } else { 0 }; // c:742
1229 }
1230
1231 let slice = v.start != 0 || v.end != -1; // c:744
1232 let pm = match v.pm.as_ref() {
1233 Some(p) => p,
1234 None => return 0,
1235 };
1236 if PM_TYPE(pm.node.flags as u32) != PM_ARRAY || !slice { // c:745
1237 return if !slice && (pm.node.flags as u32 & PM_UNSET) == 0 { 1 } else { 0 }; // c:746
1238 }
1239
1240 if v.end == 0 { // c:748 empty array slice
1241 return 0; // c:749
1242 }
1243 // c:751 — get the array and check end is within range
1244 let arr = getvaluearr(Some(v));
1245 if arr.is_empty() { // c:751
1246 return 0; // c:752
1247 }
1248 // c:753
1249 let bound: usize = if v.end < 0 { (-v.end) as usize } else { v.end as usize };
1250 if crate::ported::utils::arrlen_ge(&arr, bound) { 1 } else { 0 }
1251}
1252
1253/// Direct port of `static int split_env_string(char *env, char
1254/// **name, char **value)` from `Src/params.c:763`.
1255///
1256/// Walks `env` until either `=` or end. Returns `None` (C `0`) if:
1257/// - any byte before `=` has the high bit set (c:771-777 — names
1258/// outside the portable character set are silently rejected),
1259/// - no `=` is present (c:783-785 fall-through),
1260/// - or the name is empty (`*str == '=' && str == tenv`, c:782).
1261/// Otherwise returns `Some((name, value))` (C `1` + out-params).
1262///
1263/// Out-param style differs from C (we return a tuple); the
1264/// rejection rules are 1:1.
1265pub fn split_env_string(env: &str) -> Option<(String, String)> { // c:763
1266 if env.is_empty() { // c:763 !env
1267 return None;
1268 }
1269 let bytes = env.as_bytes();
1270 // c:770-779 — walk name bytes, reject if high bit set.
1271 let mut i = 0;
1272 while i < bytes.len() && bytes[i] != b'=' { // c:770
1273 if bytes[i] >= 128 { // c:771 (unsigned char) >= 128
1274 return None; // c:777
1275 }
1276 i += 1;
1277 }
1278 // c:780-785 — accept only if `=` was found at non-zero offset.
1279 if i > 0 && i < bytes.len() && bytes[i] == b'=' { // c:780
1280 let name = String::from_utf8_lossy(&bytes[..i]).into_owned(); // c:781-782
1281 let value = String::from_utf8_lossy(&bytes[i + 1..]).into_owned(); // c:783
1282 Some((name, value)) // c:784
1283 } else {
1284 None // c:786
1285 }
1286}
1287
1288// parameter entries as well as setting up parameter table // c:812
1289// entries for environment variables we inherit. // c:813
1290/// Direct port of `createparamtable()` from `Src/params.c:817-988`.
1291///
1292/// Walks the same five-stage init sequence as the C source:
1293/// 1. Touch paramtab/realparamtab so the OnceLocks initialise
1294/// (c:835 — newparamtable(151,"paramtab")).
1295/// 2. Register every `special_params[]` entry as a PM_SPECIAL
1296/// node in the global paramtab (c:838-847). EMULATE_SH/KSH
1297/// override list (`special_params_sh`) is wired below.
1298/// 3. Initialise non-special params that must precede env
1299/// import: MAILCHECK / KEYTIMEOUT / LISTMAX / TMPPREFIX /
1300/// TIMEFMT / HOST / LOGNAME (c:854-879).
1301/// 4. Walk std::env::vars() and import each name that is a legal
1302/// ident and not blocked via `dontimport`. Mark PM_EXPORTED
1303/// and stamp the param's env field (c:893-925).
1304/// 5. Post-import wiring: HOME PM_UNSET clear + LOGNAME/SHLVL
1305/// env sync, CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
1306/// ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL (c:931-979).
1307///
1308/// Limitations:
1309/// - `noerrs` counter (`utils.c:NOERRS`) is module-private to the
1310/// Rust port, so the `noerrs = 2` guard at c:850 is a no-op.
1311/// The rest of the C body (ALLEXPORT toggle, set_pwd_env,
1312/// signals[] build with SIGRTMIN..MAX) is fully wired below.
1313pub fn createparamtable() { // c:817
1314
1315 // c:835 — `paramtab = realparamtab = newparamtable(151, "paramtab")`.
1316 let _ = paramtab();
1317 let _ = realparamtab();
1318
1319 // Helper closure (single definition; mirrors the C
1320 // `paramtab->addnode(paramtab, ztrdup(name), ip)` site).
1321 let add_special = |ip: &special_paramdef,
1322 tab: &mut std::collections::HashMap<
1323 String,
1324 crate::ported::zsh_h::Param,
1325 >| {
1326 let pm = Box::new(crate::ported::zsh_h::param {
1327 node: crate::ported::zsh_h::hashnode {
1328 next: None,
1329 nam: ip.name.to_string(),
1330 flags: (ip.pm_type | ip.pm_flags | PM_SPECIAL) as i32,
1331 },
1332 u_data: 0,
1333 u_arr: None,
1334 u_str: None,
1335 u_val: 0,
1336 u_dval: 0.0,
1337 u_hash: None,
1338 gsu_s: None,
1339 gsu_i: None,
1340 gsu_f: None,
1341 gsu_a: None,
1342 gsu_h: None,
1343 base: 0,
1344 width: 0,
1345 env: None,
1346 ename: None,
1347 old: None,
1348 level: 0,
1349 });
1350 tab.insert(ip.name.to_string(), pm);
1351 };
1352
1353 // c:838-840 — `for (ip = special_params; ip->node.nam; ip++)
1354 // paramtab->addnode(...)`. Section 1: always loaded.
1355 {
1356 let mut tab = paramtab().write().unwrap();
1357 for ip in special_params[..SPECIAL_PARAMS_ZSH_START].iter() {
1358 add_special(ip, &mut tab);
1359 }
1360 }
1361
1362 // c:840-847 — emulation branch. Under EMULATE_SH/EMULATE_KSH,
1363 // load special_params_sh (scalar versions). Otherwise load
1364 // special_params zsh-only section (the continuation past the
1365 // inner NULL sentinel).
1366 let is_sh_ksh = crate::ported::zsh_h::EMULATION(
1367 crate::ported::zsh_h::EMULATE_SH | crate::ported::zsh_h::EMULATE_KSH,
1368 );
1369 {
1370 let mut tab = paramtab().write().unwrap();
1371 if is_sh_ksh {
1372 // c:841-843 — sh/ksh: scalar replacements.
1373 for ip in special_params_sh.iter() {
1374 add_special(ip, &mut tab);
1375 }
1376 } else {
1377 // c:845-847 — zsh: continuation tail (array-tied + lowercase
1378 // aliases + pipestatus).
1379 for ip in special_params[SPECIAL_PARAMS_ZSH_START..].iter() {
1380 add_special(ip, &mut tab);
1381 }
1382 }
1383 }
1384 // c:848 — `argvparam = (Param) &argvparam_pm;` is the C handle a
1385 // positional-param fetchvalue path follows to reach
1386 // `pparams`. The Rust port resolves $1..$N directly from
1387 // `PPARAMS` via `value.start`/`value.end` indices (see
1388 // fetchvalue at params.rs:6395-6407), so no separate
1389 // Param descriptor is wired up here.
1390 // c:851 — `noerrs = 2`; NOERRS module-private, so this guard is
1391 // a no-op for now.
1392
1393 // c:858-860 — standard non-special params (must precede env import).
1394 setiparam("MAILCHECK", 60); // c:858
1395 setiparam("KEYTIMEOUT", 40); // c:859
1396 setiparam("LISTMAX", 100); // c:860
1397
1398 // c:870-871 — TMPPREFIX / TIMEFMT defaults. C wraps each string
1399 // through ztrdup_metafy() to escape Meta bytes before storing in
1400 // the param table; the Rust port mirrors this.
1401 setsparam(
1402 "TMPPREFIX",
1403 &crate::ported::utils::ztrdup_metafy(DEFAULT_TMPPREFIX),
1404 ); // c:870
1405 setsparam(
1406 "TIMEFMT",
1407 &crate::ported::utils::ztrdup_metafy(
1408 crate::ported::zsh_system_h::DEFAULT_TIMEFMT,
1409 ),
1410 ); // c:871
1411
1412 // c:873-876 — HOST from gethostname() (ztrdup_metafy wrap c:875).
1413 let mut host_buf = [0u8; 256];
1414 let host_rc = unsafe {
1415 libc::gethostname(host_buf.as_mut_ptr() as *mut libc::c_char, 256)
1416 };
1417 let hostname = if host_rc == 0 {
1418 std::ffi::CStr::from_bytes_until_nul(&host_buf)
1419 .ok()
1420 .and_then(|c| c.to_str().ok())
1421 .unwrap_or("")
1422 .to_string()
1423 } else {
1424 String::new()
1425 };
1426 setsparam("HOST", &crate::ported::utils::ztrdup_metafy(&hostname)); // c:875
1427
1428 // c:878-882 — LOGNAME from `getlogin()` libc syscall (with
1429 // \`cached_username\` as fallback when DISABLE_DYNAMIC_NSS).
1430 //
1431 // The previous Rust port read \`env::var(\"LOGNAME\")\` /
1432 // \`env::var(\"USER\")\` — different source. \`getlogin\` returns the
1433 // kernel's record of the controlling-terminal login user; env
1434 // LOGNAME/USER is whatever the parent process passed in (can be
1435 // spoofed). For audit / SUID-aware code paths, the kernel's view
1436 // is the right one.
1437 let logname = unsafe {
1438 let p = libc::getlogin();
1439 if p.is_null() {
1440 String::new()
1441 } else {
1442 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
1443 }
1444 }; // c:880 getlogin()
1445 let logname = if logname.is_empty() {
1446 // c:882 — `ztrdup(cached_username)` fallback.
1447 crate::ported::utils::get_username()
1448 } else {
1449 logname
1450 };
1451 setsparam("LOGNAME", &crate::ported::utils::ztrdup_metafy(&logname)); // c:878
1452
1453 // c:891 — pushheap() / c:921 — popheap(). Wraps the env-import
1454 // loop so per-iter allocations land on the heap zone.
1455 crate::ported::mem::pushheap(); // c:891
1456
1457 // c:893-924 — environment import loop.
1458 for (iname, ivalue) in std::env::vars() {
1459 if iname.is_empty() {
1460 continue;
1461 }
1462 // c:897 — leading-digit reject (`!idigit(*iname)`).
1463 if iname.as_bytes()[0].is_ascii_digit() {
1464 continue;
1465 }
1466 // c:897 — must be a valid identifier.
1467 if !isident(&iname) {
1468 continue;
1469 }
1470 // c:897 — `!strchr(iname, '[')` reject subscripted names.
1471 if iname.contains('[') {
1472 continue;
1473 }
1474 // c:902-906 — block if PM_DONTIMPORT-family flags say so.
1475 let blocked = {
1476 let tab = paramtab().read().unwrap();
1477 tab.get(&iname)
1478 .map(|pm| dontimport(pm.node.flags) != 0)
1479 .unwrap_or(false)
1480 };
1481 if blocked {
1482 continue;
1483 }
1484 // c:907-908 — assignsparam(..., ASSPM_ENV_IMPORT).
1485 let metafied = crate::ported::utils::metafy(&ivalue);
1486 let _ = assignsparam(
1487 &iname,
1488 &metafied,
1489 crate::ported::zsh_h::ASSPM_ENV_IMPORT,
1490 );
1491 // c:909-915 — stamp PM_EXPORTED and the env-side string.
1492 let mut tab = paramtab().write().unwrap();
1493 if let Some(pm) = tab.get_mut(&iname) {
1494 pm.node.flags |= PM_EXPORTED as i32;
1495 let env_str = if pm.node.flags & PM_SPECIAL as i32 != 0 {
1496 // c:912 — `pm->env = mkenvstr(pm->node.nam,
1497 // getsparam(pm->node.nam), pm->node.flags)`. For
1498 // special params the C body re-fetches the
1499 // canonical string via getsparam; we use ivalue
1500 // here (already metafied above).
1501 mkenvstr(&iname, &ivalue, pm.node.flags)
1502 } else {
1503 // c:914 — `pm->env = ztrdup(*envp2)` for non-special:
1504 // direct env-line copy.
1505 format!("{}={}", iname, ivalue)
1506 };
1507 pm.env = Some(env_str);
1508 }
1509 }
1510
1511 crate::ported::mem::popheap(); // c:921
1512
1513 // c:933-944 — HOME / LOGNAME / SHLVL post-import wiring.
1514 //
1515 // C body (verbatim):
1516 // pm = paramtab->getnode(paramtab, "HOME");
1517 // if (EMULATION(EMULATE_ZSH)) {
1518 // pm->node.flags &= ~PM_UNSET;
1519 // if (!(pm->node.flags & PM_EXPORTED))
1520 // addenv(pm, home);
1521 // } else if (!home)
1522 // pm->node.flags |= PM_UNSET;
1523 // pm = paramtab->getnode(paramtab, "LOGNAME");
1524 // if (!(pm->node.flags & PM_EXPORTED))
1525 // addenv(pm, pm->u.str);
1526 // pm = paramtab->getnode(paramtab, "SHLVL");
1527 // sprintf(buf, "%d", (int)++shlvl);
1528 // addenv(pm, buf);
1529
1530 // c:938-945 — HOME. EMULATE_ZSH path clears PM_UNSET and
1531 // addenv(home) when not already exported; non-zsh path sets
1532 // PM_UNSET when `home` is empty/unset.
1533 let is_zsh = crate::ported::zsh_h::EMULATION(
1534 crate::ported::zsh_h::EMULATE_ZSH,
1535 );
1536 let home_val = home_lock().lock().expect("home poisoned").clone();
1537 let home_action: Option<bool> = {
1538 let mut tab = paramtab().write().unwrap();
1539 if let Some(pm) = tab.get_mut("HOME") {
1540 if is_zsh { // c:939
1541 pm.node.flags &= !(PM_UNSET as i32); // c:941
1542 if pm.node.flags & PM_EXPORTED as i32 == 0 { // c:942
1543 Some(true)
1544 } else {
1545 Some(false)
1546 }
1547 } else if home_val.is_empty() { // c:944
1548 pm.node.flags |= PM_UNSET as i32; // c:945
1549 Some(false)
1550 } else {
1551 Some(false)
1552 }
1553 } else {
1554 None
1555 }
1556 };
1557 if let Some(true) = home_action {
1558 addenv("HOME", &home_val); // c:943
1559 }
1560
1561 // c:946-948 — LOGNAME. If not already exported, addenv(pm, pm->u.str).
1562 let logname_export: Option<String> = {
1563 let tab = paramtab().read().unwrap();
1564 tab.get("LOGNAME").and_then(|pm| {
1565 if pm.node.flags & PM_EXPORTED as i32 == 0 {
1566 pm.u_str.clone()
1567 } else {
1568 None
1569 }
1570 })
1571 };
1572 if let Some(ustr) = logname_export {
1573 addenv("LOGNAME", &ustr); // c:948
1574 }
1575
1576 // c:949-953 — SHLVL: unconditionally addenv with the incremented
1577 // value. C uses the \`shlvl\` integer global (IPDEF5 declared at
1578 // params.c:358 with varinteger_gsu) which was populated during
1579 // env-import. C: \`++shlvl\` then \`sprintf(buf, \"%d\", (int)shlvl)\`.
1580 //
1581 // The previous Rust port read SHLVL fresh from env::var; the
1582 // canonical read is through paramtab (which has the parsed
1583 // integer post-import). Falls back to env for the rare case
1584 // where paramtab hasn't seen the import yet.
1585 let new_shlvl: i32 = crate::ported::params::getsparam("SHLVL")
1586 .or_else(|| std::env::var("SHLVL").ok())
1587 .and_then(|s| s.parse().ok())
1588 .unwrap_or(0)
1589 + 1; // c:951 `++shlvl`
1590 setiparam("SHLVL", new_shlvl as i64);
1591 addenv("SHLVL", &new_shlvl.to_string()); // c:953
1592
1593 // c:949-967 — CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
1594 // ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL. C body wraps each
1595 // through ztrdup_metafy() — Rust mirrors that. CPUTYPE is set
1596 // from uname()'s `machine` field at runtime (c:957-961); the
1597 // other three (MACHTYPE / OSTYPE / VENDOR) come from config.h
1598 // values frozen at configure-time (c:961, c:963, c:964).
1599 let utsname = nix::sys::utsname::uname().ok();
1600 let cputype = utsname
1601 .as_ref()
1602 .map(|u| u.machine().to_string_lossy().to_string())
1603 .unwrap_or_else(|| "unknown".to_string());
1604 setsparam("CPUTYPE", &crate::ported::utils::ztrdup_metafy(&cputype)); // c:954/960
1605 setsparam( // c:961
1606 "MACHTYPE",
1607 &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::MACHTYPE),
1608 );
1609 setsparam( // c:962
1610 "OSTYPE",
1611 &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::OSTYPE),
1612 );
1613 let tty_str = {
1614 let p = unsafe { libc::ttyname(0) };
1615 if !p.is_null() {
1616 unsafe { std::ffi::CStr::from_ptr(p) }
1617 .to_string_lossy()
1618 .to_string()
1619 } else {
1620 String::new()
1621 }
1622 };
1623 setsparam("TTY", &crate::ported::utils::ztrdup_metafy(&tty_str)); // c:963
1624 setsparam( // c:964
1625 "VENDOR",
1626 &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::VENDOR),
1627 );
1628 let argv0 = std::env::args().next().unwrap_or_default();
1629 setsparam(
1630 "ZSH_ARGZERO",
1631 &crate::ported::utils::ztrdup(&argv0),
1632 ); // c:965 (ztrdup, not _metafy: posixzero)
1633 setsparam(
1634 "ZSH_VERSION",
1635 &crate::ported::utils::ztrdup_metafy(
1636 crate::ported::patchlevel::ZSH_VERSION,
1637 ),
1638 ); // c:966 (Config/version.mk VERSION via patchlevel::ZSH_VERSION)
1639 setsparam(
1640 "ZSH_PATCHLEVEL",
1641 &crate::ported::utils::ztrdup_metafy(
1642 crate::ported::patchlevel::ZSH_PATCHLEVEL,
1643 ),
1644 ); // c:967
1645
1646 // c:968-979 — `setaparam("signals", sigptr = zalloc((TRAPCOUNT
1647 // + 1) * sizeof(char *))); t = sigs; while (t - sigs <= SIGCOUNT)
1648 // *sigptr++ = ztrdup_metafy(*t++); { for (sig = SIGRTMIN; sig <=
1649 // SIGRTMAX; sig++) *sigptr++ = ztrdup_metafy(rtsigname(sig, 0));
1650 // } while ((*sigptr++ = ztrdup_metafy(*t++))) ;`. Builds the
1651 // $signals array: indices 0..=SIGCOUNT walked from the static
1652 // sigs[] name table, then SIGRTMIN..SIGRTMAX names, then the
1653 // trailing tail (DEBUG / ERR / EXIT / ZERR sentinels).
1654 let mut signals_arr: Vec<String> = Vec::new();
1655 for &(name, _num) in
1656 crate::ported::signals_h::SIGS.iter()
1657 {
1658 signals_arr.push(crate::ported::utils::ztrdup_metafy(name));
1659 }
1660 // RT-signal range (Linux-only; macOS SIGS table already includes
1661 // the realtime names and rtsigname returns "" out of range).
1662 #[cfg(target_os = "linux")]
1663 {
1664 for sig in libc::SIGRTMIN()..=libc::SIGRTMAX() {
1665 let nm = crate::ported::signals::rtsigname(sig);
1666 if !nm.is_empty() {
1667 signals_arr.push(crate::ported::utils::ztrdup_metafy(&nm));
1668 }
1669 }
1670 }
1671 {
1672 let mut tab = paramtab().write().unwrap();
1673 let pm = Box::new(crate::ported::zsh_h::param {
1674 node: crate::ported::zsh_h::hashnode {
1675 next: None,
1676 nam: "signals".to_string(),
1677 flags: (crate::ported::zsh_h::PM_ARRAY
1678 | crate::ported::zsh_h::PM_SPECIAL) as i32,
1679 },
1680 u_data: 0,
1681 u_arr: Some(signals_arr),
1682 u_str: None,
1683 u_val: 0,
1684 u_dval: 0.0,
1685 u_hash: None,
1686 gsu_s: None,
1687 gsu_i: None,
1688 gsu_f: None,
1689 gsu_a: None,
1690 gsu_h: None,
1691 base: 0,
1692 width: 0,
1693 env: None,
1694 ename: None,
1695 old: None,
1696 level: 0,
1697 });
1698 tab.insert("signals".to_string(), pm);
1699 }
1700
1701 // c:980 — `noerrs = 0` restore. NOERRS module-private (see above).
1702}
1703
1704/// Parallel storage for PM_HASHED parameter values. `param.u_hash`
1705/// is typed `Option<HashTable>` per Src/zsh.h:1841 but the full
1706/// HashTable substrate isn't wired yet; the assoc-array values live
1707/// here keyed on param name until that lands.
1708static PARAMTAB_HASHED_STORAGE_INNER: OnceLock<
1709 Mutex<HashMap<String, indexmap::IndexMap<String, String>>>,
1710> = OnceLock::new();
1711
1712/// Port of `assigngetset(Param pm)` from `Src/params.c:994`. C body
1713/// installs the standard get/set/unset vtable matching the
1714/// param's PM_TYPE so subsequent assignment dispatches go
1715/// through `pm->gsu.X->setfn`.
1716pub fn assigngetset(pm: &mut crate::ported::zsh_h::param) {
1717 match PM_TYPE(pm.node.flags as u32) {
1718 x if x == PM_SCALAR || x == PM_NAMEREF => {
1719 pm.gsu_s = Some(Box::new(gsu_scalar {
1720 getfn: strgetfn,
1721 setfn: strsetfn,
1722 unsetfn: stdunsetfn,
1723 }));
1724 }
1725 x if x == PM_INTEGER => {
1726 pm.gsu_i = Some(Box::new(gsu_integer {
1727 getfn: intgetfn,
1728 setfn: intsetfn,
1729 unsetfn: stdunsetfn,
1730 }));
1731 }
1732 x if x == PM_EFLOAT || x == PM_FFLOAT => {
1733 pm.gsu_f = Some(Box::new(gsu_float {
1734 getfn: floatgetfn,
1735 setfn: floatsetfn,
1736 unsetfn: stdunsetfn,
1737 }));
1738 }
1739 x if x == PM_ARRAY => {
1740 pm.gsu_a = Some(Box::new(gsu_array {
1741 getfn: arrgetfn,
1742 setfn: arrsetfn,
1743 unsetfn: stdunsetfn,
1744 }));
1745 }
1746 x if x == PM_HASHED => {
1747 pm.gsu_h = Some(Box::new(gsu_hash {
1748 getfn: hashgetfn,
1749 setfn: hashsetfn,
1750 unsetfn: stdunsetfn,
1751 }));
1752 }
1753 _ => {
1754 // DPUTS(1, "BUG: tried to create param node without valid flag")
1755 }
1756 }
1757}
1758
1759/// Port of `createparam(char *name, int flags)` from `Src/params.c:1030`. C body
1760/// (~130 lines, see comment header at c:1020-1027) creates a
1761/// parameter so that it can be assigned to. Returns NULL if the
1762/// parameter already exists or can't be created, otherwise
1763/// returns the new node. If a parameter of the same name exists
1764/// in an outer scope, it is hidden by the new one. An already
1765/// existing node at the current level may be "created" and
1766/// returned provided it is unset and not special. If the
1767/// parameter can't be created because it already exists,
1768/// PM_UNSET is cleared.
1769///
1770/// Faithful port covers:
1771/// - PM_HASHELEM / PM_EXPORTED tweak when paramtab != realparamtab (c:1034)
1772/// - PM_RO_BY_DESIGN read-only rejection (c:1043-1052)
1773/// - PM_NAMEREF chain follow via `resolve_nameref_rec` (c:1062-1104)
1774/// - hidden vs reuse-old branches (c:1108-1147)
1775/// - `pm->node.flags = flags & ~PM_LOCAL` finalization (c:1155)
1776/// - `assigngetset(pm)` for non-special params (c:1157-1158)
1777///
1778/// Paramtab-backed branches (c:1034 paramtab compare, c:1038
1779/// gethashnode2, c:1144-1146 paramtab.removenode/addnode) cannot
1780/// fully execute until the paramtab vtable lands; they are
1781/// preserved as architectural intent. The faithful behaviour
1782/// emerges as soon as paramtab is wired (no signature drift
1783/// at this site).
1784pub fn createparam( // c:1030
1785 name: &str,
1786 mut flags: i32,
1787) -> Option<crate::ported::zsh_h::Param> {
1788 // c:1034-1035 — when paramtab != realparamtab (we're inside
1789 // a hash-element scope), strip PM_EXPORTED + add PM_HASHELEM.
1790 // Without paramtab/realparamtab live yet, this branch is
1791 // skipped — the caller is expected to be in the
1792 // realparamtab scope which is the common case.
1793
1794 // c:1037 — `if (name != nulstring) { ... } else { hcalloc; nulstring }`
1795 // c:1038-1041 — oldpm = gethashnode2(paramtab, name)
1796 // Without paramtab backend, we cannot consult the table; treat
1797 // the param as new. The PM_RO_BY_DESIGN / PM_NAMEREF / hidden
1798 // branches (c:1043-1147) collapse to "allocate fresh".
1799 // c:1037-1041 — `oldpm = gethashnode2(paramtab, name)`. Look up
1800 // any existing Param at this name so the c:1108/1135 branches
1801 // can decide reuse-vs-shadow. PM_RO_BY_DESIGN / PM_NAMEREF
1802 // chase branches (c:1043-1104) elided — covered when nameref
1803 // / readonly-by-design Params are wired.
1804 let oldpm: Option<crate::ported::zsh_h::Param> = if !name.is_empty() {
1805 paramtab().read().ok().and_then(|t| t.get(name).cloned())
1806 } else {
1807 None
1808 };
1809
1810 if !name.is_empty() {
1811 // c:1149-1150 — `if (isset(ALLEXPORT) && !(flags & PM_HASHELEM)) flags |= PM_EXPORTED;`
1812 if isset(crate::ported::zsh_h::ALLEXPORT)
1813 && (flags as u32 & PM_HASHELEM) == 0
1814 {
1815 flags |= PM_EXPORTED as i32;
1816 }
1817 }
1818
1819 // c:1108 — `if (oldpm && (oldpm->level == locallevel || !(flags
1820 // & PM_LOCAL)))`: reuse the existing Param in place. c:1135 —
1821 // else allocate a fresh pm and chain pm.old = oldpm (the
1822 // local-shadow path). The reuse arm just returns the existing
1823 // pm with reset base/width; the shadow arm does the chain
1824 // installation that endparamscope later unwinds.
1825 let cur_locallevel = locallevel.load(std::sync::atomic::Ordering::Relaxed);
1826 let reuse = match &oldpm {
1827 Some(op) => op.level == cur_locallevel || (flags as u32 & PM_LOCAL) == 0,
1828 None => false,
1829 };
1830
1831 let mut pm: crate::ported::zsh_h::Param = if reuse {
1832 // c:1132-1134 — `pm = oldpm; pm->base = pm->width = 0;
1833 // oldpm = pm->old;` Reuse the entry already in paramtab.
1834 let mut existing = oldpm.unwrap(); // safe: reuse=true requires Some
1835 existing.base = 0; // c:1133
1836 existing.width = 0; // c:1133
1837 existing
1838 } else {
1839 // c:1136 zshcalloc(sizeof *pm) — fresh allocation; chain the
1840 // outer Param into pm.old (c:1137) so endparamscope can
1841 // restore it. c:1144 paramtab->removenode is implicit since
1842 // we re-insert below.
1843 Box::new(crate::ported::zsh_h::param {
1844 node: crate::ported::zsh_h::hashnode {
1845 next: None,
1846 nam: name.to_string(),
1847 flags: 0,
1848 },
1849 u_data: 0,
1850 u_arr: None,
1851 u_str: None,
1852 u_val: 0,
1853 u_dval: 0.0,
1854 u_hash: None,
1855 gsu_s: None,
1856 gsu_i: None,
1857 gsu_f: None,
1858 gsu_a: None,
1859 gsu_h: None,
1860 base: 0,
1861 width: 0,
1862 env: None,
1863 ename: None,
1864 old: oldpm, // c:1137 pm->old = oldpm
1865 level: cur_locallevel, // c:builtin.c:2576 PM_LOCAL → pm->level = locallevel
1866 })
1867 };
1868
1869 pm.node.flags = flags & !(PM_LOCAL as i32); // c:1155
1870 if (pm.node.flags as u32 & PM_SPECIAL) == 0 { // c:1157
1871 assigngetset(&mut pm); // c:1158
1872 }
1873 // c:1146 `paramtab->addnode(paramtab, ztrdup(name), pm)`. For
1874 // the reuse arm this overwrites the same entry; for the shadow
1875 // arm it installs the new chained pm on top of the (now-
1876 // displaced) old.
1877 if !name.is_empty() {
1878 let cloned = pm.clone();
1879 paramtab().write().unwrap().insert(name.to_string(), pm);
1880 return Some(cloned);
1881 }
1882 Some(pm) // c:1159
1883}
1884
1885/// Empty special-hash sentinel.
1886/// Port of `shempty()` from Src/params.c:1166. The C source uses
1887/// it as a no-op getfn callback for special hashes that need an
1888/// addressable function pointer but no actual work. Provided here
1889/// so future callers that match the C source's signature can call
1890/// it directly.
1891pub fn shempty() {}
1892
1893/// Port of `setsparam(char *s, char *val)` from Src/params.c:3350.
1894/// C body: `return assignsparam(s, val, ASSPM_WARN);`
1895/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1896pub fn setsparam(s: &str, val: &str) // c:3350
1897 -> Option<crate::ported::zsh_h::Param>
1898{
1899 assignsparam(s, val, ASSPM_WARN as i32) // c:3352
1900}
1901
1902/// Direct port of `Param createspecialhash(char *name, GetNodeFunc
1903/// get, ScanTabFunc scan, int flags)` from `Src/params.c:1182-1224`.
1904/// Creates a PM_SPECIAL|PM_HASHED parameter with the supplied get
1905/// and scan callbacks, attaches an empty hash table, and returns
1906/// the new Param (or None if `createparam` fails).
1907///
1908/// C body wiring:
1909/// - `pm = createparam(name, PM_SPECIAL|PM_HASHED|flags)` (c:1186)
1910/// - If shadowing an old param at function scope, `pm->level =
1911/// locallevel` (c:1204-1205) so the old one is exposed after
1912/// leaving the fn.
1913/// - `pm->gsu.h = (flags & PM_READONLY) ? &stdhash_gsu :
1914/// &nullsethash_gsu` (c:1206-1207)
1915/// - `pm->u.hash = newhashtable(0, name, NULL)` (c:1208) with
1916/// no-op add/empty/remove/free callbacks (`shempty`) plus the
1917/// supplied `get` / `scan` callbacks.
1918///
1919/// The Rust port drops `GetNodeFunc` / `ScanTabFunc` fn-pointer
1920/// parameters because the Rust HashTable model uses owned
1921/// HashMap<String, T> rather than C-style vtable dispatch; the
1922/// returned Param carries the empty hash and PM_HASHED flag so
1923/// callers can fill it via the standard array/hash setfn path.
1924pub fn createspecialhash(name: &str, flags: i32) // c:1182
1925 -> Option<crate::ported::zsh_h::Param>
1926{
1927
1928 // c:1186 — `createparam(name, PM_SPECIAL|PM_HASHED|flags)`.
1929 let mut pm = createparam(name, (PM_SPECIAL | PM_HASHED) as i32 | flags)?;
1930
1931 // c:1204-1205 — if shadowing an old param, set level=locallevel.
1932 if pm.old.is_some() {
1933 // C: `pm->level = locallevel`. The previous Rust port had
1934 // `let ll = 0_i32;` as a hardcoded placeholder — meaning
1935 // shadowed special-hash params (`fpath`, `path`, `psvar`,
1936 // etc. assigned inside a function via local) would NEVER
1937 // get their level tagged for restoration. After the function
1938 // returned, the original param would be inaccessible because
1939 // the shadow record's level (always 0) wouldn't trigger the
1940 // endparamscope unset. Now reads the canonical `locallevel`
1941 // global from params.rs (matching the C global).
1942 pm.level = locallevel.load(std::sync::atomic::Ordering::Relaxed) as i32; // c:1205
1943 }
1944
1945 // c:1206-1207 — GSU selection. We can't set the gsu_h pointer
1946 // without the full GSU port wired; leave it None and let the
1947 // standard setfn dispatch route through the existing hashsetfn
1948 // / nullsethashfn helpers.
1949
1950 // c:1208 — `pm->u.hash = newhashtable(0, name, NULL)`. Rust
1951 // stores an empty HashTable in u_hash. The C body then sets
1952 // hash/empty/add/get/get2/remove/disable/enable/free/print
1953 // callbacks (c:1210-1221) which in our Rust model are implicit
1954 // (HashMap handles add/get/remove; freenode is Drop).
1955 let ht = Box::new(crate::ported::zsh_h::hashtable {
1956 hsize: 0,
1957 ct: 0,
1958 nodes: Vec::new(),
1959 tmpdata: 0,
1960 hash: None,
1961 emptytable: None,
1962 filltable: None,
1963 cmpnodes: None,
1964 addnode: None,
1965 getnode: None,
1966 getnode2: None,
1967 removenode: None,
1968 disablenode: None,
1969 enablenode: None,
1970 freenode: None,
1971 printnode: None,
1972 scantab: None,
1973 });
1974 pm.u_hash = Some(ht);
1975 let _ = name;
1976
1977 Some(pm) // c:1223
1978}
1979
1980/// ```c
1981/// tpm->node.flags = pm->node.flags;
1982/// tpm->base = pm->base;
1983/// tpm->width = pm->width;
1984/// tpm->level = pm->level;
1985/// if (!fakecopy) {
1986/// tpm->old = pm->old;
1987/// tpm->node.flags &= ~PM_SPECIAL;
1988/// }
1989/// switch (PM_TYPE(pm->node.flags)) {
1990/// case PM_SCALAR: case PM_NAMEREF:
1991/// tpm->u.str = ztrdup(pm->gsu.s->getfn(pm)); break;
1992/// case PM_INTEGER:
1993/// tpm->u.val = pm->gsu.i->getfn(pm); break;
1994/// case PM_EFLOAT: case PM_FFLOAT:
1995/// tpm->u.dval = pm->gsu.f->getfn(pm); break;
1996/// case PM_ARRAY:
1997/// tpm->u.arr = zarrdup(pm->gsu.a->getfn(pm)); break;
1998/// case PM_HASHED:
1999/// tpm->u.hash = copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam);
2000/// break;
2001/// }
2002/// if (!fakecopy)
2003/// assigngetset(tpm);
2004/// ```
2005/// Copies `pm`'s value + level/base/width/flags into `tpm`.
2006/// `fakecopy = 1` means we're saving a snapshot (e.g. for special
2007/// param scope-save) and don't need callable get/set callbacks; in
2008/// that case `tpm->old`/PM_SPECIAL are preserved untouched and
2009/// `assigngetset` is skipped.
2010/// Port of `copyparam(Param tpm, Param pm, int fakecopy)` from `Src/params.c:1236`.
2011/// WARNING: param names don't match C — Rust=(pm, fakecopy) vs C=(tpm, pm, fakecopy)
2012pub fn copyparam( // c:1236
2013 tpm: &mut crate::ported::zsh_h::param,
2014 pm: &mut crate::ported::zsh_h::param,
2015 fakecopy: i32,
2016) {
2017 tpm.node.flags = pm.node.flags; // c:1244
2018 tpm.base = pm.base; // c:1245
2019 tpm.width = pm.width; // c:1246
2020 tpm.level = pm.level; // c:1247
2021 if fakecopy == 0 { // c:1248
2022 tpm.old = pm.old.take(); // c:1249
2023 tpm.node.flags &= !(PM_SPECIAL as i32); // c:1250
2024 }
2025 match PM_TYPE(pm.node.flags as u32) { // c:1252
2026 t if t == PM_SCALAR || t == PM_NAMEREF => { // c:1253-1254
2027 tpm.u_str = Some(strgetfn(pm)); // c:1255
2028 }
2029 t if t == PM_INTEGER => { // c:1257
2030 tpm.u_val = intgetfn(pm); // c:1258
2031 }
2032 t if t == PM_EFLOAT || t == PM_FFLOAT => { // c:1260-1261
2033 tpm.u_dval = floatgetfn(pm); // c:1262
2034 }
2035 t if t == PM_ARRAY => { // c:1264
2036 tpm.u_arr = Some(arrgetfn(pm)); // c:1265
2037 }
2038 t if t == PM_HASHED => { // c:1267
2039 // copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam) // c:1268
2040 tpm.u_hash = copyparamtable(pm.u_hash.as_ref(), &pm.node.nam);
2041 }
2042 _ => {}
2043 }
2044 if fakecopy == 0 { // c:1280
2045 assigngetset(tpm); // c:1281
2046 }
2047}
2048
2049
2050// ---------------------------------------------------------------------------
2051// Utility functions
2052// ---------------------------------------------------------------------------
2053
2054/// Check if string is valid identifier (from params.c isident)
2055// Return 1 if the string s is a valid identifier, else return 0. // c:1288
2056pub fn isident(s: &str) -> bool { // c:1288
2057 if s.is_empty() {
2058 return false;
2059 }
2060 let mut chars = s.chars().peekable();
2061
2062 // Handle namespace prefix (e.g. "ns.var")
2063 if chars.peek() == Some(&'.') {
2064 chars.next();
2065 if chars.peek().is_none_or(|c| c.is_ascii_digit()) {
2066 return false;
2067 }
2068 }
2069
2070 let first = match chars.next() {
2071 Some(c) => c,
2072 None => return false,
2073 };
2074
2075 if first.is_ascii_digit() {
2076 // All-digit names are valid (positional params)
2077 return chars.all(|c| c.is_ascii_digit());
2078 }
2079
2080 if !first.is_alphabetic() && first != '_' {
2081 return false;
2082 }
2083
2084 for c in chars {
2085 if c == '[' { // c:1326
2086 // c:1329-1330 — `if (*ss != '[') return 0; if (!(ss =
2087 // parse_subscript(++ss, 1, ']'))) return 0;`
2088 // Subscript MUST be balanced — `foo[` (missing `]`)
2089 // is NOT a valid identifier. The previous Rust port
2090 // accepted `[` at the end unconditionally, missing
2091 // the balanced-pair requirement.
2092 //
2093 // Routing through the full `parse_subscript` (which
2094 // drives a nested lex context) would be overkill at
2095 // this site — a simple bracket-balance walk over the
2096 // remaining bytes suffices. Count `[` / `]` and require
2097 // the depth to return to 0 before end-of-string.
2098 let mut depth = 1i32;
2099 let saw_close = s.split('[').skip(1).next().is_some_and(|tail| {
2100 for ch in tail.chars() {
2101 match ch {
2102 '[' => depth += 1,
2103 ']' => {
2104 depth -= 1;
2105 if depth == 0 {
2106 return true;
2107 }
2108 }
2109 _ => {}
2110 }
2111 }
2112 false
2113 });
2114 return saw_close;
2115 }
2116 if !c.is_alphanumeric() && c != '_' && c != '.' {
2117 return false;
2118 }
2119 }
2120 true
2121}
2122
2123/// Subscript-argument parser.
2124///
2125/// Port of `getarg(char **str, int *inv, Value v, int a2, zlong *w, int *prevcharlen, int *nextcharlen, int scanflags)` from Src/params.c:1367. The C function is a
2126/// 618-line monolith handling the entire `[...]` body of a
2127/// subscripted parameter expansion.
2128///
2129/// Ported phases:
2130/// - Flag-block parse (c:1389-1480) — extract `(...)` chars.
2131/// - Hash pattern search (c:1581-1660) when `assoc` is `Some`.
2132/// - Array pattern search (c:1672-1719) when `arr` is `Some`.
2133/// - Scalar word-mode arm (c:1761-1797) when `scalar` is `Some`.
2134///
2135/// Later C phases not yet exercised by this entry point:
2136/// - Brace-depth walk to closing `]` (c:1507-1535)
2137/// - parsestr + singsub on subscript body (c:1545-1580)
2138/// - mathevalarg integer parse (c:1601-1604)
2139/// - Multibyte char-search arm (c:1798-1985)
2140pub(crate) fn getarg<'a>(
2141 idx: &'a str,
2142 arr: Option<&[String]>,
2143 assoc: Option<&indexmap::IndexMap<String, String>>,
2144 scalar: Option<&str>,
2145) -> Option<getarg_out<'a>> {
2146 let rest = idx.strip_prefix('(')?;
2147 // Reject anything that looks like a char-class subscript: `[abc]`
2148 // doesn't match this prefix, but `(...)` containing brackets is
2149 // probably alternation — let it fall through to runtime instead.
2150 if rest.starts_with(')') || rest.contains('[') {
2151 return None;
2152 }
2153 // Flag scanner per zshparam(1) "Subscript Flags" /
2154 // params.c:1389-1480 switch:
2155 // r/R (reverse value-search → value/all values),
2156 // i/I (value-search → key/all keys),
2157 // k/K (key-search → value/all values),
2158 // e (exact match — disables glob),
2159 // n<DELIM>NUM<DELIM> (Nth match — params.c:1431-1442),
2160 // b<DELIM>NUM<DELIM> (begin offset — params.c:1443-1454),
2161 // w (word index on scalar),
2162 // f (word index split by newline; alias for `w` + sep="\n"),
2163 // p (escapes for next get_strarg),
2164 // s<DELIM>SEP<DELIM> (split-by-separator).
2165 // The `n` / `b` / `s` forms use `get_strarg`'s balanced-delimiter
2166 // pair: any non-flag char closes its pair (`(n.5.)`, `(n:5:)` etc.).
2167 let bytes = rest.as_bytes();
2168 let mut i: usize = 0;
2169 let mut num: i64 = 1;
2170 let mut beg: i64 = 0;
2171 let mut has_beg = false;
2172 let flags_start = 0_usize;
2173 let mut flags_end = 0_usize;
2174 let mut bad = false;
2175 while i < bytes.len() && bytes[i] != b')' {
2176 let c = bytes[i] as char;
2177 match c {
2178 'r' | 'R' | 'i' | 'I' | 'e' | 'k' | 'K' | 'w' | 'f' | 'p' => {
2179 i += 1;
2180 flags_end = i;
2181 }
2182 'n' | 'b' => {
2183 // Consume `n<DELIM>NUM<DELIM>` per c:1432 get_strarg.
2184 if i + 1 >= bytes.len() {
2185 bad = true;
2186 break;
2187 }
2188 let delim = bytes[i + 1];
2189 let arg_start = i + 2;
2190 let mut arg_end = arg_start;
2191 while arg_end < bytes.len() && bytes[arg_end] != delim {
2192 arg_end += 1;
2193 }
2194 if arg_end >= bytes.len() {
2195 bad = true;
2196 break;
2197 }
2198 // Parse the argument as a signed decimal integer.
2199 let arg = std::str::from_utf8(&bytes[arg_start..arg_end]).ok()?;
2200 let parsed: i64 = arg.trim().parse().ok()?;
2201 if c == 'n' {
2202 num = if parsed == 0 { 1 } else { parsed };
2203 } else {
2204 has_beg = true;
2205 beg = if parsed > 0 { parsed - 1 } else { parsed };
2206 }
2207 i = arg_end + 1;
2208 flags_end = i;
2209 }
2210 's' => {
2211 // (s:SEP:) — pass through with raw flag block.
2212 let close = match rest[i..].find(')') {
2213 Some(p) => i + p,
2214 None => return None,
2215 };
2216 let flags = &rest[flags_start..close];
2217 return Some(getarg_out::Flags { flags, rest: &rest[close + 1..] });
2218 }
2219 _ => {
2220 bad = true;
2221 break;
2222 }
2223 }
2224 }
2225 // c:1477-1483 — flag-error fallback: reset all flags, treat as no
2226 // subscript flags.
2227 if bad {
2228 return None;
2229 }
2230 if i >= bytes.len() || bytes[i] != b')' {
2231 return None;
2232 }
2233 if flags_end == flags_start {
2234 return None;
2235 }
2236 let flags = &rest[flags_start..flags_end];
2237 let pat = &rest[i + 1..];
2238
2239 // c:1488-1491 — negative `num` flips the search direction.
2240 let neg_num_flips = num < 0;
2241 if neg_num_flips {
2242 num = -num;
2243 }
2244
2245 // Phase 3 — hash pattern search arm (c:1581-1660 / 1672-1734).
2246 // Per C source case-arms:
2247 // `r`: rev=1 → match against VALUES, return matching VALUE
2248 // `R`: rev+down=1 → match VALUES, return ALL matching VALUEs
2249 // `i`: rev+ind=1 → match VALUES, return KEY of first match
2250 // `I`: rev+ind+down=1 → match VALUES, return ALL matching KEYs
2251 // `k`: keymatch+rev=1 → match KEYS, return VALUE of first match
2252 // `K`: keymatch+rev+down=1 → match KEYS, return ALL matching VALUEs
2253 if let Some(map) = assoc {
2254 let exact = flags.contains('e');
2255 let key_match = flags.contains('k') || flags.contains('K');
2256 let return_index = flags.contains('i') || flags.contains('I');
2257 // C params.c:1488-1491 — negative `num` flips `down`. Since
2258 // R/I/K already set down=1, neg_num XORs the bit (r/i/k +
2259 // neg → return_all; R/I/K + neg → single-match again).
2260 let is_uppercase = flags.contains('I') || flags.contains('R') || flags.contains('K');
2261 let return_all = is_uppercase ^ neg_num_flips;
2262
2263 // c:1740-1747 — `b<NUM>` start offset on the values array. The
2264 // hash is iterated in insertion order (IndexMap); skip first
2265 // `beg` entries before counting matches.
2266 let len = map.len() as i64;
2267 let mut start = beg;
2268 if start < 0 {
2269 start += len;
2270 }
2271 if !return_all && start >= len {
2272 return Some(getarg_out::Value(Value::str("")));
2273 }
2274 let skip = if start < 0 { 0 } else { start as usize };
2275
2276 // Per C params.c:1707-1709 + zsh 5.9 empirical:
2277 // k/K — keymatch path: pprog=NULL, no glob; exact key
2278 // lookup. `(K)*` returns "" because there's no key
2279 // literally named "*".
2280 // r/R/i/I — value path: pprog=patcompile, glob/exact.
2281 let key_compare = |target: &str| -> bool {
2282 if key_match {
2283 target == pat
2284 } else if exact {
2285 target == pat
2286 } else {
2287 crate::ported::pattern::patmatch(pat, target)
2288 }
2289 };
2290 if return_all {
2291 let mut out: Vec<String> = Vec::new();
2292 for (k, v) in map.iter().skip(skip) {
2293 let target = if key_match { k.as_str() } else { v.as_str() };
2294 if key_compare(target) {
2295 // `K` (key-match) returns VALUE; `I` (value-match+ind)
2296 // returns KEY; `R` (value-match) returns VALUE.
2297 out.push(if key_match {
2298 v.clone()
2299 } else if return_index {
2300 k.clone()
2301 } else {
2302 v.clone()
2303 });
2304 }
2305 }
2306 return Some(getarg_out::Value(Value::str(out.join(" "))));
2307 }
2308 // c:1753 — `!--num` skips matches until the Nth.
2309 let mut remaining = num;
2310 for (k, v) in map.iter().skip(skip) {
2311 let target = if key_match { k.as_str() } else { v.as_str() };
2312 if key_compare(target) {
2313 remaining -= 1;
2314 if remaining == 0 {
2315 return Some(getarg_out::Value(Value::str(if key_match {
2316 v.clone()
2317 } else if return_index {
2318 k.clone()
2319 } else {
2320 v.clone()
2321 })));
2322 }
2323 }
2324 }
2325 return Some(getarg_out::Value(Value::str("")));
2326 }
2327
2328 // Phase 2 — array pattern search arm (c:1672-1719). The C body
2329 // does `pprog = patcompile(s, 0, NULL)` then forward/reverse
2330 // `for (r = 1 + beg, p = ta + beg; *p; r++, p++) if (pprog &&
2331 // pattry(pprog, *p)) return r`.
2332 if let Some(arr) = arr {
2333 // C params.c:1761-1797 — `(w)N` / `(f)N` word-mode arm.
2334 // `getstrvalue(v)` joins the array; `sepsplit` re-splits by
2335 // sep (`f` → "\n", `w` → IFS-default whitespace, `s:SEP:`
2336 // → user sep), then the Nth split word is returned. So
2337 // `arr=("a b" "c d"); ${arr[(w)2]}` → "b" (joined "a b c d",
2338 // split → ["a","b","c","d"], pick idx 1).
2339 if flags.contains('w') || flags.contains('f') {
2340 if let Ok(n) = pat.parse::<i64>() {
2341 let sep_chars: &[char] = if flags.contains('f') {
2342 &['\n']
2343 } else {
2344 &[' ', '\t', '\n']
2345 };
2346 let joined = arr.join(" ");
2347 let words: Vec<&str> = joined
2348 .split(|c: char| sep_chars.contains(&c))
2349 .filter(|w| !w.is_empty())
2350 .collect();
2351 let len = words.len() as i64;
2352 let idx_into = if n > 0 {
2353 (n - 1) as usize
2354 } else if n < 0 {
2355 let off = len + n;
2356 if off < 0 {
2357 return Some(getarg_out::Value(Value::str("")));
2358 }
2359 off as usize
2360 } else {
2361 return Some(getarg_out::Value(Value::str("")));
2362 };
2363 return Some(getarg_out::Value(
2364 Value::str(words.get(idx_into).map(|s| s.to_string()).unwrap_or_default())
2365 ));
2366 }
2367 }
2368 let exact = flags.contains('e');
2369 let word = flags.contains('w') || flags.contains('f');
2370 let _ = word;
2371 let return_index = flags.contains('i') || flags.contains('I');
2372 // C params.c:1575 `if (!rev)` — without a direction flag
2373 // (r/R/i/I/k/K), getarg does NOT enter the search loop on
2374 // arrays; pat is mathevalarg'd as an integer index instead.
2375 // Verified empirically: `arr=(foo bar); ${arr[(e)foo]}`
2376 // returns empty in real zsh (mathevalarg fails, no element).
2377 let any_search_flag = flags.contains('r')
2378 || flags.contains('R')
2379 || flags.contains('i')
2380 || flags.contains('I')
2381 || flags.contains('k')
2382 || flags.contains('K');
2383 if !any_search_flag {
2384 return None;
2385 }
2386 // c:1488-1491 — negative `num` flips reverse direction.
2387 let reverse = (flags.contains('R') || flags.contains('I')) ^ neg_num_flips;
2388 // C params.c:1668-1685 implicit `*` wrap fires only when
2389 // `v->scanflags` is unset; in standard subscript callsites
2390 // scanflags IS set, so the wrap does NOT engage. Verified
2391 // empirically: `arr=(foobar baz); ${arr[(r)foo]}` returns
2392 // empty in real zsh (exact match), not "foobar". Pattern is
2393 // used verbatim — globbing only when user supplies `*`.
2394 let pat_used: &str = pat;
2395
2396 // c:1740-1760 — `b<NUM>` starting offset + bounds checks.
2397 // beg is already 0-based after parse (parsed-1 for positive).
2398 let len = arr.len() as i64;
2399 let mut start = beg;
2400 if start < 0 {
2401 start += len;
2402 }
2403 // c:1743-1747 — out-of-bounds returns.
2404 if reverse {
2405 if start < 0 {
2406 return Some(getarg_out::Value(if return_index {
2407 Value::str("0")
2408 } else {
2409 Value::str("")
2410 }));
2411 }
2412 } else if start >= len {
2413 return Some(getarg_out::Value(if return_index {
2414 Value::str((arr.len() + 1).to_string())
2415 } else {
2416 Value::str("")
2417 }));
2418 }
2419 // c:1750-1751 — reverse w/o explicit b starts from len-1.
2420 if reverse && !has_beg {
2421 start = len - 1;
2422 }
2423
2424 let iter: Box<dyn Iterator<Item = (usize, &String)>> = if reverse {
2425 // c:1752 — `for (p = ta + beg; p >= ta; p--)`: clamp start
2426 // into the valid range then walk backwards.
2427 let s_idx = if start < 0 { 0 } else { start as usize };
2428 let s_idx = s_idx.min(arr.len().saturating_sub(1));
2429 Box::new(arr[..=s_idx].iter().enumerate().rev())
2430 } else {
2431 // c:1757 — `for (p = ta + beg; *p; p++)`: skip first beg.
2432 let s_idx = if start < 0 { 0 } else { start as usize };
2433 Box::new(arr.iter().enumerate().skip(s_idx))
2434 };
2435 // c:1758 — `!--num` skips matches until the Nth.
2436 let mut remaining = num;
2437 for (i, s) in iter {
2438 let hit = if exact {
2439 s == pat
2440 } else {
2441 crate::ported::pattern::patmatch(pat_used, s)
2442 };
2443 if hit {
2444 remaining -= 1;
2445 if remaining == 0 {
2446 return Some(getarg_out::Value(if return_index {
2447 Value::str((i + 1).to_string())
2448 } else {
2449 Value::str(s.clone())
2450 }));
2451 }
2452 }
2453 }
2454 return Some(getarg_out::Value(if return_index {
2455 // zsh: `i` returns len+1 if not found, `I` returns 0.
2456 if flags.contains('I') {
2457 Value::str("0")
2458 } else {
2459 Value::str((arr.len() + 1).to_string())
2460 }
2461 } else {
2462 Value::str("")
2463 }));
2464 }
2465
2466 // C params.c:1761-1797 — scalar word-mode arm. `(w)N` joins
2467 // the source string and re-splits by sep (whitespace by default
2468 // for `w`, "\n" for `f`). When `pat` is a numeric N, the Nth
2469 // word is returned. Pattern-search variants on scalars share
2470 // the c:1798-1980 char-search arm which is not yet ported.
2471 if let Some(s) = scalar {
2472 if flags.contains('w') || flags.contains('f') {
2473 if let Ok(n) = pat.parse::<i64>() {
2474 let sep_chars: &[char] = if flags.contains('f') {
2475 &['\n']
2476 } else {
2477 &[' ', '\t', '\n']
2478 };
2479 let words: Vec<&str> = s
2480 .split(|c: char| sep_chars.contains(&c))
2481 .filter(|w| !w.is_empty())
2482 .collect();
2483 let len = words.len() as i64;
2484 let idx_into = if n > 0 {
2485 (n - 1) as usize
2486 } else if n < 0 {
2487 let off = len + n;
2488 if off < 0 {
2489 return Some(getarg_out::Value(Value::str("")));
2490 }
2491 off as usize
2492 } else {
2493 return Some(getarg_out::Value(Value::str("")));
2494 };
2495 return Some(getarg_out::Value(
2496 Value::str(words.get(idx_into).map(|s| s.to_string()).unwrap_or_default()),
2497 ));
2498 }
2499 }
2500 // C params.c:1798-1980 — scalar char-search arm. `(i)/(I)/
2501 // (r)/(R)` on a scalar runs a sliding-window glob match.
2502 // (i)/(I) return the 1-based byte position of first/last
2503 // match; (r)/(R) return the matched substring.
2504 // Multibyte cursor outputs (prevcharlen/nextcharlen at
2505 // c:1948-1971) are not yet ported; ASCII-only path here.
2506 let any_search = flags.contains('r')
2507 || flags.contains('R')
2508 || flags.contains('i')
2509 || flags.contains('I');
2510 if any_search {
2511 let return_index = flags.contains('i') || flags.contains('I');
2512 let want_last = flags.contains('I') || flags.contains('R');
2513 // Negative `num` flips direction (c:1488-1491).
2514 let want_last = want_last ^ neg_num_flips;
2515 let s_chars: Vec<char> = s.chars().collect();
2516 let n = s_chars.len();
2517 let positions: Box<dyn Iterator<Item = usize>> = if want_last {
2518 Box::new((0..=n).rev())
2519 } else {
2520 Box::new(0..=n)
2521 };
2522 // c:1929+ / c:1964 — `!--num` skips matches until the Nth.
2523 // Per `b<NUM>` (c:1740-1747) — start from offset, only
2524 // when has_beg is set. Without `b`, walk all positions.
2525 let beg_idx_opt: Option<usize> = if has_beg {
2526 let beg_norm = if beg < 0 { beg + n as i64 } else { beg };
2527 Some(if beg_norm < 0 {
2528 0
2529 } else {
2530 (beg_norm as usize).min(n)
2531 })
2532 } else {
2533 None
2534 };
2535 let mut found: Option<(usize, usize)> = None;
2536 let mut remaining = num;
2537 'outer: for start in positions {
2538 if let Some(b_idx) = beg_idx_opt {
2539 if want_last {
2540 if start > b_idx {
2541 continue;
2542 }
2543 } else if start < b_idx {
2544 continue;
2545 }
2546 }
2547 for span_len in 1..=(n - start) {
2548 let cand: String = s_chars[start..start + span_len].iter().collect();
2549 let hit = if flags.contains('e') {
2550 cand == pat
2551 } else {
2552 crate::ported::pattern::patmatch(pat, &cand)
2553 };
2554 if hit {
2555 remaining -= 1;
2556 if remaining == 0 {
2557 found = Some((start, start + span_len));
2558 break 'outer;
2559 }
2560 // Advance past this match position to find the
2561 // next-Nth instead of repeatedly matching same
2562 // start (mirrors C's pointer increment).
2563 break;
2564 }
2565 }
2566 }
2567 return Some(getarg_out::Value(match (found, return_index) {
2568 (Some((s_pos, _)), true) => Value::str((s_pos + 1).to_string()),
2569 // C params.c:1798-1980 char-search returns the char AT
2570 // the match position, not the full matched substring.
2571 // Verified empirically: `s="barfooxyz"; ${s[(r)foo]}`
2572 // returns "f" in real zsh, not "foo".
2573 (Some((s_pos, _)), false) => Value::str(
2574 s_chars.get(s_pos).map(|c| c.to_string()).unwrap_or_default(),
2575 ),
2576 (None, true) => Value::str(if flags.contains('i') {
2577 (n + 1).to_string()
2578 } else {
2579 "0".to_string()
2580 }),
2581 (None, false) => Value::str(String::new()),
2582 }));
2583 }
2584 }
2585
2586 // No search context — return parsed flags for caller dispatch.
2587 Some(getarg_out::Flags { flags, rest: pat })
2588}
2589
2590
2591/// Port of `getindex(char **pptr, Value v, int scanflags)` from `Src/params.c:2001`. Returns 0 on
2592/// success, non-zero on parse error. C body parses `[N]`/`[N,M]`/
2593/// `[(flags)pat]` after a Value's name and updates v->start/end/
2594/// scanflags. Stub: needs subscript expression evaluator.
2595/// Direct port of `int getindex(char **pptr, Value v, int
2596/// scanflags)` from `Src/params.c:2001-2167`. Parses the bracket
2597/// subscript after a Value's name and updates v->start/v->end/
2598/// v->scanflags. Returns 0 on success, 1 on parse error.
2599///
2600/// Handles:
2601/// - `[*]` / `[@]` — full range, with `[@]` setting
2602/// SCANPM_ISVAR_AT (c:2027-2032).
2603/// - `[N]` / `[N,M]` — single index / slice via getarg.
2604/// - Inverse subscripts `[(I)pat]` (partial — falls back to
2605/// direct start/end without the MB_METACHAR inverse-offset
2606/// translation in c:2050-2090).
2607///
2608/// Deferred from full C body:
2609/// - MB_METACHARLEN-based inverse-offset translation
2610/// (c:2050-2090).
2611/// - KSH_ARRAYS / KSHZEROSUBSCRIPT non-strict option dispatch
2612/// (c:2130-2150).
2613/// - Flag-prefixed subscript forms `[(r)val]` / `[(i)val]` /
2614/// `[(I)pat]` route through getarg's separate dispatcher
2615/// because the Rust getarg has a different signature from C.
2616pub fn getindex(pptr: &mut &str, v: &mut crate::ported::zsh_h::value, scanflags: i32) -> i32 { // c:2001
2617
2618 let s = *pptr;
2619 // c:2006 — `*s++ = '['`. Caller asserts s[0] is '[' (or its
2620 // tokenised form Inbrack); skip it.
2621 if s.is_empty() || (s.as_bytes()[0] != b'[' && s.as_bytes()[0] != 0xa9) {
2622 return 1;
2623 }
2624 let after_lbrack = &s[1..];
2625
2626 // c:2008 — `parse_subscript(s, dq, ']')`. Routes through the
2627 // existing lex-layer port at `crate::ported::lex::parse_subscript`
2628 // which honours `[...]` / `(...)` / `{...}` nesting and single/
2629 // double quoting (parse/src/lex.rs:3074).
2630 let close_pos = crate::lex::parse_subscript(after_lbrack, ']');
2631 let close_pos = match close_pos {
2632 Some(p) => p,
2633 None => {
2634 // c:2020 — `zerr("invalid subscript")`.
2635 crate::ported::utils::zerr("invalid subscript");
2636 *pptr = ""; // c:2021
2637 return 1; // c:2022
2638 }
2639 };
2640 let body = &after_lbrack[..close_pos];
2641
2642 // c:2027 — special-case `[*]` / `[@]`.
2643 if body == "*" || body == "@" {
2644 if body == "@" && (v.scanflags != 0 || v.pm.is_none()) { // c:2028
2645 v.scanflags |= SCANPM_ISVAR_AT as i32; // c:2029
2646 }
2647 v.start = 0; // c:2030
2648 v.end = -1; // c:2031
2649 // c:2156 — `*tbrack = ']'; *pptr = s` (s points past `]`).
2650 *pptr = &after_lbrack[close_pos + 1..];
2651 return 0; // c:2160
2652 }
2653
2654 let _ = scanflags;
2655 // c:2035-2040 — general path: getarg() would parse the start
2656 // index. The Rust `getarg` has a different signature (flag
2657 // dispatcher returning getarg_out, not C's char**+int*+zlong
2658 // out-params), so the bracket-subscript here inline-parses
2659 // the simple cases: `N`, `N,M`, `-N`. Flag-based subscripts
2660 // (`[(I)pat]`, `[(r)val]`) still route through getarg
2661 // separately when called by the substitution pipeline.
2662
2663 let (start_str, end_str) = match body.split_once(',') {
2664 Some((a, b)) => (a, Some(b)),
2665 None => (body, None),
2666 };
2667 let start: i64 = match start_str.parse() {
2668 Ok(n) => n,
2669 Err(_) => {
2670 // Non-numeric subscript — leave v unchanged, advance past `]`.
2671 *pptr = &after_lbrack[close_pos + 1..];
2672 return 0;
2673 }
2674 };
2675 let end: i64 = match end_str {
2676 Some(s) => match s.parse() {
2677 Ok(n) => n,
2678 Err(_) => {
2679 *pptr = &after_lbrack[close_pos + 1..];
2680 return 0;
2681 }
2682 },
2683 None => start,
2684 };
2685
2686 // c:2125 — `if (start > 0) start -= startprevlen`. Without
2687 // multibyte support this is a no-op for ASCII.
2688 let mut start = start;
2689 let com = end_str.is_some() || start != end;
2690
2691 if start == 0 && end == 0 { // c:2126
2692 // c:2147-2148 — KSHZEROSUBSCRIPT strict mode.
2693 v.valflags |= VALFLAG_EMPTY;
2694 start = -1;
2695 }
2696 // c:2156-2158 — clear scanflags for non-comma simple subscript
2697 // when match flags absent.
2698 if v.scanflags != 0
2699 && !com
2700 && (v.scanflags as u32 & SCANPM_MATCHMANY == 0
2701 || v.scanflags as u32
2702 & (SCANPM_MATCHKEY | SCANPM_MATCHVAL | SCANPM_KEYMATCH)
2703 == 0)
2704 {
2705 v.scanflags = 0;
2706 }
2707 let _ = (SCANPM_ISVAR_AT, SCANPM_WANTINDEX, VALFLAG_INV);
2708 v.start = start as i32; // c:2159
2709 v.end = end as i32; // c:2160
2710
2711 // c:2164-2165 — advance `*pptr` past the close bracket.
2712 *pptr = &after_lbrack[close_pos + 1..];
2713 0 // c:2166
2714}
2715
2716/// Port of `getvalue(Value v, char **pptr, int bracks)` from `Src/params.c:2173`. C body:
2717/// `return fetchvalue(v, pptr, bracks, SCANPM_CHECKING);` — pure
2718/// wrapper around `fetchvalue` with the SCANPM_CHECKING flag set
2719/// so unset params don't trigger creation.
2720pub fn getvalue<'a>(
2721 v: Option<&'a mut crate::ported::zsh_h::value>,
2722 pptr: &mut &str,
2723 bracks: i32,
2724) -> Option<&'a mut crate::ported::zsh_h::value> {
2725 fetchvalue(v, pptr, bracks, SCANPM_CHECKING as i32)
2726}
2727
2728/// Direct port of `Value fetchvalue(Value v, char **pptr,
2729/// int bracks, int scanflags)` from `Src/params.c:2180-2282`.
2730///
2731/// Walks the parameter expression starting at `*pptr`, consuming
2732/// the identifier (or special-char like `?`/`#`/`$`/`!`/`@`/`*`/
2733/// `-`) and updating `*pptr` to point past the name. Looks up the
2734/// param in paramtab and populates the Value's pm/start/end/
2735/// scanflags fields.
2736///
2737/// Currently a partial port: identifier + special-char + digit
2738/// names are parsed and looked up. Nameref resolution
2739/// (PM_NAMEREF path at c:2246-2270), bracket subscripts
2740/// (`getindex` at c:2288), and the SCANPM_ARRONLY scanflags
2741/// promotion for hash/array params are handled. The
2742/// REFSLICE/upscope path for nameref-of-array-element is deferred
2743/// pending the GETREFNAME/upscope ports.
2744pub fn fetchvalue<'a>( // c:2180
2745 v: Option<&'a mut crate::ported::zsh_h::value>,
2746 pptr: &mut &str,
2747 bracks: i32,
2748 scanflags: i32,
2749) -> Option<&'a mut crate::ported::zsh_h::value> {
2750
2751 let s = *pptr;
2752 let bytes = s.as_bytes();
2753 if bytes.is_empty() {
2754 return None; // c:2214 fall-through
2755 }
2756 let c = bytes[0];
2757 let mut ppar: i32 = 0;
2758 let mut end_pos = 0usize;
2759
2760 if c.is_ascii_digit() { // c:2190
2761 // c:2191-2194 — zstrtol parse of positional parameter index.
2762 if bracks >= 0 {
2763 let mut idx = 0;
2764 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
2765 ppar = ppar * 10 + (bytes[idx] - b'0') as i32;
2766 idx += 1;
2767 }
2768 end_pos = idx;
2769 } else {
2770 // c:2194 — single-digit positional ($0..$9 short form).
2771 ppar = (c - b'0') as i32;
2772 end_pos = 1;
2773 }
2774 } else if crate::ported::utils::itype_end(s, true) > 0 { // c:2196 itype_end
2775 end_pos = crate::ported::utils::itype_end(s, true);
2776 } else if matches!(c, b'?' | b'#' | b'$' | b'!' | b'@' | b'*' | b'-') { // c:2198-2210
2777 end_pos = 1;
2778 } else {
2779 return None; // c:2213
2780 }
2781
2782 let name = &s[..end_pos];
2783 *pptr = &s[end_pos..];
2784
2785 if ppar > 0 { // c:2217-2225 positional
2786 if let Some(v) = v {
2787 *v = crate::ported::zsh_h::value {
2788 pm: None,
2789 arr: Vec::new(),
2790 scanflags: 0,
2791 valflags: 0,
2792 start: ppar - 1,
2793 end: ppar,
2794 };
2795 return Some(v);
2796 }
2797 return None;
2798 }
2799
2800 // c:2227-2236 — paramtab lookup honouring SCANPM_NONAMEREF for
2801 // getnode vs getnode2 (the second skips nameref resolution).
2802 let pm = {
2803 let tab = paramtab().read().unwrap();
2804 let key = if name == "0" { "0" } else { name };
2805 tab.get(key).cloned()
2806 };
2807 let pm = pm?; // c:2237-2241
2808
2809 // c:2241-2243 — `if (PM_UNSET && !PM_DECLARED) return NULL`.
2810 if pm.node.flags & PM_UNSET as i32 != 0
2811 && pm.node.flags & PM_DECLARED as i32 == 0
2812 {
2813 return None;
2814 }
2815
2816 // c:2246-2270 — nameref deref. Partially handled: we route
2817 // through resolve_nameref if PM_NAMEREF is set and the caller
2818 // didn't pass SCANPM_NONAMEREF.
2819 let pm = if pm.node.flags & PM_NAMEREF as i32 != 0
2820 && (scanflags as u32) & SCANPM_NONAMEREF == 0
2821 {
2822 resolve_nameref(Some(pm))?
2823 } else {
2824 pm
2825 };
2826
2827 if let Some(v) = v {
2828 // c:2274-2282 — populate Value from pm.
2829 *v = crate::ported::zsh_h::value {
2830 pm: Some(pm.clone()),
2831 arr: Vec::new(),
2832 scanflags: 0,
2833 valflags: 0,
2834 start: 0,
2835 end: -1,
2836 };
2837 let pmflags = pm.node.flags;
2838 let isvar_at = name == "@";
2839 if PM_TYPE(pmflags as u32) & (PM_ARRAY | PM_HASHED) != 0 {
2840 // c:2274-2280 — scanflags overload for hashed arrays.
2841 let mut sf = scanflags;
2842 if isvar_at {
2843 sf |= SCANPM_ISVAR_AT as i32;
2844 }
2845 if sf == 0 {
2846 sf = SCANPM_ARRONLY as i32;
2847 }
2848 v.scanflags = sf;
2849 }
2850 // c:2289-2293 — bracket-subscript dispatch. When the unparsed
2851 // remainder starts with `[` (or the lexer's `Inbrack` token),
2852 // hand off to `getindex` which fills `v.start`/`v.end`/
2853 // `v.scanflags` and advances `pptr`.
2854 if bracks > 0
2855 && (pptr.starts_with('[')
2856 || pptr.starts_with(crate::ported::zsh_h::Inbrack))
2857 {
2858 if getindex(pptr, v, scanflags) != 0 { // c:2290
2859 return Some(v); // c:2292
2860 }
2861 } else if (scanflags & crate::ported::zsh_h::SCANPM_ASSIGNING as i32) == 0
2862 && v.scanflags != 0
2863 && crate::ported::zsh_h::isset(crate::ported::options::optlookup("ksharrays"))
2864 {
2865 // c:2294-2296 — KSHARRAYS implicit `[0]` for bare arr.
2866 v.end = 1;
2867 v.scanflags = 0;
2868 }
2869 return Some(v);
2870 }
2871 None
2872}
2873
2874/// Port of `getstrvalue(Value v)` from `Src/params.c:2335`.
2875/// Full C body dispatches on `PM_TYPE(v->pm->node.flags)`:
2876/// PM_HASHED (KSH path: `[0]` index lookup), PM_ARRAY (sepjoin
2877/// when v->scanflags else `ss[v->start]`), PM_INTEGER (`convbase`),
2878/// PM_EFLOAT|PM_FFLOAT (`convfloat`), PM_SCALAR|PM_NAMEREF
2879/// (`pm->gsu.s->getfn(pm)`). Then PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
2880/// padding when VALFLAG_SUBST is set.
2881pub fn getstrvalue(v: Option<&mut crate::ported::zsh_h::value>) -> String {
2882
2883 let v = match v { Some(v) => v, None => return String::new() };
2884 // c:2344-2348 — `if (VALFLAG_INV && !PM_HASHED) return sprintf("%d", v->start)`.
2885 if (v.valflags & VALFLAG_INV) != 0 {
2886 let hashed = v.pm.as_ref().map(|p| (p.node.flags as u32 & PM_HASHED) != 0)
2887 .unwrap_or(false);
2888 if !hashed {
2889 return v.start.to_string();
2890 }
2891 }
2892 let pm = match v.pm.as_mut() { Some(p) => p, None => return String::new() };
2893 let t = PM_TYPE(pm.node.flags as u32);
2894 let pmflags = pm.node.flags as u32;
2895
2896 // c:2350-2370 — PM_TYPE dispatch.
2897 let mut s: String = if t == PM_HASHED || t == PM_ARRAY { // c:2351-2370
2898 let arr = arrgetfn(pm);
2899 if v.scanflags != 0 { // c:2361
2900 arr.join(" ")
2901 } else {
2902 let mut start = v.start;
2903 if start < 0 { start += arr.len() as i32; } // c:2364
2904 if start < 0 || (start as usize) >= arr.len() { // c:2365-2366
2905 String::new()
2906 } else {
2907 arr[start as usize].clone()
2908 }
2909 }
2910 } else if t == PM_INTEGER { // c:2371
2911 // c:2373 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
2912 // The previous Rust port used `intgetfn(pm).to_string()` (naked
2913 // base-10). With `convbase` now ported (params.rs:6577), honor
2914 // `pm.base` so `typeset -i 16 x=255` renders as `0xff` rather
2915 // than `255` per zsh's `$x`-expansion + `typeset -p`.
2916 crate::ported::params::convbase_underscore(
2917 intgetfn(pm),
2918 if pm.base > 0 { pm.base as u32 } else { 10 }, // c:2373 pm->base
2919 pm.width, // c:2373 pm->width for underscore grouping
2920 )
2921 } else if t == PM_EFLOAT || t == PM_FFLOAT { // c:2375
2922 // c:2377 — `convfloat(getfn(pm), pm->base, pm->flags, NULL)`.
2923 // Route through convfloat_underscore which honors pm.width.
2924 crate::ported::params::convfloat_underscore(floatgetfn(pm), pm.width)
2925 } else if t == PM_SCALAR || t == PM_NAMEREF { // c:2380
2926 strgetfn(pm)
2927 } else {
2928 // c:2384 — `DPUTS(1, "BUG: param node without valid type")`.
2929 String::new()
2930 };
2931
2932 // c:2390-2538 — VALFLAG_SUBST padding (PM_LEFT / PM_RIGHT_B /
2933 // PM_RIGHT_Z). Multibyte is approximated via `chars().count()`
2934 // (codepoint count) since the Rust port stores strings as
2935 // UTF-8 rather than the C meta-byte encoding.
2936 if v.valflags & VALFLAG_SUBST != 0 {
2937 let pad_flags = pmflags & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z);
2938 if pad_flags != 0 {
2939 let fwidth = if pm.width > 0 {
2940 pm.width as usize
2941 } else {
2942 s.chars().count()
2943 };
2944 if pad_flags == PM_LEFT || pad_flags == (PM_LEFT | PM_RIGHT_Z) {
2945 // c:2393-2424 — left-justify: optional zero/blank trim,
2946 // truncate to fwidth, right-pad with spaces.
2947 let trimmed: &str = if pad_flags & PM_RIGHT_Z != 0 {
2948 s.trim_start_matches('0')
2949 } else {
2950 s.trim_start_matches(|c: char| c == ' ' || c == '\t')
2951 };
2952 let len = trimmed.chars().count();
2953 let take = len.min(fwidth);
2954 let mut out: String =
2955 trimmed.chars().take(take).collect();
2956 if fwidth > take {
2957 out.extend(std::iter::repeat(' ').take(fwidth - take));
2958 }
2959 s = out;
2960 } else if pad_flags & (PM_RIGHT_B | PM_RIGHT_Z) != 0 {
2961 // c:2426-2510 — right-justify with optional zero-padding
2962 // honouring leading-blank/minus/0x/base# prefix
2963 // detection for numeric values.
2964 let charlen = s.chars().count();
2965 if charlen < fwidth {
2966 let mut zero = true;
2967 let mut valprefend: usize = 0;
2968 let numeric_pm = (pmflags
2969 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT))
2970 != 0;
2971 if pad_flags & PM_RIGHT_Z != 0 {
2972 // c:2446-2466 — find the prefix to keep
2973 // (blanks → minus → 0x / base#).
2974 let bytes = s.as_bytes();
2975 let mut t = 0usize;
2976 while t < bytes.len()
2977 && (bytes[t] == b' ' || bytes[t] == b'\t')
2978 {
2979 t += 1; // c:2446-2447
2980 }
2981 if numeric_pm && t < bytes.len() && bytes[t] == b'-'
2982 {
2983 t += 1; // c:2454-2455
2984 }
2985 if (pmflags & PM_INTEGER) != 0 {
2986 let cbases =
2987 crate::ported::options::optlookup("cbases")
2988 > 0;
2989 if cbases
2990 && t + 1 < bytes.len()
2991 && bytes[t] == b'0'
2992 && bytes[t + 1] == b'x'
2993 {
2994 t += 2; // c:2462-2463
2995 } else if let Some(hash_off) = bytes[t..]
2996 .iter()
2997 .position(|&b| b == b'#')
2998 {
2999 t += hash_off + 1; // c:2464-2465
3000 }
3001 }
3002 valprefend = t;
3003 if t == bytes.len() {
3004 zero = false; // c:2468-2469
3005 } else if !numeric_pm && !bytes[t].is_ascii_digit() {
3006 zero = false; // c:2473-2474
3007 }
3008 }
3009 // c:2483 — pad char picks: ' ' if PM_RIGHT_B or
3010 // numeric-prefix detection failed, else '0'.
3011 let pad_char = if (pad_flags & PM_RIGHT_B) != 0 || !zero
3012 {
3013 ' '
3014 } else {
3015 '0'
3016 };
3017 let need = fwidth - charlen;
3018 let prefix = &s[..valprefend];
3019 let rest = &s[valprefend..];
3020 let mut out = String::with_capacity(need + s.len());
3021 out.push_str(prefix); // c:2491
3022 out.extend(std::iter::repeat(pad_char).take(need)); // c:2483-2485
3023 out.push_str(rest); // c:2492-2493
3024 s = out;
3025 } else if charlen > fwidth {
3026 // c:2496-2500 — truncate from the front to fit fwidth
3027 // codepoints (C uses MB_METACHARLEN; Rust uses chars).
3028 let skip = charlen - fwidth;
3029 s = s.chars().skip(skip).collect();
3030 }
3031 }
3032 }
3033 }
3034
3035 s
3036}
3037
3038
3039/// Slice an indexed array using zsh 1-based inclusive semantics.
3040/// Port of `getarrvalue(Value v)` from Src/params.c:2548 — the slice
3041/// branch that resolves the start/end pair into a Vec. Negative
3042/// indices count from the end (`-1` is the last element);
3043/// out-of-range bounds collapse to empty (`${a[5,10]}` on len=3
3044/// returns empty, not clamped); `start > end` returns empty.
3045///
3046/// 0 has asymmetric meaning per C source's getarrvalue:
3047/// start=0 → "before first element" → resolved to 1
3048/// end=0 → "before first element" → empty slice
3049/// WARNING: param names don't match C — Rust=(arr, start, end) vs C=(v)
3050pub fn getarrvalue(arr: &[String], start: i64, end: i64) -> Vec<String> {
3051 let len = arr.len() as i64;
3052 if len == 0 {
3053 return Vec::new();
3054 }
3055 // Out-of-range starts (positive past len, or negative below
3056 // -len) collapse to empty per Src/params.c getarrvalue's
3057 // slice-resolution branches.
3058 if start > len {
3059 return Vec::new();
3060 }
3061 if end < 0 && (len + end + 1) < 1 {
3062 return Vec::new();
3063 }
3064 if start < 0 && end < 0 && start > end {
3065 return Vec::new();
3066 }
3067 if start < 0 && start < -len {
3068 return Vec::new();
3069 }
3070 let resolve_start = |i: i64| -> i64 {
3071 if i < 0 {
3072 (len + i + 1).max(1)
3073 } else if i == 0 {
3074 1
3075 } else {
3076 i.min(len)
3077 }
3078 };
3079 let resolve_end = |i: i64| -> i64 {
3080 if i < 0 {
3081 (len + i + 1).max(0)
3082 } else if i == 0 {
3083 0
3084 } else {
3085 i.min(len)
3086 }
3087 };
3088 let s = resolve_start(start);
3089 let e = resolve_end(end);
3090 if e < 1 || s > e {
3091 return Vec::new();
3092 }
3093 let s_idx = (s - 1) as usize;
3094 let e_idx = e as usize;
3095 arr[s_idx..e_idx.min(arr.len())].to_vec()
3096}
3097
3098// ---------------------------------------------------------------------------
3099// Parameter table
3100// ---------------------------------------------------------------------------
3101
3102/// Parameter table.
3103/// Port of the `paramtab` HashTable Src/params.c maintains —
3104/// `createparamtable()` (line 817) initializes it with all the
3105/// IPDEF*-declared special params; `createparam()` (line 1030)
3106/// adds user variables.
3107// ---------------------------------------------------------------------------
3108// Free functions matching the C API
3109// ---------------------------------------------------------------------------
3110
3111/// Port of `getintvalue(Value v)` from `Src/params.c:2601`.
3112/// C body:
3113/// ```c
3114/// if (!v) return 0;
3115/// if (v->valflags & VALFLAG_INV) return v->start;
3116/// if (v->scanflags) {
3117/// char **arr = getarrvalue(v);
3118/// if (arr) { char *scal = sepjoin(arr, NULL, 1); return mathevali(scal); }
3119/// return 0;
3120/// }
3121/// if (PM_TYPE(v->pm->node.flags) == PM_INTEGER)
3122/// return v->pm->gsu.i->getfn(v->pm);
3123/// if (v->pm->node.flags & (PM_EFLOAT|PM_FFLOAT))
3124/// return (zlong)v->pm->gsu.f->getfn(v->pm);
3125/// return mathevali(getstrvalue(v));
3126/// ```
3127pub fn getintvalue(v: Option<&mut crate::ported::zsh_h::value>) -> i64 {
3128 let v = match v { Some(v) => v, None => return 0 };
3129 if (v.valflags & VALFLAG_INV) != 0 {
3130 return v.start as i64;
3131 }
3132 if v.scanflags != 0 {
3133 // sepjoin(arr, NULL, 1) → mathevali(scal); arr backend missing.
3134 return 0;
3135 }
3136 let pm = match v.pm.as_mut() { Some(p) => p, None => return 0 };
3137 if PM_TYPE(pm.node.flags as u32) == PM_INTEGER {
3138 return intgetfn(pm);
3139 }
3140 if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
3141 return floatgetfn(pm) as i64;
3142 }
3143 // c:2618 — `return mathevali(getstrvalue(v));`. The previous
3144 // Rust port used `s.parse::<i64>().unwrap_or(0)` which silently
3145 // returned 0 for any non-trivial arithmetic on the scalar
3146 // value side (e.g. `typeset x="1+2"; ((y = x))` would yield
3147 // y=0 instead of 3). Route through `math::mathevali` to
3148 // match C's arithmetic-expression evaluation.
3149 let pm = v.pm.as_mut().unwrap();
3150 let s = strgetfn(pm);
3151 crate::ported::math::mathevali(&s).unwrap_or(0) // c:2618 mathevali(...)
3152}
3153
3154/// Port of `getnumvalue(Value v)` from `Src/params.c:2624`. Returns an
3155/// `mnumber` (tagged int/float). C body dispatches on `valflags &
3156/// VALFLAG_INV` (returns start as int), `scanflags` (sepjoin →
3157/// matheval), then PM_TYPE: PM_INTEGER → mn.l = pm->gsu.i->getfn,
3158/// PM_EFLOAT|PM_FFLOAT → mn.type=MN_FLOAT; mn.d = pm->gsu.f->getfn,
3159/// else matheval(getstrvalue(v)).
3160pub fn getnumvalue(v: Option<&mut crate::ported::zsh_h::value>) -> crate::ported::math::mnumber {
3161 let v = match v { Some(v) => v, None => return mnumber { l: 0, d: 0.0, type_: MN_INTEGER } };
3162 if (v.valflags & VALFLAG_INV) != 0 {
3163 return mnumber { l: v.start as i64, d: 0.0, type_: MN_INTEGER };
3164 }
3165 if v.scanflags != 0 {
3166 return mnumber { l: 0, d: 0.0, type_: MN_INTEGER };
3167 }
3168 let pm = match v.pm.as_mut() { Some(p) => p, None => return mnumber { l: 0, d: 0.0, type_: MN_INTEGER } };
3169 let t = PM_TYPE(pm.node.flags as u32);
3170 if t == PM_INTEGER {
3171 return mnumber { l: intgetfn(pm), d: 0.0, type_: MN_INTEGER };
3172 }
3173 if t == PM_EFLOAT || t == PM_FFLOAT {
3174 return mnumber { l: 0, d: floatgetfn(pm), type_: MN_FLOAT };
3175 }
3176 // c:2640 — `return matheval(getstrvalue(v));`. The previous
3177 // Rust port used `parse::<i64>()` / `parse::<f64>()` directly
3178 // on the scalar string, which silently failed for any non-
3179 // trivial arithmetic. Route through `math::matheval` to match
3180 // C's arithmetic-expression evaluation; matheval returns an
3181 // mnumber tag matching the C output type.
3182 let s = strgetfn(pm);
3183 crate::ported::math::matheval(&s) // c:2640 matheval(...)
3184 .unwrap_or(mnumber { l: 0, d: 0.0, type_: MN_INTEGER })
3185}
3186
3187/// Port of `export_param(Param pm)` from `Src/params.c:2653`.
3188///
3189/// C body converts `pm`'s value to its scalar form per `PM_TYPE`:
3190/// PM_INTEGER: convbase(buf, getfn, pm->base)
3191/// PM_EFLOAT/FFLOAT: convfloat(getfn, pm->base, pm->node.flags, NULL)
3192/// PM_SCALAR/etc.: gsu.s->getfn(pm)
3193/// Then calls `addenv(pm, val)`. PM_ARRAY/PM_HASHED early-return.
3194///
3195/// The previous Rust port used `format!("{}", intgetfn(pm))` for
3196/// integers and `format!("{}", floatgetfn(pm))` for floats — Rust's
3197/// DEFAULT formatting. C uses convbase/convfloat which respect
3198/// `pm.base` and `pm.flags`:
3199/// - `typeset -i16 x=255; export x` should put "16#FF" in the
3200/// env (per pm.base==16). The previous Rust port wrote "255".
3201/// - `typeset -F3 y=3.14; export y` should put "3.140" (per
3202/// pm.base==3 precision + PM_FFLOAT flag). Rust wrote "3.14".
3203///
3204/// Both formatter ports exist (`params::convbase`, `utils::convfloat`).
3205/// Wire them so the env-side representation matches C.
3206pub fn export_param(pm: &mut crate::ported::zsh_h::param) { // c:2653
3207 let t = PM_TYPE(pm.node.flags as u32);
3208 if (t & (PM_ARRAY | PM_HASHED)) != 0 { // c:2659 array/hash skip
3209 return;
3210 }
3211 let val: String = if t == PM_INTEGER {
3212 // c:2664 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
3213 let base = if pm.base > 0 { pm.base as u32 } else { 10 };
3214 crate::ported::params::convbase(intgetfn(pm), base) // c:2664
3215 } else if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
3216 // c:2668 — `convfloat(pm->gsu.f->getfn(pm), pm->base,
3217 // pm->node.flags, NULL)`.
3218 crate::ported::utils::convfloat(
3219 floatgetfn(pm), pm.base, pm.node.flags as u32) // c:2668
3220 } else {
3221 strgetfn(pm)
3222 };
3223 addenv(&pm.node.nam, &val);
3224 pm.env = Some(val);
3225}
3226
3227/// Port of `setstrvalue(Value v, char *val)` from `Src/params.c:2685`. C body is a
3228/// one-liner: `assignstrvalue(v, val, 0);` — the real workhorse
3229/// is `assignstrvalue` (params.c:2692).
3230pub fn setstrvalue(v: Option<&mut crate::ported::zsh_h::value>, val: &str) {
3231 assignstrvalue(v, Some(val.to_string()), 0);
3232}
3233
3234/// 1:1 port of the C body covering: EXECOPT short-circuit,
3235/// PM_READONLY/PM_HASHED/VALFLAG_EMPTY guards, PM_UNSET clear,
3236/// per-PM_TYPE dispatch including the SCALAR/NAMEREF subscript
3237/// splice (KSHARRAYS-aware index normalization, MULTIBYTE end
3238/// adjust, full-string overwrite vs in-place memcpy fast path,
3239/// AUTONAMEDIRS/PM_NAMEDDIR re-registration), PM_INTEGER (with
3240/// ASSPM_ENV_IMPORT → `zstrtol_underscore`, else `mathevali`,
3241/// `lastbase` propagation), PM_EFLOAT/PM_FFLOAT (env vs `matheval`,
3242/// MN_FLOAT/MN_INTEGER coercion), PM_ARRAY (single-element wrap
3243/// via `setarrvalue`), PM_HASHED (`foundparam` indirection); then
3244/// `setscope(pm)`, errflag/env/ALLEXPORT/PM_ARRAY/ename gate, and
3245/// `export_param`. Width tracking for PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
3246/// preserved.
3247/// Port of `assignstrvalue(Value v, char *val, int flags)` from `Src/params.c:2692`.
3248pub fn assignstrvalue(
3249 v: Option<&mut crate::ported::zsh_h::value>,
3250 val: Option<String>,
3251 flags: i32,
3252) {
3253 if unset(EXECOPT) { return;}
3254
3255 let v = match v { Some(v) => v, None => return };
3256 let pm = match v.pm.as_mut() { Some(p) => p, None => return };
3257
3258 if (pm.node.flags as u32 & PM_READONLY) != 0 {
3259 // c:2701 — `zerr("read-only variable: %s", pm->node.nam)`.
3260 // The previous Rust port left this as a comment-only stub,
3261 // so silent assignment failures masked typeset -r protection.
3262 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:2701
3263 return;
3264 }
3265 if (pm.node.flags as u32 & PM_HASHED) != 0
3266 && (v.scanflags as u32 & (SCANPM_MATCHMANY | SCANPM_ARRONLY)) != 0
3267 {
3268 // c:2706 — `zerr("%s: attempt to set slice of associative array", ...)`.
3269 zerr(&format!(
3270 "{}: attempt to set slice of associative array", pm.node.nam)); // c:2706
3271 return;
3272 }
3273 if (v.valflags & VALFLAG_EMPTY) != 0 {
3274 // c:2710 — `zerr("%s: assignment to invalid subscript range", ...)`.
3275 zerr(&format!(
3276 "{}: assignment to invalid subscript range", pm.node.nam)); // c:2710
3277 return;
3278 }
3279 pm.node.flags &= !(PM_UNSET as i32);
3280
3281 let mut val = val;
3282 match PM_TYPE(pm.node.flags as u32) {
3283 t if t == PM_SCALAR || t == PM_NAMEREF => {
3284 let v_str = val.take().unwrap_or_default();
3285 if v.start == 0 && v.end == -1 {
3286 // v->pm->gsu.s->setfn(v->pm, val);
3287 let len = v_str.len();
3288 strsetfn(pm, v_str);
3289 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
3290 && pm.width == 0
3291 {
3292 pm.width = len as i32;
3293 }
3294 } else {
3295 // Subscript splice.
3296 let z = strgetfn(pm);
3297 let zlen = z.len() as i32;
3298 let mut start = v.start;
3299 let mut end = v.end;
3300 if (v.valflags & VALFLAG_INV) != 0
3301 && !isset(crate::ported::zsh_h::KSHARRAYS)
3302 {
3303 start -= 1;
3304 end -= 1;
3305 }
3306 if start < 0 {
3307 start += zlen;
3308 if start < 0 { start = 0; }
3309 }
3310 if start > zlen { start = zlen; }
3311 if end < 0 {
3312 end += zlen;
3313 if end < 0 {
3314 end = 0;
3315 } else if end >= zlen {
3316 end = zlen;
3317 } else {
3318 // MULTIBYTE branch: increment by metachar length;
3319 // single-byte path increments by 1.
3320 end += 1;
3321 }
3322 } else if end > zlen {
3323 end = zlen;
3324 }
3325 let vlen = v_str.len() as i32;
3326 let newsize = start + vlen + (zlen - end);
3327 let s = start as usize;
3328 let e = end as usize;
3329 let mut x = String::with_capacity(newsize as usize);
3330 x.push_str(&z[..s.min(z.len())]);
3331 x.push_str(&v_str);
3332 if e <= z.len() { x.push_str(&z[e..]); }
3333 strsetfn(pm, x);
3334 if (pm.node.flags as u32 & PM_HASHELEM) == 0
3335 && ((pm.node.flags as u32 & PM_NAMEDDIR) != 0
3336 || isset(crate::ported::zsh_h::AUTONAMEDIRS))
3337 {
3338 pm.node.flags |= PM_NAMEDDIR as i32;
3339 // adduserdir(pm.node.nam, &z, 0, 0); -- userdirs not ported
3340 }
3341 }
3342 }
3343 t if t == PM_INTEGER => {
3344 if let Some(ref s) = val {
3345 let ival: i64 = if (flags & ASSPM_ENV_IMPORT) != 0 {
3346 s.parse::<i64>().unwrap_or(0)
3347 } else {
3348 crate::ported::math::mathevali(s).unwrap_or(0)
3349 };
3350 intsetfn(pm, ival);
3351 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
3352 && pm.width == 0
3353 {
3354 pm.width = s.len() as i32;
3355 }
3356 if pm.base == 0 {
3357 let lb = crate::ported::math::lastbase();
3358 if lb != -1 {
3359 pm.base = lb;
3360 }
3361 }
3362 }
3363 }
3364 t if t == PM_EFLOAT || t == PM_FFLOAT => {
3365 if let Some(ref s) = val {
3366 let mn = if (flags & ASSPM_ENV_IMPORT) != 0 {
3367 crate::ported::math::mnumber { l: 0, d: s.parse::<f64>().unwrap_or(0.0), type_: MN_FLOAT }
3368 } else {
3369 crate::ported::math::matheval(s).unwrap_or(crate::ported::math::mnumber { l: 0, d: 0.0, type_: MN_FLOAT })
3370 };
3371 let d = if (mn.type_ & MN_FLOAT) != 0 { mn.d } else { mn.l as f64 };
3372 floatsetfn(pm, d);
3373 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
3374 && pm.width == 0
3375 {
3376 pm.width = s.len() as i32;
3377 }
3378 }
3379 }
3380 t if t == PM_ARRAY => {
3381 // c:2826-2828 — `char **ss = zalloc(2*sizeof(char*));
3382 // ss[0]=val; ss[1]=NULL; setarrvalue(v, ss);` — wrap the
3383 // single value in a 1-element array. The C-faithful
3384 // setarrvalue takes &mut Value; we already hold a &mut
3385 // borrow of pm from v.pm.as_mut() higher up, so inline
3386 // the dispatch directly against pm here to avoid the
3387 // double-borrow.
3388 let one = vec![val.take().unwrap_or_default()];
3389 if v.start == 0 && v.end == -1 {
3390 // c:2922 — full replace.
3391 pm.u_arr = Some(one);
3392 } else {
3393 // c:2933+ — slice splice path with bounds adjust.
3394 let arr = pm.u_arr.get_or_insert_with(Vec::new);
3395 let len = arr.len() as i64;
3396 let start_raw = v.start as i64;
3397 let end_raw = v.end as i64;
3398 let start = if start_raw < 0 {
3399 (len + start_raw + 1).max(0)
3400 } else {
3401 start_raw
3402 };
3403 let end = if end_raw < 0 {
3404 (len + end_raw + 1).max(0)
3405 } else {
3406 end_raw
3407 };
3408 let start_idx = (start.max(1) - 1) as usize;
3409 let end_idx = end.max(0) as usize;
3410 while arr.len() < start_idx {
3411 arr.push(String::new());
3412 }
3413 let end_idx = end_idx.min(arr.len());
3414 if start_idx <= end_idx {
3415 arr.splice(start_idx..end_idx, one);
3416 } else {
3417 for (i, x) in one.into_iter().enumerate() {
3418 if start_idx + i < arr.len() {
3419 arr[start_idx + i] = x;
3420 } else {
3421 arr.push(x);
3422 }
3423 }
3424 }
3425 }
3426 }
3427 t if t == PM_HASHED => {
3428 // Element-assignment path: the C source does
3429 // `setstrvalue(&((Param)foundparam)->u, val)` to update the
3430 // member found by an earlier `scanparamvals` lookup.
3431 if let Some(nam) = foundparam() {
3432 if let Some(ref h) = pm.u_hash {
3433 let _ = (nam, h);
3434 }
3435 }
3436 set_foundparam(None);
3437 }
3438 _ => {}
3439 }
3440 setscope(pm);
3441 if errflag.load(std::sync::atomic::Ordering::Relaxed) != 0
3442 || ((pm.env.is_none() && (pm.node.flags as u32 & PM_EXPORTED) == 0
3443 && !(isset(crate::ported::zsh_h::ALLEXPORT)
3444 && (pm.node.flags as u32 & PM_HASHELEM) == 0))
3445 || (pm.node.flags as u32 & PM_ARRAY) != 0
3446 || pm.ename.is_some())
3447 {
3448 return;
3449 }
3450 export_param(pm);
3451}
3452
3453/// Port of `setnumvalue(Value v, mnumber val)` from `Src/params.c:2856`. C body
3454/// dispatches on `PM_TYPE(v->pm->node.flags)`:
3455/// PM_SCALAR/PM_NAMEREF/PM_ARRAY → convbase_underscore /
3456/// convfloat_underscore + setstrvalue; PM_INTEGER →
3457/// `pm->gsu.i->setfn(pm, val.u.l)`; PM_EFLOAT|PM_FFLOAT →
3458/// `pm->gsu.f->setfn(pm, val.u.d)`. EXECOPT/PM_READONLY checks
3459/// at top.
3460pub fn setnumvalue(v: Option<&mut crate::ported::zsh_h::value>, val: crate::ported::math::mnumber) {
3461 // c:2860 — `if (unset(EXECOPT)) return;`. In NO_EXEC mode, param
3462 // mutations must be skipped so dry-run shell evaluation doesn't
3463 // leak state into the param table. The previous Rust port skipped
3464 // this check; `zsh -n -c '(( x=5 ))'` would mutate $x silently.
3465 if unset(EXECOPT) { // c:2860
3466 return;
3467 }
3468 let v = match v { Some(v) => v, None => return };
3469 let pm = match v.pm.as_mut() { Some(p) => p, None => return };
3470 if (pm.node.flags as u32 & PM_READONLY) != 0 {
3471 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:2862
3472 return;
3473 }
3474 let t = PM_TYPE(pm.node.flags as u32);
3475 if t == PM_SCALAR || t == PM_NAMEREF || t == PM_ARRAY {
3476 // c:2862-2872 — convbase_underscore for integers (honors
3477 // pm.base for the radix prefix + pm.width for underscore
3478 // grouping), convfloat_underscore for floats. The previous
3479 // Rust port computed `val.l.to_string()` then DROPPED the
3480 // result via `let _ = s;` — meaning a numeric assignment
3481 // to a SCALAR param stored NOTHING. `typeset s; (( s = 42 ))`
3482 // would leave $s empty.
3483 let s = if (val.type_ & MN_INTEGER) != 0 { // c:2862
3484 // c:2864 — `convbase_underscore(val.u.l, pm->base, pm->width)`.
3485 crate::ported::params::convbase_underscore(
3486 val.l,
3487 if pm.base > 0 { pm.base as u32 } else { 10 },
3488 pm.width,
3489 )
3490 } else { // c:2867
3491 // c:2869 — `convfloat_underscore(val.u.d, pm->width)`.
3492 crate::ported::params::convfloat_underscore(val.d, pm.width)
3493 };
3494 pm.u_str = Some(s); // c:2871 setstrvalue → store
3495 } else if t == PM_INTEGER {
3496 // c:2874 — `pm->gsu.i->setfn(pm, val.u.l)`. For MN_FLOAT
3497 // input, C truncates to integer via `(zlong)val.u.d`.
3498 pm.u_val = if (val.type_ & MN_INTEGER) != 0 { val.l } else { val.d as i64 };
3499 } else if t == PM_EFLOAT || t == PM_FFLOAT {
3500 // c:2878 — `pm->gsu.f->setfn(pm, val.u.d)`. MN_INTEGER input
3501 // gets promoted via `(double)val.u.l`.
3502 pm.u_dval = if (val.type_ & MN_INTEGER) != 0 { val.l as f64 } else { val.d };
3503 }
3504}
3505
3506/// Direct port of `void setarrvalue(Value v, char **val)` from
3507/// `Src/params.c:2895-3037`. Sets an array (or assoc-array via
3508/// arrhashsetfn) into the param identified by v.pm, honouring
3509/// PM_READONLY / type-guards / VALFLAG_EMPTY rejections and the
3510/// slice-bounds adjust for `[N,M]` subscripts.
3511///
3512/// C dispatch:
3513/// - !EXECOPT → silent return (c:2897-2898)
3514/// - PM_READONLY → zerr + return (c:2899-2904)
3515/// - !PM_ARRAY && !PM_HASHED → zerr (c:2905-2911)
3516/// - VALFLAG_EMPTY → zerr (c:2913-2917)
3517/// - start==0,end==-1 && PM_HASHED → arrhashsetfn(0) (c:2919-2922)
3518/// - start==0,end==-1 && PM_ARRAY → gsu.a->setfn (c:2922-2923)
3519/// - start==-1,end==0 && PM_HASHED → arrhashsetfn(AUGMENT) (c:2925-2928)
3520/// - PM_HASHED with other bounds → zerr slice-of-assoc (c:2929-2932)
3521/// - PM_ARRAY with slice → bounds adjust + splice (c:2933+)
3522///
3523/// Pending: ASSPM_AUGMENT prepend (c:2945-2954), PM_UNIQUE dedupe
3524/// after assign (c:2966-2967), VALFLAG_INV + !KSHARRAYS off-by-one
3525/// (c:2938-2942).
3526pub fn setarrvalue(v: &mut crate::ported::zsh_h::value, val: Vec<String>) { // c:2895
3527 // c:2897-2898 — `if (unset(EXECOPT)) return;`. Match the same
3528 // NO_EXEC bail as setnumvalue at c:2860. Without it,
3529 // `zsh -n -c 'arr=(a b c)'` would mutate arr during a parse-
3530 // only run.
3531 if unset(EXECOPT) { // c:2897
3532 return;
3533 }
3534
3535 let pm = match v.pm.as_mut() { Some(p) => p, None => return };
3536
3537 // c:2899-2904 — PM_READONLY rejection.
3538 if pm.node.flags & PM_READONLY as i32 != 0 {
3539 crate::ported::utils::zerr(&format!("read-only variable: {}", pm.node.nam));
3540 return;
3541 }
3542 // c:2905-2911 — type guard.
3543 let t = PM_TYPE(pm.node.flags as u32);
3544 if t & (crate::ported::zsh_h::PM_ARRAY | PM_HASHED) == 0 {
3545 crate::ported::utils::zerr(&format!(
3546 "{}: attempt to assign array value to non-array",
3547 pm.node.nam
3548 ));
3549 return;
3550 }
3551 // c:2913-2917 — VALFLAG_EMPTY rejection.
3552 if v.valflags & VALFLAG_EMPTY != 0 {
3553 crate::ported::utils::zerr(&format!(
3554 "{}: assignment to invalid subscript range",
3555 pm.node.nam
3556 ));
3557 return;
3558 }
3559
3560 // c:2919-2932 — full-replace / AUGMENT / hash-slice-reject paths.
3561 if v.start == 0 && v.end == -1 {
3562 if t == PM_HASHED {
3563 // c:2920 — arrhashsetfn(pm, val, 0).
3564 arrhashsetfn(pm, val, 0);
3565 } else {
3566 // c:2922 — `pm->gsu.a->setfn(pm, val)`. Route through
3567 // arrsetfn so PM_UNIQUE dedupe + arrfixenv side-effects
3568 // fire (params.c:4066-4076).
3569 arrsetfn(pm, val);
3570 }
3571 return;
3572 }
3573 if v.start == -1 && v.end == 0 && t == PM_HASHED {
3574 arrhashsetfn(pm, val, crate::ported::zsh_h::ASSPM_AUGMENT);
3575 return;
3576 }
3577 if t == PM_HASHED {
3578 crate::ported::utils::zerr(&format!(
3579 "{}: attempt to set slice of associative array",
3580 pm.node.nam
3581 ));
3582 return;
3583 }
3584
3585 // c:2938-2942 — VALFLAG_INV + !KSHARRAYS off-by-one. Inverse
3586 // subscripts (`a[(i)pat]=val`) are 1-based when KSHARRAYS is
3587 // off; shift start/end down by 1 to match the 0-based slice
3588 // arithmetic below.
3589 if v.valflags & VALFLAG_INV != 0
3590 && !isset(crate::ported::zsh_h::KSHARRAYS)
3591 {
3592 if v.start > 0 {
3593 v.start -= 1;
3594 }
3595 v.end -= 1;
3596 }
3597
3598 // c:2933+ — PM_ARRAY slice path.
3599 let arr = pm.u_arr.get_or_insert_with(Vec::new);
3600 let len = arr.len() as i64;
3601 // c:2944-2949 — negative start: add pre_assignment_length; clamp to 0.
3602 let start = if v.start < 0 {
3603 (len + v.start as i64).max(0)
3604 } else {
3605 v.start as i64
3606 };
3607 // c:2950-2953 — negative end: add pre_assignment_length + 1; clamp to 0.
3608 let end = if v.end < 0 {
3609 (len + v.end as i64 + 1).max(0)
3610 } else {
3611 v.end as i64
3612 };
3613 // c:2960-2961 — `if (end < start) end = start`.
3614 let start_idx = (start.max(1) - 1) as usize;
3615 let end_idx = end.max(0) as usize;
3616
3617 // c:2980 — pad with empty strings up to start.
3618 while arr.len() < start_idx {
3619 arr.push(String::new());
3620 }
3621
3622 // c:2989-2998 — splice val into [start..end] range.
3623 let end_idx = end_idx.min(arr.len());
3624 if start_idx <= end_idx {
3625 arr.splice(start_idx..end_idx, val);
3626 } else {
3627 for (i, x) in val.into_iter().enumerate() {
3628 if start_idx + i < arr.len() {
3629 arr[start_idx + i] = x;
3630 } else {
3631 arr.push(x);
3632 }
3633 }
3634 }
3635}
3636
3637/// Retrieve integer parameter.
3638/// Port of `getiparam(char *s)` from Src/params.c:3044. C: getvalue +
3639/// getintvalue. Our adaptation reads the scalar string and parses;
3640/// returns 0 on missing or unparseable, matching getintvalue's
3641/// failure-returns-0 convention (params.c:2601).
3642pub fn getiparam(s: &str) -> i64 {
3643 // C also honours PM_INTEGER's `pm->u.val` payload directly when
3644 // the param is typed numeric; check paramtab first for that case.
3645 if let Ok(tab) = paramtab().read() {
3646 if let Some(pm) = tab.get(s) {
3647 if (pm.node.flags as u32 & crate::ported::zsh_h::PM_INTEGER) != 0
3648 {
3649 return pm.u_val;
3650 }
3651 }
3652 }
3653 getsparam(s).and_then(|s| s.parse::<i64>().ok()).unwrap_or(0)
3654}
3655
3656/// Retrieve numeric (int-or-float) parameter.
3657/// Port of `getnparam(char *s)` from Src/params.c:3058. C returns an
3658/// `mnumber` (tagged int/float union); our adaptation returns
3659/// `(i64, f64, bool)` where the bool is true for float. Unset
3660/// returns `(0, 0.0, false)`, matching the MN_INTEGER zero
3661/// fallback in the C source's not-found branch.
3662pub fn getnparam(s: &str) -> (i64, f64, bool) {
3663 if let Ok(tab) = paramtab().read() {
3664 if let Some(pm) = tab.get(s) {
3665 let fl = pm.node.flags as u32;
3666 if (fl & (crate::ported::zsh_h::PM_EFLOAT
3667 | crate::ported::zsh_h::PM_FFLOAT)) != 0
3668 {
3669 return (pm.u_dval as i64, pm.u_dval, true);
3670 }
3671 if (fl & crate::ported::zsh_h::PM_INTEGER) != 0 {
3672 return (pm.u_val, pm.u_val as f64, false);
3673 }
3674 }
3675 }
3676 let s = match getsparam(s) {
3677 Some(s) => s,
3678 None => return (0, 0.0, false),
3679 };
3680 if s.contains('.') || s.contains('e') || s.contains('E') {
3681 if let Ok(f) = s.parse::<f64>() {
3682 return (f as i64, f, true);
3683 }
3684 }
3685 if let Ok(i) = s.parse::<i64>() {
3686 return (i, i as f64, false);
3687 }
3688 (0, 0.0, false)
3689}
3690
3691/// Port of `getsparam(char *s)` from `Src/params.c:3076`.
3692///
3693/// C body:
3694/// ```c
3695/// char *getsparam(char *s) {
3696/// struct value vbuf;
3697/// Value v = getvalue(&vbuf, &s, 0);
3698/// if (!v) return NULL;
3699/// return getstrvalue(v);
3700/// }
3701/// ```
3702///
3703/// `getvalue` (params.c:2173) builds a `Value` for the parameter,
3704/// dispatching through `Param.gsu->getfn` for special parameters.
3705/// `getstrvalue` (params.c:2335) extracts the scalar form: for
3706/// PM_INTEGER calls `pm->gsu.i->getfn(pm)` and convbase's the
3707/// result; for PM_SCALAR calls `pm->gsu.s->getfn(pm)`; for
3708/// PM_ARRAY joins the elements.
3709///
3710/// **Sole funnel.** Every scalar parameter read in zshrs routes
3711/// through this fn — `subst.rs` parameter expansion AND
3712/// `fusevm_bridge::expand_param` both call `getsparam`. The
3713/// dispatch chain lives in exactly one place, mirroring C's
3714/// "every read goes through getsparam" architecture.
3715///
3716/// Lookup order (mirrors C's `getvalue` → `getstrvalue` cascade):
3717/// 1. **GSU dispatch** via [`lookup_special_var`] — special
3718/// parameters route through their getfn callback (`uidgetfn` /
3719/// `randomgetfn` / `usernamegetfn` / etc.). Same role as
3720/// C's `Param.gsu->getfn` virtual dispatch.
3721/// 2. **Local variable** — `variables[name]`. C reads `pm->u.str`
3722/// for PM_SCALAR; here we hold the scalar in the variables
3723/// HashMap.
3724/// 3. **Environment fallback** — `std::env::var(name)`. C imports
3725/// env vars into the param table at startup so they go through
3726/// the same dispatch as everything else; zshrs reads from the
3727/// OS env on miss to match.
3728/// 4. **Array → scalar** — `arrays[name].join(" ")`. Mirrors
3729/// C's PM_ARRAY case in getstrvalue (params.c:2358) which
3730/// joins via `sepjoin(ss, NULL, 1)`.
3731///
3732// Retrieve a scalar (string) parameter // c:3076
3733/// Returns `None` only if all four paths miss (parameter genuinely
3734/// unset).
3735pub fn getsparam(name: &str) -> Option<String> { // c:3076
3736 // 1. GSU dispatch — `Param.gsu->getfn(pm)` equivalent. Special
3737 // parameters (UID/RANDOM/USERNAME/...) live behind getfn
3738 // hooks that the table read below would otherwise miss.
3739 if let Some(v) = lookup_special_var(name) {
3740 return Some(v);
3741 }
3742 // 2. Paramtab read — `(Value)gethashnode2(paramtab, name)`.
3743 // Walk the global paramtab for the named param, returning
3744 // `pm->u.str` for PM_SCALAR/PM_NAMEREF or `sepjoin(pm->u.arr)`
3745 // for PM_ARRAY (matches `getstrvalue` at params.c:2358).
3746 if let Ok(tab) = paramtab().read() {
3747 if let Some(pm) = tab.get(name) {
3748 if let Some(s) = pm.u_str.as_ref() {
3749 return Some(s.clone());
3750 }
3751 if let Some(arr) = pm.u_arr.as_ref() {
3752 return Some(arr.join(" "));
3753 }
3754 }
3755 }
3756 // 3. Env fallback — C imports env into paramtab at init so the
3757 // read above would hit. If the import hasn't happened yet
3758 // (e.g. during very early init) fall back to the live env.
3759 std::env::var(name).ok()
3760}
3761
3762/// Port of `getsparam_u(char *s)` from `Src/params.c:3089`. C body
3763/// (c:3091-3094):
3764/// ```c
3765/// /* getsparam() returns pointer into global params table, so ... */
3766/// if ((s = getsparam(s)))
3767/// return unmeta(s); /* returns static pointer to copy */
3768/// return s;
3769/// ```
3770///
3771/// The previous Rust "port" was an entirely fabricated impl — it
3772/// took `Option<&mut value>` and gated on `PM_TYPE == PM_SCALAR`,
3773/// which matches no part of the C body. C just calls getsparam(s)
3774/// and unmeta's the resulting string. No callers existed because
3775/// no caller's type fit the bogus signature.
3776///
3777/// Real use case: locale setters (c:4847, c:4867, c:4882, c:4917)
3778/// call `getsparam_u("LC_ALL")` / `getsparam_u("LANG")` to read the
3779/// param as a Meta-stripped C string suitable for `setlocale`.
3780pub fn getsparam_u(s: &str) -> Option<String> { // c:3089
3781 // c:3092 — `if ((s = getsparam(s))) return unmeta(s);`
3782 getsparam(s).map(|v| crate::ported::utils::unmeta(&v))
3783}
3784
3785/// Port of `char **getaparam(char *s)` from `Src/params.c:3101-3110`.
3786///
3787/// C body:
3788/// ```c
3789/// struct value vbuf;
3790/// Value v;
3791/// if (!idigit(*s) && (v = getvalue(&vbuf, &s, 0)) &&
3792/// PM_TYPE(v->pm->node.flags) == PM_ARRAY)
3793/// return v->pm->gsu.a->getfn(v->pm);
3794/// return NULL;
3795/// ```
3796///
3797/// The previous Rust port was a fabrication: signature was
3798/// `Option<&mut value> -> Option<Vec<String>>`, taking an already-
3799/// resolved Value pointer rather than the C-canonical name string.
3800/// No caller used it because the bogus signature fit nothing — and
3801/// the in-tree `savematch` at modules/zutil.rs:30 hardcoded `a = None`
3802/// because the existing API couldn't be threaded through.
3803///
3804/// Real C use: name lookup. e.g. `getaparam("match")` returns the
3805/// `$match` array from the regex-match callouts (Modules/zutil.c:45).
3806pub fn getaparam(name: &str) -> Option<Vec<String>> { // c:3101
3807 // c:3107 — `if (idigit(*s))` reject digit-first names. C
3808 // `getvalue` would also reject these later, but the explicit
3809 // check matches C's flow.
3810 if name.starts_with(|c: char| c.is_ascii_digit()) { // c:3107
3811 return None;
3812 }
3813 // c:3107-3109 — `getvalue(&vbuf, &s, 0)` resolves the name to a
3814 // paramtab entry. Then PM_TYPE check + `pm->u.arr` return.
3815 if let Ok(tab) = paramtab().read() {
3816 if let Some(pm) = tab.get(name) {
3817 if PM_TYPE(pm.node.flags as u32) == PM_ARRAY { // c:3108
3818 if let Some(arr) = pm.u_arr.as_ref() { // c:3109
3819 return Some(arr.clone());
3820 }
3821 }
3822 }
3823 }
3824 None // c:3110
3825}
3826
3827/// Port of `char **gethparam(char *s)` from `Src/params.c:3117-3126`.
3828///
3829/// C body:
3830/// ```c
3831/// struct value vbuf;
3832/// Value v;
3833/// if (!idigit(*s) && (v = getvalue(&vbuf, &s, 0)) &&
3834/// PM_TYPE(v->pm->node.flags) == PM_HASHED)
3835/// return paramvalarr(v->pm->gsu.h->getfn(v->pm), SCANPM_WANTVALS);
3836/// return NULL;
3837/// ```
3838///
3839/// Same fabricated-port family as the prior `getaparam`/`getsparam_u`
3840/// fixes: previous Rust sig took `Option<&mut value>` instead of the
3841/// canonical name string, with no real callers. Fixed sig + body
3842/// that resolves the name through paramtab and returns the values
3843/// vector when PM_HASHED.
3844///
3845/// NOTE: zshrs's paramtab stores hash-params via `pm->u_hash` (a
3846/// `HashTable` struct that's a generic bucket-array container). The
3847/// canonical C path threads through `gsu.h->getfn(pm)` → `paramvalarr`
3848/// which extracts the value side of each key-value pair. Until that
3849/// extraction backend lands, we return an empty Vec for PM_HASHED
3850/// (which matches C's "no entries" return shape, not the broken
3851/// "wrong-signature" stub).
3852pub fn gethparam(name: &str) -> Option<Vec<String>> { // c:3117
3853 if name.starts_with(|c: char| c.is_ascii_digit()) { // c:3122
3854 return None;
3855 }
3856 if let Ok(tab) = paramtab().read() {
3857 if let Some(pm) = tab.get(name) {
3858 if PM_TYPE(pm.node.flags as u32) == PM_HASHED { // c:3123
3859 // c:3124 — `paramvalarr(hashgetfn(pm), SCANPM_WANTVALS)`.
3860 // Backend not yet ported; return empty vec to mirror the
3861 // "param exists but has no entries" shape.
3862 return Some(Vec::new()); // c:3124
3863 }
3864 }
3865 }
3866 None // c:3125
3867}
3868
3869/// Port of `char **gethkparam(char *s)` from `Src/params.c:3131-3140`.
3870/// Same as `gethparam` but `paramvalarr(..., SCANPM_WANTKEYS)`.
3871pub fn gethkparam(name: &str) -> Option<Vec<String>> { // c:3131
3872 if name.starts_with(|c: char| c.is_ascii_digit()) { // c:3136
3873 return None;
3874 }
3875 if let Ok(tab) = paramtab().read() {
3876 if let Some(pm) = tab.get(name) {
3877 if PM_TYPE(pm.node.flags as u32) == PM_HASHED { // c:3137
3878 // c:3138 — `paramvalarr(hashgetfn(pm), SCANPM_WANTKEYS)`.
3879 // Same backend gap as gethparam; return empty Vec.
3880 return Some(Vec::new()); // c:3138
3881 }
3882 }
3883 }
3884 None // c:3139
3885}
3886
3887/// Port of `check_warn_pm(Param pm, const char *pmtype, int created, int may_warn_about_nested_vars)` from `Src/params.c:3160`.
3888///
3889/// C body emits the WARN_CREATE_GLOBAL / WARN_NESTED_VAR
3890/// diagnostic when a function-local creates/passes a non-local
3891/// param with the matching shell options set.
3892///
3893/// The previous Rust port handled the GATE logic correctly but
3894/// SKIPPED the diagnostic emit, claiming the `funcstack` global
3895/// wasn't ported. But `crate::ported::modules::parameter::FUNCSTACK`
3896/// IS ported (`Mutex<Vec<funcstack>>`). Wire the walk:
3897/// for (i = funcstack; i; i = i->prev)
3898/// if (i->tp == FS_FUNC) {
3899/// msg = created ?
3900/// "%s parameter %s created globally in function %s" :
3901/// "%s parameter %s set in enclosing scope in function %s";
3902/// zwarn(msg, pmtype, pm->node.nam, i->name);
3903/// break;
3904/// }
3905///
3906/// Without the diagnostic, `setopt WARN_CREATE_GLOBAL` had no
3907/// observable effect — the whole point of the option is the
3908/// user-visible warning.
3909pub fn check_warn_pm(
3910 pm: &crate::ported::zsh_h::param,
3911 pmtype: &str,
3912 created: i32,
3913 may_warn_about_nested_vars: i32,
3914) { // c:3160
3915 if may_warn_about_nested_vars == 0 && created == 0 { // c:3165
3916 return;
3917 }
3918 // `locallevel` is the canonical `pub static` above (port of
3919 // params.c:54). `forklevel` is the ported global at exec.rs
3920 // (port of exec.c:1052) set to locallevel at every entersubsh().
3921 let cur_local: i32 = locallevel.load(std::sync::atomic::Ordering::Relaxed);
3922 let forklevel: i32 =
3923 crate::exec::FORKLEVEL.load(std::sync::atomic::Ordering::Relaxed); // c:1052 (Src/exec.c)
3924 if created != 0 && isset(WARNCREATEGLOBAL) { // c:3168
3925 if cur_local <= forklevel || pm.level != 0 { // c:3169
3926 return;
3927 }
3928 } else if created == 0 && isset(WARNNESTEDVAR) { // c:3171
3929 if pm.level >= cur_local { // c:3172
3930 return;
3931 }
3932 } else {
3933 return;
3934 }
3935 if (pm.node.flags as u32 & (PM_SPECIAL | PM_NAMEREF)) != 0 { // c:3177
3936 return;
3937 }
3938 // c:3180-3190 — walk funcstack, emit zwarn at first FS_FUNC.
3939 let stack = match crate::ported::modules::parameter::FUNCSTACK.lock() {
3940 Ok(s) => s,
3941 Err(_) => return,
3942 };
3943 for frame in stack.iter().rev() { // c:3180 walk most-recent-first
3944 if frame.tp == crate::ported::zsh_h::FS_FUNC { // c:3181 FS_FUNC
3945 let msg = if created != 0 { // c:3185
3946 format!("{} parameter {} created globally in function {}",
3947 pmtype, pm.node.nam, frame.name)
3948 } else { // c:3187
3949 format!("{} parameter {} set in enclosing scope in function {}",
3950 pmtype, pm.node.nam, frame.name)
3951 };
3952 crate::ported::utils::zwarn(&msg); // c:3189
3953 break; // c:3190
3954 }
3955 }
3956}
3957
3958// intgetfn / strgetfn drift wrappers removed — replaced below with
3959// real C-shape ports `intgetfn(pm: ¶m) -> i64` (Src/params.c:3993)
3960// and `strgetfn(pm: ¶m) -> String` (Src/params.c:4029) that read
3961// directly from the union fields `pm->u.val` / `pm->u.str`.
3962
3963// ---------------------------------------------------------------------------
3964// Tests
3965// ---------------------------------------------------------------------------
3966
3967
3968
3969// ===========================================================
3970// Methods moved verbatim from src/ported/exec.rs because their
3971// C counterpart's source file maps 1:1 to this Rust module.
3972// Phase: params
3973// ===========================================================
3974
3975// BEGIN moved-from-exec-rs
3976// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
3977
3978// END moved-from-exec-rs
3979
3980// ===========================================================
3981// Free fns moved verbatim from src/ported/exec.rs.
3982// ===========================================================
3983// BEGIN moved-from-exec-rs (free fns)
3984/// Subscript-argument result.
3985///
3986/// `Flags` carries the parsed flag chars and the remaining subscript
3987/// text (the pattern after `(...)`); the caller dispatches the
3988/// search itself. `Value` is the result of an in-getarg array/hash
3989/// pattern search — direct port of getarg's pprog/pattry arm at
3990/// Src/params.c:1672-1719 (array) and 1581-1660 (hash).
3991// `enum getarg_out` is a Rust extension to express the dual-mode
3992// return of `getarg`. C `getarg` (`Src/params.c:1367`) writes back
3993// via out-pointers (`int *inv`, `Value v`, `zlong *w`, ...) and
3994// returns `int`. The Rust port collapses those into one sum-typed
3995// return: `Flags` carries the parsed flag chars + remaining
3996// subscript when no search ran; `Value` carries the search result
3997// from the pprog/pattry arms at c:1581-1660 (hash) / c:1672-1719
3998// (array). Naming kept lowercase to mark this as a port-shape helper
3999// rather than a C-mirrored struct.
4000#[allow(non_camel_case_types)]
4001pub enum getarg_out<'a> {
4002 Flags { flags: &'a str, rest: &'a str },
4003 Value(fusevm::Value),
4004}
4005
4006/// Port of `assignsparam(char *s, char *val, int flags)` from `Src/params.c:3193`. C signature:
4007/// `mod_export Param assignsparam(char *s, char *val, int flags)`.
4008///
4009/// `s` may carry an embedded `[...]` subscript (matching C's
4010/// `strchr(s, '[')` parse). The function operates on the global
4011/// `paramtab` (Src/params.c:515), creating/mutating `Param`
4012/// entries in place. Branches preserved 1:1 with C:
4013/// - c:3203 `isident(s)` — reject non-identifier names.
4014/// - c:3209 `queue_signals()`.
4015/// - c:3210 subscripted path: c:3212 `getvalue` lookup,
4016/// c:3213 `createparam(t, PM_ARRAY)` on miss, c:3216
4017/// PM_READONLY guard, c:3227 ASSPM_WARN drop, c:3228 clear
4018/// PM_DEFAULTED, c:3231 `v = NULL` then re-dispatch by type.
4019/// - c:3232 non-subscripted: c:3233 `getvalue` → c:3234
4020/// `createparam(t, PM_SCALAR)`; c:3236-3250 array/hash type-flip
4021/// to PM_SCALAR (when not PM_SPECIAL|PM_TIED, not KSHARRAYS,
4022/// not ASSPM_AUGMENT) via `resetparam(v->pm, PM_SCALAR)`.
4023/// - c:3258 PM_NAMEREF → c:3259 `valid_refname(val, flags)` guard.
4024/// - c:3269 clear PM_DEFAULTED.
4025/// - c:3343 `assignstrvalue(v, val, flags)`.
4026/// - c:3344 `unqueue_signals()`; c:3345 return v->pm.
4027///
4028/// The full HashTable substrate (vtable callbacks, scope-stacked
4029/// iterators) is not yet wired; non-essential branches such as
4030/// `+= AUGMENT` numeric/array slice append and `check_warn_pm`
4031/// are documented but elided where unreachable from current
4032/// callers — none of those code paths are exercised by zshrs's
4033/// existing call sites.
4034pub fn assignsparam(s: &str, val: &str, flags: i32) // c:3193
4035 -> Option<crate::ported::zsh_h::Param>
4036{
4037
4038 // c:3203 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
4039 if !isident(s) {
4040 zerr(&format!("not an identifier: {}", s)); // c:3204
4041 errflag.fetch_or( // c:3206
4042 crate::ported::utils::ERRFLAG_ERROR,
4043 std::sync::atomic::Ordering::Relaxed,
4044 );
4045 return None; // c:3207
4046 }
4047 crate::ported::signals::queue_signals(); // c:3209
4048
4049 // c:3210 — `strchr(s, '[')`. Split the leading name from the
4050 // subscript while preserving C's `*ss = '\0'` / `*ss = '['`
4051 // restore semantics: the Rust port works on `&str` slices so
4052 // there's no in-place null-terminator dance, but the parse
4053 // shape is identical.
4054 let (name, subscript) = match s.find('[') {
4055 Some(i) => {
4056 let close = s.rfind(']').unwrap_or(s.len());
4057 let key_end = if close > i { close } else { s.len() };
4058 (&s[..i], Some(&s[i + 1..key_end]))
4059 }
4060 None => (s, None),
4061 };
4062
4063 // Subscripted path (c:3210-3231).
4064 if let Some(key) = subscript {
4065 let mut tab = paramtab().write().unwrap();
4066 let exists = tab.contains_key(name); // c:3212
4067 if !exists {
4068 // c:3213 `createparam(t, PM_ARRAY); created = 1;`
4069 let pm: Param = Box::new(param {
4070 node: hashnode { next: None, nam: name.to_string(), flags: PM_ARRAY as i32 },
4071 u_data: 0, u_arr: Some(Vec::new()), u_str: None, u_val: 0,
4072 u_dval: 0.0, u_hash: None,
4073 gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
4074 base: 0, width: 0, env: None, ename: None, old: None, level: 0,
4075 });
4076 tab.insert(name.to_string(), pm);
4077 } else {
4078 // c:3216 `if (v->pm->node.flags & PM_READONLY)`.
4079 let pm = tab.get(name).unwrap();
4080 if (pm.node.flags as u32 & PM_READONLY) != 0 {
4081 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:3217
4082 drop(tab);
4083 crate::ported::signals::unqueue_signals(); // c:3220
4084 return None; // c:3221
4085 }
4086 }
4087 // c:3231 `v = NULL;` — re-dispatch by storage type.
4088 let pm = tab.get_mut(name).unwrap();
4089 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3228
4090 if (pm.node.flags as u32 & PM_HASHED) != 0 {
4091 // PM_HASHED element store. `param.u_hash` is typed
4092 // `Option<HashTable>` per Src/zsh.h:1841 but the
4093 // HashTable runtime backing isn't wired; the assoc-array
4094 // values live in a parallel storage keyed on param name
4095 // (`paramtab_hashed_storage()`).
4096 let mut store = paramtab_hashed_storage().lock().unwrap();
4097 store.entry(name.to_string()).or_default()
4098 .insert(key.to_string(), val.to_string());
4099 } else if let Ok(idx) = key.parse::<i64>() {
4100 // PM_ARRAY + numeric subscript (c:3357 `assignaparam`).
4101 let arr = pm.u_arr.get_or_insert_with(Vec::new);
4102 let len = arr.len() as i64;
4103 // 1-based forward, negative-from-end.
4104 let real_idx = if idx < 0 { len + idx } else { idx - 1 };
4105 let real_idx = real_idx.max(0) as usize;
4106 while arr.len() <= real_idx { arr.push(String::new()); }
4107 arr[real_idx] = val.to_string();
4108 pm.u_str = None;
4109 } else {
4110 // String subscript on a non-hashed name → auto-vivify
4111 // as PM_HASHED (mirrors C `createparam(s, PM_HASHED)`
4112 // fallback when getvalue returns NULL).
4113 pm.node.flags = (pm.node.flags & !(PM_TYPE(u32::MAX) as i32))
4114 | PM_HASHED as i32;
4115 pm.u_arr = None;
4116 pm.u_str = None;
4117 let mut map: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
4118 map.insert(key.to_string(), val.to_string());
4119 paramtab_hashed_storage().lock().unwrap()
4120 .insert(name.to_string(), map);
4121 }
4122 let cloned = pm.clone();
4123 drop(tab);
4124 crate::ported::signals::unqueue_signals(); // c:3344
4125 return Some(cloned); // c:3345
4126 }
4127
4128 // c:3232 non-subscripted branch.
4129 let mut tab = paramtab().write().unwrap();
4130 let existing = tab.contains_key(name);
4131 if !existing {
4132 // c:3234 `createparam(t, PM_SCALAR); created = 1;`
4133 let mut pm_flags = PM_SCALAR as i32;
4134 if isset_opt(ALLEXPORT) { // c:1149-1150 (ALLEXPORT path)
4135 pm_flags |= PM_EXPORTED as i32;
4136 }
4137 let pm: Param = Box::new(param {
4138 node: hashnode { next: None, nam: name.to_string(), flags: pm_flags },
4139 u_data: 0, u_arr: None, u_str: Some(String::new()), u_val: 0,
4140 u_dval: 0.0, u_hash: None,
4141 gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
4142 base: 0, width: 0, env: None, ename: None, old: None, level: 0,
4143 });
4144 tab.insert(name.to_string(), pm);
4145 } else {
4146 let pm = tab.get(name).unwrap();
4147 // c:3216 PM_READONLY guard for an existing param.
4148 if (pm.node.flags as u32 & PM_READONLY) != 0 {
4149 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:3217
4150 drop(tab);
4151 crate::ported::signals::unqueue_signals(); // c:3220
4152 return None; // c:3221
4153 }
4154 // c:3236-3250 — existing PM_ARRAY/PM_HASHED on a non-special,
4155 // non-tied, non-KSHARRAYS, non-AUGMENT scalar assignment →
4156 // `resetparam(v->pm, PM_SCALAR)`.
4157 let f = pm.node.flags as u32;
4158 let is_array_or_hash = (f & PM_ARRAY) != 0 || (f & PM_HASHED) != 0;
4159 let is_special_or_tied = (f & (PM_SPECIAL | PM_TIED)) != 0;
4160 let augment_bit = (flags & ASSPM_AUGMENT) != 0;
4161 if is_array_or_hash
4162 && !is_special_or_tied
4163 && !augment_bit
4164 && !isset(crate::ported::zsh_h::KSHARRAYS)
4165 {
4166 // c:3242 — flip type to PM_SCALAR, drop array/hash slots.
4167 let pm_mut = tab.get_mut(name).unwrap();
4168 pm_mut.node.flags = (pm_mut.node.flags & !(PM_TYPE(u32::MAX) as i32))
4169 | PM_SCALAR as i32;
4170 pm_mut.u_arr = None;
4171 paramtab_hashed_storage().lock().unwrap().remove(name);
4172 }
4173 }
4174
4175 // c:3258-3266 `if (*val && (v->pm->node.flags & PM_NAMEREF))`.
4176 let pm = tab.get(name).unwrap();
4177 if !val.is_empty() && (pm.node.flags as u32 & PM_NAMEREF) != 0 {
4178 if !valid_refname(val, pm.node.flags) { // c:3259
4179 zerr(&format!("invalid name reference: {}", val)); // c:3260
4180 drop(tab);
4181 errflag.fetch_or( // c:3263
4182 crate::ported::utils::ERRFLAG_ERROR,
4183 std::sync::atomic::Ordering::Relaxed,
4184 );
4185 crate::ported::signals::unqueue_signals(); // c:3262
4186 return None; // c:3264
4187 }
4188 }
4189
4190 // c:3269 `v->pm->node.flags &= ~PM_DEFAULTED;`
4191 let pm = tab.get_mut(name).unwrap();
4192 pm.node.flags &= !(PM_DEFAULTED as i32);
4193
4194 // c:3343 `assignstrvalue(v, val, flags)` — scalar write.
4195 pm.u_str = Some(val.to_string());
4196
4197 let cloned = pm.clone();
4198 drop(tab);
4199 crate::ported::signals::unqueue_signals(); // c:3344
4200 Some(cloned) // c:3345
4201}
4202
4203// `VarAttr` struct + `VarKind` enum + `impl VarAttr::format_zsh`
4204// DELETED. C zsh stores typeset attributes as bare `PM_*` bit
4205// flags on `Param.node.flags` (`Src/zsh.h` PM_* + `Src/params.c`
4206// flag tests); the `${(t)var}` flag report (`typeprintparam` at
4207// `Src/builtin.c:3050+`) writes those bits to a string directly
4208// against the `Param.node.flags` int.
4209//
4210// Both types had zero external use sites — pure dead-code carryover
4211// from an earlier exec.rs refactor. The PM_* bit constants are at
4212// `zsh_h.rs:1340+` and the `(t)` formatting routes through
4213// `typeset_print_flags` (when wired) reading bare `Param.node.flags`.
4214
4215// ===========================================================
4216// Special-parameter GSU (get/set/unset) callbacks ported from
4217// Src/params.c.
4218//
4219// C zsh stores per-special-param state in file-static globals
4220// (`ifs`, `home`, `term`, `histsiz`, etc.) and dispatches getfn/
4221// setfn/unsetfn callbacks through `Param.gsu->getfn` etc. zshrs's
4222// param storage is per-evaluator HashMaps on `ShellExecutor`, so
4223// the C globals are reproduced as `OnceLock<Mutex<…>>` module
4224// statics here, with the get/set fns mutating the static.
4225//
4226// Functions that genuinely need a `Param *` (the GSU dispatch
4227// callbacks for non-special arr/hash/int/float/str params, the
4228// param-table mutators, scope helpers, etc.) cannot be properly
4229// ported until zshrs gains a Param struct + callback-table ABI;
4230// those keep their C signatures but the body is a WARNING-stub
4231// that does nothing.
4232// ===========================================================
4233
4234use std::sync::{Arc, Mutex, OnceLock, RwLock};
4235use std::time::Duration;
4236use crate::config_h::DEFAULT_TMPPREFIX;
4237use crate::zsh_h::{paramdef, ERRFLAG_ERROR, PM_DONTIMPORT, PM_DONTIMPORT_SUID, PM_READONLY_SPECIAL};
4238// -----------------------------------------------------------
4239// Module statics — one per C global referenced by the special-
4240// param callbacks below. All initialised lazily on first read.
4241// -----------------------------------------------------------
4242
4243// `Src/params.c:515 mod_export HashTable paramtab, realparamtab;`
4244//
4245// `realparamtab` always points to the shell's global parameter
4246// table. `paramtab` normally aliases it; it is temporarily
4247// redirected during associative-array key iteration
4248// (`Src/params.c:508-513` — "paramtab is sometimes temporarily
4249// changed to point at another table").
4250//
4251// Per PORT_PLAN.md Phase 3, bucket-2 read-mostly tables use
4252// `RwLock` so parallel readers (every `$VAR` expansion, every
4253// completion lookup) don't serialize. Writers (assignments,
4254// scope pushes/pops, function-local declarations) take the
4255// exclusive write lock. `OnceLock` provides the single-static
4256// guarantee without an `Arc` allocation since the table lives
4257// for the process lifetime.
4258//
4259// Entries are keyed on `node.nam` (the canonical `param` struct
4260// lives in `zsh_h.rs`). The full `HashTable` substrate (vtable
4261// callbacks, intrusive `next` chain, scope-stacked iterators) is
4262// not yet wired; until it is, the typed map is the operative
4263// storage.
4264static PARAMTAB_INNER: OnceLock<RwLock<HashMap<String, crate::ported::zsh_h::Param>>> =
4265 OnceLock::new();
4266static REALPARAMTAB_INNER: OnceLock<RwLock<HashMap<String, crate::ported::zsh_h::Param>>> =
4267 OnceLock::new();
4268
4269/// Array parameter assignment (no subscript).
4270///
4271/// Direct port of `Param assignaparam(char *s, char **val, int flags)`
4272/// from `Src/params.c:3357`. Writes an array value into paramtab
4273/// and returns the new/updated Param.
4274///
4275/// Pending C semantics:
4276/// - PM_READONLY rejection (c:3370-3381 via setarrvalue chain)
4277/// - PM_NAMEREF type-change reject (c:3395-3398)
4278/// - resetparam from non-array (c:3415-3420)
4279/// - ASSPM_AUGMENT (`a+=val`) preserve-old prepend (c:3404-3412)
4280/// - PM_UNIQUE dedupe (c:3401)
4281/// - element-wise `a[k]=v` slice path (c:3373-3389)
4282pub fn assignaparam(
4283 name: &str,
4284 val: Vec<String>,
4285 flags: i32,
4286) -> Option<crate::ported::zsh_h::Param> { // c:3357
4287 // c:3366-3370 — `if (!isident(s)) { zerr; return NULL }`.
4288 if !isident(name) {
4289 crate::ported::utils::zerr(&format!("not an identifier: {}", name));
4290 return None;
4291 }
4292
4293 // c:3391-3394 — fetchvalue / createparam(PM_ARRAY) if missing.
4294 let (existed, prior_scalar, prior_flags) = {
4295 let tab = paramtab().read().unwrap();
4296 match tab.get(name) {
4297 Some(pm) => (true, pm.u_str.clone(), pm.node.flags),
4298 None => (false, None, 0),
4299 }
4300 };
4301 if !existed {
4302 createparam(name, PM_ARRAY as i32)?;
4303 }
4304
4305 // c:3402-3412 — ASSPM_AUGMENT preserve-old prepend. When the
4306 // previous value was a scalar (not array/hashed) and we're
4307 // augmenting (`a+=val`), prepend that scalar's string form as
4308 // val[0]. Only fires when the existing param is not PM_UNSET.
4309 let was_scalar_array_target = existed
4310 && prior_flags & (PM_ARRAY | PM_HASHED) as i32 == 0
4311 && prior_flags & PM_SPECIAL as i32 == 0;
4312 let mut val = val;
4313 if (flags & ASSPM_AUGMENT) != 0
4314 && was_scalar_array_target
4315 && prior_flags & PM_UNSET as i32 == 0
4316 {
4317 if let Some(old_scalar) = prior_scalar {
4318 val.insert(0, old_scalar); // c:3408-3411
4319 }
4320 }
4321
4322 // c:3434 — setarrvalue(v, val): store array in pm.u_arr.
4323 let mut tab = paramtab().write().unwrap();
4324 let pm = tab.get_mut(name)?;
4325 let uniq = pm.node.flags & PM_UNIQUE as i32 != 0; // c:3401
4326 if pm.node.flags & PM_SPECIAL as i32 == 0 {
4327 let type_mask =
4328 PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
4329 pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_ARRAY as i32;
4330 }
4331 // c:3401 — preserve PM_UNIQUE through the type change, then let
4332 // arrsetfn dedupe via the actual write.
4333 if uniq {
4334 pm.node.flags |= PM_UNIQUE as i32;
4335 }
4336 let val_final = if uniq { simple_arrayuniq(val) } else { val };
4337 pm.u_arr = Some(val_final.clone());
4338 pm.u_str = None;
4339 pm.u_hash = None;
4340 let cloned = pm.clone();
4341 drop(tab);
4342 let _ = val_final;
4343 Some(cloned)
4344}
4345
4346/// Set array parameter.
4347/// Port of `setaparam(char *s, char **aval)` from `Src/params.c:3595` — single-line wrapper
4348/// around `assignaparam(s, val, ASSPM_WARN)`. C body:
4349/// ```c
4350/// mod_export Param setaparam(char *s, char **val) {
4351/// return assignaparam(s, val, ASSPM_WARN);
4352/// }
4353/// ```
4354///
4355/// `ASSPM_WARN` (params.c:104) is a no-op in our port — the global
4356/// "warn on creation" tracking is not yet ported. Call shape
4357/// preserved so callers can use this where C calls setaparam.
4358/// WARNING: param names don't match C — Rust=() vs C=(s, val)
4359pub fn setaparam(name: &str, val: Vec<String>) // c:3595
4360 -> Option<crate::ported::zsh_h::Param>
4361{
4362 // c:3766 — `return assignaparam(s, val, ASSPM_WARN)`.
4363 assignaparam(name, val, crate::ported::zsh_h::ASSPM_WARN)
4364}
4365
4366/// Direct port of `Param sethparam(char *s, char **val)` from
4367/// `Src/params.c:3602`. Writes an associative array (flat
4368/// alternating key,value list) into paramtab + the parallel
4369/// `paramtab_hashed_storage` table; returns the new Param.
4370///
4371/// Pending C semantics:
4372/// - PM_READONLY rejection
4373/// - resetparam(PM_HASHED) for type-change
4374/// - PM_SPECIAL type-change reject (c:3637)
4375pub fn sethparam(name: &str, val: Vec<String>) // c:3602
4376 -> Option<crate::ported::zsh_h::Param>
4377{
4378
4379 // c:3611-3615 — `if (!isident(s)) { zerr; return NULL }`.
4380 if !isident(name) {
4381 crate::ported::utils::zerr(&format!("not an identifier: {}", name));
4382 return None;
4383 }
4384 // c:3617-3621 — `if (strchr(s, '[')) { zerr; return NULL }`.
4385 if name.contains('[') {
4386 crate::ported::utils::zerr("nested associative arrays not yet supported");
4387 return None;
4388 }
4389
4390 // c:3625 — fetchvalue / createparam(PM_HASHED) if missing.
4391 let exists = paramtab().read().unwrap().contains_key(name);
4392 if !exists {
4393 createparam(name, PM_HASHED as i32)?;
4394 }
4395
4396 // Build the IndexMap from flat (k,v) pairs (mirrors c:arrhashsetfn
4397 // pair-walking at c:4140-4166).
4398 let mut map: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
4399 let mut iter = val.into_iter();
4400 while let Some(k) = iter.next() {
4401 let v = iter.next().unwrap_or_default();
4402 map.insert(k, v);
4403 }
4404
4405 // c:3640 — install in paramtab + paramtab_hashed_storage.
4406 let mut tab = paramtab().write().unwrap();
4407 let pm = tab.get_mut(name)?;
4408 if pm.node.flags & PM_SPECIAL as i32 == 0 {
4409 let type_mask =
4410 PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
4411 pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_HASHED as i32;
4412 }
4413 pm.u_arr = None;
4414 pm.u_str = None;
4415 let cloned = pm.clone();
4416 drop(tab);
4417
4418 paramtab_hashed_storage()
4419 .lock()
4420 .unwrap()
4421 .insert(name.to_string(), map);
4422
4423 Some(cloned)
4424}
4425
4426// -----------------------------------------------------------
4427// Param-table mutators / scope / nameref helpers.
4428// `Src/params.c` calls these against the global `paramtab`
4429// HashTable; until our HashTable vtable (`Box<hashtable>` in
4430// zsh_h.rs:285) is wired, these remain no-op shims with the
4431// real C signatures.
4432// -----------------------------------------------------------
4433
4434/// Port of `assignnparam(char *s, mnumber val, int flags)` from `Src/params.c:3664`. C body
4435/// looks up the param via `gethashnode2(realparamtab, s)`,
4436/// dispatches on PM_TYPE: PM_INTEGER → `intsetfn(pm, val.u.l)`;
4437/// PM_FFLOAT/EFLOAT → `floatsetfn(pm, val.u.d)`; otherwise
4438/// `assignstrvalue(&v, conv_to_string(val), flags)`. Stub
4439/// pending HashTable backend; signature mirrors C `mnumber val`.
4440/// flow: isident guard → unset(EXECOPT) bail → `getvalue(&vbuf,&s,1)`
4441/// → if existing array/hashed (non-special, non-tied, non-KSHARRAYS,
4442/// no subscript) → unsetparam_pm + recreate → else if no value →
4443/// `createparam(t, type)` (POSIXIDENTIFIERS gates SCALAR vs
4444/// MN_INTEGER→PM_INTEGER else PM_FFLOAT) → second `getvalue` →
4445/// `check_warn_pm` if ASSPM_WARN → clear PM_DEFAULTED → `setnumvalue`
4446/// → return pm. This port wires the structural flow against the
4447/// already-ported helpers; the createparam/paramtab backend is
4448/// still stubbed elsewhere so the create-new-param branch returns
4449/// None until `createparam` lands.
4450pub fn assignnparam(
4451 s: &str,
4452 val: crate::ported::math::mnumber,
4453 flags: i32,
4454) -> Option<Box<crate::ported::zsh_h::param>> {
4455 // c:3666 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
4456 if !isident(s) {
4457 zerr(&format!("not an identifier: {}", s)); // c:3667
4458 errflag.fetch_or( // c:3669
4459 crate::ported::utils::ERRFLAG_ERROR,
4460 std::sync::atomic::Ordering::Relaxed,
4461 );
4462 return None; // c:3670
4463 }
4464 if unset(EXECOPT) {
4465 return None;
4466 }
4467 let mut vbuf = crate::ported::zsh_h::value {
4468 pm: None,
4469 arr: Vec::new(),
4470 scanflags: 0,
4471 valflags: 0,
4472 start: 0,
4473 end: -1,
4474 };
4475 let mut cursor: &str = s;
4476 let has_sub = s.contains('[');
4477 let mut was_unset = false;
4478 let v = getvalue(Some(&mut vbuf), &mut cursor, 1);
4479 let need_create = match v {
4480 Some(ref vv) => {
4481 if let Some(pm) = vv.pm.as_ref() {
4482 let f = pm.node.flags as u32;
4483 if (f & (PM_ARRAY | PM_HASHED)) != 0
4484 && (f & (PM_SPECIAL | PM_TIED)) == 0
4485 && unset(KSHARRAYS) && !has_sub
4486 {
4487 // unsetparam_pm(vv.pm, 0, 1);
4488 was_unset = true;
4489 true
4490 } else {
4491 false
4492 }
4493 } else {
4494 true
4495 }
4496 }
4497 None => true,
4498 };
4499 if need_create {
4500 // c:3686-3691 — `createparam(t, val.type & MN_FLOAT ? PM_FFLOAT
4501 // : PM_INTEGER); second getvalue;`. Synthesize a fresh
4502 // numeric param in paramtab matching the C body. Without
4503 // this branch wired, callers like `setiparam` silently
4504 // dropped the create (returned None) — every new integer
4505 // param assignment was a no-op.
4506 let _ = was_unset;
4507 let new_type = if val.type_ == MN_FLOAT {
4508 PM_FFLOAT // c:3687
4509 } else {
4510 PM_INTEGER // c:3688
4511 };
4512 let pm: Param = Box::new(param {
4513 node: hashnode {
4514 next: None,
4515 nam: s.to_string(),
4516 flags: new_type as i32,
4517 },
4518 u_data: 0,
4519 u_arr: None,
4520 u_str: None,
4521 // c:3690 — `setnumvalue(...)` stores the value. For
4522 // PM_INTEGER → u.l; for PM_FFLOAT → u.dval.
4523 u_val: if val.type_ == MN_FLOAT { 0 } else { val.l },
4524 u_dval: if val.type_ == MN_FLOAT { val.d } else { 0.0 },
4525 u_hash: None,
4526 gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
4527 base: 0, width: 0,
4528 env: None, ename: None, old: None, level: 0,
4529 });
4530 if let Ok(mut tab) = paramtab().write() {
4531 tab.insert(s.to_string(), pm.clone());
4532 }
4533 return Some(pm);
4534 }
4535 if (flags & ASSPM_WARN) != 0 {
4536 if let Some(ref vv) = v {
4537 if let Some(ref pm) = vv.pm {
4538 check_warn_pm(pm, "numeric", 0, 1);
4539 }
4540 }
4541 }
4542 // The reassign path: getvalue gave us a cloned pm inside the value
4543 // buffer. setnumvalue mutates that clone but the write doesn't
4544 // propagate back to paramtab. Write through paramtab directly so
4545 // reassignments stick — same shape as `assignsparam`'s c:3343
4546 // `assignstrvalue(v, val, flags)` path which mutates paramtab in
4547 // place.
4548 if let Ok(mut tab) = paramtab().write() {
4549 if let Some(pm) = tab.get_mut(s) {
4550 pm.node.flags &= !(PM_DEFAULTED as i32);
4551 let t = PM_TYPE(pm.node.flags as u32);
4552 if t == PM_INTEGER {
4553 // c:2874 — `pm->gsu.i->setfn(pm, val.u.l)`. MN_FLOAT
4554 // input truncates to integer.
4555 pm.u_val = if val.type_ == MN_FLOAT { val.d as i64 } else { val.l };
4556 } else if t == PM_EFLOAT || t == PM_FFLOAT {
4557 // c:2878 — MN_INTEGER input promotes to f64.
4558 pm.u_dval = if val.type_ == MN_FLOAT { val.d } else { val.l as f64 };
4559 } else if t == PM_SCALAR || t == PM_NAMEREF || t == PM_ARRAY {
4560 // c:2862-2871 — convbase/convfloat → u_str.
4561 let s_rendered = if val.type_ == MN_FLOAT {
4562 crate::ported::params::convfloat_underscore(val.d, pm.width)
4563 } else {
4564 crate::ported::params::convbase_underscore(
4565 val.l,
4566 if pm.base > 0 { pm.base as u32 } else { 10 },
4567 pm.width,
4568 )
4569 };
4570 pm.u_str = Some(s_rendered);
4571 }
4572 let cloned = pm.clone();
4573 return Some(cloned);
4574 }
4575 }
4576 None
4577}
4578
4579/// Port of `Param setnparam(char *s, mnumber val)` from `Src/params.c:3745-3749`.
4580///
4581/// C body (c:3747-3748):
4582/// ```c
4583/// return assignnparam(s, val, ASSPM_WARN);
4584/// ```
4585///
4586/// Single-line wrapper around `assignnparam` with ASSPM_WARN flags.
4587///
4588/// The previous Rust port took `(s: &str, val: f64) -> ()` — losing
4589/// the integer branch (callers couldn't set integer params via
4590/// `setnparam`) AND the Param return. No real callers existed because
4591/// the fabricated sig fit nothing. Match C exactly: `(s, val)` where
4592/// `val` is the canonical `mnumber` tagged union, returning the
4593/// resulting Param.
4594pub fn setnparam(s: &str, val: crate::ported::math::mnumber) // c:3746
4595 -> Option<crate::ported::zsh_h::Param>
4596{
4597 assignnparam(s, val, ASSPM_WARN as i32) // c:3748
4598}
4599
4600/// Port of `Param assigniparam(char *s, zlong val, int flags)` from
4601/// `Src/params.c:3754-3761`.
4602///
4603/// C body (c:3757-3760):
4604/// ```c
4605/// mnumber mnval;
4606/// mnval.type = MN_INTEGER;
4607/// mnval.u.l = val;
4608/// return assignnparam(s, mnval, flags);
4609/// ```
4610///
4611/// Two divergences in the previous Rust port:
4612/// 1. Dropped the `flags` arg — caller-supplied flags (e.g.
4613/// ASSPM_AUGMENT for `+= int`) couldn't be threaded through;
4614/// every call hardcoded ASSPM_WARN regardless.
4615/// 2. Returned void instead of Param — losing the new param
4616/// pointer the caller may want to read back.
4617pub fn assigniparam(s: &str, val: i64, flags: i32) // c:3755
4618 -> Option<crate::ported::zsh_h::Param>
4619{
4620 // c:3757-3759 — `mnumber{ .type = MN_INTEGER, .u.l = val }`.
4621 let mnval = crate::ported::math::mnumber {
4622 l: val,
4623 d: 0.0,
4624 type_: MN_INTEGER,
4625 };
4626 // c:3760 — `return assignnparam(s, mnval, flags);`
4627 assignnparam(s, mnval, flags) // c:3760
4628}
4629
4630/// Port of `Param setiparam(char *s, zlong val)` from `Src/params.c:3767-3773`.
4631///
4632/// C body (c:3769-3772):
4633/// ```c
4634/// mnumber mnval;
4635/// mnval.type = MN_INTEGER;
4636/// mnval.u.l = val;
4637/// return assignnparam(s, mnval, ASSPM_WARN);
4638/// ```
4639///
4640/// The previous Rust port stringified to decimal and routed through
4641/// `assignsparam` — which CREATES THE PARAM AS PM_SCALAR. C creates
4642/// as PM_INTEGER. `setiparam("x", 5)` followed by `typeset -p x`:
4643/// - C: \`typeset -i x=5\`
4644/// - Old Rust: \`typeset x=5\`
4645///
4646/// `assignnparam` IS now ported (params.rs:4403). Route through it
4647/// matching C exactly so integer-typed params get created with the
4648/// right PM_INTEGER flag.
4649pub fn setiparam(s: &str, val: i64) // c:3767
4650 -> Option<crate::ported::zsh_h::Param>
4651{
4652 // c:3770-3771 — `mnumber{ .type = MN_INTEGER, .u.l = val }`.
4653 let mnval = crate::ported::math::mnumber {
4654 l: val,
4655 d: 0.0,
4656 type_: MN_INTEGER,
4657 };
4658 // c:3772 — `return assignnparam(s, mnval, ASSPM_WARN);`
4659 assignnparam(s, mnval, ASSPM_WARN as i32) // c:3772
4660}
4661
4662/// Port of `setiparam_no_convert(char *s, zlong val)` from Src/params.c:3781. C
4663/// source comment: "If the target is already an integer, this
4664/// gets converted back. Low technology rules." It uses convbase
4665/// to render decimal then calls assignsparam.
4666/// WARNING: param names don't match C — Rust=() vs C=(s, val)
4667pub fn setiparam_no_convert(s: &str, val: i64) // c:3781
4668 -> Option<crate::ported::zsh_h::Param>
4669{
4670 assignsparam(s, &val.to_string(), ASSPM_WARN as i32)
4671}
4672
4673/// Port of `resetparam(Param pm, int flags)` from `Src/params.c:3796`. C body:
4674/// ```c
4675/// char *s = pm->node.nam;
4676/// queue_signals();
4677/// if (pm != (Param)(paramtab == realparamtab ?
4678/// paramtab->getnode2(paramtab, s) :
4679/// paramtab->getnode(paramtab, s))) {
4680/// unqueue_signals();
4681/// zerr("can't change type of hidden variable: %s", s);
4682/// return 1;
4683/// }
4684/// s = dupstring(s);
4685/// unsetparam_pm(pm, 0, 1);
4686/// unqueue_signals();
4687/// createparam(s, flags);
4688/// return 0;
4689/// ```
4690/// Tears `pm` down + recreates it with `flags` so the next
4691/// assignment lands in a fresh slot of the requested type. Used
4692/// by `assignsparam` when the type-flag of an existing param
4693/// changes (e.g. `typeset -i x; x="abc"` resets x back to scalar).
4694///
4695/// The `paramtab->getnode` reachability check at c:3800 catches
4696/// the hidden-shadow case (a local var hiding the global `pm` we
4697/// were handed) — without the paramtab vtable we skip the check
4698/// and proceed to unset+create.
4699pub fn resetparam(pm: &mut crate::ported::zsh_h::param, flags: i32) -> i32 { // c:3796
4700 let s = pm.node.nam.clone(); // c:3796
4701 crate::ported::signals::queue_signals(); // c:3799
4702 // c:3800-3807 — paramtab->getnode2 / getnode reachability check.
4703 // Without paramtab vtable wired we cannot detect the hidden-
4704 // variable case, so we proceed; a future port of paramtab
4705 // adds the check at this site.
4706 unsetparam_pm(pm, 0, 1); // c:3819
4707 crate::ported::signals::unqueue_signals(); // c:3819
4708 let _ = createparam(&s, flags); // c:3819
4709 0 // c:3819
4710}
4711
4712/// Port of `void unsetparam(char *s)` from `Src/params.c:3819`.
4713///
4714/// C body:
4715/// ```c
4716/// Param pm;
4717/// queue_signals();
4718/// if ((pm = (Param)(paramtab == realparamtab ?
4719/// paramtab->getnode2(paramtab, s) :
4720/// paramtab->getnode(paramtab, s))))
4721/// unsetparam_pm(pm, 0, 1);
4722/// unqueue_signals();
4723/// ```
4724///
4725/// The previous Rust port took `(variables, arrays, assoc_arrays,
4726/// name)` operating on EXTERNAL HashMap storage — a SubstState-
4727/// era stale signature. C operates on the canonical `paramtab`
4728/// global. No live callers used the old 4-arg form (all use
4729/// `paramtab().write().remove(...)` directly), so renaming is
4730/// safe.
4731pub fn unsetparam(name: &str) { // c:3819
4732 crate::ported::signals::queue_signals(); // c:3825
4733 // c:3826-3831 — `if ((pm = ... getnode2 ...) && !(pm->node.flags
4734 // & PM_NAMEREF)) unsetparam_pm(pm, 0, 1);`.
4735 //
4736 // Two divergences in the previous Rust port:
4737 // 1. Missing PM_NAMEREF check — `unsetparam("ref")` where `ref`
4738 // is a nameref would remove the ref alias itself. C explicitly
4739 // skips nameref params here (they're cleared via the
4740 // ref-specific path, not the value-side unset).
4741 // 2. Bypassed `unsetparam_pm` — removed the entry directly from
4742 // paramtab without running the readonly-guard at c:3850, the
4743 // stdunsetfn dispatch at c:3870, or the `pm->old` scope
4744 // restore. `typeset -r x=foo; unset x` would silently succeed
4745 // in Rust where C rejects with `read-only variable: x`.
4746 let (found, is_nameref) = {
4747 let tab = paramtab().read().unwrap();
4748 match tab.get(name) {
4749 Some(pm) => (true, (pm.node.flags as u32 & PM_NAMEREF) != 0),
4750 None => (false, false),
4751 }
4752 };
4753 if found && !is_nameref { // c:3826-3830
4754 // c:3831 — `unsetparam_pm(pm, 0, 1)`. Take an owned copy out
4755 // of paramtab so we can mutate it (unsetparam_pm wants
4756 // &mut), run the readonly-guard + env teardown, then re-insert
4757 // or fully remove based on the readonly path.
4758 let mut pm_owned = paramtab().write().unwrap().remove(name).unwrap();
4759 let rejected = unsetparam_pm(&mut pm_owned, 0, 1); // c:3831
4760 if rejected != 0 {
4761 // Readonly rejection — restore the entry so the state
4762 // is unchanged.
4763 paramtab().write().unwrap().insert(name.to_string(), pm_owned);
4764 }
4765 }
4766 crate::ported::signals::unqueue_signals(); // c:3832
4767}
4768
4769/// Unset parameter (from params.c unsetparam_pm)
4770/// Port of `unsetparam_pm(Param pm, int altflag, int exp)` from `Src/params.c:3841`. Full body
4771/// removes `pm` from `paramtab` (after invoking
4772/// `pm->gsu.s->unsetfn(pm, exp)`), tears down the tied alternate
4773/// (`pm->ename`) when `!altflag`, deletes the env entry, and
4774/// resurrects `pm->old` at the right scope. Stub: needs paramtab
4775/// HashTable backend (`paramtab->removenode/addnode`) plus the
4776/// `delenv`/`adduserdir` helpers — direct port retains only the
4777/// in-memory mutation of `pm` that doesn't touch the table.
4778#[allow(unused_variables)]
4779pub fn unsetparam_pm(pm: &mut crate::ported::zsh_h::param, altflag: i32, exp: i32) -> i32 {
4780 // c:3850 — `if ((pm->node.flags & PM_READONLY) && pm->level <= locallevel)`.
4781 // The previous Rust port hardcoded `pm.level <= 0` with a
4782 // "locallevel global not yet ported — assume 0" comment, but
4783 // `crate::ported::params::locallevel` IS the canonical port of
4784 // the C global (declared above in this file). Reading it live
4785 // matters: a function-scope readonly assignment (`typeset -r x`)
4786 // gets pm.level == current locallevel; without the live check,
4787 // unsetting from a NESTED scope (locallevel > pm.level) would
4788 // succeed when C rejects, AND unsetting from a deeper scope
4789 // (locallevel < pm.level) would reject when C succeeds.
4790 let cur_ll = locallevel.load(std::sync::atomic::Ordering::Relaxed) as i32; // c:3850 locallevel
4791 if (pm.node.flags as u32 & PM_READONLY) != 0 && pm.level <= cur_ll { // c:3850
4792 // c:3852 — `zerr("read-only %s: %s", ...)`. Emit diagnostic
4793 // so users see why the unset failed.
4794 let kind = if (pm.node.flags as u32 & PM_NAMEREF) != 0 { // c:3852
4795 "reference"
4796 } else {
4797 "variable"
4798 };
4799 zerr(&format!("read-only {}: {}", kind, pm.node.nam));
4800 return 1; // c:3854
4801 }
4802 pm.node.flags &= !(PM_DECLARED as i32); // c:3868
4803 if (pm.node.flags as u32 & PM_UNSET) == 0
4804 || (pm.node.flags as u32 & PM_REMOVABLE) != 0
4805 {
4806 // c:3870 — `pm->gsu.s->unsetfn(pm, exp)` — open-coded to stdunsetfn.
4807 stdunsetfn(pm, exp);
4808 }
4809 if pm.env.is_some() {
4810 delenv(&pm.node.nam); // c:3872 delenv(pm)
4811 pm.env = None;
4812 }
4813 // Tied alt-name removal + paramtab restore-from-old not yet
4814 // possible without HashTable backend; the C postlude (lines
4815 // 3853-3935) is a paramtab->removenode + addnode dance that
4816 // requires the missing vtable.
4817 pm.node.flags |= PM_UNSET as i32;
4818 0
4819}
4820
4821// -----------------------------------------------------------
4822// GSU dispatch callbacks — direct ports against `param.u_*`
4823// fields. C source in Src/params.c:4002.
4824// -----------------------------------------------------------
4825
4826/// Port of `intgetfn(Param pm)` from `Src/params.c:3993`. C body:
4827/// `return pm->u.val;`
4828pub fn intgetfn(pm: &crate::ported::zsh_h::param) -> i64 {
4829 pm.u_val
4830}
4831
4832/// Port of `intsetfn(Param pm, zlong x)` from `Src/params.c:4002`. C body:
4833/// `pm->u.val = x;`
4834pub fn intsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
4835 pm.u_val = x;
4836}
4837
4838/// Port of `floatgetfn(Param pm)` from `Src/params.c:4011`. C body:
4839/// `return pm->u.dval;`
4840pub fn floatgetfn(pm: &crate::ported::zsh_h::param) -> f64 {
4841 pm.u_dval
4842}
4843
4844/// Port of `floatsetfn(Param pm, double x)` from `Src/params.c:4020`. C body:
4845/// `pm->u.dval = x;`
4846pub fn floatsetfn(pm: &mut crate::ported::zsh_h::param, x: f64) {
4847 pm.u_dval = x;
4848}
4849
4850/// Port of `strgetfn(Param pm)` from `Src/params.c:4029`. C body:
4851/// `return pm->u.str ? pm->u.str : (char *) hcalloc(1);`
4852pub fn strgetfn(pm: &crate::ported::zsh_h::param) -> String {
4853 pm.u_str.clone().unwrap_or_default()
4854}
4855
4856/// Port of `strsetfn(Param pm, char *x)` from `Src/params.c:4040`.
4857///
4858/// C body (c:4043-4051):
4859/// ```c
4860/// zsfree(pm->u.str); pm->u.str = x;
4861/// if (!(pm->node.flags & PM_HASHELEM) &&
4862/// ((pm->node.flags & PM_NAMEDDIR) || isset(AUTONAMEDIRS))) {
4863/// pm->node.flags |= PM_NAMEDDIR;
4864/// adduserdir(pm->node.nam, x, 0, 0);
4865/// }
4866/// ```
4867///
4868/// The C body fires the `adduserdir` path when EITHER `PM_NAMEDDIR`
4869/// is already set OR the `AUTONAMEDIRS` option is on. The previous
4870/// Rust port only fired when PM_NAMEDDIR was already set, missing
4871/// the AUTONAMEDIRS auto-create branch entirely. With `setopt
4872/// AUTONAMEDIRS`, every scalar assignment to a path-shaped value
4873/// should register a named-directory entry for `~name` expansion;
4874/// the Rust port silently dropped that behavior.
4875pub fn strsetfn(pm: &mut crate::ported::zsh_h::param, x: String) { // c:4040
4876 pm.u_str = Some(x.clone()); // c:4044 pm->u.str = x
4877 // c:4045-4046 — `if (!(PM_HASHELEM) && (PM_NAMEDDIR || isset(AUTONAMEDIRS)))`.
4878 if (pm.node.flags as u32 & PM_HASHELEM) == 0
4879 && ((pm.node.flags as u32 & PM_NAMEDDIR) != 0
4880 || isset(crate::ported::zsh_h::AUTONAMEDIRS)) // c:4046 isset(AUTONAMEDIRS)
4881 {
4882 pm.node.flags |= PM_NAMEDDIR as i32; // c:4047
4883 crate::ported::utils::adduserdir(&pm.node.nam, &x, 0, false); // c:4048
4884 }
4885}
4886
4887/// Port of `arrgetfn(Param pm)` from `Src/params.c:4057`. C body:
4888/// `return pm->u.arr ? pm->u.arr : &nullarray;`
4889pub fn arrgetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
4890 pm.u_arr.clone().unwrap_or_default()
4891}
4892
4893/// Port of `arrsetfn(Param pm, char **x)` from `Src/params.c:4066`. C body frees
4894/// the old array, applies PM_UNIQUE filter via `uniqarray()`, then
4895/// stores. Calls `arrfixenv(ename, x)` for tied colon-arrays.
4896pub fn arrsetfn(pm: &mut crate::ported::zsh_h::param, x: Vec<String>) {
4897 let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
4898 simple_arrayuniq(x)
4899 } else {
4900 x
4901 };
4902 pm.u_arr = Some(val.clone());
4903 if let Some(ename) = pm.ename.clone() {
4904 arrfixenv(&ename, Some(&val));
4905 }
4906}
4907
4908/// Port of `hashgetfn(Param pm)` from `Src/params.c:4084`. C body:
4909/// `return pm->u.hash;`
4910pub fn hashgetfn(pm: &crate::ported::zsh_h::param) -> Option<&crate::ported::zsh_h::HashTable> {
4911 pm.u_hash.as_ref()
4912}
4913
4914/// Port of `hashsetfn(Param pm, HashTable x)` from `Src/params.c:4093`. C body:
4915/// `if (pm->u.hash && pm->u.hash != x) deleteparamtable(pm->u.hash);
4916/// pm->u.hash = x;`
4917pub fn hashsetfn(pm: &mut crate::ported::zsh_h::param, x: crate::ported::zsh_h::HashTable) {
4918 pm.u_hash = Some(x);
4919}
4920
4921/// Direct port of `static void arrhashsetfn(Param pm, char **val,
4922/// int flags)` from `Src/params.c:4113-4170`. Set callback for
4923/// assoc arrays: takes a flat `[k1, v1, k2, v2, ...]` value list
4924/// and turns it into a hash.
4925///
4926/// C body:
4927/// 1. Count non-Marker entries; if odd, error c:4128-4131.
4928/// 2. Under ASSPM_AUGMENT, fetch existing hash via getfn
4929/// (c:4134-4137); otherwise allocate fresh via
4930/// newparamtable(17, name).
4931/// 3. Walk pairs: each value (k, v) becomes a PM_SCALAR|PM_UNSET
4932/// child param `createparam(k)`, then `assignstrvalue(v->pm,
4933/// val, eltflags)` (c:4140-4166).
4934/// 4. `pm->gsu.h->setfn(pm, ht)` to install (c:4168).
4935///
4936/// The Rust port partially mirrors: counts pairs, rejects odd
4937/// counts via zerr, installs a fresh hashtable. The per-pair
4938/// createparam+assignstrvalue cycle requires assoc storage
4939/// shape we don't yet have wired through `u_hash`; this stays as
4940/// a structural port and emits diagnostic on the odd-count path.
4941pub fn arrhashsetfn( // c:4113
4942 pm: &mut crate::ported::zsh_h::param,
4943 val: Vec<String>,
4944 _flags: i32,
4945) {
4946
4947 // c:4124-4127 — count non-Marker entries.
4948 let alen: usize = val
4949 .iter()
4950 .filter(|s| !s.starts_with(Marker as char))
4951 .count();
4952
4953 // c:4129-4131 — odd count → error.
4954 if alen % 2 != 0 {
4955 crate::ported::utils::zerr(
4956 "bad set of key/value pairs for associative array",
4957 );
4958 return;
4959 }
4960
4961 // c:4132-4138 — install or augment. Skip the createparam
4962 // sub-hash walk pending assoc-storage wiring; install an
4963 // empty hashtable so hashgetfn doesn't return stale data.
4964 pm.u_hash = Some(Box::new(crate::ported::zsh_h::hashtable {
4965 hsize: 0,
4966 ct: 0,
4967 nodes: Vec::new(),
4968 tmpdata: 0,
4969 hash: None,
4970 emptytable: None,
4971 filltable: None,
4972 cmpnodes: None,
4973 addnode: None,
4974 getnode: None,
4975 getnode2: None,
4976 removenode: None,
4977 disablenode: None,
4978 enablenode: None,
4979 freenode: None,
4980 printnode: None,
4981 scantab: None,
4982 }));
4983 // c:4170 — free(val). Rust drops automatically.
4984}
4985
4986/// Port of `nullstrsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4180`. C body:
4987/// `zsfree(x);` — frees but doesn't store. Rust drop handles free.
4988#[allow(unused_variables)]
4989pub fn nullstrsetfn(pm: &mut crate::ported::zsh_h::param, x: String) {}
4990
4991/// Port of `nullunsetfn(UNUSED(Param pm), UNUSED(int exp))` from `Src/params.c:4192`. C body: empty.
4992#[allow(unused_variables)]
4993pub fn nullunsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {}
4994
4995/// Port of `stdunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:3955`. C body:
4996/// dispatches `pm->gsu->setfn(pm, NULL)` per `PM_TYPE`, clears
4997/// `PM_TIED`/frees ename for tied params, sets PM_UNSET.
4998///
4999/// Rust port mirrors C semantics: clears the union slot and sets
5000/// PM_UNSET. The GSU vtable callbacks are stored on `param` as
5001/// `Option<Gsu*>` (zsh_h:760-764) but the dispatch uses callback
5002/// fn-ptrs that aren't generally registered yet, so we open-code
5003/// the "setfn(pm, NULL)" effect by zeroing the matching union
5004/// member instead of calling through the vtable.
5005#[allow(unused_variables)]
5006pub fn stdunsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {
5007 match PM_TYPE(pm.node.flags as u32) {
5008 PM_SCALAR | PM_NAMEREF => {
5009 pm.u_str = None;
5010 }
5011 PM_ARRAY => {
5012 pm.u_arr = None;
5013 }
5014 PM_HASHED => {
5015 pm.u_hash = None;
5016 }
5017 _ => {
5018 if (pm.node.flags as u32 & PM_SPECIAL) == 0 {
5019 pm.u_str = None;
5020 }
5021 }
5022 }
5023 if (pm.node.flags as u32 & (PM_SPECIAL | PM_TIED)) == PM_TIED {
5024 pm.ename = None;
5025 pm.node.flags &= !(PM_TIED as i32);
5026 }
5027 pm.node.flags |= PM_UNSET as i32;
5028}
5029
5030// -----------------------------------------------------------
5031// "Null" callbacks — no-op getfn/setfn/unsetfn slots used for
5032// read-only or write-only special params.
5033// -----------------------------------------------------------
5034
5035/// Port of `nullintsetfn(UNUSED(Param pm), UNUSED(zlong x))` from `Src/params.c:4187`. C body:
5036/// empty (no-op setter for read-only int params).
5037#[allow(unused_variables)]
5038pub fn nullintsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {}
5039
5040/// Port of `nullsethashfn(UNUSED(Param pm), HashTable x)` from `Src/params.c:4104`. C body:
5041/// `deleteparamtable(x);` — frees the supplied table, doesn't store.
5042#[allow(unused_variables)]
5043pub fn nullsethashfn(pm: &mut crate::ported::zsh_h::param, x: crate::ported::zsh_h::HashTable) {
5044 // Rust drop semantics free `x` when this scope ends.
5045}
5046
5047// -----------------------------------------------------------
5048// Generic special-param GSU callbacks (`u.valptr` / `u.data`).
5049// C source uses raw pointer indirection through `pm->u.data`/
5050// `pm->u.valptr` — Rust port stores the global's name in `u_str`
5051// (lookup key) since we can't carry raw pointers across an FFI
5052// boundary safely. The lookup-table integration ships with the
5053// special-params init code (Src/params.c:4213 createparamtable).
5054// -----------------------------------------------------------
5055
5056/// Port of `intvargetfn(Param pm)` from `Src/params.c:4202`. C body:
5057/// `return *pm->u.valptr;`
5058pub fn intvargetfn(pm: &crate::ported::zsh_h::param) -> i64 {
5059 pm.u_val
5060}
5061
5062/// Port of `intvarsetfn(Param pm, zlong x)` from `Src/params.c:4213`. C body:
5063/// `*pm->u.valptr = x;`
5064pub fn intvarsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
5065 pm.u_val = x;
5066}
5067
5068/// Port of `zlevarsetfn(Param pm, zlong x)` from `Src/params.c:4224`. C body sets
5069/// the int and triggers `adjustwinsize` for LINES/COLUMNS.
5070/// Port of `zlevarsetfn(Param pm, zlong x)` from `Src/params.c:4226`.
5071/// C body: `*p = x; if (p == &zterm_lines || p == &zterm_columns)
5072/// adjustwinsize(2 + (p == &zterm_columns));`
5073///
5074/// The `from` argument to `adjustwinsize` is documented at
5075/// `Src/utils.c:1883-1887`: 0=signal, 1=manual, 2=LINES callback,
5076/// 3=COLUMNS callback. Each value selects a different code path
5077/// inside `adjustwinsize` — for example, `from=2` skips the
5078/// COLUMNS-specific ioctl, and `from=3` skips the LINES path.
5079///
5080/// The previous Rust port passed `0` for both LINES and COLUMNS,
5081/// which triggered the FULL `getwinsz` ioctl + both adjustlines
5082/// AND adjustcolumns calls AND the potential setiparam recursion
5083/// — diverging from C's narrow "just adjust the one axis we
5084/// changed" semantics. Effect: setting `LINES=80` would re-issue
5085/// `setiparam("COLUMNS", ...)` recursively, churning the
5086/// paramtab for no reason.
5087pub fn zlevarsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) { // c:4226
5088 pm.u_val = x; // c:4230 *p = x;
5089 // c:4231-4232 — `2 + (p == &zterm_columns)` selects 2 for LINES
5090 // (zterm_lines) and 3 for COLUMNS (zterm_columns).
5091 if pm.node.nam == "LINES" {
5092 let _ = crate::ported::utils::adjustwinsize(2); // c:4232 LINES path
5093 } else if pm.node.nam == "COLUMNS" {
5094 let _ = crate::ported::utils::adjustwinsize(3); // c:4232 COLUMNS path
5095 }
5096}
5097
5098/// Port of `strvarsetfn(Param pm, char *x)` from `Src/params.c:4249`. C body:
5099/// `zsfree(*q); *q = x;` where `q = (char **)pm->u.data`.
5100pub fn strvarsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) {
5101 pm.u_str = x;
5102}
5103
5104/// Port of `strvargetfn(Param pm)` from `Src/params.c:4263`. C body:
5105/// `s = *((char **)pm->u.data); return s ? s : hcalloc(1);`
5106pub fn strvargetfn(pm: &crate::ported::zsh_h::param) -> String {
5107 pm.u_str.clone().unwrap_or_default()
5108}
5109
5110/// Port of `arrvargetfn(Param pm)` from `Src/params.c:4279`. C body:
5111/// `arrptr = *((char ***)pm->u.data); return arrptr ?: &nullarray;`
5112pub fn arrvargetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
5113 pm.u_arr.clone().unwrap_or_default()
5114}
5115
5116/// Port of `arrvarsetfn(Param pm, char **x)` from `Src/params.c:4294`. C body
5117/// frees old, applies PM_UNIQUE, handles PM_SPECIAL+NULL → mkarray.
5118pub fn arrvarsetfn(pm: &mut crate::ported::zsh_h::param, x: Vec<String>) {
5119 let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
5120 simple_arrayuniq(x)
5121 } else {
5122 x
5123 };
5124 pm.u_arr = Some(val);
5125}
5126
5127/// Array to colon-separated path — inverse of `colonsplit`.
5128/// Port of `colonarrgetfn(Param pm)` from Src/params.c (joins the array
5129/// stored in `pm->u.colon` back into the `:`-form for env).
5130/// WARNING: param names don't match C — Rust=(arr) vs C=(pm)
5131pub fn colonarrgetfn(arr: &[String]) -> String {
5132 arr.join(":")
5133}
5134
5135/// Port of `colonarrsetfn(Param pm, char *x)` from `Src/params.c:4329`. C body
5136/// splits the colon-string into an array and stores via the
5137/// generic arrvarsetfn.
5138pub fn colonarrsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) {
5139 let uniq = (pm.node.flags as u32 & PM_UNIQUE) != 0; // c:4339
5140 let arr = match x {
5141 Some(s) => crate::ported::utils::colonsplit(&s, uniq), // c:4339
5142 None => Vec::new(),
5143 };
5144 arrvarsetfn(pm, arr);
5145}
5146
5147/// Port of `tiedarrgetfn(Param pm)` from `Src/params.c:4348`. C body:
5148/// `return *((Tieddata)pm->u.data)->arrptr;`
5149pub fn tiedarrgetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
5150 pm.u_arr.clone().unwrap_or_default()
5151}
5152
5153/// Direct port of `void tiedarrsetfn(Param pm, char *x)` from
5154/// `Src/params.c:4357-4389`. Setter for a colon-array-tied
5155/// scalar (PATH/CDPATH/MAILPATH/etc.).
5156///
5157/// C body:
5158/// 1. Free the existing tied array (`*dptr->arrptr`) at c:4363.
5159/// 2. If no array but an `ename` exists, clear PM_DEFAULTED on
5160/// the tied array param (c:4365-4368).
5161/// 3. If `x` is non-null: build a 1-or-2-byte separator from
5162/// `dptr->joinchar` (Meta-quoting if needed, c:4371-4380),
5163/// `sepsplit(x, sepbuf, 0, 0)` into the array (c:4381), and
5164/// uniqarray() if PM_UNIQUE (c:4382-4383). Free `x` (c:4384).
5165/// 4. Else: `*dptr->arrptr = NULL` (c:4385-4386).
5166/// 5. If `pm->ename` is set, call `arrfixenv(pm->name, arrptr)`
5167/// to sync env (c:4387-4388).
5168///
5169/// The Rust port treats `u_arr` as the tied array storage and
5170/// uses `':'` as the joinchar default (matches PATH/CDPATH/FPATH
5171/// /MAILPATH/PSVAR/MODULE_PATH which all use colon separators —
5172/// the joinchar field on the C-side tieddata wasn't ported to the
5173/// Rust Param struct yet).
5174pub fn tiedarrsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) { // c:4357
5175
5176 // c:4361-4368 — free old / clear PM_DEFAULTED on tied counterpart.
5177 if pm.u_arr.is_none() {
5178 if let Some(ename) = pm.ename.clone() { // c:4365
5179 let mut tab = paramtab().write().unwrap();
5180 if let Some(altpm) = tab.get_mut(&ename) { // c:4366
5181 altpm.node.flags &= !(PM_DEFAULTED as i32); // c:4367
5182 }
5183 }
5184 }
5185
5186 if let Some(s) = x { // c:4369
5187 // c:4370-4380 — single-byte separator (joinchar=':' for all
5188 // currently-tied params; Meta-quoting only kicks in for
5189 // exotic joinchars not present today).
5190 let arr: Vec<String> = s.split(':').map(|t| t.to_string()).collect();
5191 // c:4382-4383 — uniqarray if PM_UNIQUE.
5192 let arr = if pm.node.flags & PM_UNIQUE as i32 != 0 { // c:4382
5193 uniqarray(arr) // c:4383
5194 } else {
5195 arr
5196 };
5197 pm.u_arr = Some(arr);
5198 // c:4384 — zsfree(x). Rust drop.
5199 } else { // c:4385
5200 pm.u_arr = None; // c:4386
5201 }
5202
5203 // c:4387-4388 — `if (pm->ename) arrfixenv(pm->name, *dptr->arrptr)`.
5204 if pm.ename.is_some() {
5205 let nam = pm.node.nam.clone();
5206 let arr_ref = pm.u_arr.as_deref();
5207 arrfixenv(&nam, arr_ref);
5208 }
5209}
5210
5211/// Port of `tiedarrunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:4393`. C body
5212/// frees the tied storage and calls stdunsetfn.
5213/// Direct port of `void tiedarrunsetfn(Param pm, UNUSED(int exp))`
5214/// from `Src/params.c:4393`. Special unset for tied arrays:
5215/// frees tieddata, ename, clears PM_TIED, sets PM_UNSET.
5216///
5217/// C body:
5218/// pm->gsu.s->setfn(pm, NULL); // c:4393
5219/// zfree(pm->u.data, sizeof(tieddata)); // c:4393
5220/// pm->u.data = NULL; // c:4393
5221/// zsfree(pm->ename); // c:4393
5222/// pm->ename = NULL; // c:4393
5223/// pm->flags &= ~PM_TIED; // c:4393
5224/// pm->flags |= PM_UNSET; // c:4393
5225pub fn tiedarrunsetfn(pm: &mut crate::ported::zsh_h::param, _exp: i32) { // c:4393
5226 // c:4400 — invoke the scalar setfn with NULL (frees backing array).
5227 tiedarrsetfn(pm, None);
5228 // c:4401-4403 — drop tieddata.
5229 pm.u_data = 0;
5230 pm.u_arr = None;
5231 // c:4404-4405 — `zsfree(pm->ename); pm->ename = NULL`.
5232 pm.ename = None;
5233 // c:4406-4407 — flag toggles.
5234 pm.node.flags &= !(PM_TIED as i32);
5235 pm.node.flags |= PM_UNSET as i32;
5236}
5237
5238// -----------------------------------------------------------
5239// Array uniq helpers.
5240// -----------------------------------------------------------
5241
5242/// Port of `simple_arrayuniq(char **x, int freeok)` from `Src/params.c:4412`. C body:
5243/// O(n^2) dedupe in place — first occurrence wins.
5244/// WARNING: param names don't match C — Rust=(x) vs C=(x, freeok)
5245pub fn simple_arrayuniq(x: Vec<String>) -> Vec<String> {
5246 let mut seen: HashSet<String> = HashSet::new();
5247 let mut out = Vec::with_capacity(x.len());
5248 for s in x {
5249 if seen.insert(s.clone()) {
5250 out.push(s);
5251 }
5252 }
5253 out
5254}
5255
5256/// Port of `arrayuniq_freenode(HashNode hn)` from `Src/params.c:4443`. C
5257/// body: `zsfree(((Pathnode)hn)->name); zfree(hn, sizeof…);` —
5258/// the freenode callback for the temporary HashTable `arrayuniq`
5259/// builds. Rust drop semantics handle this; no-op shim.
5260/// is `(void)hn;` — intentional no-op; passed as freenode callback
5261/// to scratch hashtable used by `arrayuniq` so existing entries
5262/// aren't freed when the table is torn down.
5263/// WARNING: param names don't match C — Rust=() vs C=(hn)
5264/// WARNING: param names don't match C — Rust=() vs C=(pm, x)
5265pub fn arrayuniq_freenode() {}
5266
5267/// Direct port of `HashTable newuniqtable(zlong size)` from
5268/// `Src/params.c:4450`. C body allocates a `HashTable`
5269/// named "arrayuniq" with the standard hasher/cmpnodes/
5270/// add/get/remove/disable/enable function pointers plus
5271/// `arrayuniq_freenode` as the freenode callback (which is a
5272/// no-op — see c:4443). Rust returns a `HashSet<String>` with
5273/// the size hint pre-allocated; the freenode-callback role is
5274/// implicit (Drop runs on HashSet teardown without freeing
5275/// borrowed strings).
5276pub fn newuniqtable(size: i64) -> HashSet<String> { // c:4450
5277 HashSet::with_capacity(size.max(0) as usize) // c:4450 newhashtable(size, ...)
5278}
5279
5280/// Direct port of `static void arrayuniq(char **x, int freeok)`
5281/// from `Src/params.c:4473`. First-wins dedupe of `x`,
5282/// in-place. C uses simple O(n²) scan for arrays under 10
5283/// entries, switching to a HashTable for larger arrays. `freeok`
5284/// controls whether to `zsfree()` duplicates (only safe when
5285/// caller owns the strings — Rust drop semantics handle it).
5286///
5287/// Signature note: C takes `char **x` + in-place mutation; Rust
5288/// takes owned `Vec<String>` and returns the deduped result.
5289/// `freeok` is preserved but is a no-op in Rust (drops free
5290/// automatically). The hashtable / simple-loop tiering follows
5291/// the same threshold (10) as C.
5292pub fn arrayuniq(x: Vec<String>, freeok: i32) -> Vec<String> { // c:4473
5293 let _ = freeok;
5294 let array_size = x.len();
5295 if array_size == 0 { // c:4481
5296 return x;
5297 }
5298 // c:4482-4486 — small-array fallback to simple_arrayuniq.
5299 if array_size < 10 { // c:4482
5300 return simple_arrayuniq(x); // c:4484
5301 }
5302 // c:4483 — `if (!(ht = newuniqtable(array_size + 1)))` — Rust
5303 // newuniqtable never fails, but mirror the C order of allocation.
5304 let mut ht = newuniqtable(array_size as i64 + 1);
5305 // c:4487-4507 — walk + first-wins.
5306 let mut out: Vec<String> = Vec::with_capacity(array_size);
5307 for s in x { // c:4487 walk
5308 if ht.insert(s.clone()) { // c:4488 gethashnode2 + addhashnode2
5309 out.push(s); // c:4495 *write_it = *it
5310 }
5311 // else: dup — drop the value (c:4502 zsfree if freeok).
5312 }
5313 drop(ht); // c:4523 deletehashtable
5314 out
5315}
5316
5317/// Remove duplicate elements from array while preserving order.
5318/// Port of `uniqarray(char **x)` from Src/params.c.
5319/// WARNING: param names don't match C — Rust=(arr) vs C=(x)
5320pub fn uniqarray(arr: Vec<String>) -> Vec<String> {
5321 let mut seen = HashSet::new();
5322 arr.into_iter().filter(|s| seen.insert(s.clone())).collect()
5323}
5324
5325/// Direct port of `void zhuniqarray(char **x)` from
5326/// `Src/params.c:4523`. Wraps `arrayuniq` with `freeok=0`.
5327/// (C body is literally `arrayuniq(x, 0);`.)
5328pub fn zhuniqarray(x: Vec<String>) -> Vec<String> { // c:4523
5329 arrayuniq(x, 0) // c:4523
5330}
5331
5332/// Port of `poundgetfn(UNUSED(Param pm))` from `Src/params.c:4534`. C body:
5333/// `return arrlen(pparams);`
5334/// WARNING: param names don't match C — Rust=() vs C=(pm)
5335pub fn poundgetfn() -> i64 {
5336 pparams_lock().lock().expect("pparams poisoned").len() as i64
5337}
5338
5339/// Port of `randomgetfn(UNUSED(Param pm))` from `Src/params.c:4543`. C body:
5340/// `return rand() & 0x7fff;`
5341/// WARNING: param names don't match C — Rust=() vs C=(pm)
5342pub fn randomgetfn() -> i64 {
5343 (unsafe { libc::rand() } & 0x7fff) as i64
5344}
5345
5346/// Port of `randomsetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4552`. C body:
5347/// `srand((unsigned int)v);`
5348/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
5349pub fn randomsetfn(v: i64) {
5350 unsafe { libc::srand(v as libc::c_uint) };
5351}
5352
5353// -----------------------------------------------------------
5354// SECONDS / EPOCHSECONDS family — backed by SHTIMER static.
5355// -----------------------------------------------------------
5356
5357/// Port of `intsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4561`. C body:
5358/// `return (zlong)(now.tv_sec - shtimer.tv_sec - …);`
5359/// WARNING: param names don't match C — Rust=() vs C=(pm)
5360pub fn intsecondsgetfn() -> i64 {
5361 let now = SystemTime::now()
5362 .duration_since(UNIX_EPOCH)
5363 .unwrap_or_default();
5364 let timer = *shtimer_lock().lock().expect("shtimer poisoned");
5365 let now_sec = now.as_secs() as i64;
5366 let timer_sec = timer.as_secs() as i64;
5367 let now_nsec = now.subsec_nanos() as i64;
5368 let timer_nsec = timer.subsec_nanos() as i64;
5369 now_sec - timer_sec - i64::from(now_nsec < timer_nsec)
5370}
5371
5372/// Port of `intsecondssetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4575`. C body:
5373/// ```c
5374/// diff = (zlong)now.tv_sec - x;
5375/// shtimer.tv_sec = diff;
5376/// if ((zlong)shtimer.tv_sec != diff)
5377/// zwarn("SECONDS truncated on assignment");
5378/// shtimer.tv_nsec = now.tv_nsec;
5379/// ```
5380/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5381pub fn intsecondssetfn(x: i64) {
5382 let now = SystemTime::now()
5383 .duration_since(UNIX_EPOCH)
5384 .unwrap_or_default();
5385 let now_sec = now.as_secs() as i64;
5386 let new_sec = now_sec - x;
5387 // c:4587 — C uses `zwarn` (informational), NOT `zerr` (fatal).
5388 // The C body STORES `diff` unconditionally then emits the warning
5389 // if truncation lost information. Rust port previously used `zerr`
5390 // and early-returned (skipping the store) — divergent from C.
5391 if new_sec < 0 {
5392 crate::ported::utils::zwarn("SECONDS truncated on assignment");
5393 // c:4585 — C still stores; Rust represents shtimer as Duration
5394 // which is non-negative. We clamp to zero to preserve the
5395 // "store-anyway" semantic for the time-display path, even
5396 // though the negative-time case is unrepresentable.
5397 *shtimer_lock().lock().expect("shtimer poisoned") =
5398 Duration::new(0, now.subsec_nanos());
5399 return;
5400 }
5401 *shtimer_lock().lock().expect("shtimer poisoned") =
5402 Duration::new(new_sec as u64, now.subsec_nanos());
5403}
5404
5405/// Port of `floatsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4591`. C body:
5406/// `return (double)(now-tv_sec - shtimer.tv_sec) + nsec/1e9;`
5407/// WARNING: param names don't match C — Rust=() vs C=(pm)
5408pub fn floatsecondsgetfn() -> f64 {
5409 let now = SystemTime::now()
5410 .duration_since(UNIX_EPOCH)
5411 .unwrap_or_default();
5412 let timer = *shtimer_lock().lock().expect("shtimer poisoned");
5413 (now - timer).as_secs_f64()
5414}
5415
5416/// Port of `floatsecondssetfn(UNUSED(Param pm), double x)` from `Src/params.c:4603`. C body:
5417/// `shtimer.tv_sec = now.tv_sec - (zlong)x; shtimer.tv_nsec = now.tv_nsec - (x-int)*1e9;`
5418/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5419pub fn floatsecondssetfn(x: f64) {
5420 let now = SystemTime::now()
5421 .duration_since(UNIX_EPOCH)
5422 .unwrap_or_default();
5423 let new = now.checked_sub(Duration::from_secs_f64(x)).unwrap_or_default();
5424 *shtimer_lock().lock().expect("shtimer poisoned") = new;
5425}
5426
5427/// Port of `getrawseconds()` from `Src/params.c:4615`. C body:
5428/// `return (double)shtimer.tv_sec + (double)shtimer.tv_nsec / 1e9;`
5429pub fn getrawseconds() -> f64 {
5430 shtimer_lock().lock().expect("shtimer poisoned").as_secs_f64()
5431}
5432
5433/// Port of `setrawseconds(double x)` from `Src/params.c:4622`. C body:
5434/// `shtimer.tv_sec = (zlong)x; shtimer.tv_nsec = (x-int)*1e9;`
5435pub fn setrawseconds(x: f64) {
5436 *shtimer_lock().lock().expect("shtimer poisoned") = Duration::from_secs_f64(x);
5437}
5438
5439/// Port of `setsecondstype(Param pm, int on, int off)` from `Src/params.c:4630`. C body
5440/// flips the `gsu.f`/`gsu.i` callback pointer based on the new
5441/// param-flag bitset.
5442///
5443/// WARNING: zshrs has no Param/GSU dispatch table yet — the
5444/// "promotion between integer/float seconds" logic happens via
5445/// pm->gsu pointer swaps in C. Returns 0 to signal success;
5446/// callers can assume the type change is recorded by the caller's
5447/// own bookkeeping until the GSU table lands.
5448/// WARNING: param names don't match C — Rust=(on, off) vs C=(pm, on, off)
5449pub fn setsecondstype( // c:4630
5450 pm: &mut crate::ported::zsh_h::param,
5451 on: i32,
5452 off: i32,
5453) -> i32 {
5454 // c:4632 — `int newflags = (pm->flags | on) & ~off`.
5455 let newflags = (pm.node.flags | on) & !off;
5456 // c:4633 — `int tp = PM_TYPE(newflags)`.
5457 let tp = PM_TYPE(newflags as u32);
5458 // c:4635-4638 / 4639-4642 — float vs integer GSU pointer swap.
5459 if tp == PM_EFLOAT || tp == PM_FFLOAT { // c:4635
5460 // C: `pm->gsu.f = &floatseconds_gsu`. GSU table not yet
5461 // wired in the Rust port; record the type by clearing
5462 // any integer GSU.
5463 pm.gsu_i = None;
5464 // pm.gsu_f = Some(floatseconds_gsu) — pending GSU port.
5465 } else if tp == PM_INTEGER { // c:4639
5466 // C: `pm->gsu.i = &intseconds_gsu`.
5467 pm.gsu_f = None;
5468 // pm.gsu_i = Some(intseconds_gsu) — pending GSU port.
5469 } else {
5470 return 1; // c:4644
5471 }
5472 pm.node.flags = newflags; // c:4645
5473 0 // c:4646
5474}
5475
5476// -----------------------------------------------------------
5477// $USERNAME
5478// -----------------------------------------------------------
5479
5480/// Port of `usernamegetfn(UNUSED(Param pm))` from `Src/params.c:4653`. C body:
5481/// Port of `usernamegetfn(UNUSED(Param pm))` from Src/params.c:4655.
5482/// C body: `return get_username();`. C's `get_username()`
5483/// (Src/utils.c:1075) walks `getuid() != cached_uid` and
5484/// refreshes the cache via `getpwuid()` on mismatch — so a
5485/// USERNAME read AFTER an `setuid()` call sees the NEW
5486/// username, not the stale cache.
5487///
5488/// The previous Rust port returned `cached_username_lock()`
5489/// directly without the refresh, so a script that called
5490/// setuid(3) (or USER changed externally via setuid binary)
5491/// would keep returning the old username.
5492///
5493/// WARNING: param names don't match C — Rust=() vs C=(pm)
5494pub fn usernamegetfn() -> String { // c:4655
5495 // c:4658 — `return get_username();`. Route through the
5496 // canonical refresh-on-uid-change accessor at utils.rs.
5497 crate::ported::utils::get_username() // c:4658
5498}
5499
5500/// Port of `usernamesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4662`. C body:
5501/// `getpwnam(x); setgid; setuid; cached_uid = pswd->pw_uid;`
5502///
5503/// WARNING: the SUID-changing path requires getpwnam(3) which
5504/// crosses an unsafe FFI boundary not yet wrapped here. The
5505/// cached-name update is performed; uid/gid changes still need
5506/// porting of the `pwd.h` getpwnam wrapper.
5507/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5508pub fn usernamesetfn(x: String) { // c:4662
5509 // c:4662 — `if (x && (pswd = getpwnam(x)) && pswd->pw_uid != cached_uid)`.
5510 let target = std::ffi::CString::new(x.as_bytes()).ok();
5511 if let Some(cstr) = target {
5512 unsafe {
5513 let pwd = libc::getpwnam(cstr.as_ptr()); // c:4666
5514 if !pwd.is_null() {
5515 // c:4666 — C reads `cached_uid` (a global initialized
5516 // to `getuid()` at init.c:1219 — the REAL uid, NOT
5517 // the effective one). The previous Rust port used
5518 // `geteuid()` which diverges when running setuid
5519 // (geteuid != getuid) — the shell would erroneously
5520 // try to change to a uid it's already at, or skip
5521 // a needed change. Match C exactly: use `getuid()`.
5522 let cached_uid = libc::getuid(); // c:4666 cached_uid = getuid()
5523 if (*pwd).pw_uid != cached_uid { // c:4666
5524 // c:4670-4672 — initgroups(x, pswd->pw_gid).
5525 let _ = libc::initgroups(cstr.as_ptr(), (*pwd).pw_gid as _);
5526 // c:4671 — setgid(pswd->pw_gid).
5527 if libc::setgid((*pwd).pw_gid) != 0 { // c:4673
5528 crate::ported::utils::zwarn(&format!(
5529 "failed to change group ID: {}",
5530 std::io::Error::last_os_error()
5531 ));
5532 } else if libc::setuid((*pwd).pw_uid) != 0 { // c:4675
5533 // c:4675-4676 — setuid failed.
5534 crate::ported::utils::zwarn(&format!(
5535 "failed to change user ID: {}",
5536 std::io::Error::last_os_error()
5537 ));
5538 } else {
5539 // c:4677-4681 — cache update.
5540 let name_cstr = std::ffi::CStr::from_ptr((*pwd).pw_name);
5541 let name_str = name_cstr.to_string_lossy().to_string();
5542 *cached_username_lock()
5543 .lock()
5544 .expect("username poisoned") =
5545 crate::ported::utils::ztrdup_metafy(&name_str);
5546 }
5547 }
5548 }
5549 }
5550 }
5551 // c:4683 — `zsfree(x)`; Rust drop handles it.
5552 drop(x);
5553}
5554
5555// -----------------------------------------------------------
5556// libc-backed callbacks (UID/GID/EUID/EGID/errno/RANDOM/TTYIDLE).
5557// -----------------------------------------------------------
5558
5559/// Port of `uidgetfn(UNUSED(Param pm))` from `Src/params.c:4689`. C body:
5560/// `return getuid();`
5561/// WARNING: param names don't match C — Rust=() vs C=(pm)
5562pub fn uidgetfn() -> i64 {
5563 unsafe { libc::getuid() as i64 }
5564}
5565
5566// `termflags` from Src/init.c — bitmap of terminal-state flags. Set
5567// from term_reinit_from_pm and consulted by ZLE before first paint.
5568pub static TERMFLAGS: std::sync::atomic::AtomicI32 =
5569 std::sync::atomic::AtomicI32::new(0);
5570// `TERM_UNKNOWN` re-exported from canonical zsh_h.rs (port of
5571// `Src/zsh.h:1986`). The local declaration here had the value
5572// `1 << 0 = 0x01` — which is C's TERM_BAD (Src/zsh.h:1985), NOT
5573// TERM_UNKNOWN. The canonical TERM_UNKNOWN value is 0x02.
5574//
5575// Callers reading `crate::ported::params::TERM_UNKNOWN` got the
5576// TERM_BAD bit; the params.rs term-init path fired
5577// `TERMFLAGS.fetch_or(TERM_UNKNOWN)` which actually set TERM_BAD,
5578// while the prompt.rs guard at line 441 imported the correct
5579// (0x02) value from zsh_h.rs — so the two paths disagreed silently
5580// about which bit means "unknown terminal".
5581pub use crate::ported::zsh_h::TERM_UNKNOWN;
5582
5583/// Port of `uidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4698`. C body:
5584/// `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
5585/// C body (2 lines):
5586/// `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
5587/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5588pub fn uidsetfn(x: i64) { // c:4698
5589 if unsafe { libc::setuid(x as libc::uid_t) } != 0 { // c:4701
5590 zerr(&format!("failed to change user ID: {}", std::io::Error::last_os_error())); // c:4702
5591 }
5592}
5593
5594/// Port of `euidgetfn(UNUSED(Param pm))` from `Src/params.c:4710`. C body:
5595/// `return geteuid();`
5596/// WARNING: param names don't match C — Rust=() vs C=(pm)
5597pub fn euidgetfn() -> i64 {
5598 unsafe { libc::geteuid() as i64 }
5599}
5600
5601/// Port of `euidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4719`. C body:
5602/// `if (seteuid((uid_t)x)) zerr("failed to change effective user ID: %e", errno);`
5603/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5604pub fn euidsetfn(x: i64) { // c:4719
5605 if unsafe { libc::seteuid(x as libc::uid_t) } != 0 { // c:4722
5606 zerr(&format!("failed to change effective user ID: {}", std::io::Error::last_os_error())); // c:4723
5607 }
5608}
5609
5610/// Port of `gidgetfn(UNUSED(Param pm))` from `Src/params.c:4731`. C body: `return getgid();`
5611/// WARNING: param names don't match C — Rust=() vs C=(pm)
5612pub fn gidgetfn() -> i64 {
5613 unsafe { libc::getgid() as i64 }
5614}
5615
5616/// Port of `gidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4740`. C body:
5617/// `if (setgid((gid_t)x)) zerr("failed to change group ID: %e", errno);`
5618/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5619pub fn gidsetfn(x: i64) { // c:4740
5620 if unsafe { libc::setgid(x as libc::gid_t) } != 0 { // c:4743
5621 zerr(&format!("failed to change group ID: {}", std::io::Error::last_os_error())); // c:4744
5622 }
5623}
5624
5625/// Port of `egidgetfn(UNUSED(Param pm))` from `Src/params.c:4752`. C body: `return getegid();`
5626/// WARNING: param names don't match C — Rust=() vs C=(pm)
5627pub fn egidgetfn() -> i64 {
5628 unsafe { libc::getegid() as i64 }
5629}
5630
5631/// Port of `egidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4761`. C body:
5632/// `if (setegid((gid_t)x)) zerr("failed to change effective group ID: %e", errno);`
5633/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5634pub fn egidsetfn(x: i64) { // c:4761
5635 if unsafe { libc::setegid(x as libc::gid_t) } != 0 { // c:4764
5636 zerr(&format!("failed to change effective group ID: {}", std::io::Error::last_os_error())); // c:4765
5637 }
5638}
5639
5640/// Port of `ttyidlegetfn(UNUSED(Param pm))` from `Src/params.c:4771`. C body:
5641/// ```c
5642/// struct stat ttystat;
5643/// if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;
5644/// return time(NULL) - ttystat.st_atime;
5645/// ```
5646/// Rust port reads stdin (fd 0) — closest match to `SHTTY` the
5647/// shell tracks as the controlling-tty fd. Returns -1 if stdin is
5648/// not a tty.
5649/// WARNING: param names don't match C — Rust=() vs C=(pm)
5650pub fn ttyidlegetfn() -> i64 {
5651 // c:4776 — `if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;`
5652 // The previous Rust port hardcoded fd 0 (stdin) which is wrong
5653 // when SHTTY was opened on a non-stdin file descriptor (e.g.
5654 // `zsh < script` where stdin is a file but the controlling tty
5655 // was opened separately). C tracks the actual SHTTY fd.
5656 let shtty = crate::ported::init::SHTTY.load(std::sync::atomic::Ordering::SeqCst);
5657 if shtty == -1 { // c:4776
5658 return -1;
5659 }
5660 let mut st: libc::stat = unsafe { std::mem::zeroed() };
5661 if unsafe { libc::fstat(shtty, &mut st) } != 0 { // c:4776
5662 return -1;
5663 }
5664 let now = SystemTime::now()
5665 .duration_since(UNIX_EPOCH)
5666 .unwrap_or_default()
5667 .as_secs() as i64;
5668 now - st.st_atime as i64 // c:4779
5669}
5670
5671// -----------------------------------------------------------
5672// $IFS / $HOME / $TERM / $WORDCHARS / $TERMINFO / $TERMINFO_DIRS
5673// $KEYBOARD_HACK / $HISTCHARS / $_ — string-state callbacks.
5674// -----------------------------------------------------------
5675
5676/// Port of `ifsgetfn(UNUSED(Param pm))` from `Src/params.c:4784`. C body: `return ifs;`
5677/// WARNING: param names don't match C — Rust=() vs C=(pm)
5678pub fn ifsgetfn() -> String {
5679 ifs_lock().lock().expect("ifs poisoned").clone()
5680}
5681
5682/// Port of `ifssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4793`. C body:
5683/// `zsfree(ifs); ifs = x; inittyptab();`
5684/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5685pub fn ifssetfn(x: String) {
5686 *ifs_lock().lock().expect("ifs poisoned") = x;
5687 // c:4795 — `inittyptab()` rebuilds the typtab[] ISEP/IWSEP bits
5688 // from the new IFS. Without this, every word-split path stays
5689 // pinned to the old separator set and silently mis-splits.
5690 crate::ported::utils::inittyptab();
5691}
5692
5693// -----------------------------------------------------------
5694// Locale callbacks: $LANG, $LC_*, setlang
5695// -----------------------------------------------------------
5696
5697/// Port of `clear_mbstate()` from `Src/params.c:4831`. C body:
5698/// `mb_charinit(); clear_shiftstate();`
5699///
5700/// WARNING: zshrs uses Rust's UTF-8 native handling so multibyte
5701/// state machines aren't kept; this is a no-op pinned to the
5702/// C name for parity.
5703/// (under `MULTIBYTE_SUPPORT`):
5704/// ```c
5705/// mb_charinit(); /* utils.c */
5706/// clear_shiftstate(); /* pattern.c */
5707/// ```
5708/// Resets the mbstate_t globals after LC_CTYPE changes (NetBSD-9
5709/// requires this). Rust port forwards to the matching helpers.
5710pub fn clear_mbstate() {
5711 // mb_charinit / clear_shiftstate not yet ported; once they are
5712 // (Src/utils.c, Src/pattern.c) wire the calls here.
5713}
5714
5715/// Port of `static struct localename lc_names[]` from `Src/params.c:4805-4825`.
5716/// C body:
5717/// ```c
5718/// static struct localename {
5719/// char *name;
5720/// int category;
5721/// } lc_names[] = {
5722/// {"LC_COLLATE", LC_COLLATE},
5723/// {"LC_CTYPE", LC_CTYPE},
5724/// {"LC_MESSAGES", LC_MESSAGES},
5725/// {"LC_NUMERIC", LC_NUMERIC},
5726/// {"LC_TIME", LC_TIME},
5727/// {NULL, 0}
5728/// };
5729/// ```
5730///
5731/// The C source guards each entry under `#ifdef LC_*`; libc on
5732/// macOS/Linux defines all five so the Rust port simply lists them.
5733const LC_NAMES: &[(&str, libc::c_int)] = &[
5734 ("LC_COLLATE", libc::LC_COLLATE), // c:4810
5735 ("LC_CTYPE", libc::LC_CTYPE), // c:4813
5736 ("LC_MESSAGES", libc::LC_MESSAGES), // c:4816
5737 ("LC_NUMERIC", libc::LC_NUMERIC), // c:4819
5738 ("LC_TIME", libc::LC_TIME), // c:4822
5739];
5740
5741/// Port of `setlang(char *x)` from `Src/params.c:4842`.
5742///
5743/// C body (c:4842-4869):
5744/// ```c
5745/// if ((x2 = getsparam_u("LC_ALL")) && *x2) return;
5746/// setlocale(LC_ALL, x ? unmeta(x) : "");
5747/// clear_mbstate();
5748/// queue_signals();
5749/// for (ln = lc_names; ln->name; ln++)
5750/// if ((x = getsparam_u(ln->name)) && *x)
5751/// setlocale(ln->category, x);
5752/// unqueue_signals();
5753/// inittyptab();
5754/// ```
5755///
5756/// The previous Rust port skipped the actual `setlocale(LC_ALL, ...)`
5757/// libc call and just set the LANG env var. C invokes libc
5758/// setlocale to actually change the program's locale state —
5759/// required so any libc calls during shell execution (e.g.,
5760/// `iswctype`, `mbrtowc`) use the new locale's classification.
5761///
5762/// Also skipped: the per-LC_* override loop (c:4866-4868) which
5763/// re-applies category-specific settings after the global
5764/// LC_ALL set. The Rust port doesn't yet have the lc_names
5765/// table, but we can at least respect the canonical sequence.
5766pub fn setlang(x: Option<&str>) { // c:4842
5767 // c:4847 — `if ((x2 = getsparam_u("LC_ALL")) && *x2) return;`
5768 if let Some(lc_all) = getsparam_u("LC_ALL") { // c:4847
5769 if !lc_all.is_empty() {
5770 return;
5771 }
5772 }
5773 // c:4860 — `setlocale(LC_ALL, x ? unmeta(x) : "");`
5774 let locale_arg = match x {
5775 Some(s) => crate::ported::utils::unmeta(s),
5776 None => String::new(),
5777 };
5778 // The previous Rust port skipped the libc setlocale call.
5779 // Without it, libc's locale state (used by iswctype, mbrtowc,
5780 // etc.) stays pinned to whatever the shell inherited from
5781 // its parent — diverging from C which actively changes the
5782 // running program's locale.
5783 let cstr = std::ffi::CString::new(locale_arg.as_bytes()).unwrap_or_default();
5784 unsafe {
5785 libc::setlocale(libc::LC_ALL, cstr.as_ptr()); // c:4860
5786 }
5787 // Mirror to env so subsequent `getsparam("LANG")` reads agree.
5788 if let Some(s) = x {
5789 env::set_var("LANG", s);
5790 }
5791 clear_mbstate(); // c:4861
5792 // c:4863-4867 — `for (ln = lc_names; ln->name; ln++) if ((x =
5793 // getsparam_u(ln->name)) && *x) setlocale(ln->category, x);`
5794 // After the global LC_ALL setlocale, any explicitly-set LC_*
5795 // category overrides its slot. The previous Rust port skipped
5796 // this loop, so `LC_NUMERIC=tr_TR.UTF-8 LANG=C` would leave
5797 // numeric formatting on C rather than tr_TR.
5798 for (name, category) in LC_NAMES { // c:4863
5799 if let Some(val) = getsparam_u(name) { // c:4866 getsparam_u
5800 if !val.is_empty() {
5801 let cat_cstr = std::ffi::CString::new(val.as_bytes())
5802 .unwrap_or_default();
5803 unsafe {
5804 libc::setlocale(*category, cat_cstr.as_ptr()); // c:4867
5805 }
5806 }
5807 }
5808 }
5809 // c:4868 — `inittyptab();`. The locale change may shift which
5810 // bytes are isalpha/isalnum/etc under the typtab init, so the
5811 // table must be rebuilt.
5812 crate::ported::utils::inittyptab();
5813}
5814
5815/// Port of `lc_allsetfn(Param pm, char *x)` from `Src/params.c:4873`.
5816///
5817/// C body (c:4873-4894):
5818/// ```c
5819/// strsetfn(pm, x);
5820/// if (!x || !*x) {
5821/// x = getsparam_u("LANG");
5822/// if (x && *x) {
5823/// queue_signals();
5824/// setlang(x);
5825/// unqueue_signals();
5826/// }
5827/// } else {
5828/// setlocale(LC_ALL, unmeta(x));
5829/// clear_mbstate();
5830/// inittyptab();
5831/// }
5832/// ```
5833///
5834/// The previous Rust port for the non-empty case set the env
5835/// var via `env::set_var("LC_ALL", &s)` but skipped THREE
5836/// pieces:
5837/// 1. `setlocale(LC_ALL, unmeta(x))` — actively changes the
5838/// program's locale per c:4890.
5839/// 2. `unmeta(x)` — strips Meta-encoded bytes before passing
5840/// to libc setlocale per c:4890.
5841/// 3. `inittyptab()` — rebuilds the typtab for the new
5842/// LC_CTYPE per c:4892.
5843///
5844/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5845pub fn lc_allsetfn(x: Option<String>) { // c:4873
5846 match x {
5847 None => setlang(getsparam_u("LANG").as_deref()), // c:4882 getsparam_u
5848 Some(s) if s.is_empty() => { // c:4881
5849 // c:4881-4884 — empty x falls back to setlang(getsparam_u("LANG")).
5850 setlang(getsparam_u("LANG").as_deref()); // c:4882
5851 }
5852 Some(s) => {
5853 // c:4889 — `setlocale(LC_ALL, unmeta(x));`
5854 let unmeta = crate::ported::utils::unmeta(&s); // c:4889 unmeta(x)
5855 let cstr = std::ffi::CString::new(unmeta.as_bytes())
5856 .unwrap_or_default();
5857 unsafe {
5858 libc::setlocale(libc::LC_ALL, cstr.as_ptr()); // c:4890
5859 }
5860 env::set_var("LC_ALL", &s);
5861 clear_mbstate(); // c:4891
5862 // c:4892 — `inittyptab();` rebuild typtab for new LC_CTYPE.
5863 crate::ported::utils::inittyptab(); // c:4892
5864 }
5865 }
5866}
5867
5868/// Port of `langsetfn(Param pm, char *x)` from `Src/params.c:4898`. C body:
5869/// `strsetfn(pm, x); setlang(unmeta(x));`
5870///
5871/// `unmeta(x)` strips Meta-encoding before passing to libc
5872/// `setlocale` — locale names are normally ASCII but Meta bytes
5873/// in the assigned value (from a `LANG="$value"` round-trip
5874/// through metafied param storage) would otherwise reach
5875/// setlocale literally. The previous Rust port passed raw `x`
5876/// without unmeta'ing — divergent.
5877///
5878/// `strsetfn(pm, x)` stores the value in the param slot. The Rust
5879/// adaptation doesn't have a `pm` in scope; the assign path that
5880/// reaches langsetfn already stored the value in the paramtab,
5881/// so this body only runs the post-store side effect (locale).
5882///
5883/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x).
5884pub fn langsetfn(x: String) { // c:4898
5885 // c:4901 — `setlang(unmeta(x));`. Strip Meta bytes before
5886 // passing to libc setlocale.
5887 let unmeta_x = crate::ported::utils::unmeta(&x); // c:4901 unmeta(x)
5888 setlang(Some(&unmeta_x));
5889}
5890
5891/// Port of `lcsetfn(Param pm, char *x)` from `Src/params.c:4906`. C body
5892/// (c:4912-4931):
5893/// ```c
5894/// strsetfn(pm, x);
5895/// if ((x2 = getsparam("LC_ALL")) && *x2) return;
5896/// queue_signals();
5897/// if (!x || !*x) x = getsparam("LANG");
5898/// if (x && *x) {
5899/// for (ln = lc_names; ln->name; ln++)
5900/// if (!strcmp(ln->name, pm->node.nam))
5901/// setlocale(ln->category, unmeta(x));
5902/// }
5903/// unqueue_signals();
5904/// clear_mbstate();
5905/// inittyptab();
5906/// ```
5907///
5908/// Two divergences in the previous Rust port:
5909/// 1. Missed `inittyptab()` call at c:4932 — LC_CTYPE changes
5910/// shift which bytes are isalpha/iblank/isep, but the
5911/// typtab stayed pinned to the prior locale's classes.
5912/// `setopt POSIX_BUILTINS; LC_NUMERIC=tr_TR.UTF-8; ...`
5913/// would still classify with the old C locale's tables.
5914/// 2. The Meta-unmeta'ing on the value passed to setlocale
5915/// wasn't applied. C uses `setlocale(cat, unmeta(x))`.
5916pub fn lcsetfn(pm: &str, x: Option<String>) { // c:4906
5917 // c:4912-4913 — `if ((x2 = getsparam("LC_ALL")) && *x2) return;`.
5918 if let Some(lc_all) = getsparam("LC_ALL") { // c:4912
5919 if !lc_all.is_empty() {
5920 return;
5921 }
5922 }
5923 // c:4916-4917 — `if (!x || !*x) x = getsparam("LANG");`.
5924 let val = x
5925 .filter(|s| !s.is_empty())
5926 .or_else(|| getsparam("LANG").filter(|s| !s.is_empty())); // c:4917
5927 // c:4924-4928 — apply `setlocale(category, unmeta(x))` for the
5928 // matching LC_* category. The previous Rust port skipped the
5929 // actual libc setlocale call and only wrote the env var, so
5930 // assigning `LC_NUMERIC=tr_TR.UTF-8` never flipped libc's
5931 // numeric-formatting category.
5932 if let Some(v) = val {
5933 let unmeta = crate::ported::utils::unmeta(&v); // c:4928 unmeta(x)
5934 env::set_var(pm, &unmeta);
5935 for (name, category) in LC_NAMES { // c:4925
5936 if *name == pm { // c:4926 strcmp
5937 let cstr = std::ffi::CString::new(unmeta.as_bytes())
5938 .unwrap_or_default();
5939 unsafe {
5940 libc::setlocale(*category, cstr.as_ptr()); // c:4927
5941 }
5942 break;
5943 }
5944 }
5945 }
5946 // c:4930 — `clear_mbstate();` — LC_CTYPE may have changed.
5947 clear_mbstate();
5948 // c:4931 — `inittyptab();` — rebuild typtab classifications.
5949 // The previous Rust port skipped this; char-classification
5950 // predicates would stay pinned to the prior locale's class
5951 // set even after `LC_CTYPE=` was assigned.
5952 crate::ported::utils::inittyptab(); // c:4931
5953}
5954
5955/// Direct port of `static void argzerosetfn(UNUSED(Param pm),
5956/// char *x)` from `Src/params.c:4937-4946`. Setter for `$0` —
5957/// POSIX mode rejects assignment (read-only), zsh mode replaces
5958/// `argzero`.
5959///
5960/// C body:
5961/// if (x) {
5962/// if (isset(POSIXARGZERO))
5963/// zerr("read-only variable: 0");
5964/// else {
5965/// zsfree(argzero);
5966/// argzero = ztrdup(x);
5967/// }
5968/// zsfree(x);
5969/// }
5970/// Port of `argzerosetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4937`.
5971/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
5972pub fn argzerosetfn(x: String) { // c:4937
5973 // c:4937 — if (x).
5974 if !x.is_empty() {
5975 // c:4940 — isset(POSIXARGZERO) reject.
5976 if isset(crate::ported::zsh_h::POSIXARGZERO) {
5977 crate::ported::utils::zerr("read-only variable: 0"); // c:4941
5978 } else {
5979 // c:4943-4944 — zsfree(argzero); argzero = ztrdup(x).
5980 crate::ported::utils::set_argzero(Some(crate::ported::utils::ztrdup(&x)));
5981 }
5982 // c:4946 — `zsfree(x)`. Rust drop handles via move.
5983 }
5984}
5985
5986// -----------------------------------------------------------
5987// $0 / $#
5988// -----------------------------------------------------------
5989
5990/// Port of `argzerogetfn(UNUSED(Param pm))` from `Src/params.c:4954`. C body:
5991/// `return isset(POSIXARGZERO) ? posixzero : argzero;`
5992///
5993/// Both `argzero` and `posixzero` live in `utils.rs` (OnceLock storage).
5994/// The previous Rust port ALWAYS returned `argzero`, defeating the
5995/// POSIXARGZERO option entirely. After `exec -a foo` or function-call
5996/// argv-rewrite, `$0` under POSIXARGZERO should report the ORIGINAL
5997/// startup `argv[0]`, not the rewritten name. Now wired via
5998/// `isset(POSIXARGZERO)` + the canonical posixzero accessor.
5999/// WARNING: param names don't match C — Rust=() vs C=(pm)
6000pub fn argzerogetfn() -> String {
6001 if isset(crate::ported::zsh_h::POSIXARGZERO) { // c:4958
6002 crate::ported::utils::posixzero().unwrap_or_default() // c:4959
6003 } else {
6004 crate::ported::utils::argzero().unwrap_or_default() // c:4960
6005 }
6006}
6007
6008// -----------------------------------------------------------
6009// $HISTSIZE / $SAVEHIST
6010// -----------------------------------------------------------
6011
6012/// Port of `histsizegetfn(UNUSED(Param pm))` from `Src/params.c:4965`. C body: `return histsiz;`
6013/// WARNING: param names don't match C — Rust=() vs C=(pm)
6014pub fn histsizegetfn() -> i64 {
6015 *histsiz_lock().lock().expect("histsiz poisoned")
6016}
6017
6018/// Port of `histsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4974`. C body:
6019/// `if ((histsiz = v) < 1) histsiz = 1; resizehistents();`
6020///
6021/// The previous Rust port noted `resizehistents()` as "pending the
6022/// history-table port", but `crate::ported::hist::resizehistents`
6023/// IS available — was a stale comment. Without the resize call,
6024/// setting HISTSIZE to a smaller value left the in-memory ring
6025/// over-sized until the next implicit prune (next entry added).
6026/// Wired the call now per c:4977.
6027/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
6028pub fn histsizesetfn(v: i64) {
6029 *histsiz_lock().lock().expect("histsiz poisoned") = v.max(1);
6030 // c:4977 — mirror into the hist.rs atomic so resizehistents()
6031 // sees the new size, then trigger the prune.
6032 crate::ported::hist::histsiz.store(v.max(1), std::sync::atomic::Ordering::SeqCst);
6033 crate::ported::hist::resizehistents(); // c:4977
6034}
6035
6036/// Port of `savehistsizegetfn(UNUSED(Param pm))` from `Src/params.c:4985`. C body:
6037/// `return savehistsiz;`
6038/// WARNING: param names don't match C — Rust=() vs C=(pm)
6039pub fn savehistsizegetfn() -> i64 {
6040 *savehistsiz_lock().lock().expect("savehistsiz poisoned")
6041}
6042
6043/// Port of `savehistsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4994`. C body:
6044/// `if ((savehistsiz = v) < 0) savehistsiz = 0;`
6045///
6046/// The Rust port has TWO mirrors of `savehistsiz`: a `Mutex<i64>`
6047/// in params.rs (read by `savehistsizegetfn`) AND an AtomicI64
6048/// in hist.rs (read by the history-file writer at
6049/// `Src/hist.c:savehistfile` per c:3878). The previous Rust port
6050/// only wrote to the params.rs lock; `hist.rs::savehistsiz`
6051/// stayed pinned to its initial 0 value, so `SAVEHIST=10000`
6052/// would store the limit in `savehistsiz_lock` (visible to
6053/// `$SAVEHIST` reads) but the history-file writer would still
6054/// cap at the original AtomicI64 value (effectively saving zero
6055/// lines). Sync both storages so reads + writes agree.
6056///
6057/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
6058pub fn savehistsizesetfn(v: i64) { // c:4994
6059 let clamped = v.max(0); // c:4998
6060 *savehistsiz_lock().lock().expect("savehistsiz poisoned") = clamped;
6061 // Mirror to hist.rs::savehistsiz so the writer-side cap
6062 // matches the just-assigned value. C uses a single global;
6063 // the Rust port's twin-storage requires sync writes.
6064 crate::ported::hist::savehistsiz.store(
6065 clamped, std::sync::atomic::Ordering::SeqCst); // c:4994
6066}
6067
6068/// Port of `errnosetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:5004`. C body:
6069/// `errno = (int)x; if ((zlong)errno != x) zwarn("errno truncated on assignment");`
6070///
6071/// Rust note: `errno` is a libc thread-local; Rust uses `std::io::Error`
6072/// which captures the *last* call. To set errno for subsequent
6073/// `last_os_error()` reads on macOS / Linux, write through the libc
6074/// `__error()`/`__errno_location()` accessor.
6075/// C body (Src/params.c:5004):
6076/// `errno = (int)x;
6077/// if ((zlong)errno != x) zwarn("errno truncated on assignment");`
6078/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6079pub fn errnosetfn(x: i64) { // c:5004
6080 let truncated = x as i32;
6081 unsafe { *errno_ptr() = truncated; } // c:5006 errno = (int)x
6082 // c:5009-5010 — C uses `zwarn` (informational), NOT `zerr`. The
6083 // store happens unconditionally; the warning fires only on
6084 // truncation. Previously used `zerr` — divergent.
6085 if truncated as i64 != x { // c:5008
6086 crate::ported::utils::zwarn("errno truncated on assignment"); // c:5009
6087 }
6088}
6089
6090/// !!! RUST-ONLY HELPER — no direct C counterpart. C accesses
6091/// `errno` through the standard macro which the compiler resolves
6092/// to the per-platform getter (`__error()` on macOS, `__errno_location()`
6093/// on Linux). Rust libc exposes both as raw FFI; this helper picks
6094/// the right one per target so errnosetfn/errnogetfn stay one-liners.
6095#[inline]
6096unsafe fn errno_ptr() -> *mut libc::c_int {
6097 #[cfg(target_os = "macos")] { libc::__error() }
6098 #[cfg(target_os = "linux")] { libc::__errno_location() }
6099 #[cfg(not(any(target_os = "macos", target_os = "linux")))] { std::ptr::null_mut() }
6100}
6101
6102/// Port of `errnogetfn(UNUSED(Param pm))` from `Src/params.c:5015`. C body: `return errno;`
6103///
6104/// Reads the libc errno directly through the per-platform accessor
6105/// (matching C's `return errno;` semantics). Previously routed
6106/// through `std::io::Error::last_os_error()` which is NOT errno —
6107/// it's a snapshot taken at the most recent stdlib syscall. That
6108/// silently broke `$ERRNO` round-trip: `ERRNO=42` followed by
6109/// `$ERRNO` could return any stale value.
6110/// WARNING: param names don't match C — Rust=() vs C=(pm)
6111pub fn errnogetfn() -> i64 {
6112 let p = unsafe { errno_ptr() }; // c:5017 return errno
6113 if p.is_null() {
6114 // Non-Linux/macOS fallback: best-effort via std API.
6115 std::io::Error::last_os_error().raw_os_error().unwrap_or(0) as i64
6116 } else {
6117 unsafe { *p as i64 }
6118 }
6119}
6120
6121/// Port of `keyboardhackgetfn(UNUSED(Param pm))` from `Src/params.c:5024`. C body:
6122/// `static char buf[2]; buf[0] = keyboardhackchar; return buf;`
6123/// WARNING: param names don't match C — Rust=() vs C=(pm)
6124pub fn keyboardhackgetfn() -> String {
6125 let c = *keyboardhack_lock()
6126 .lock()
6127 .expect("keyboardhack poisoned");
6128 if c == 0 {
6129 String::new()
6130 } else {
6131 (c as char).to_string()
6132 }
6133}
6134
6135/// Port of `keyboardhacksetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5040-5060`. C body:
6136/// ```c
6137/// if (x) {
6138/// unmetafy(x, &len);
6139/// if (len > 1) { len = 1; zwarn("Only one KEYBOARD_HACK character can be defined"); }
6140/// for (i = 0; i < len; i++)
6141/// if (!isascii((unsigned char) x[i])) {
6142/// zwarn("KEYBOARD_HACK can only contain ASCII characters");
6143/// return;
6144/// }
6145/// keyboardhackchar = len ? (unsigned char) x[0] : '\0';
6146/// } else
6147/// keyboardhackchar = '\0';
6148/// ```
6149///
6150/// The C source `unmetafy(x, &len)` strips Meta-encoded prefix
6151/// bytes (collapsing every `Meta + (b^32)` pair to the original
6152/// byte) BEFORE the length and ASCII checks. The previous Rust
6153/// port skipped unmetafy, so:
6154/// - `len > 1` warning fired on every assignment of a Meta-
6155/// encoded single byte (the byte-length was 2 pre-unmetafy).
6156/// - ASCII check ran against the raw Meta byte (0x83) instead
6157/// of the demetafied result, falsely rejecting valid ASCII
6158/// characters that happened to round-trip through Meta
6159/// encoding in the assignment pipeline.
6160///
6161/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6162pub fn keyboardhacksetfn(x: String) { // c:5040
6163 // c:5044 — `unmetafy(x, &len)` — strip Meta-encoded pairs.
6164 // Run on the byte buffer so the protocol matches C's pointer
6165 // walk; the Rust `unmeta()` helper does the same fold.
6166 let unmeta = crate::ported::utils::unmeta(&x); // c:5044 unmetafy(x)
6167 let bytes = unmeta.as_bytes();
6168 // c:5046-5049 — `if (len > 1) { len = 1; zwarn(...); }`. The
6169 // length check happens AFTER unmetafy so a 2-byte Meta pair
6170 // representing a single byte doesn't trigger the warning.
6171 if bytes.len() > 1 {
6172 crate::ported::utils::zwarn("Only one KEYBOARD_HACK character can be defined");
6173 }
6174 let c = bytes.first().copied().unwrap_or(0);
6175 // c:5050-5054 — ASCII check runs on the unmetafied byte, NOT
6176 // the raw Meta byte. With unmetafy now in place this works as
6177 // C intended.
6178 if c >= 0x80 { // c:5051 !isascii(...)
6179 crate::ported::utils::zwarn("KEYBOARD_HACK can only contain ASCII characters");
6180 return;
6181 }
6182 // c:5056 — `keyboardhackchar = len ? (unsigned char) x[0] : '\0';`
6183 *keyboardhack_lock().lock().expect("keyboardhack poisoned") = c;
6184}
6185
6186/// Port of `histcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5064`. C body:
6187/// ```c
6188/// static char buf[4];
6189/// buf[0] = bangchar; buf[1] = hatchar; buf[2] = hashchar; buf[3] = '\0';
6190/// return buf;
6191/// ```
6192/// Reads from the three canonical atomic globals
6193/// (`crate::ported::hist::{bangchar, hatchar, hashchar}`) to mirror C
6194/// which reads from three separate `unsigned char` globals.
6195/// WARNING: param names don't match C — Rust=() vs C=(pm)
6196pub fn histcharsgetfn() -> String {
6197 use std::sync::atomic::Ordering;
6198 let b = crate::ported::hist::bangchar.load(Ordering::SeqCst) as u8;
6199 let h = crate::ported::hist::hatchar.load(Ordering::SeqCst) as u8;
6200 let p = crate::ported::hist::hashchar.load(Ordering::SeqCst) as u8;
6201 // c:5068-5073 — terminal NUL trims unset chars (default-`!^#` is
6202 // 3 non-NUL bytes); explicit NULs are skipped to match C `buf[3]
6203 // = '\0'` C-string truncation semantics.
6204 let mut s = String::new();
6205 for &byte in &[b, h, p] {
6206 if byte != 0 {
6207 s.push(byte as char);
6208 }
6209 }
6210 s
6211}
6212
6213/// Port of `histcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5081`. C body
6214/// validates ASCII, takes up to 3 chars; defaults `!^#` if NULL.
6215///
6216/// C `unmetafy(x, &len)` (c:5086) strips Meta-encoded pairs BEFORE
6217/// the length truncation and ASCII guard. The previous Rust port
6218/// skipped unmetafy entirely:
6219/// - `len > 3` truncation ran on raw byte length, so a Meta-pair
6220/// would inflate the byte count and skip valid chars.
6221/// - ASCII check ran against raw Meta bytes (0x83), falsely
6222/// rejecting valid round-tripped values.
6223///
6224/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6225pub fn histcharssetfn(x: Option<String>) { // c:5081
6226 use std::sync::atomic::Ordering;
6227 let new_chars: [u8; 3] = match x {
6228 None => {
6229 // c:5100-5103 — defaults `!^#` when x is NULL.
6230 [b'!', b'^', b'#']
6231 }
6232 Some(s) => {
6233 // c:5086 — `unmetafy(x, &len)`. Strip Meta pairs first.
6234 let unmeta = crate::ported::utils::unmeta(&s); // c:5086 unmetafy(x)
6235 let bytes = unmeta.as_bytes();
6236 // c:5087-5088 — `if (len > 3) len = 3;`. Truncation
6237 // applies AFTER unmetafy.
6238 let bytes = if bytes.len() > 3 { &bytes[..3] } else { bytes };
6239 for &b in bytes.iter() {
6240 if b >= 0x80 { // c:5090-5093
6241 // c:5091 — C uses `zwarn` (informational), NOT
6242 // `zerr` (fatal). Function returns early without
6243 // updating any globals.
6244 crate::ported::utils::zwarn(
6245 "HISTCHARS can only contain ASCII characters");
6246 return;
6247 }
6248 }
6249 // c:5095-5097 — `bangchar = x[0]; hatchar = x[1]; hashchar = x[2]`.
6250 // C uses `len ? x[0] : '\0'` etc — for short strings the
6251 // unset bytes are NUL.
6252 let mut chars = [0u8; 3];
6253 for (i, &b) in bytes.iter().enumerate() {
6254 chars[i] = b;
6255 }
6256 chars
6257 }
6258 };
6259 // c:5079 — set histchars table.
6260 *histchars_lock().lock().expect("histchars poisoned") = new_chars;
6261 // c:5095-5097 — `bangchar = x[0]; hatchar = x[1]; hashchar = x[2]`.
6262 // Sync all three per-char atomic globals so lex/hist callers
6263 // see the new HISTCHARS. (Previously hashchar was a `const char`
6264 // in lex.rs — promoted to atomic this iteration.)
6265 crate::ported::hist::bangchar.store(new_chars[0] as i32, Ordering::SeqCst);
6266 crate::ported::hist::hatchar.store(new_chars[1] as i32, Ordering::SeqCst);
6267 crate::ported::hist::hashchar.store(new_chars[2] as i32, Ordering::SeqCst);
6268 // c:5104 — `inittyptab();`. The bangchar special bit in typtab
6269 // depends on the current `bangchar` global; reseed.
6270 crate::ported::utils::inittyptab();
6271}
6272
6273/// Port of `homegetfn(UNUSED(Param pm))` from `Src/params.c:5109`. C body: `return home;`
6274/// WARNING: param names don't match C — Rust=() vs C=(pm)
6275pub fn homegetfn() -> String {
6276 home_lock().lock().expect("home poisoned").clone()
6277}
6278
6279/// Port of `homesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5118`. C body:
6280/// ```c
6281/// zsfree(home);
6282/// if (x && isset(CHASELINKS) && (home = xsymlink(x, 0)))
6283/// zsfree(x);
6284/// else
6285/// home = x ? x : ztrdup("");
6286/// finddir(NULL);
6287/// ```
6288/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6289pub fn homesetfn(x: String) {
6290 // c:5121-5126 — CHASELINKS path resolves symlinks before storing.
6291 // Falls through to the plain `x` store when CHASELINKS is off or
6292 // xsymlink fails.
6293 let resolved = if !x.is_empty()
6294 && crate::ported::zsh_h::isset(crate::ported::zsh_h::CHASELINKS)
6295 {
6296 crate::ported::utils::xsymlink(&x).unwrap_or(x)
6297 } else {
6298 x
6299 };
6300 *home_lock().lock().expect("home poisoned") = resolved;
6301 // c:5127 — `finddir(NULL)` invalidates zsh's cached named-directory
6302 // lookups. zshrs's finddir port has no cache (per hashnameddir.rs
6303 // createnameddirtable note); the call is a no-op here.
6304}
6305
6306/// Port of `wordcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5132`. C body:
6307/// `return wordchars;`
6308/// WARNING: param names don't match C — Rust=() vs C=(pm)
6309pub fn wordcharsgetfn() -> String {
6310 wordchars_lock()
6311 .lock()
6312 .expect("wordchars poisoned")
6313 .clone()
6314}
6315
6316/// Port of `wordcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5141`. C body:
6317/// `zsfree(wordchars); wordchars = x; inittyptab();`
6318/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6319pub fn wordcharssetfn(x: String) {
6320 *wordchars_lock().lock().expect("wordchars poisoned") = x;
6321 // c:5143 — `inittyptab()` rebuilds typtab IWORD bits from the
6322 // new WORDCHARS. Without this, every IWORD lookup stays pinned
6323 // to the old set and silently mis-classifies word boundaries.
6324 crate::ported::utils::inittyptab();
6325}
6326
6327/// Port of `underscoregetfn(UNUSED(Param pm))` from `Src/params.c:5152`. C body:
6328/// `char *u = dupstring(zunderscore); untokenize(u); return u;`
6329///
6330/// C runs `untokenize(u)` on the cloned string before returning, so
6331/// ITOK bytes (Pound..Nularg per `Src/zsh.h:159-194`) in `$_` get
6332/// replaced/dropped via the canonical `ztokens[]` table. The previous
6333/// Rust port skipped untokenize entirely — every `$_` read that
6334/// included a lexer-injected token byte exposed the raw token in user
6335/// output (e.g. `$_` containing `$cmd` would surface as raw Stringg
6336/// instead of the literal `$`).
6337/// WARNING: param names don't match C — Rust=() vs C=(pm)
6338pub fn underscoregetfn() -> String {
6339 let u = zunderscore_lock()
6340 .lock()
6341 .expect("zunderscore poisoned")
6342 .clone();
6343 crate::ported::lex::untokenize(&u) // c:5156 untokenize(u)
6344}
6345
6346/// Port of `term_reinit_from_pm()` from `Src/params.c:5163`.
6347/// C: `static void term_reinit_from_pm(void)` →
6348/// `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;
6349/// else init_term();`
6350pub fn term_reinit_from_pm() { // c:5163
6351 // c:5167 — `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;`
6352 let interactive = crate::ported::zsh_h::isset(crate::ported::options::optlookup("interactive"));
6353 let term = term_lock().lock().map(|s| s.clone()).unwrap_or_default();
6354 if !interactive || term.is_empty() { // c:5167
6355 TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed); // c:5168
6356 } else {
6357 // c:5170 — `init_term();` lives in ZLE; flag the next prompt
6358 // to re-init via TERM_UNKNOWN so the lazy path picks it up.
6359 TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed); // c:5170
6360 }
6361}
6362
6363/// Port of `termgetfn(UNUSED(Param pm))` from `Src/params.c:5176`. C body: `return term;`
6364/// WARNING: param names don't match C — Rust=() vs C=(pm)
6365pub fn termgetfn() -> String {
6366 term_lock().lock().expect("term poisoned").clone()
6367}
6368
6369/// Port of `termsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5185`. C body:
6370/// `zsfree(term); term = x ? x : ""; term_reinit_from_pm();`
6371/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6372pub fn termsetfn(x: String) {
6373 *term_lock().lock().expect("term poisoned") = x;
6374 term_reinit_from_pm();
6375}
6376
6377/// Port of `terminfogetfn(UNUSED(Param pm))` from `Src/params.c:5196`. C body:
6378/// `return zsh_terminfo ? zsh_terminfo : "";`
6379/// WARNING: param names don't match C — Rust=() vs C=(pm)
6380pub fn terminfogetfn() -> String {
6381 zsh_terminfo_lock()
6382 .lock()
6383 .expect("zsh_terminfo poisoned")
6384 .clone()
6385}
6386
6387/// Port of `int rprompt_indent` from `Src/init.c`. Set to 1 by
6388/// `init_term()` and reset by `rprompt_indent_unsetfn` when the
6389/// `RPROMPT_INDENT` parameter is unset.
6390pub static RPROMPT_INDENT: std::sync::Mutex<i32> = std::sync::Mutex::new(1);
6391
6392/// Port of `terminfosetfn(Param pm, char *x)` from `Src/params.c:5205`. C body:
6393/// `zsfree(zsh_terminfo); zsh_terminfo = x; addenv if exported; term_reinit_from_pm();`
6394/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6395pub fn terminfosetfn(x: String) {
6396 *zsh_terminfo_lock()
6397 .lock()
6398 .expect("zsh_terminfo poisoned") = x.clone();
6399 env::set_var("TERMINFO", &x);
6400 term_reinit_from_pm();
6401}
6402
6403/// Port of `terminfodirsgetfn(UNUSED(Param pm))` from `Src/params.c:5224`. C body:
6404/// `return zsh_terminfodirs ? zsh_terminfodirs : "";`
6405/// WARNING: param names don't match C — Rust=() vs C=(pm)
6406pub fn terminfodirsgetfn() -> String {
6407 zsh_terminfodirs_lock()
6408 .lock()
6409 .expect("zsh_terminfodirs poisoned")
6410 .clone()
6411}
6412
6413/// Port of `terminfodirssetfn(Param pm, char *x)` from `Src/params.c:5233`. C body
6414/// mirrors `terminfosetfn` for the TERMINFO_DIRS env var.
6415/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6416pub fn terminfodirssetfn(x: String) {
6417 *zsh_terminfodirs_lock()
6418 .lock()
6419 .expect("zsh_terminfodirs poisoned") = x.clone();
6420 env::set_var("TERMINFO_DIRS", &x);
6421 term_reinit_from_pm();
6422}
6423
6424// -----------------------------------------------------------
6425// $pipestatus
6426// -----------------------------------------------------------
6427
6428/// Port of `pipestatgetfn(UNUSED(Param pm))` from `Src/params.c:5251`. C body
6429/// snapshots the `pipestats[]` C array as a heap-allocated
6430/// `char **`. Rust port returns the cloned snapshot.
6431/// WARNING: param names don't match C — Rust=() vs C=(pm)
6432pub fn pipestatgetfn() -> Vec<String> {
6433 pipestats_lock()
6434 .lock()
6435 .expect("pipestats poisoned")
6436 .iter()
6437 .map(|n| n.to_string())
6438 .collect()
6439}
6440
6441/// Port of `pipestatsetfn(UNUSED(Param pm), char **x)` from `Src/params.c:5270`. C body:
6442/// `for (i=0; *x && i<MAX_PIPESTATS; i++) pipestats[i] = atoi(*x++); numpipestats = i;`
6443/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
6444pub fn pipestatsetfn(x: Option<Vec<String>>) {
6445 const MAX_PIPESTATS: usize = 256;
6446 let mut guard = pipestats_lock().lock().expect("pipestats poisoned");
6447 guard.clear();
6448 if let Some(v) = x {
6449 for s in v.iter().take(MAX_PIPESTATS) {
6450 guard.push(s.parse::<i32>().unwrap_or(0));
6451 }
6452 }
6453}
6454
6455/// Port of `arrfixenv(char *s, char **t)` from `Src/params.c:5285`. C body re-syncs
6456/// the env entry for an array param after mutation, joining with
6457/// the param's `joinchar`. Rust port joins with ':' (the default
6458/// for PATH-style arrays) and updates the env var.
6459/// Direct port of `void arrfixenv(char *s, char **t)` from
6460/// `Src/params.c:5285`. Re-syncs the env-side entry for an
6461/// array parameter after mutation. Order of operations (C body):
6462/// 1. If `t == path`, flush the command-name cache (c:5291).
6463/// 2. Look up the param node by name (c:5294); skip if
6464/// PM_HASHELEM is set (c:5300-5301).
6465/// 3. Under ALLEXPORT, mark PM_EXPORTED (c:5304); always clear
6466/// PM_DEFAULTED (c:5305).
6467/// 4. Skip if not PM_EXPORTED (c:5311-5312).
6468/// 5. joinchar = ':' for PM_SPECIAL else
6469/// `((struct tieddata *)pm->u.data)->joinchar` (c:5314-5318).
6470/// 6. `addenv(pm, t ? zjoin(t, joinchar, 1) : "")` (c:5319).
6471pub fn arrfixenv(s: &str, t: Option<&[String]>) { // c:5285
6472
6473 // c:5291 — `if (t == path) cmdnamtab->emptytable(cmdnamtab)`.
6474 // PATH change invalidates the command-name cache.
6475 if s == "PATH" || s == "path" {
6476 crate::ported::hashtable::emptycmdnamtable();
6477 }
6478
6479 // c:5294 — `pm = paramtab->getnode(paramtab, s)`.
6480 let pm_arc_data = {
6481 let tab = paramtab().read().unwrap();
6482 tab.get(s).map(|pm| (pm.node.flags, pm.gsu_a.is_some()))
6483 };
6484 let (flags, _has_gsu_a) = match pm_arc_data {
6485 Some(x) => x,
6486 None => {
6487 // No param yet — just sync via env::set_var as fallback.
6488 let val = t.map(|v| v.join(":")).unwrap_or_default();
6489 env::set_var(s, val);
6490 return;
6491 }
6492 };
6493
6494 // c:5300-5301 — `if (pm->flags & PM_HASHELEM) return`.
6495 if flags & PM_HASHELEM as i32 != 0 {
6496 return;
6497 }
6498
6499 // c:5304 — `if (isset(ALLEXPORT)) pm->flags |= PM_EXPORTED`.
6500 let allexport = isset(ALLEXPORT);
6501 // c:5305 — `pm->flags &= ~PM_DEFAULTED` always.
6502 {
6503 let mut tab = paramtab().write().unwrap();
6504 if let Some(pm) = tab.get_mut(s) {
6505 if allexport {
6506 pm.node.flags |= PM_EXPORTED as i32;
6507 }
6508 pm.node.flags &= !(PM_DEFAULTED as i32);
6509 }
6510 }
6511
6512 // c:5311-5312 — `if (!(pm->flags & PM_EXPORTED)) return`.
6513 let new_flags = {
6514 let tab = paramtab().read().unwrap();
6515 tab.get(s).map(|pm| pm.node.flags).unwrap_or(0)
6516 };
6517 if new_flags & PM_EXPORTED as i32 == 0 {
6518 return;
6519 }
6520
6521 // c:5314-5317 — joinchar selection.
6522 let joinchar = if new_flags & PM_SPECIAL as i32 != 0 {
6523 ':' // c:5315
6524 } else {
6525 // c:5317 — tieddata.joinchar; not modelled in current Param —
6526 // default to ':' which is correct for all currently-tied
6527 // array params (PATH/CDPATH/FPATH/etc.).
6528 ':'
6529 };
6530
6531 // c:5319 — `addenv(pm, t ? zjoin(t, joinchar, 1) : "")`.
6532 let joined = match t {
6533 Some(arr) => arr.join(&joinchar.to_string()),
6534 None => String::new(),
6535 };
6536 addenv(s, &joined);
6537}
6538
6539/// Direct port of `int zputenv(char *str)` from
6540/// `Src/params.c:5325-5382` (USE_SET_UNSET_ENV branch). Splits
6541/// `str` at the first `=`, validates the name is in the portable
6542/// character set (rejects any byte >= 128), and calls
6543/// `setenv(name, value, 1)`.
6544///
6545/// C body walks `str` byte-by-byte looking for either a high-byte
6546/// (reject) or `=` (split). On a clean ASCII `name=value`, it
6547/// temporarily writes `\0` at the `=` to splice off the name,
6548/// calls setenv, then restores the `=`. On `=`-less input, it
6549/// flags via DPUTS and still calls setenv with the whole string
6550/// as the name (with value pointing at the trailing `\0`). Rust
6551/// equivalent: split, set_var; the in-place mutation isn't
6552/// observable since we copy.
6553/// Port of `zputenv(char *str)` from `Src/params.c:5325`.
6554pub fn zputenv(str: &str) -> i32 { // c:5325
6555 if str.is_empty() {
6556 // c:5328 — DPUTS(!str, ...); treat as no-op.
6557 return 0;
6558 }
6559 let bytes = str.as_bytes();
6560 // c:5339-5341 — walk until `=` or high byte; reject high bytes.
6561 let mut ptr = 0;
6562 while ptr < bytes.len() && bytes[ptr] != b'=' && bytes[ptr] < 128 { // c:5339
6563 ptr += 1;
6564 }
6565 if ptr < bytes.len() && bytes[ptr] >= 128 { // c:5342
6566 // c:5351 — `return 1` to reject non-portable name.
6567 return 1;
6568 }
6569 if ptr < bytes.len() { // c:5352 `else if (*ptr)`
6570 // c:5353-5355 — write `\0` at `=`, setenv(name, value), restore.
6571 let name = &str[..ptr];
6572 let value = &str[ptr + 1..];
6573 env::set_var(name, value);
6574 0
6575 } else { // c:5356-5359
6576 // C: DPUTS(1, "bad environment string"); setenv(str, ptr, 1).
6577 // With no `=`, treat `str` as a bare name with empty value.
6578 env::set_var(str, "");
6579 0
6580 }
6581}
6582
6583/// Direct port of `int findenv(char *name, int *pos)` from
6584/// `Src/params.c:5391`. Walks `environ` looking for an
6585/// entry whose name component (bytes up to `=`) matches `name`.
6586/// Returns Some(index) on a match; the C source writes the
6587/// index into `*pos` and returns 1.
6588///
6589/// Rust signature differs (no out-param; returns `Option<usize>`)
6590/// — the C int-with-out-param idiom maps to `Option<index>` here.
6591/// Walks std::env::vars_os() which preserves the same ordering
6592/// as the underlying libc environ array.
6593pub fn findenv(name: &str) -> Option<usize> { // c:5391
6594 // c:5391 — `eq = strchr(name, '=')`. Strip any trailing `=value`.
6595 let nlen = name.find('=').unwrap_or(name.len()); // c:5397
6596 let bare = &name[..nlen];
6597
6598 // c:5398-5404 — walk environ until match. Use std::env::vars()
6599 // which preserves the same ordering as the underlying libc
6600 // environ.
6601 for (i, (k, _)) in std::env::vars_os().enumerate() {
6602 if let Some(s) = k.to_str() {
6603 if s == bare {
6604 return Some(i); // c:5401-5403
6605 }
6606 }
6607 }
6608 None // c:5406
6609}
6610
6611// -----------------------------------------------------------
6612// env management (zsh's wrapper around setenv/unsetenv).
6613// -----------------------------------------------------------
6614
6615/// Port of `zgetenv(char *name)` from `Src/params.c:5416`. C body walks
6616/// `environ` byte-by-byte. Rust port uses `std::env::var`.
6617pub fn zgetenv(name: &str) -> Option<String> {
6618 env::var(name).ok()
6619}
6620
6621/// Direct port of `static void copyenvstr(char *s, char *value,
6622/// int flags)` from `Src/params.c:5434`. Unmetafies `value`
6623/// into `s` (Meta NEXT pairs collapse to NEXT^32) and applies
6624/// PM_LOWER / PM_UPPER case folding per byte.
6625pub fn copyenvstr(buf: &mut String, value: &str, flags: i32) { // c:5434
6626 let flags_u = flags as u32;
6627 let mut it = value.bytes();
6628 while let Some(b) = it.next() { // c:5436
6629 let mut ch = b;
6630 if ch == crate::ported::zsh_h::META as u8 { // c:5437
6631 ch = match it.next() {
6632 Some(next) => next ^ 32, // c:5438
6633 None => break,
6634 };
6635 }
6636 if flags_u & crate::ported::zsh_h::PM_LOWER != 0 { // c:5439
6637 ch = ch.to_ascii_lowercase(); // c:5440
6638 } else if flags_u & crate::ported::zsh_h::PM_UPPER != 0 { // c:5441
6639 ch = ch.to_ascii_uppercase(); // c:5442
6640 }
6641 buf.push(ch as char);
6642 }
6643}
6644
6645/// Direct port of `void addenv(Param pm, char *value)` from
6646/// `Src/params.c:5448` (USE_SET_UNSET_ENV branch — the
6647/// portable one). C body:
6648/// 1. `newenv = mkenvstr(pm->nam, value, pm->flags)` (c:5463)
6649/// 2. `if (zputenv(newenv)) { free; pm->env=NULL; return }` (c:5464-5468)
6650/// 3. Otherwise: `if (pm->env) free(pm->env); pm->env = newenv;
6651/// pm->flags |= PM_EXPORTED` (c:5482-5484)
6652///
6653/// Rust takes `name` instead of `Param pm` and looks up the
6654/// `pm` node internally — the C body's only reads of `pm` are
6655/// `pm->nam`, `pm->flags`, `pm->env`, all available from
6656/// paramtab. The return type changes from `void` to `i32` so
6657/// callers can chain it; 0 = success, 1 = zputenv failed.
6658pub fn addenv(name: &str, value: &str) -> i32 { // c:5448
6659
6660 // c:5463 — `newenv = mkenvstr(pm->nam, value, pm->flags)`.
6661 let flags = {
6662 let tab = paramtab().read().unwrap();
6663 tab.get(name).map(|pm| pm.node.flags).unwrap_or(0)
6664 };
6665 let newenv = mkenvstr(name, value, flags);
6666 // c:5464-5468 — `if (zputenv(newenv)) { free; pm->env=NULL; return }`.
6667 if zputenv(&newenv) != 0 {
6668 let mut tab = paramtab().write().unwrap();
6669 if let Some(pm) = tab.get_mut(name) {
6670 pm.env = None;
6671 }
6672 return 1;
6673 }
6674 // c:5482-5484 — `pm->env = newenv; pm->flags |= PM_EXPORTED`.
6675 let mut tab = paramtab().write().unwrap();
6676 if let Some(pm) = tab.get_mut(name) {
6677 pm.env = Some(newenv);
6678 pm.node.flags |= PM_EXPORTED as i32;
6679 }
6680 0
6681}
6682
6683/// Direct port of `static char *mkenvstr(char *name, char *value,
6684/// int flags)` from `Src/params.c:5513`. Builds `name=value`
6685/// in a fresh heap-string, where `value` is unmetafied and
6686/// case-folded according to `flags` (PM_LOWER → lower, PM_UPPER →
6687/// upper). The C source computes the unmetafied length first via
6688/// the `while (*s && (*s++ != Meta || *s++ != 32))` loop, then
6689/// allocates and writes via copyenvstr; the Rust port appends to
6690/// a `String` so the length pre-scan is implicit.
6691pub fn mkenvstr(name: &str, value: &str, flags: i32) -> String { // c:5513
6692 let mut buf = String::with_capacity(name.len() + value.len() + 2);
6693 buf.push_str(name); // c:5522 strcpy(s, name)
6694 buf.push('='); // c:5524 *s = '='
6695 if !value.is_empty() { // c:5525
6696 copyenvstr(&mut buf, value, flags); // c:5526
6697 }
6698 buf // c:5530
6699}
6700
6701/// Direct port of `void delenvvalue(char *x)` from
6702/// `Src/params.c:5542`. Removes `x` from environ by walking
6703/// to its pointer and shifting subsequent entries down one slot.
6704///
6705/// C body operates on the environ array directly. The Rust port
6706/// uses `env::remove_var(name)` since Rust's env is mediated by
6707/// libc::unsetenv internally — same shift semantics.
6708pub fn delenvvalue(name: &str) { // c:5542
6709 env::remove_var(name); // c:5542 equivalent
6710}
6711
6712/// Direct port of `void delenv(Param pm)` from
6713/// `Src/params.c:5563-5582`. Removes the param's env entry and
6714/// clears `pm->env`. Under USE_SET_UNSET_ENV (the portable
6715/// branch) the C body is:
6716/// unsetenv(pm->node.nam);
6717/// zsfree(pm->env);
6718/// pm->env = NULL;
6719///
6720/// "Note we don't remove PM_EXPORT from the flags. This may be
6721/// asking for trouble but we need to know later if we restore
6722/// this parameter to its old value." (c:5575-5577)
6723///
6724/// Rust signature drift: takes `&str` (the param name) instead
6725/// of `&mut Param`. The pm.env field is cleared via the paramtab
6726/// lookup; PM_EXPORTED is intentionally preserved per the C
6727/// comment.
6728pub fn delenv(name: &str) { // c:5563
6729 // c:5563 — `unsetenv(pm->node.nam)`.
6730 env::remove_var(name);
6731 // c:5568 / c:5572 — `pm->env = NULL`. PM_EXPORTED stays set.
6732 let mut tab = paramtab().write().unwrap();
6733 if let Some(pm) = tab.get_mut(name) {
6734 pm.env = None;
6735 }
6736}
6737
6738/// Port of `convbase_ptr(char *s, zlong v, int base, int *ndigits)` from `Src/params.c:5586`. C body
6739/// converts `v` into base `base` (negative `base` suppresses the
6740/// "0x"/"N#" discriminator), writing the digits into `s` and
6741/// returning the digit count via `*ndigits`. Rust port returns
6742/// `(formatted_string, digit_count)` since Rust strings own
6743/// their buffer.
6744/// WARNING: param names don't match C — Rust=(v, base) vs C=(s, v, base, ndigits)
6745pub fn convbase_ptr(v: i64, base: i32) -> (String, i32) {
6746 let mut s = String::new();
6747 let mut value = v;
6748 if value < 0 {
6749 s.push('-');
6750 value = -value;
6751 }
6752 let mut b = base;
6753 if (-1..=1).contains(&b) {
6754 b = -10;
6755 }
6756 if b > 0 {
6757 if isset(crate::ported::zsh_h::CBASES) && b == 16 {
6758 s.push_str("0x");
6759 } else if isset(crate::ported::zsh_h::CBASES)
6760 && b == 8
6761 && isset(crate::ported::zsh_h::OCTALZEROES)
6762 {
6763 s.push('0');
6764 } else if b != 10 {
6765 s.push_str(&format!("{}#", b));
6766 }
6767 } else {
6768 b = -b;
6769 }
6770 let base_u = b as u64;
6771 let mut x = value as u64;
6772 let mut digs: i32 = 0;
6773 while x != 0 {
6774 x /= base_u;
6775 digs += 1;
6776 }
6777 if digs == 0 {
6778 digs = 1;
6779 }
6780 let mut digits: Vec<u8> = vec![0u8; digs as usize];
6781 let mut i = digs - 1;
6782 let mut x = value as u64;
6783 while i >= 0 {
6784 let dig = (x % base_u) as u8;
6785 digits[i as usize] = if dig < 10 {
6786 b'0' + dig
6787 } else {
6788 b'A' + dig - 10
6789 };
6790 x /= base_u;
6791 i -= 1;
6792 }
6793 s.push_str(std::str::from_utf8(&digits).unwrap_or(""));
6794 (s, digs)
6795}
6796
6797// ---------------------------------------------------------------------------
6798// Integer/Float conversion (from convbase/convfloat)
6799// ---------------------------------------------------------------------------
6800
6801/// Port of `convbase(char *s, zlong v, int base)` from
6802/// `Src/params.c:5632`. C body (single statement):
6803/// `convbase_ptr(s, v, base, NULL);`
6804/// Rust takes (v, base) and returns the formatted string since Rust
6805/// strings own their buffer; the discarded `ndigits` out-param of
6806/// `convbase_ptr` is `.1` of the returned tuple.
6807/// WARNING: param names don't match C — Rust=(val, base) vs C=(s, v, base)
6808pub fn convbase(val: i64, base: u32) -> String { // c:5632
6809 convbase_ptr(val, base as i32).0 // c:5634
6810}
6811
6812/// Convert integer to string with underscores for readability
6813/// Port of `convbase_underscore(char *s, zlong v, int base, int underscore)` from `Src/params.c:5646`.
6814/// WARNING: param names don't match C — Rust=(val, base, underscore) vs C=(s, v, base, underscore)
6815pub fn convbase_underscore(val: i64, base: u32, underscore: i32) -> String {
6816 let s = convbase(val, base);
6817 if underscore <= 0 {
6818 return s;
6819 }
6820
6821 // Find the digits portion
6822 let (prefix, digits) = if let Some(rest) = s.strip_prefix('-') {
6823 let digit_start = rest
6824 .find(|c: char| c.is_ascii_digit() || c.is_ascii_uppercase())
6825 .unwrap_or(0);
6826 (&s[..1 + digit_start], &rest[digit_start..])
6827 } else {
6828 let digit_start = s
6829 .find(|c: char| c.is_ascii_digit() || c.is_ascii_uppercase())
6830 .unwrap_or(0);
6831 (&s[..digit_start], &s[digit_start..])
6832 };
6833
6834 if digits.len() <= underscore as usize {
6835 return s;
6836 }
6837
6838 let u = underscore as usize;
6839 let mut result = prefix.to_string();
6840 let chars: Vec<char> = digits.chars().collect();
6841 let first_group = chars.len() % u;
6842 if first_group > 0 {
6843 result.extend(&chars[..first_group]);
6844 if first_group < chars.len() {
6845 result.push('_');
6846 }
6847 }
6848 for (i, chunk) in chars[first_group..].chunks(u).enumerate() {
6849 if i > 0 {
6850 result.push('_');
6851 }
6852 result.extend(chunk);
6853 }
6854 result
6855}
6856
6857/// Port of `convfloat(double dval, int digits, int flags, FILE *fout)` from `Src/params.c:5689`.
6858///
6859/// C signature: `char *convfloat(double dval, int digits, int flags,
6860/// FILE *fout)` — picks `%e` / `%f` / `%g` based on PM_EFLOAT /
6861/// PM_FFLOAT (line 5705-5727), then snprintf'd with `digits` precision.
6862/// When neither E nor F flag is set, zsh uses `%.*g` with a default
6863/// of 17 significant digits (line 5712-5714). E-flag with N significant
6864/// figures decrements `digits` because `%e` counts decimal places not
6865/// significants (line 5720-5725).
6866///
6867/// Rust signature drops the `fout` parameter — every caller wanted the
6868/// returned string. IEEE specials (inf/nan) hand-formatted to `Inf`/
6869/// `-Inf`/`NaN` ahead of the snprintf, matching the C source's Inf/NaN
6870/// shortcuts at lines 5733-5736 / 5742-5744. The trailing-dot rule for
6871/// integer-valued floats (`5` -> `5.`) is added by the caller (params'
6872/// internal printing path) in C zsh; mirrored here for the no-flag case
6873/// so `MathNum::(crate::ported::math::mn_format_subst(Float(5.0)))` produces `5.` not `5`.
6874/// WARNING: param names don't match C — Rust=(dval, digits, pm_flags) vs C=(dval, digits, flags, fout)
6875pub fn convfloat(dval: f64, digits: i32, pm_flags: u32) -> String {
6876 if dval.is_infinite() { // c:5742
6877 return if dval < 0.0 {
6878 "-Inf".to_string()
6879 } else {
6880 "Inf".to_string()
6881 };
6882 }
6883 if dval.is_nan() { // c:5744
6884 return "NaN".to_string();
6885 }
6886 // Pick fmt char + adjust digits per the C cascade at 5705-5727.
6887 let (fmt_char, digits) = if (pm_flags & crate::ported::zsh_h::PM_EFLOAT) != 0 { // c:5715
6888 let d = if digits <= 0 { 10 } else { digits }; // c:5718
6889 ('e', (d - 1).max(0)) // c:5725
6890 } else if (pm_flags & crate::ported::zsh_h::PM_FFLOAT) != 0 { // c:5716
6891 let d = if digits <= 0 { 10 } else { digits }; // c:5718
6892 ('f', d)
6893 } else {
6894 let d = if digits == 0 { 17 } else { digits }; // c:5713
6895 ('g', d)
6896 };
6897 // Mirror zsh's snprintf path (Src/params.c:5751) — the C source
6898 // uses `VARARR(char, buf, 512 + digits)` for %f's full integer-
6899 // part expansion. 512 + 17 = 529 covers the zsh general case;
6900 // wider buffers below for the unbounded %f.
6901 let buf_len = 512usize + digits as usize + 4;
6902 let mut buf = vec![0u8; buf_len];
6903 let fmt = match fmt_char {
6904 'e' => c"%.*e",
6905 'f' => c"%.*f",
6906 _ => c"%.*g",
6907 };
6908 // SAFETY: buf has the C-required size for any double precision; fmt
6909 // is a NUL-terminated literal; snprintf writes ASCII only.
6910 let n = unsafe {
6911 libc::snprintf(
6912 buf.as_mut_ptr() as *mut libc::c_char,
6913 buf_len,
6914 fmt.as_ptr(),
6915 digits as libc::c_int,
6916 dval,
6917 )
6918 };
6919 if n < 0 {
6920 return format!("{}", dval);
6921 }
6922 let len = (n as usize).min(buf_len - 1);
6923 buf.truncate(len);
6924 let mut s = String::from_utf8(buf).unwrap_or_else(|_| format!("{}", dval));
6925 // zsh's general-format (%g) callers (math `$(( ))` substitution)
6926 // append `.` when the output has no `e` and no `.`, so integer-
6927 // valued floats like `5` render as `5.`. PM_EFLOAT/PM_FFLOAT skip
6928 // this rule (the format spec already pins shape).
6929 if fmt_char == 'g' && !s.contains('e') && !s.contains('.') {
6930 s.push('.');
6931 }
6932 s
6933}
6934
6935/// Start a parameter scope.
6936/// Port of `startparamscope()` (Src/init.c) — the C source pushes the
6937/// current scope counter so `local`-declared params disappear on function
6938/// exit. Rust port operates on the bucket-2 holder `paramtab` via a
6939/// `&mut crate::ported::zsh_h::HashTable` argument.
6940pub fn startparamscope(_table: &mut crate::ported::zsh_h::HashTable) {
6941 crate::ported::utils::inc_locallevel();
6942}
6943
6944/// Port of `endparamscope()` from `Src/params.c:5857`. C signature:
6945/// `mod_export void endparamscope(void)`. Decrements `locallevel`,
6946/// pops any pushed history stack, then iterates `paramtab` calling
6947/// `scanendscope` to restore/unset every param whose `level`
6948/// exceeds the new `locallevel`. Operates on the global `paramtab`
6949/// just like C — no parameter, no fake injection wrapper.
6950pub fn endparamscope() {
6951 queue_signals();
6952 crate::ported::utils::dec_locallevel(); // c:5861 locallevel--
6953 // c:5863 — `saveandpophiststack(0, HFILE_USE_OPTIONS);`. Pop
6954 // all stack entries with locallevel > current.
6955 crate::ported::hist::saveandpophiststack(0, crate::ported::zsh_h::HFILE_USE_OPTIONS as i32);
6956 let ll = crate::ported::utils::locallevel();
6957 // c:5867 scanhashtable(paramtab, 0, 0, 0, scanendscope, 0). Walk
6958 // the live paramtab (HashMap-backed until the hashtable.c vtable
6959 // is wired) and apply scanendscope's `pm->level > locallevel`
6960 // filter, restoring the `pm.old` chain or removing the entry.
6961 if let Ok(mut tab) = paramtab().write() {
6962 let stale: Vec<String> = tab.iter()
6963 .filter_map(|(k, pm)| if pm.level > ll { Some(k.clone()) } else { None })
6964 .collect();
6965 for n in stale {
6966 // c:scanendscope:5903 — non-special path: restore pm.old
6967 // (or remove if no outer binding existed).
6968 if let Some(pm) = tab.remove(&n) {
6969 if let Some(prev) = pm.old { // c:scanendscope:5933 pm->old = tpm->old
6970 tab.insert(n, prev); // restore outer binding (Box<param>)
6971 }
6972 // else: c:5966 unsetparam_pm — name unset entirely
6973 }
6974 }
6975 }
6976 unqueue_signals();
6977}
6978
6979/// Port of `scanendscope(HashNode hn, UNUSED(int flags))` from `Src/params.c:5900`. Per-node
6980/// callback used by `endparamscope` (params.c:5867 calls
6981/// `scanhashtable(paramtab, 0, 0, 0, scanendscope, 0)`) when a
6982/// function returns. C body:
6983/// ```c
6984/// Param pm = (Param)hn;
6985/// if (pm->level > locallevel) {
6986/// if ((pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL) {
6987/// /* Non-removable special — restore from pm->old in-place. */
6988/// Param tpm = pm->old;
6989/// #ifdef USE_LOCALE
6990/// if (!strncmp(pm->node.nam, "LC_", 3) ||
6991/// !strcmp(pm->node.nam, "LANG"))
6992/// lc_update_needed = 1;
6993/// #endif
6994/// if (!strcmp(pm->node.nam, "SECONDS")) {
6995/// setsecondstype(pm, PM_TYPE(tpm->node.flags),
6996/// PM_TYPE(pm->node.flags));
6997/// setrawseconds(tpm->u.dval);
6998/// tpm->node.flags |= PM_NORESTORE;
6999/// }
7000/// pm->old = tpm->old;
7001/// pm->node.flags = (tpm->node.flags & ~PM_NORESTORE);
7002/// pm->level = tpm->level;
7003/// pm->base = tpm->base;
7004/// pm->width = tpm->width;
7005/// if (pm->env) delenv(pm);
7006/// if (!(tpm->node.flags & (PM_NORESTORE|PM_READONLY)))
7007/// switch (PM_TYPE(pm->node.flags)) {
7008/// case PM_SCALAR: case PM_NAMEREF:
7009/// pm->gsu.s->setfn(pm, tpm->u.str); break;
7010/// case PM_INTEGER:
7011/// pm->gsu.i->setfn(pm, tpm->u.val); break;
7012/// case PM_EFLOAT: case PM_FFLOAT:
7013/// pm->gsu.f->setfn(pm, tpm->u.dval); break;
7014/// case PM_ARRAY:
7015/// pm->gsu.a->setfn(pm, tpm->u.arr); break;
7016/// case PM_HASHED:
7017/// pm->gsu.h->setfn(pm, tpm->u.hash); break;
7018/// }
7019/// zfree(tpm, sizeof(*tpm));
7020/// if (pm->node.flags & PM_EXPORTED) export_param(pm);
7021/// } else
7022/// unsetparam_pm(pm, 0, 0);
7023/// }
7024/// ```
7025/// Rust port mirrors the structure 1:1. `locallevel` is a global
7026/// in C (Src/init.c) — we accept it as a parameter since the
7027/// global isn't yet ported. `setsecondstype`/`setrawseconds`/
7028/// `delenv` are not yet in zshrs and route through best-effort
7029/// no-ops for now (C macros / Src/params.c:5900 / Src/params.c:5900).
7030pub fn scanendscope(pm: &mut crate::ported::zsh_h::param, _flags: i32) { // c:5900
7031 let cur_local = locallevel.load(std::sync::atomic::Ordering::Relaxed);
7032 if pm.level <= cur_local { // c:5903
7033 return;
7034 }
7035 let pmflags = pm.node.flags as u32;
7036 if (pmflags & (PM_SPECIAL | PM_REMOVABLE)) == PM_SPECIAL {
7037 // Take ownership of the saved old param.
7038 let mut tpm = match pm.old.take() {
7039 Some(t) => t,
7040 None => {
7041 // C uses DPUTS — fatal in debug, silent in release.
7042 return;
7043 }
7044 };
7045
7046 // USE_LOCALE branch: LC_*/LANG bumps lc_update_needed.
7047 // Global not yet ported; placeholder comment retains intent.
7048 if pm.node.nam.starts_with("LC_") || pm.node.nam == "LANG" {
7049 LC_UPDATE_NEEDED.store(1, std::sync::atomic::Ordering::SeqCst);
7050 }
7051
7052 if pm.node.nam == "SECONDS" {
7053 // setsecondstype(pm, PM_TYPE(tpm.flags), PM_TYPE(pm.flags));
7054 // setrawseconds(tpm.u_dval);
7055 tpm.node.flags |= PM_NORESTORE as i32;
7056 }
7057
7058 // pm->old = tpm->old;
7059 pm.old = tpm.old.take();
7060 // pm->node.flags = tpm->node.flags & ~PM_NORESTORE;
7061 pm.node.flags = (tpm.node.flags as u32 & !PM_NORESTORE) as i32;
7062 pm.level = tpm.level;
7063 pm.base = tpm.base;
7064 pm.width = tpm.width;
7065
7066 if pm.env.is_some() {
7067 delenv(&pm.node.nam);
7068 pm.env = None;
7069 }
7070
7071 let restore = (tpm.node.flags as u32 & (PM_NORESTORE | PM_READONLY)) == 0;
7072 if restore {
7073 match PM_TYPE(pm.node.flags as u32) {
7074 t if t == PM_SCALAR || t == PM_NAMEREF => {
7075 // pm->gsu.s->setfn(pm, tpm->u.str)
7076 pm.u_str = tpm.u_str.clone();
7077 }
7078 t if t == PM_INTEGER => {
7079 pm.u_val = tpm.u_val;
7080 }
7081 t if t == PM_EFLOAT || t == PM_FFLOAT => {
7082 pm.u_dval = tpm.u_dval;
7083 }
7084 t if t == PM_ARRAY => {
7085 pm.u_arr = tpm.u_arr.clone();
7086 }
7087 t if t == PM_HASHED => {
7088 pm.u_hash = tpm.u_hash.take();
7089 }
7090 _ => {}
7091 }
7092 }
7093 // zfree(tpm) — Rust drops the Box at end of scope.
7094 drop(tpm);
7095
7096 if (pm.node.flags as u32 & PM_EXPORTED) != 0 {
7097 export_param(pm);
7098 }
7099 } else {
7100 unsetparam_pm(pm, 0, 0);
7101 }
7102}
7103
7104/// Direct port of `void freeparamnode(HashNode hn)` from
7105/// `Src/params.c:5977-5994`. Frees a Param node, including
7106/// running its unsetfn callback when the global `delunset` flag
7107/// is set.
7108///
7109/// C body:
7110/// if (delunset)
7111/// pm->gsu.s->unsetfn(pm, 1); // c:5977
7112/// zsfree(pm->node.nam); // c:5977
7113/// if (!(pm->flags & PM_SPECIAL)) // c:5977
7114/// zsfree(pm->ename); // c:5977
7115/// zfree(pm, sizeof(struct param)); // c:5977
7116///
7117/// Rust's Drop handles every zsfree/zfree above; the explicit
7118/// step here is the optional unsetfn dispatch when `DELUNSET` is
7119/// non-zero. The remaining drop cascade fires when `_hn`
7120/// (`Box<param>`) leaves scope.
7121pub fn freeparamnode(mut _hn: crate::ported::zsh_h::Param) { // c:5977
7122 // c:5977-5987 — `if (delunset) pm->gsu.s->unsetfn(pm, 1);`.
7123 if DELUNSET.load(std::sync::atomic::Ordering::Relaxed) != 0 {
7124 // The Rust port's stdunsetfn writes the unset state back to
7125 // paramtab; calling it on the about-to-drop param re-marks
7126 // its slot in the table so consumers that read the table
7127 // see PM_UNSET on the next lookup.
7128 stdunsetfn(_hn.as_mut(), 1); // c:5987
7129 }
7130 // c:5988-5992 — drop cascade frees nam / ename (non-PM_SPECIAL)
7131 // / struct itself when _hn goes out of scope.
7132}
7133
7134/// Port of `printparamvalue(Param p, int printflags)` from `Src/params.c:6035`. C body
7135/// dispatches on `PM_TYPE(p->node.flags)` and writes the value
7136/// (no `name=` prefix unless `!PRINT_KV_PAIR`, which prints `=`
7137/// first). PM_SCALAR/PM_NAMEREF: `quotedzputs(t)`; PM_INTEGER:
7138/// `printf("%ld")`; PM_EFLOAT/PM_FFLOAT: `convfloat(...)`;
7139/// PM_ARRAY: `( v1 v2 ... )` with `\n ` separators on
7140/// PRINT_LINE; PM_HASHED: same shape via scan callback.
7141pub fn printparamvalue(p: &mut crate::ported::zsh_h::param, printflags: i32) {
7142 if (printflags & PRINT_KV_PAIR) == 0 {
7143 print!("=");
7144 }
7145 let t = PM_TYPE(p.node.flags as u32);
7146 if t == PM_SCALAR || t == PM_NAMEREF {
7147 let s = strgetfn(p);
7148 // c:6053 — `quotedzputs(t, stdout)`. The previous Rust port
7149 // used `print!("{}", s)` (raw), losing the shell-quoting
7150 // that `typeset -p VAR` expects. Without quoting, `eval
7151 // "$(typeset -p VAR)"` round-trip is BROKEN for any value
7152 // with spaces, special chars, or shell metacharacters.
7153 print!("{}", crate::ported::utils::quotedzputs(&s)); // c:6053
7154 } else if t == PM_INTEGER {
7155 print!("{}", intgetfn(p));
7156 } else if t == PM_EFLOAT || t == PM_FFLOAT {
7157 // c:6063 — `convfloat(p->gsu.f->getfn(p), p->base, p->node.flags,
7158 // stdout)`. Honors pm.base for precision and
7159 // pm.flags for PM_EFLOAT/PM_FFLOAT format selection. The
7160 // previous Rust port used `print!("{}", floatgetfn(p))`
7161 // which always renders in Rust's default float format
7162 // (which differs from C's printf %g / %e formats).
7163 print!("{}", crate::ported::utils::convfloat(
7164 floatgetfn(p), p.base, p.node.flags as u32)); // c:6063
7165 } else if t == PM_ARRAY {
7166 if (printflags & PRINT_KV_PAIR) == 0 {
7167 print!("(");
7168 if (printflags & PRINT_LINE) == 0 {
7169 print!(" ");
7170 }
7171 }
7172 let arr = arrgetfn(p);
7173 if !arr.is_empty() {
7174 if (printflags & PRINT_LINE) != 0 {
7175 if (printflags & PRINT_KV_PAIR) != 0 {
7176 print!(" ");
7177 } else {
7178 print!("\n ");
7179 }
7180 }
7181 print!("{}", arr[0]);
7182 for el in &arr[1..] {
7183 if (printflags & PRINT_LINE) != 0 {
7184 print!("\n ");
7185 } else {
7186 print!(" ");
7187 }
7188 print!("{}", el);
7189 }
7190 if (printflags & (PRINT_LINE | PRINT_KV_PAIR)) == PRINT_LINE {
7191 println!();
7192 }
7193 }
7194 if (printflags & PRINT_KV_PAIR) == 0 {
7195 if (printflags & PRINT_LINE) == 0 {
7196 print!(" ");
7197 }
7198 print!(")");
7199 }
7200 } else if t == PM_HASHED {
7201 if (printflags & PRINT_KV_PAIR) == 0 {
7202 print!("(");
7203 if (printflags & PRINT_LINE) == 0 {
7204 print!(" ");
7205 }
7206 }
7207 // scanhashtable + ht->printnode — backend not yet wired.
7208 if (printflags & PRINT_KV_PAIR) == 0 {
7209 print!(")");
7210 }
7211 }
7212}
7213
7214/// Port of `printparamnode(HashNode hn, int printflags)` from `Src/params.c:6123`. Real C
7215/// body is ~200 lines emitting the typeset/declare-style listing
7216/// for one param honouring PRINT_NAMEONLY / PRINT_TYPESET /
7217/// PRINT_KV_PAIR / PRINT_LINE / PRINT_INCLUDEVALUE /
7218/// PRINT_POSIX_READONLY / PRINT_POSIX_EXPORT / PRINT_WITH_NAMESPACE
7219/// and the per-paramtypes attribute table. Faithful direct port
7220/// of the common path: skip-on-`.`-prefix without WITH_NAMESPACE,
7221/// skip-on-PM_UNSET (with the POSIX preserve), AUTOLOAD gating,
7222/// then `nam` + `=value` via `printparamvalue`.
7223pub fn printparamnode(hn: &mut crate::ported::zsh_h::param, mut printflags: i32) {
7224 const PRINT_WITH_NAMESPACE: i32 = 1 << 8; // matches createspecial print enum
7225 let f = hn.node.flags as u32;
7226 if (f & PM_HASHELEM) == 0
7227 && (printflags & PRINT_WITH_NAMESPACE) == 0
7228 && hn.node.nam.starts_with('.')
7229 {
7230 return;
7231 }
7232 if (f & PM_UNSET) != 0 {
7233 // c:6133-6143 — POSIX readonly/exported keep + PM_DEFAULTED
7234 // path: show as readonly/exported even if unset, with no
7235 // value (NAMEONLY).
7236 let posix_keep = (printflags & (PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0
7237 && (f & (PM_READONLY | PM_EXPORTED)) != 0;
7238 let defaulted = (f & PM_DEFAULTED) == PM_DEFAULTED; // c:6137
7239 if posix_keep || defaulted {
7240 printflags |= PRINT_NAMEONLY;
7241 } else {
7242 return;
7243 }
7244 }
7245 if (f & PM_AUTOLOAD) != 0 {
7246 printflags |= PRINT_NAMEONLY;
7247 }
7248 if (printflags & (PRINT_TYPESET | PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0 {
7249 if (f & PM_AUTOLOAD) != 0 {
7250 return;
7251 }
7252 // c:6157-6163 — PM_RO_BY_DESIGN with level check. C uses
7253 // `if (hn->level != locallevel) return;` — only show the
7254 // entry when its level matches the current scope. The
7255 // previous Rust port hardcoded `locallevel = 0` with a
7256 // "global not yet wired" comment, but the canonical
7257 // global IS at params.rs (declared above). Read it live.
7258 if (f & PM_RO_BY_DESIGN) != 0 {
7259 let cur_ll = locallevel
7260 .load(std::sync::atomic::Ordering::Relaxed) as i32;
7261 if hn.level != cur_ll { // c:6157
7262 return;
7263 }
7264 }
7265 if (printflags & PRINT_POSIX_EXPORT) != 0 {
7266 if (f & PM_EXPORTED) == 0 { return; }
7267 print!("export ");
7268 } else if (printflags & PRINT_POSIX_READONLY) != 0 {
7269 if (f & PM_READONLY) == 0 { return; }
7270 print!("readonly ");
7271 } else {
7272 print!("typeset ");
7273 }
7274 }
7275 if (printflags & PRINT_KV_PAIR) != 0 {
7276 // hashelem path: print key without name= leader.
7277 }
7278 print!("{}", hn.node.nam);
7279 if (printflags & PRINT_NAMEONLY) != 0 {
7280 if (printflags & PRINT_KV_PAIR) == 0 { println!(); }
7281 return;
7282 }
7283 if (printflags & (PRINT_INCLUDEVALUE | PRINT_TYPESET)) != 0
7284 || (printflags & PRINT_NAMEONLY) == 0
7285 {
7286 printparamvalue(hn, printflags);
7287 }
7288 if (printflags & PRINT_KV_PAIR) == 0 {
7289 println!();
7290 }
7291}
7292
7293/// Port of `resolve_nameref(Param pm)` from `Src/params.c:6325`. C body:
7294/// ```c
7295/// mod_export Param
7296/// resolve_nameref(Param pm)
7297/// {
7298/// return resolve_nameref_rec(pm, NULL, 0);
7299/// }
7300/// ```
7301/// Public entry point that walks the nameref alias chain to the
7302/// final non-nameref `param`. Stop-pm and keep_lastref are
7303/// internal; this wrapper hardcodes both per the C body.
7304/// WARNING: param names don't match C — Rust=() vs C=(pm)
7305pub fn resolve_nameref( // c:6325
7306 pm: Option<crate::ported::zsh_h::Param>,
7307) -> Option<crate::ported::zsh_h::Param> {
7308 resolve_nameref_rec(pm, None, 0) // c:6327
7309}
7310
7311/// Port of `resolve_nameref_rec(Param pm, const Param stop, int keep_lastref)` from `Src/params.c:6332`. C
7312/// recursive helper for `resolve_nameref()`. Walks the chain of
7313/// `${(P)var}` indirections via `gethashnode2(realparamtab, refname)`
7314/// + `loadparamnode(paramtab, upscope(pm, ref), refname)`,
7315/// checking PM_TAGGED for cycle detection, and returns the
7316/// final non-nameref Param. Returns the input `pm` unchanged
7317/// for the early-exit path (no NAMEREF / UNSET / has subscript /
7318/// empty refname). Full chain walk requires `gethashnode2` on
7319/// `realparamtab` — pending the HashTable vtable.
7320#[allow(unused_variables)]
7321pub fn resolve_nameref_rec(
7322 pm: Option<crate::ported::zsh_h::Param>,
7323 stop: Option<&crate::ported::zsh_h::param>,
7324 keep_lastref: i32,
7325) -> Option<crate::ported::zsh_h::Param> {
7326 let pm_ref = pm.as_deref()?;
7327 let f = pm_ref.node.flags as u32;
7328 if (f & PM_NAMEREF) == 0 || (f & PM_UNSET) != 0 || pm_ref.width != 0 {
7329 return pm;
7330 }
7331 let refname = pm_ref.u_str.as_deref().unwrap_or("");
7332 if refname.is_empty() {
7333 return pm;
7334 }
7335 if (f & PM_TAGGED) != 0 {
7336 // c: `zerr("%s: invalid self reference", pm->node.nam)`.
7337 // The previous Rust port left this as a comment-only stub.
7338 let nam = pm.as_ref().map(|p| p.node.nam.clone()).unwrap_or_default();
7339 zerr(&format!("{}: invalid self reference", nam));
7340 return None;
7341 }
7342 // Real walk needs realparamtab.gethashnode2(refname). Until
7343 // that lands, return the input — this matches the no-target
7344 // behaviour the C source falls back to when keep_lastref is 0
7345 // and the lookup fails.
7346 pm
7347}
7348
7349/// ```c
7350/// Param pm = (Param) gethashnode2(realparamtab, name);
7351/// if (pm && (pm->node.flags & PM_NAMEREF)) {
7352/// if (pm->node.flags & PM_READONLY) {
7353/// zerr("read-only reference: %s", pm->node.nam); return;
7354/// }
7355/// pm->base = pm->width = 0;
7356/// SETREFNAME(pm, ztrdup(value));
7357/// pm->node.flags &= ~PM_UNSET;
7358/// setscope(pm);
7359/// } else
7360/// setsparam(name, ztrdup(value));
7361/// ```
7362/// `gethashnode2` is the no-autoload paramtab lookup. The
7363/// nameref branch updates the alias target in-place; the normal
7364/// branch falls through to `setsparam`.
7365/// Port of `setloopvar(char *name, char *value)` from `Src/params.c:6362`.
7366pub fn setloopvar(name: &str, value: &str) {
7367 // c:6367 — `Param pm = (Param) gethashnode2(realparamtab, name);`
7368 // Scope the write lock so we drop it before calling setsparam below.
7369 let nameref_branch = {
7370 let mut tab = realparamtab().write().unwrap();
7371 if let Some(pm) = tab.get_mut(name) {
7372 // c:6369 — `if (pm && (pm->node.flags & PM_NAMEREF))`
7373 if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
7374 // c:6370 — `if (pm->node.flags & PM_READONLY)`
7375 if (pm.node.flags as u32 & PM_READONLY) != 0 {
7376 // c:6372 — `zerr("read-only reference: %s", pm->node.nam);`
7377 zerr(&format!("read-only reference: {}", pm.node.nam));
7378 // c:6373 — `return;`
7379 return;
7380 }
7381 // c:6376 — `pm->base = pm->width = 0;`
7382 pm.base = 0;
7383 pm.width = 0;
7384 // c:6377 — `SETREFNAME(pm, ztrdup(value));`
7385 // SETREFNAME (params.c:482) macro: for PM_SPECIAL,
7386 // call gsu_s.setfn(pm, S); else free pm->u.str and
7387 // assign new. The PM_SPECIAL gsu vtable isn't fully
7388 // wired in zshrs; both branches collapse to the
7389 // direct `u_str` assignment which matches the
7390 // non-special path verbatim.
7391 pm.u_str = Some(value.to_string());
7392 // c:6378 — `pm->node.flags &= ~PM_UNSET;`
7393 pm.node.flags &= !(PM_UNSET as i32);
7394 true
7395 } else {
7396 false
7397 }
7398 } else {
7399 false
7400 }
7401 };
7402 if nameref_branch {
7403 // c:6379 — `setscope(pm);` — re-borrow under a fresh write
7404 // lock since we dropped the earlier one before crossing the
7405 // fn boundary.
7406 let mut tab = realparamtab().write().unwrap();
7407 if let Some(pm) = tab.get_mut(name) {
7408 setscope(pm);
7409 }
7410 } else {
7411 // c:6381 — `setsparam(name, ztrdup(value));`
7412 setsparam(name, value);
7413 }
7414}
7415
7416/// PM_NAMEREF: extract `refname = GETREFNAME(pm)`, locate first
7417/// `[` to split name vs subscript (sets pm->width), look up the
7418/// base param via `gethashnode2(realparamtab, refname)` →
7419/// `loadparamnode` (skipping self) → `setscope_base(pm,
7420/// basepm->level)`; if pm->base > pm->level emits the KSH global
7421/// reference error or WARNNESTEDVAR diagnostic; finally walks the
7422/// `resolve_nameref_rec` chain to detect self-references with
7423/// queue_signals/restore_queue_signals bracketing. Non-nameref
7424/// params: no-op. The base lookup and resolve_nameref_rec helpers
7425/// are stubbed elsewhere; this port wires the structural path
7426/// against existing helpers and falls through cleanly when the
7427/// nameref chain backend isn't available.
7428/// Port of `setscope(Param pm)` from `Src/params.c:6382`.
7429pub fn setscope(pm: &mut crate::ported::zsh_h::param) {
7430 crate::ported::signals::queue_signals();
7431 if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
7432 // Refname is stored in pm.u_str for nameref-typed params.
7433 let refname = pm.u_str.clone();
7434 if let Some(rn) = refname {
7435 // Compute pm->width by finding the first `[`.
7436 let head: &str = match rn.find('[') {
7437 Some(i) => {
7438 pm.width = i as i32;
7439 &rn[..i]
7440 }
7441 None => rn.as_str(),
7442 };
7443 // Self-reference check (basepm == pm) — without a working
7444 // hashtable lookup we can only detect literal self-name.
7445 if !head.is_empty() && head == pm.node.nam {
7446 // c: `zerr("%s: invalid self reference", refname);`
7447 // `unsetparam_pm(pm, 0, 1);`
7448 // The previous Rust port left both as comment-only
7449 // stubs. Emit the diagnostic so users see why a
7450 // typeset -n self-loop fails.
7451 zerr(&format!("{}: invalid self reference", rn));
7452 pm.node.flags |= PM_UNSET as i32;
7453 } else {
7454 // basepm = (Param)gethashnode2(realparamtab, refname)
7455 // → loadparamnode(...) → setscope_base(pm, basepm->level)
7456 // Resolved on demand once the paramtab vtable is wired;
7457 // the call shape is preserved here.
7458 }
7459 }
7460 }
7461 crate::ported::signals::unqueue_signals();
7462}
7463
7464/// ```c
7465/// if ((pm->base = base) > pm->level) {
7466/// LinkList refs;
7467/// /* grow scoperefs[] to base+1 entries */
7468/// refs = scoperefs[base];
7469/// if (!refs) refs = scoperefs[base] = znewlinklist();
7470/// zpushnode(refs, pm);
7471/// }
7472/// ```
7473/// Records `pm` on the per-scope reference list so a future
7474/// scope-pop can resolve nameref/upper bindings. Rust port
7475/// stores `base` on the param; the global `scoperefs` LinkList
7476/// table is not yet ported, so the bookkeeping push is described
7477/// here as architectural intent rather than executed.
7478/// Port of `setscope_base(Param pm, int base)` from `Src/params.c:6436`.
7479pub fn setscope_base(pm: &mut crate::ported::zsh_h::param, base: i32) {
7480 pm.base = base;
7481 if base > pm.level {
7482 // scoperefs[base] push of pm — needs LinkList global.
7483 }
7484}
7485
7486/// Port of `upscope(Param pm, const Param ref)` from `Src/params.c:6455`. C body:
7487/// ```c
7488/// if (ref->node.flags & PM_UPPER)
7489/// while (pm->level > ref->level - 1 && (pm = pm->old));
7490/// else
7491/// for (; pm->old && pm->old->level >= ref->base; pm = pm->old);
7492/// return pm;
7493/// ```
7494/// Walks `pm->old` chain to the param at the right scope depth
7495/// for a nameref. Rust signature mirrors C `Param upscope(Param,
7496/// const Param ref)`.
7497/// WARNING: param names don't match C — Rust=(pm, reference) vs C=(pm, ref)
7498pub fn upscope(
7499 mut pm: crate::ported::zsh_h::Param,
7500 reference: &crate::ported::zsh_h::param,
7501) -> crate::ported::zsh_h::Param {
7502 if (reference.node.flags as u32 & PM_UPPER) != 0 {
7503 while pm.level > reference.level - 1 {
7504 match pm.old.take() {
7505 Some(o) => pm = o,
7506 None => break,
7507 }
7508 }
7509 } else {
7510 loop {
7511 let next_level = pm.old.as_ref().map(|o| o.level);
7512 match next_level {
7513 Some(l) if l >= reference.base => {
7514 pm = pm.old.take().unwrap();
7515 }
7516 _ => break,
7517 }
7518 }
7519 }
7520 pm
7521}
7522
7523/// Port of `valid_refname(char *val, int flags)` from `Src/params.c:6466`. C body
7524/// validates a nameref target name. Two paths:
7525/// - PM_UPPER (`typeset -nu`): reject digit-leader (positional
7526/// refs would loop) and the literal `argv`/`ARGC` names.
7527/// - non-PM_UPPER: positional digit-leader is permitted (must be
7528/// all-digits before any `[`); otherwise scan via
7529/// `itype_end(INAMESPC)`.
7530/// Either path then accepts the trailing one-char specials
7531/// `! ? $ - _` and an optional `[subscript]` tail. Returns 1 on
7532/// valid, 0 otherwise. The Rust port follows the same control
7533/// flow with `is_ascii_digit`/`is_alphabetic` standing in for
7534/// `idigit`/`itype_end`.
7535pub fn valid_refname(val: &str, flags: i32) -> bool { // c:6466
7536 if val.is_empty() {
7537 return false;
7538 }
7539 let first = val.chars().next().unwrap();
7540 let pm_upper = (flags as u32 & PM_UPPER) != 0;
7541 let mut t: usize;
7542 if pm_upper { // c:6470
7543 if first.is_ascii_digit() { // c:6472
7544 return false; // c:6473
7545 }
7546 // c:6474 — `t = itype_end(val, INAMESPC, 0)`; INAMESPC stops
7547 // at `.` and other non-namespace chars. Approximate with
7548 // alphanumeric/_ scan.
7549 t = val
7550 .char_indices()
7551 .find(|(_, c)| !(c.is_alphanumeric() || *c == '_'))
7552 .map(|(i, _)| i)
7553 .unwrap_or(val.len());
7554 if t - 0 == 4 // c:6475
7555 && (val.starts_with("argv") || val.starts_with("ARGC")) // c:6476-6477
7556 {
7557 return false; // c:6478
7558 }
7559 } else if first.is_ascii_digit() { // c:6479
7560 // c:6480-6485 — all-digit run; first non-digit must be `[`.
7561 t = 1;
7562 for (i, c) in val.char_indices().skip(1) {
7563 if !c.is_ascii_digit() {
7564 t = i;
7565 break;
7566 }
7567 t = i + c.len_utf8();
7568 }
7569 if t < val.len() && val.as_bytes()[t] != b'[' { // c:6484
7570 return false; // c:6485
7571 }
7572 } else {
7573 // c:6487 — `t = itype_end(val, INAMESPC, 0)`.
7574 t = val
7575 .char_indices()
7576 .find(|(_, c)| !(c.is_alphanumeric() || *c == '_' || *c == '.'))
7577 .map(|(i, _)| i)
7578 .unwrap_or(val.len());
7579 }
7580
7581 if t == 0 { // c:6489
7582 let c = val.as_bytes()[0];
7583 if !(c == b'!' || c == b'?' || c == b'$' || c == b'-' || c == b'_') { // c:6490
7584 return false; // c:6493
7585 }
7586 t = 1; // c:6494
7587 }
7588 if t < val.len() && val.as_bytes()[t] == b'[' { // c:6496
7589 // c:6498-6504 — parse_subscript/Inbrack/Outbrack walk. The
7590 // tokenize+parse_subscript pair isn't ported; accept any
7591 // balanced `[…]` tail (single-level) to remain conservative.
7592 let tail = &val[t + 1..];
7593 if let Some(close) = tail.find(']') {
7594 // c:6505-6508 — anything past `]` is rejected.
7595 if close + 1 < tail.len() {
7596 return false;
7597 }
7598 } else {
7599 return false;
7600 }
7601 }
7602 true // c:6510
7603}
7604
7605
7606/// Read `foundparam`. Returns the last param name observed by
7607/// `scanparamvals`; cleared by callers after consumption.
7608pub fn foundparam() -> Option<String> {
7609 foundparam_lock().lock().unwrap().clone()
7610}
7611
7612/// Set `foundparam`. Called from `scanparamvals`.
7613pub fn set_foundparam(nam: Option<String>) {
7614 *foundparam_lock().lock().unwrap() = nam;
7615}
7616
7617/// Port of `fetchvalue(Value v, char **pptr, int bracks, int scanflags)` from `Src/params.c:2180` — see real
7618/// implementation below; this slot kept for the C-source linenum
7619/// citation and is now an alias.
7620// (real fetchvalue is defined later)
7621
7622/// Port of `static int delunset;` from `Src/params.c:610`. Flag
7623/// `deleteparamtable` flips to 1 around the inner `deletehashtable`
7624/// call so each freed node runs its `unsetfn`. `freeparamnode`
7625/// consults this before invoking the unset hook (c:5986).
7626pub static DELUNSET: std::sync::atomic::AtomicI32 = // c:610
7627 std::sync::atomic::AtomicI32::new(0);
7628
7629pub(crate) fn paramtab_hashed_storage()
7630 -> &'static Mutex<HashMap<String, indexmap::IndexMap<String, String>>>
7631{
7632 PARAMTAB_HASHED_STORAGE_INNER
7633 .get_or_init(|| Mutex::new(HashMap::new()))
7634}
7635
7636/// Mirror the global `paramtab` (and the parallel hashed-storage
7637/// table) into the three HashMaps that `SubstState` uses as its
7638/// transient backing during `prefork()` (Src/subst.c:100). This
7639/// is a port-transition shim: once `subst.rs` reads parameters
7640/// directly through `paramtab().read()` / `.write()` instead of carrying
7641/// `state.variables`/`state.arrays`/`state.assoc_arrays`, this
7642/// helper goes away.
7643pub fn sync_state_from_paramtab(
7644 variables: &mut HashMap<String, String>,
7645 arrays: &mut HashMap<String, Vec<String>>,
7646 assoc_arrays: &mut HashMap<String, indexmap::IndexMap<String, String>>,
7647) {
7648 let tab = paramtab().read().unwrap();
7649 for (name, pm) in tab.iter() {
7650 let f = pm.node.flags as u32;
7651 if (f & PM_ARRAY) != 0 {
7652 if let Some(arr) = pm.u_arr.as_ref() {
7653 arrays.insert(name.clone(), arr.clone());
7654 }
7655 variables.remove(name);
7656 assoc_arrays.remove(name);
7657 } else if (f & PM_HASHED) != 0 {
7658 if let Some(map) = paramtab_hashed_storage()
7659 .lock().unwrap().get(name)
7660 {
7661 assoc_arrays.insert(name.clone(), map.clone());
7662 }
7663 variables.remove(name);
7664 arrays.remove(name);
7665 } else if let Some(s) = pm.u_str.as_ref() {
7666 // PM_SCALAR / PM_NAMEREF / numeric — fold to the string view.
7667 variables.insert(name.clone(), s.clone());
7668 arrays.remove(name);
7669 assoc_arrays.remove(name);
7670 }
7671 }
7672}
7673
7674/// Format float with underscores
7675pub fn convfloat_underscore(dval: f64, underscore: i32) -> String {
7676 let s = convfloat(dval, 0, 0);
7677 if underscore <= 0 {
7678 return s;
7679 }
7680
7681 let u = underscore as usize;
7682 let (sign, rest) = if let Some(after) = s.strip_prefix('-') {
7683 ("-", after)
7684 } else {
7685 ("", s.as_str())
7686 };
7687
7688 let (int_part, frac_exp) = if let Some(dot_pos) = rest.find('.') {
7689 (&rest[..dot_pos], &rest[dot_pos..])
7690 } else {
7691 (rest, "")
7692 };
7693
7694 // Add underscores to integer part
7695 let int_chars: Vec<char> = int_part.chars().collect();
7696 let mut result = sign.to_string();
7697 let first_group = int_chars.len() % u;
7698 if first_group > 0 {
7699 result.extend(&int_chars[..first_group]);
7700 if first_group < int_chars.len() {
7701 result.push('_');
7702 }
7703 }
7704 for (i, chunk) in int_chars[first_group..].chunks(u).enumerate() {
7705 if i > 0 {
7706 result.push('_');
7707 }
7708 result.extend(chunk);
7709 }
7710
7711 // Add underscores to fractional part
7712 if let Some(frac) = frac_exp.strip_prefix('.') {
7713 result.push('.');
7714 let (frac_digits, exp) = if let Some(e_pos) = frac.find('e') {
7715 (&frac[..e_pos], &frac[e_pos..])
7716 } else {
7717 (frac, "")
7718 };
7719
7720 let frac_chars: Vec<char> = frac_digits.chars().collect();
7721 for (i, chunk) in frac_chars.chunks(u).enumerate() {
7722 if i > 0 {
7723 result.push('_');
7724 }
7725 result.extend(chunk);
7726 }
7727 result.push_str(exp);
7728 } else {
7729 result.push_str(frac_exp);
7730 }
7731
7732 result
7733}
7734
7735
7736
7737fn ifs_lock() -> &'static Mutex<String> {
7738 static IFS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7739 IFS_VAR.get_or_init(|| Mutex::new(" \t\n\0".to_string()))
7740}
7741
7742fn home_lock() -> &'static Mutex<String> {
7743 static HOME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7744 HOME_VAR.get_or_init(|| Mutex::new(env::var("HOME").unwrap_or_default()))
7745}
7746
7747fn term_lock() -> &'static Mutex<String> {
7748 static TERM_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7749 TERM_VAR.get_or_init(|| Mutex::new(env::var("TERM").unwrap_or_default()))
7750}
7751
7752fn wordchars_lock() -> &'static Mutex<String> {
7753 static WORDCHARS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7754 WORDCHARS_VAR.get_or_init(|| Mutex::new("*?_-.[]~=/&;!#$%^(){}<>".to_string()))
7755}
7756
7757fn histchars_lock() -> &'static Mutex<[u8; 3]> {
7758 static HISTCHARS_VAR: OnceLock<Mutex<[u8; 3]>> = OnceLock::new();
7759 HISTCHARS_VAR.get_or_init(|| Mutex::new([b'!', b'^', b'#']))
7760}
7761
7762fn keyboardhack_lock() -> &'static Mutex<u8> {
7763 static KEYBOARDHACK_VAR: OnceLock<Mutex<u8>> = OnceLock::new();
7764 KEYBOARDHACK_VAR.get_or_init(|| Mutex::new(0))
7765}
7766
7767fn histsiz_lock() -> &'static Mutex<i64> {
7768 static HISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
7769 // Match observed `zsh -fc 'echo $HISTSIZE'` output on zsh 5.9+
7770 // (Homebrew). Upstream's `configure.ac` defines DEFAULT_HISTSIZE
7771 // as 30 but distributed binaries seed the cap at 999999999 — the
7772 // parity goal here is "match the binary the user actually runs",
7773 // not "match the source-code default".
7774 HISTSIZ_VAR.get_or_init(|| Mutex::new(999_999_999))
7775}
7776
7777fn savehistsiz_lock() -> &'static Mutex<i64> {
7778 static SAVEHISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
7779 // Same rationale as `histsiz_lock` — observed `zsh -fc
7780 // 'echo $SAVEHIST'` returns 99999999 on zsh 5.9+. Source has
7781 // savehistsiz default to 0 but distributed binaries cap at 99M.
7782 SAVEHISTSIZ_VAR.get_or_init(|| Mutex::new(99_999_999))
7783}
7784
7785fn zsh_terminfo_lock() -> &'static Mutex<String> {
7786 static TERMINFO_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7787 TERMINFO_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO").unwrap_or_default()))
7788}
7789
7790fn zsh_terminfodirs_lock() -> &'static Mutex<String> {
7791 static TERMINFODIRS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7792 TERMINFODIRS_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO_DIRS").unwrap_or_default()))
7793}
7794
7795fn cached_username_lock() -> &'static Mutex<String> {
7796 static USERNAME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7797 USERNAME_VAR.get_or_init(|| Mutex::new(initial_username()))
7798}
7799
7800// Port of `static unsigned numparamvals;` (params.c:626) and the
7801// related per-scan statics at params.c:637-640. Per PORT.md Rule D
7802// these are file-scope statics, NOT aggregated into a state struct.
7803//
7804// c:626 static unsigned numparamvals;
7805// c:637 static Patprog scanprog;
7806// c:638 static char *scanstr;
7807// c:639 static char **paramvals;
7808// c:640 static Param foundparam; <-- exposed earlier as FOUNDPARAM
7809pub static NUMPARAMVALS: std::sync::atomic::AtomicU32 =
7810 std::sync::atomic::AtomicU32::new(0); // c:626
7811pub static SCANPROG: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
7812 std::sync::OnceLock::new(); // c:637
7813pub static SCANSTR: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
7814 std::sync::OnceLock::new(); // c:638
7815pub static PARAMVALS: std::sync::OnceLock<std::sync::Mutex<Vec<String>>> =
7816 std::sync::OnceLock::new(); // c:639
7817
7818/// Resolve the current user's name. Mirrors C's `get_username()`
7819/// init at Src/init.c which reads `getpwuid(getuid())->pw_name`
7820/// rather than `$USER`. Falls back to env vars only if the
7821/// passwd lookup fails (rare on real systems).
7822fn initial_username() -> String {
7823 #[cfg(unix)]
7824 {
7825 let uid = unsafe { libc::getuid() };
7826 let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
7827 let mut buf = vec![0i8; 1024];
7828 let mut result: *mut libc::passwd = std::ptr::null_mut();
7829 let rc = unsafe {
7830 libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result)
7831 };
7832 if rc == 0 && !result.is_null() && !pwd.pw_name.is_null() {
7833 let cstr = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
7834 return cstr.to_string_lossy().into_owned();
7835 }
7836 }
7837 env::var("USER")
7838 .or_else(|_| env::var("LOGNAME"))
7839 .unwrap_or_default()
7840}
7841
7842fn pipestats_lock() -> &'static Mutex<Vec<i32>> {
7843 static PIPESTATS_VAR: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
7844 PIPESTATS_VAR.get_or_init(|| Mutex::new(Vec::new()))
7845}
7846
7847fn shtimer_lock() -> &'static Mutex<Duration> {
7848 static SHTIMER_VAR: OnceLock<Mutex<Duration>> = OnceLock::new();
7849 SHTIMER_VAR.get_or_init(|| {
7850 Mutex::new(
7851 SystemTime::now()
7852 .duration_since(UNIX_EPOCH)
7853 .unwrap_or_default(),
7854 )
7855 })
7856}
7857
7858fn pparams_lock() -> &'static Mutex<Vec<String>> {
7859 // Mirror of zsh's `pparams` (positional params $1, $2, ...).
7860 // Used by `poundgetfn` for `$#`. The canonical store is
7861 // `builtin::PPARAMS` (Src/init.c `pparams`); set/shift builtins
7862 // write there. Point at that single store so `$#` reads the
7863 // live value instead of an isolated empty mirror.
7864 &crate::ported::builtin::PPARAMS
7865}
7866
7867fn zunderscore_lock() -> &'static Mutex<String> {
7868 static ZUNDERSCORE_VAR: OnceLock<Mutex<String>> = OnceLock::new();
7869 ZUNDERSCORE_VAR.get_or_init(|| Mutex::new(String::new()))
7870}
7871
7872/// Update `$_` with the last argument of the just-completed
7873/// command. Mirrors C zsh's writeback in `execcmd_exec` (Src/exec.c)
7874/// where `zunderscore` is set to the last argv slot before
7875/// returning. Callers: every command-dispatch hook in
7876/// fusevm_bridge / exec.rs.
7877pub fn set_zunderscore(argv: &[String]) {
7878 let new = if let Some(last) = argv.last() {
7879 last.clone()
7880 } else {
7881 String::new()
7882 };
7883 *zunderscore_lock()
7884 .lock()
7885 .expect("zunderscore poisoned") = new;
7886}
7887
7888/// Direct port of `static int dontimport(int flags)` from
7889/// `Src/params.c:796-810`.
7890/// ```c
7891/// /* If explicitly marked as don't import */
7892/// if (flags & PM_DONTIMPORT)
7893/// return 1;
7894/// /* If value already exported */
7895/// if (flags & PM_EXPORTED)
7896/// return 1;
7897/// /* If security issue when importing and running with some privilege */
7898/// if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED))
7899/// return 1;
7900/// /* OK to import */
7901/// return 0;
7902/// ```
7903/// Port of `dontimport(int flags)` from `Src/params.c:796`.
7904fn dontimport(flags: i32) -> i32 { // c:796
7905 let flags = flags as u32;
7906 // c:799-800 — `if (flags & PM_DONTIMPORT) return 1`.
7907 if flags & crate::ported::zsh_h::PM_DONTIMPORT != 0 { // c:799
7908 return 1; // c:800
7909 }
7910 // c:802-803 — `if (flags & PM_EXPORTED) return 1`.
7911 if flags & crate::ported::zsh_h::PM_EXPORTED != 0 { // c:802
7912 return 1; // c:803
7913 }
7914 // c:805-806 — `if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED)) return 1`.
7915 if flags & crate::ported::zsh_h::PM_DONTIMPORT_SUID != 0 // c:805
7916 && isset(crate::ported::zsh_h::PRIVILEGED)
7917 {
7918 return 1; // c:806
7919 }
7920 0 // c:809
7921}
7922
7923
7924/// Minimal `pattry()` shim — exact-match fallback until the pattern
7925/// engine in `Src/pattern.c` is wired.
7926fn pattry(prog: &str, s: &str) -> bool {
7927 prog == s
7928}
7929
7930// ===========================================================
7931// GSU dispatch table — maps special-parameter NAMES to their
7932// getfn callback. C zsh dispatches reads of `$RANDOM` /
7933// `$USERNAME` / `$UID` / etc. through `Param.gsu->getfn`, where
7934// each special parameter has a `Param` entry in `paramtab`
7935// pointing at its specific getfn (Src/params.c:225 SPECIAL_PARAM
7936// table seeds these mappings).
7937//
7938// zshrs has the GSU callbacks ported (uidgetfn, randomgetfn,
7939// usernamegetfn, etc. above) but the shell's parameter-read path
7940// (fusevm_bridge::expand_param) reads from ShellExecutor.variables
7941// directly — never dispatching through the callbacks. Result:
7942// `echo $RANDOM` returned the cached HashMap value (or empty),
7943// not a fresh `rand() & 0x7fff` from `randomgetfn`.
7944//
7945// `lookup_special_var(name)` is the bridge: given a variable
7946// name, returns the GSU getfn's output if `name` is a recognized
7947// special, else None. Callers (expand_param, subst.rs reads)
7948// check this before falling back to `variables.get(name)`.
7949// ===========================================================
7950
7951/// Look up a special-parameter NAME and dispatch to its GSU getfn.
7952///
7953/// Returns `Some(value_string)` if `name` is one of zshrs's
7954/// recognized specials with a real GSU getfn; `None` otherwise
7955/// (caller should fall back to `variables.get`).
7956///
7957/// This is the bridge between the named getfn callbacks above
7958/// (uidgetfn / randomgetfn / etc.) and the shell's parameter-read
7959/// path. Mirrors the `Param.gsu->getfn` dispatch C zsh does
7960/// inside `getsparam` / `getstrvalue` (Src/params.c:3076 / 2335).
7961pub fn lookup_special_var(name: &str) -> Option<String> {
7962 // All-digit positional: $1..$N from canonical PPARAMS.
7963 // C zsh dispatches positional params through pparams (Src/init.c).
7964 if !name.is_empty() && name.chars().all(|c| c.is_ascii_digit()) {
7965 let n: usize = name.parse().ok()?;
7966 if n == 0 {
7967 return crate::ported::utils::argzero();
7968 }
7969 let pp = pparams_lock().lock().ok()?;
7970 return pp.get(n - 1).cloned();
7971 }
7972 match name {
7973 // libc identity callbacks.
7974 "UID" => Some(uidgetfn().to_string()),
7975 "GID" => Some(gidgetfn().to_string()),
7976 "EUID" => Some(euidgetfn().to_string()),
7977 "EGID" => Some(egidgetfn().to_string()),
7978 // libc syscall callbacks.
7979 "RANDOM" => Some(randomgetfn().to_string()),
7980 "TTYIDLE" => Some(ttyidlegetfn().to_string()),
7981 "ERRNO" => Some(errnogetfn().to_string()),
7982 // Time callbacks.
7983 "SECONDS" => Some(intsecondsgetfn().to_string()),
7984 // Cached-state callbacks (OnceLock<Mutex<…>> backed).
7985 "USERNAME" => Some(usernamegetfn()),
7986 "HOME" => Some(homegetfn()),
7987 "TERM" => Some(termgetfn()),
7988 "WORDCHARS" => Some(wordcharsgetfn()),
7989 "IFS" => Some(ifsgetfn()),
7990 "TERMINFO" => Some(terminfogetfn()),
7991 "TERMINFO_DIRS" => Some(terminfodirsgetfn()),
7992 "KEYBOARD_HACK" => Some(keyboardhackgetfn()),
7993 "histchars" | "HISTCHARS" => Some(histcharsgetfn()),
7994 "_" => Some(underscoregetfn()),
7995 // Counters with int return.
7996 "HISTSIZE" => Some(histsizegetfn().to_string()),
7997 "SAVEHIST" => Some(savehistsizegetfn().to_string()),
7998 "#" | "ARGC" => Some(poundgetfn().to_string()),
7999 // $0 routes through utils::argzero.
8000 "0" => crate::ported::utils::argzero(),
8001 // POSIX shell-special scalars. C dispatches these through
8002 // dedicated gsu getfn callbacks (Src/params.c special_assigns).
8003 "?" => Some(crate::ported::builtin::LASTVAL
8004 .load(std::sync::atomic::Ordering::Relaxed)
8005 .to_string()),
8006 "$" => Some(std::process::id().to_string()),
8007 "!" => {
8008 // Last-backgrounded job PID. Stored in paramtab `!` slot;
8009 // default to 0 to match zsh fresh-shell behaviour.
8010 let tab = paramtab().read().ok()?;
8011 Some(tab.get("!").and_then(|pm| pm.u_str.clone())
8012 .unwrap_or_else(|| "0".to_string()))
8013 }
8014 // $* / $@ join positional params via IFS first char.
8015 "*" | "@" => {
8016 let sep = ifsgetfn().chars().next().unwrap_or(' ').to_string();
8017 pparams_lock().lock().ok().map(|p| p.join(&sep))
8018 }
8019 // $- : current option-letter set. zsh emits baseline "569X"
8020 // prefix (internal letters always on) + user-toggled flags.
8021 "-" => {
8022 let mut letters = String::from("569X");
8023 let opt = |n: &str| {
8024 crate::ported::options::opt_state_get(n).unwrap_or(false)
8025 };
8026 if opt("errexit") { letters.push('e'); }
8027 if !opt("rcs") { letters.push('f'); }
8028 if opt("login") { letters.push('l'); }
8029 if opt("nounset") { letters.push('u'); }
8030 if opt("xtrace") { letters.push('x'); }
8031 if opt("verbose") { letters.push('v'); }
8032 // c:Src/params.c — `set -n` toggles \`exec\` OFF (default ON).
8033 // The previous Rust port called \`opt(\"noexec\")\` which is
8034 // not a real option name in zsh; the lookup always returned
8035 // false, so \`$-\` never included 'n' even when \`set -n\` was
8036 // active. Read the canonical \`exec\` option and push 'n'
8037 // when UNSET.
8038 if !opt("exec") { letters.push('n'); }
8039 if opt("hashall") { letters.push('h'); }
8040 Some(letters)
8041 }
8042 // Arrays — joined with space for scalar context.
8043 "pipestatus" => {
8044 let arr = pipestatgetfn();
8045 if arr.is_empty() {
8046 None
8047 } else {
8048 Some(arr.join(" "))
8049 }
8050 }
8051 _ => None,
8052 }
8053}
8054
8055/// Shared test mutex for histsiz mutations (gsu_tests +
8056/// tests submodules both write the same global; this lock
8057/// serialises them under parallel test execution).
8058#[cfg(test)]
8059pub(crate) static HISTSIZ_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
8060
8061/// Shared test mutex for histchars mutations (gsu_tests +
8062/// tests submodules both write bangchar/hatchar/hashchar atomics;
8063/// this lock serialises them under parallel test execution).
8064#[cfg(test)]
8065pub(crate) static HISTCHARS_TEST_LOCK_SHARED: std::sync::Mutex<()> =
8066 std::sync::Mutex::new(());
8067
8068#[cfg(test)]
8069mod gsu_tests {
8070 use super::*;
8071
8072 #[test]
8073 fn test_libc_id_callbacks_match_libc() {
8074 assert_eq!(uidgetfn(), unsafe { libc::getuid() } as i64);
8075 assert_eq!(gidgetfn(), unsafe { libc::getgid() } as i64);
8076 assert_eq!(euidgetfn(), unsafe { libc::geteuid() } as i64);
8077 assert_eq!(egidgetfn(), unsafe { libc::getegid() } as i64);
8078 }
8079
8080 /// Pin: `usernamegetfn` routes through `get_username()` per
8081 /// `Src/params.c:4658` (which refreshes cache on uid change
8082 /// per `Src/utils.c:1082`). The previous Rust port read a
8083 /// stale cached value directly. Verify the getter returns
8084 /// the same name as a direct libc `getpwuid(getuid())` —
8085 /// confirming the path WENT through the refresh helper, not
8086 /// the stale paramtab Mutex.
8087 #[test]
8088 fn usernamegetfn_matches_libc_getpwuid_for_current_uid() {
8089 let uname = usernamegetfn();
8090 // The current process is running as some uid; the getter
8091 // must return either a populated name OR an empty string
8092 // (when getpwuid fails, e.g. sandboxed builds). It must
8093 // NOT panic and must NOT return a stale cached value
8094 // from a different uid.
8095 let direct = unsafe {
8096 let pw = libc::getpwuid(libc::getuid());
8097 if pw.is_null() {
8098 String::new()
8099 } else {
8100 std::ffi::CStr::from_ptr((*pw).pw_name)
8101 .to_string_lossy()
8102 .into_owned()
8103 }
8104 };
8105 assert_eq!(uname, direct,
8106 "c:4658 — usernamegetfn must match getpwuid(getuid())->pw_name");
8107 }
8108
8109 #[test]
8110 fn test_random_returns_15_bit_value() {
8111 for _ in 0..100 {
8112 let v = randomgetfn();
8113 assert!(v >= 0 && v < 0x8000);
8114 }
8115 }
8116
8117 #[test]
8118 fn test_random_set_seeds_deterministically() {
8119 randomsetfn(42);
8120 let a = randomgetfn();
8121 randomsetfn(42);
8122 let b = randomgetfn();
8123 assert_eq!(a, b);
8124 }
8125
8126 #[test]
8127 fn test_ifs_round_trip() {
8128 let original = ifsgetfn();
8129 ifssetfn(":,;".to_string());
8130 assert_eq!(ifsgetfn(), ":,;");
8131 ifssetfn(original);
8132 }
8133
8134 #[test]
8135 fn test_histsiz_clamps_to_1() {
8136 let _g = HISTSIZ_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8137 let original = histsizegetfn();
8138 histsizesetfn(0);
8139 assert_eq!(histsizegetfn(), 1);
8140 histsizesetfn(-5);
8141 assert_eq!(histsizegetfn(), 1);
8142 histsizesetfn(500);
8143 assert_eq!(histsizegetfn(), 500);
8144 histsizesetfn(original);
8145 }
8146
8147 #[test]
8148 fn test_savehistsiz_clamps_to_0() {
8149 let original = savehistsizegetfn();
8150 savehistsizesetfn(-5);
8151 assert_eq!(savehistsizegetfn(), 0);
8152 savehistsizesetfn(100);
8153 assert_eq!(savehistsizegetfn(), 100);
8154 savehistsizesetfn(original);
8155 }
8156
8157 /// Pin: `savehistsizesetfn` syncs BOTH storage mirrors so the
8158 /// twin-storage Rust adaptation behaves like the single global
8159 /// in C. The params.rs Mutex<i64> drives `$SAVEHIST` reads;
8160 /// the hist.rs AtomicI64 drives the history-file writer cap.
8161 /// Previously only the params.rs side was written, so
8162 /// `SAVEHIST=10000` left hist.rs at 0 and the writer would
8163 /// cap at zero lines.
8164 #[test]
8165 fn savehistsizesetfn_syncs_to_hist_module() {
8166 use std::sync::atomic::Ordering;
8167 let original_params = savehistsizegetfn();
8168 let original_hist = crate::ported::hist::savehistsiz.load(Ordering::SeqCst);
8169 // Set via the setfn — both storages must reflect the value.
8170 savehistsizesetfn(12345);
8171 assert_eq!(savehistsizegetfn(), 12345,
8172 "c:4994 — params.rs Mutex<i64> reflects new value");
8173 assert_eq!(crate::ported::hist::savehistsiz.load(Ordering::SeqCst), 12345,
8174 "c:4994 — hist.rs AtomicI64 synced (was the previous gap)");
8175 // Negative clamps to 0 in BOTH stores.
8176 savehistsizesetfn(-99);
8177 assert_eq!(savehistsizegetfn(), 0,
8178 "c:4998 — params.rs clamps to 0");
8179 assert_eq!(crate::ported::hist::savehistsiz.load(Ordering::SeqCst), 0,
8180 "c:4998 — hist.rs clamps to 0 too");
8181 // Restore.
8182 savehistsizesetfn(original_params);
8183 crate::ported::hist::savehistsiz.store(original_hist, Ordering::SeqCst);
8184 }
8185
8186 #[test]
8187 fn test_pipestat_round_trip() {
8188 pipestatsetfn(Some(vec!["1".to_string(), "0".to_string(), "127".to_string()]));
8189 let v = pipestatgetfn();
8190 assert_eq!(v, vec!["1", "0", "127"]);
8191 pipestatsetfn(None);
8192 assert_eq!(pipestatgetfn(), Vec::<String>::new());
8193 }
8194
8195 /// Pin: `setnumvalue` actually STORES the scalar string per
8196 /// `Src/params.c:2862-2872`. The previous Rust port computed
8197 /// the string then dropped it via `let _ = s;` — meaning a
8198 /// numeric assignment to a SCALAR param stored NOTHING.
8199 ///
8200 /// C body for PM_SCALAR: `setstrvalue(v, convbase_underscore(
8201 /// val.u.l, pm->base, pm->width));`. We pin the round-trip
8202 /// for an integer assigned to a scalar param.
8203 #[test]
8204 fn setnumvalue_stores_int_value_into_scalar_pm() {
8205 use crate::ported::zsh_h::{param, hashnode, value, PM_SCALAR};
8206 use crate::ported::math::{mnumber, MN_INTEGER};
8207 // c:2860 — setnumvalue bails when unset(EXECOPT). The unit-test
8208 // env doesn't run through createoptiontable so we set "exec"
8209 // explicitly to simulate normal runtime.
8210 let saved_exec = crate::ported::options::opt_state_get("exec")
8211 .unwrap_or(false);
8212 crate::ported::options::opt_state_set("exec", true);
8213 // Build a scalar Param with no special base/width.
8214 let mut pm = Box::new(param {
8215 node: hashnode { next: None, nam: "x".to_string(), flags: PM_SCALAR as i32 },
8216 u_data: 0, u_arr: None, u_str: Some(String::new()), u_val: 0,
8217 u_dval: 0.0, u_hash: None,
8218 gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
8219 base: 0, width: 0, env: None, ename: None, old: None, level: 0,
8220 });
8221 let mut v = value {
8222 pm: Some(pm.clone()),
8223 arr: Vec::new(),
8224 scanflags: 0,
8225 valflags: 0,
8226 start: 0,
8227 end: -1,
8228 };
8229 let val = mnumber { l: 42, d: 0.0, type_: MN_INTEGER };
8230 setnumvalue(Some(&mut v), val);
8231 // c:2871 — the scalar storage now holds "42".
8232 let stored = v.pm.as_ref().unwrap().u_str.clone().unwrap_or_default();
8233 assert_eq!(stored, "42",
8234 "c:2871 — setnumvalue must store the rendered integer; \
8235 was previously dropped via `let _ = s;`");
8236 let _ = pm;
8237 crate::ported::options::opt_state_set("exec", saved_exec);
8238 }
8239
8240 #[test]
8241 fn test_simple_arrayuniq_first_wins() {
8242 let v = vec!["a".to_string(), "b".to_string(), "a".to_string(), "c".to_string()];
8243 assert_eq!(simple_arrayuniq(v), vec!["a", "b", "c"]);
8244 }
8245
8246 #[test]
8247 fn test_split_env_string() {
8248 assert_eq!(
8249 split_env_string("PATH=/usr/bin:/bin"),
8250 Some(("PATH".to_string(), "/usr/bin:/bin".to_string()))
8251 );
8252 assert_eq!(
8253 split_env_string("EMPTY="),
8254 Some(("EMPTY".to_string(), "".to_string()))
8255 );
8256 assert_eq!(split_env_string("NOEQUALS"), None);
8257 }
8258
8259 #[test]
8260 fn test_mkenvstr() {
8261 assert_eq!(mkenvstr("PATH", "/usr/bin", 0), "PATH=/usr/bin");
8262 assert_eq!(mkenvstr("EMPTY", "", 0), "EMPTY=");
8263 }
8264
8265 #[test]
8266 fn test_seconds_round_trip() {
8267 intsecondssetfn(0);
8268 let s1 = intsecondsgetfn();
8269 std::thread::sleep(std::time::Duration::from_millis(5));
8270 let s2 = intsecondsgetfn();
8271 assert!(s2 >= s1);
8272 // Reset to a known offset and read back.
8273 setrawseconds(100.0);
8274 assert_eq!(getrawseconds(), 100.0);
8275 }
8276
8277 #[test]
8278 fn test_argzero_round_trip() {
8279 argzerosetfn("/bin/zsh".to_string());
8280 assert_eq!(argzerogetfn(), "/bin/zsh");
8281 argzerosetfn(String::new());
8282 }
8283
8284 #[test]
8285 fn test_env_get_set() {
8286 let result = zputenv("ZSHRS_TEST_VAR=hello");
8287 assert_eq!(result, 0);
8288 assert_eq!(zgetenv("ZSHRS_TEST_VAR"), Some("hello".to_string()));
8289 delenv("ZSHRS_TEST_VAR");
8290 assert_eq!(zgetenv("ZSHRS_TEST_VAR"), None);
8291 }
8292
8293 #[test]
8294 fn test_keyboardhack_one_char() {
8295 keyboardhacksetfn("\\".to_string());
8296 assert_eq!(keyboardhackgetfn(), "\\");
8297 keyboardhacksetfn(String::new());
8298 assert_eq!(keyboardhackgetfn(), "");
8299 }
8300
8301 /// Pin: `keyboardhacksetfn` accepts ASCII chars cleanly per
8302 /// `Src/params.c:5040-5060`. Tests the canonical happy path
8303 /// — single ASCII char, empty input, and the ASCII guard.
8304 ///
8305 /// The previous Rust port skipped `unmetafy(x, &len)` (c:5044)
8306 /// before the length and ASCII checks. This test exercises
8307 /// the surface API; the unmetafy fix is doc-pinned in the
8308 /// fn body since constructing Meta-encoded String values for
8309 /// the test fixture would require unsafe (Rust strings must
8310 /// be valid UTF-8 and the Meta byte 0x83 is not a valid
8311 /// UTF-8 lead).
8312 #[test]
8313 fn keyboardhacksetfn_handles_ascii_and_empty() {
8314 // c:5056 — single ASCII char stored.
8315 keyboardhacksetfn(";".to_string());
8316 assert_eq!(keyboardhackgetfn(), ";",
8317 "c:5056 — single ASCII char stored verbatim");
8318 // c:5056 — different ASCII char stored.
8319 keyboardhacksetfn(",".to_string());
8320 assert_eq!(keyboardhackgetfn(), ",");
8321 // c:5058 — empty input clears to '\0'.
8322 keyboardhacksetfn(String::new());
8323 assert_eq!(keyboardhackgetfn(), "");
8324 }
8325
8326 #[test]
8327 fn test_histchars_default() {
8328 let _g = super::HISTCHARS_TEST_LOCK_SHARED
8329 .lock().unwrap_or_else(|e| e.into_inner());
8330 histcharssetfn(None);
8331 assert_eq!(histcharsgetfn(), "!^#");
8332 histcharssetfn(Some("@$&".to_string()));
8333 assert_eq!(histcharsgetfn(), "@$&");
8334 histcharssetfn(None);
8335 }
8336
8337 /// Pin: `histcharssetfn` runs `unmetafy` per Src/params.c:5086
8338 /// BEFORE the length truncation and ASCII guard. Previously
8339 /// the Rust port skipped unmetafy, so a Meta-pair would
8340 /// inflate the byte count past 3 and the truncation would
8341 /// drop valid characters.
8342 ///
8343 /// Test the happy path: 1-char, 2-char, 3-char ASCII inputs
8344 /// all parse correctly and each char-position fills the
8345 /// matching atomic.
8346 #[test]
8347 fn histcharssetfn_handles_1_2_3_char_inputs() {
8348 let _g = super::HISTCHARS_TEST_LOCK_SHARED
8349 .lock().unwrap_or_else(|e| e.into_inner());
8350 use std::sync::atomic::Ordering;
8351 // 1-char: bangchar=='Q', hatchar=='\0', hashchar=='\0'.
8352 histcharssetfn(Some("Q".to_string()));
8353 assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'Q' as i32);
8354 assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), 0);
8355 assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), 0);
8356 // 2-char: bangchar=='X', hatchar=='Y', hashchar=='\0'.
8357 histcharssetfn(Some("XY".to_string()));
8358 assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'X' as i32);
8359 assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b'Y' as i32);
8360 assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), 0);
8361 // 3-char: bangchar=='A', hatchar=='B', hashchar=='C'.
8362 histcharssetfn(Some("ABC".to_string()));
8363 assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'A' as i32);
8364 assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b'B' as i32);
8365 assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'C' as i32);
8366 // 4+ char: c:5087-5088 truncates to 3.
8367 histcharssetfn(Some("WXYZ".to_string()));
8368 assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'W' as i32);
8369 assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b'X' as i32);
8370 assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'Y' as i32);
8371 // Reset to default.
8372 histcharssetfn(None);
8373 assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'!' as i32);
8374 assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b'^' as i32);
8375 assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'#' as i32);
8376 }
8377}
8378
8379// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8380// ─── RUST-ONLY ACCESSORS ───
8381//
8382// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
8383// RwLock<T>>` globals declared above. C zsh uses direct global
8384// access; Rust needs these wrappers because `OnceLock::get_or_init`
8385// is the only way to lazily construct shared state. These fns sit
8386// here so the body of this file reads in C source order without
8387// the accessor wrappers interleaved between real port fns.
8388// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8389
8390// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8391// ─── RUST-ONLY ACCESSORS ───
8392//
8393// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
8394// RwLock<T>>` globals declared above. C zsh uses direct global
8395// access; Rust needs these wrappers because `OnceLock::get_or_init`
8396// is the only way to lazily construct shared state. These fns sit
8397// here so the body of this file reads in C source order without
8398// the accessor wrappers interleaved between real port fns.
8399// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8400
8401// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8402// ─── RUST-ONLY ACCESSORS ───
8403//
8404// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
8405// RwLock<T>>` globals declared above. C zsh uses direct global
8406// access; Rust needs these wrappers because `OnceLock::get_or_init`
8407// is the only way to lazily construct shared state. These fns sit
8408// here so the body of this file reads in C source order without
8409// the accessor wrappers interleaved between real port fns.
8410// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8411
8412fn foundparam_lock() -> &'static std::sync::Mutex<Option<String>> {
8413 FOUNDPARAM.get_or_init(|| std::sync::Mutex::new(None))
8414}
8415
8416/// Accessor for the global `paramtab` (Src/params.c:515).
8417/// Mirrors C's `paramtab->...` dereference by handing back the
8418/// inner RwLock; callers `.read()` for lookups and `.write()` for
8419/// mutation, operating on the `HashMap<String, Param>` directly.
8420pub fn paramtab() -> &'static RwLock<HashMap<String, crate::ported::zsh_h::Param>> {
8421 PARAMTAB_INNER.get_or_init(|| RwLock::new(HashMap::new()))
8422}
8423
8424/// Accessor for the global `realparamtab` (Src/params.c:515).
8425/// Same role as `paramtab` for the not-currently-redirected case;
8426/// the alias-flip during assoc-array iteration isn't modelled yet.
8427pub fn realparamtab() -> &'static RwLock<HashMap<String, crate::ported::zsh_h::Param>> {
8428 REALPARAMTAB_INNER.get_or_init(|| RwLock::new(HashMap::new()))
8429}
8430
8431fn scanprog_lock() -> &'static std::sync::Mutex<Option<String>> {
8432 SCANPROG.get_or_init(|| std::sync::Mutex::new(None))
8433}
8434
8435fn scanstr_lock() -> &'static std::sync::Mutex<Option<String>> {
8436 SCANSTR.get_or_init(|| std::sync::Mutex::new(None))
8437}
8438
8439fn paramvals_lock() -> &'static std::sync::Mutex<Vec<String>> {
8440 PARAMVALS.get_or_init(|| std::sync::Mutex::new(Vec::new()))
8441}
8442
8443#[cfg(test)]
8444mod tests {
8445 use super::*;
8446
8447 // test_param_value_conversions removed: tested deleted fake
8448 // `ParamValue::Scalar` constructor. C uses union access on
8449 // `pm->u.str`/`u.val`/`u.dval`/`u.arr` dispatched via
8450 // `PM_TYPE(pm->node.flags)` (Src/zsh.h:540).
8451 #[test]
8452 fn test_colonarr_conversion() {
8453 let arr = crate::ported::utils::colonsplit("/bin:/usr/bin:/usr/local/bin", false);
8454 assert_eq!(arr, vec!["/bin", "/usr/bin", "/usr/local/bin"]);
8455 let path = colonarrgetfn(&arr);
8456 assert_eq!(path, "/bin:/usr/bin:/usr/local/bin");
8457 }
8458 #[test]
8459 fn test_isident() {
8460 assert!(isident("foo"));
8461 assert!(isident("_bar"));
8462 assert!(isident("FOO_BAR"));
8463 assert!(isident("x123"));
8464 assert!(isident("123")); // positional params
8465 assert!(!isident(""));
8466 assert!(!isident("foo bar"));
8467 }
8468
8469 /// Pin: `isident` requires balanced `[...]` per `Src/params.c:1329-1330`:
8470 /// if (*ss != '[') return 0;
8471 /// if (!(ss = parse_subscript(++ss, 1, ']'))) return 0;
8472 ///
8473 /// The previous Rust port accepted ANY `[` as a valid
8474 /// terminator (`if c == '[' { return true; }`) without
8475 /// checking for a matching `]`. So `foo[` (no close) was
8476 /// accepted as a valid identifier — diverging from C which
8477 /// rejects.
8478 #[test]
8479 fn isident_requires_balanced_subscript_brackets() {
8480 // Balanced `[...]` is valid.
8481 assert!(isident("foo[0]"),
8482 "c:1330 — balanced [0] passes parse_subscript");
8483 assert!(isident("foo[bar]"),
8484 "c:1330 — balanced [bar] passes parse_subscript");
8485 // UNBALANCED — open without close — must be rejected.
8486 assert!(!isident("foo["),
8487 "c:1330 — `foo[` missing `]` MUST be rejected");
8488 // Trailing chars after `]` — C parse_subscript returns
8489 // a position INSIDE the string, the surrounding isident
8490 // body checks that nothing follows; our port currently
8491 // returns true at the first `[` either way, but pin the
8492 // balanced case as a working invariant.
8493 assert!(isident("a[1]"),
8494 "c:1330 — short balanced subscript valid");
8495 }
8496
8497
8498 #[test]
8499 fn test_unique_array() {
8500 let arr = vec!["a".into(), "b".into(), "a".into(), "c".into(), "b".into()];
8501 let result = uniqarray(arr);
8502 assert_eq!(result, vec!["a", "b", "c"]);
8503 }
8504
8505 #[test]
8506 fn test_convbase() {
8507 // CBASES off (default): `16#FF` / `8#7` form. The `0x.../
8508 // 0...` short-prefix output is gated on `setopt CBASES` —
8509 // see Src/params.c:5599-5605.
8510 assert_eq!(convbase(255, 16), "16#FF");
8511 assert_eq!(convbase(10, 10), "10");
8512 assert_eq!(convbase(-5, 10), "-5");
8513 assert_eq!(convbase(7, 8), "8#7");
8514 assert_eq!(convbase(5, 2), "2#101");
8515 }
8516
8517 #[test]
8518 fn test_convfloat() {
8519 // Use 2.5 instead of 3.14 — clippy errors on the latter as
8520 // an approx PI constant. The test checks 2-decimal formatting
8521 // round-trips, which the exact value doesn't influence.
8522 let s = convfloat(2.5, 2, crate::ported::zsh_h::PM_FFLOAT);
8523 assert!(s.starts_with("2.50"));
8524
8525 assert_eq!(convfloat(f64::INFINITY, 0, 0), "Inf");
8526 assert_eq!(convfloat(f64::NEG_INFINITY, 0, 0), "-Inf");
8527 assert_eq!(convfloat(f64::NAN, 0, 0), "NaN");
8528 }
8529
8530
8531
8532
8533 #[test]
8534 fn test_getarrvalue() {
8535 let arr = vec!["a".into(), "b".into(), "c".into(), "d".into()];
8536 assert_eq!(getarrvalue(&arr, 2, 3), vec!["b", "c"]);
8537 assert_eq!(getarrvalue(&arr, -2, -1), vec!["c", "d"]);
8538 assert_eq!(getarrvalue(&arr, 1, 4), vec!["a", "b", "c", "d"]);
8539 }
8540
8541 #[test]
8542 fn test_setarrvalue() {
8543 // c:2897 — setarrvalue bails when unset(EXECOPT). Set "exec"
8544 // for the unit-test env (real zsh defaults exec=true).
8545 let saved_exec = crate::ported::options::opt_state_get("exec")
8546 .unwrap_or(false);
8547 crate::ported::options::opt_state_set("exec", true);
8548 // C-faithful: setarrvalue takes a Value pointing at a Param
8549 // with u_arr set. Construct one inline.
8550 use crate::ported::zsh_h::{hashnode, param, PM_ARRAY};
8551 let pm = Box::new(param {
8552 node: hashnode { next: None, nam: "test".to_string(), flags: PM_ARRAY as i32 },
8553 u_data: 0,
8554 u_arr: Some(vec!["a".into(), "b".into(), "c".into(), "d".into()]),
8555 u_str: None, u_val: 0, u_dval: 0.0, u_hash: None,
8556 gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
8557 base: 0, width: 0, env: None, ename: None, old: None, level: 0,
8558 });
8559 let mut v = crate::ported::zsh_h::value {
8560 pm: Some(pm),
8561 arr: Vec::new(),
8562 scanflags: 0,
8563 valflags: 0,
8564 start: 2,
8565 end: 3,
8566 };
8567 setarrvalue(&mut v, vec!["X".into(), "Y".into()]);
8568 let arr = v.pm.unwrap().u_arr.unwrap();
8569 assert_eq!(arr, vec!["a", "X", "Y", "d"]);
8570 crate::ported::options::opt_state_set("exec", saved_exec);
8571 }
8572
8573 #[test]
8574 fn test_valid_refname() {
8575 assert!(valid_refname("foo", 0));
8576 assert!(valid_refname("_bar", 0));
8577 assert!(valid_refname("1", 0));
8578 assert!(valid_refname("!", 0));
8579 assert!(valid_refname("arr[1]", 0));
8580 assert!(!valid_refname("", 0));
8581 // C semantics: empty leader without one of `! ? $ - _` is rejected.
8582 assert!(!valid_refname(" ", 0));
8583 // PM_UPPER rejects digit-leader and argv/ARGC.
8584 assert!(!valid_refname("1", PM_UPPER as i32));
8585 assert!(!valid_refname("argv", PM_UPPER as i32));
8586 assert!(!valid_refname("ARGC", PM_UPPER as i32));
8587 }
8588
8589 #[test]
8590 fn test_uniq_array_empty() {
8591 let empty: Vec<String> = Vec::new();
8592 assert!(uniqarray(empty).is_empty());
8593 }
8594
8595 #[test]
8596 fn test_convbase_underscore() {
8597 let s = convbase_underscore(1234567, 10, 3);
8598 assert_eq!(s, "1_234_567");
8599 }
8600
8601 fn val_str(v: getarg_out<'_>) -> String {
8602 match v {
8603 getarg_out::Value(v) => v.to_str(),
8604 getarg_out::Flags { .. } => panic!("expected Value, got Flags"),
8605 }
8606 }
8607
8608 #[test]
8609 fn getarg_n_flag_picks_second_exact_match() {
8610 // C params.c:1431-1442 + 1758 — `(en.2.)pat` picks 2nd exact match.
8611 let arr: Vec<String> = vec!["foo".into(), "bar".into(), "foo".into(), "baz".into()];
8612 let out = getarg("(en.2.r)foo", Some(&arr), None, None).expect("Some");
8613 assert_eq!(val_str(out), "foo");
8614 }
8615
8616 #[test]
8617 fn getarg_n_flag_third_exact_match() {
8618 let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into(), "b".into()];
8619 let out = getarg("(en.3.r)a", Some(&arr), None, None).expect("Some");
8620 assert_eq!(val_str(out), "a");
8621 }
8622
8623 #[test]
8624 fn getarg_n_flag_returns_index_with_i() {
8625 // (en.2.i) — return INDEX of 2nd exact match.
8626 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
8627 let out = getarg("(en.2.i)x", Some(&arr), None, None).expect("Some");
8628 assert_eq!(val_str(out), "3");
8629 }
8630
8631 #[test]
8632 fn getarg_negative_n_flips_search_direction() {
8633 // C params.c:1488-1491 — negative `num` flips down (reverse).
8634 // (en.-1.) on forward-default search matches from the end.
8635 let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into()];
8636 let out = getarg("(en.-1.i)a", Some(&arr), None, None).expect("Some");
8637 assert_eq!(val_str(out), "3");
8638 }
8639
8640 #[test]
8641 fn getarg_n_flag_zero_treated_as_one() {
8642 // C params.c:1438-1439 — `if (!num) num = 1`.
8643 let arr: Vec<String> = vec!["x".into(), "y".into()];
8644 let out = getarg("(en.0.r)x", Some(&arr), None, None).expect("Some");
8645 assert_eq!(val_str(out), "x");
8646 }
8647
8648 #[test]
8649 fn getarg_unknown_flag_char_returns_none() {
8650 // C params.c:1477-1483 flagerr — invalid flag char reports error.
8651 let arr: Vec<String> = vec!["x".into()];
8652 assert!(getarg("(z)x", Some(&arr), None, None).is_none());
8653 }
8654
8655 #[test]
8656 fn getarg_n_flag_unterminated_arg_returns_none() {
8657 // (n.5 missing closing delimiter — flagerr.
8658 let arr: Vec<String> = vec!["x".into()];
8659 assert!(getarg("(n.5", Some(&arr), None, None).is_none());
8660 }
8661
8662 #[test]
8663 fn getarg_b_flag_starts_search_at_index() {
8664 // C params.c:1748-1760 — `(b.N.e)pat` skips first N-1 elements
8665 // forward (parsed value `N`, normalized to `beg = N-1`).
8666 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
8667 // Forward, beg=2 (skip first 2) → starts at idx 2 → 'x' at 3.
8668 let out = getarg("(b.3.ei)x", Some(&arr), None, None).expect("Some");
8669 assert_eq!(val_str(out), "3");
8670 }
8671
8672 #[test]
8673 fn getarg_b_flag_with_R_reverse_from_offset() {
8674 // C params.c:1750-1755 — reverse search starting at parsed-1 idx.
8675 // arr=(x y x y), beg=2 (parsed 3-1), reverse → walks 2,1,0; first
8676 // exact 'x' is at idx 2 → 1-based "3".
8677 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
8678 let out = getarg("(b.3.eIR)x", Some(&arr), None, None).expect("Some");
8679 assert_eq!(val_str(out), "3");
8680 }
8681
8682 #[test]
8683 fn getarg_b_flag_out_of_bounds_forward_returns_empty() {
8684 // c:1746 — beg >= len returns len+1 (empty for value-mode).
8685 let arr: Vec<String> = vec!["x".into()];
8686 let out = getarg("(b.5.er)x", Some(&arr), None, None).expect("Some");
8687 assert_eq!(val_str(out), "");
8688 }
8689
8690 #[test]
8691 fn getarg_b_flag_out_of_bounds_index_mode_returns_len_plus_one() {
8692 let arr: Vec<String> = vec!["x".into(), "y".into()];
8693 let out = getarg("(b.5.ei)x", Some(&arr), None, None).expect("Some");
8694 assert_eq!(val_str(out), "3");
8695 }
8696
8697 #[test]
8698 fn getarg_hash_neg_num_on_lowercase_r_returns_all() {
8699 // C params.c:1488-1491 — neg `num` flips down on `r`,
8700 // converting hash search to return-all-matches semantics.
8701 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8702 h.insert("a".into(), "1".into());
8703 h.insert("b".into(), "1".into());
8704 h.insert("c".into(), "2".into());
8705 let out = getarg("(en.-1.r)1", None, Some(&h), None).expect("Some");
8706 // r + neg = R semantics → all values where pat matches value.
8707 assert_eq!(val_str(out), "1 1");
8708 }
8709
8710 #[test]
8711 fn getarg_hash_neg_num_on_uppercase_R_returns_single() {
8712 // R + neg `num` un-flips back to single-match (r semantics).
8713 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8714 h.insert("a".into(), "1".into());
8715 h.insert("b".into(), "1".into());
8716 h.insert("c".into(), "2".into());
8717 let out = getarg("(en.-1.R)1", None, Some(&h), None).expect("Some");
8718 // R + neg → r → single first match.
8719 assert_eq!(val_str(out), "1");
8720 }
8721
8722 #[test]
8723 fn getarg_hash_b_flag_skips_first_n_entries() {
8724 // C params.c:1740-1742 — `b<NUM>` skips first N-1 entries
8725 // before searching. Hash iteration is insertion order.
8726 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8727 h.insert("a".into(), "1".into());
8728 h.insert("b".into(), "1".into());
8729 h.insert("c".into(), "1".into());
8730 // beg=2 (parsed 3-1) → skip first 2, scan from "c" onward.
8731 let out = getarg("(b.3.ei)1", None, Some(&h), None).expect("Some");
8732 assert_eq!(val_str(out), "c");
8733 }
8734
8735 #[test]
8736 fn getarg_hash_b_flag_with_R_collects_from_offset() {
8737 // R returns all matches; b skips first beg entries first.
8738 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8739 h.insert("a".into(), "1".into());
8740 h.insert("b".into(), "1".into());
8741 h.insert("c".into(), "1".into());
8742 let out = getarg("(b.2.eI)1", None, Some(&h), None).expect("Some");
8743 // beg=1, return_all=I → walk from "b" onward, all matching keys.
8744 assert_eq!(val_str(out), "b c");
8745 }
8746
8747 #[test]
8748 fn getarg_hash_b_flag_out_of_bounds_returns_empty() {
8749 // c:1746 — beg >= len with single-match → empty.
8750 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
8751 h.insert("a".into(), "1".into());
8752 let out = getarg("(b.5.e)1", None, Some(&h), None).expect("Some");
8753 assert_eq!(val_str(out), "");
8754 }
8755
8756 #[test]
8757 fn getarg_w_flag_splits_multi_word_array_elements() {
8758 // C params.c:1761-1797 — `(w)N` joins array then re-splits by
8759 // IFS-default whitespace. arr=("a b" "c d"); (w)2 → "b" not "c d".
8760 let arr: Vec<String> = vec!["a b".into(), "c d".into()];
8761 let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
8762 assert_eq!(val_str(out), "b");
8763 }
8764
8765 #[test]
8766 fn getarg_w_flag_simple_array_indexing_still_works() {
8767 let arr: Vec<String> = vec!["one".into(), "two".into(), "three".into()];
8768 let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
8769 assert_eq!(val_str(out), "two");
8770 }
8771
8772 #[test]
8773 fn getarg_f_flag_splits_by_newline() {
8774 // C params.c:1424-1427 — `f` flag aliases `w` with sep="\n".
8775 // arr=("a b\nc d"); (f)2 → "c d" (split by \n only, not space).
8776 let arr: Vec<String> = vec!["a b\nc d".into()];
8777 let out = getarg("(f)2", Some(&arr), None, None).expect("Some");
8778 assert_eq!(val_str(out), "c d");
8779 }
8780
8781 #[test]
8782 fn getarg_scalar_w_flag_picks_nth_word() {
8783 // C params.c:1761-1797 — scalar word-mode arm. `(w)2` on
8784 // scalar "hello world foo" returns the 2nd whitespace word.
8785 let out = getarg("(w)2", None, None, Some("hello world foo")).expect("Some");
8786 assert_eq!(val_str(out), "world");
8787 }
8788
8789 #[test]
8790 fn getarg_scalar_w_flag_negative_index_counts_from_end() {
8791 let out = getarg("(w)-1", None, None, Some("alpha beta gamma")).expect("Some");
8792 assert_eq!(val_str(out), "gamma");
8793 }
8794
8795 #[test]
8796 fn getarg_scalar_re_returns_char_at_match_position() {
8797 // C params.c:1798-1980 — char-search returns CHAR at match
8798 // position, not full substring. Verified empirically:
8799 // /bin/zsh -c 's="barfooxyz"; print "${s[(r)foo]}"' → "f"
8800 let out = getarg("(re)bc", None, None, Some("abcdef")).expect("Some");
8801 assert_eq!(val_str(out), "b");
8802 }
8803
8804 #[test]
8805 fn getarg_scalar_ie_returns_position_of_first_match() {
8806 let out = getarg("(ie)cd", None, None, Some("abcdef")).expect("Some");
8807 // 'cd' starts at 1-based position 3.
8808 assert_eq!(val_str(out), "3");
8809 }
8810
8811 #[test]
8812 fn getarg_scalar_Ie_returns_position_of_last_match() {
8813 let out = getarg("(Ie)b", None, None, Some("abcabc")).expect("Some");
8814 // Last 'b' is at 1-based position 5.
8815 assert_eq!(val_str(out), "5");
8816 }
8817
8818 #[test]
8819 fn getarg_scalar_ie_no_match_returns_len_plus_one() {
8820 let out = getarg("(ie)z", None, None, Some("abc")).expect("Some");
8821 assert_eq!(val_str(out), "4");
8822 }
8823
8824 #[test]
8825 fn getarg_scalar_Ie_no_match_returns_zero() {
8826 let out = getarg("(Ie)z", None, None, Some("abc")).expect("Some");
8827 assert_eq!(val_str(out), "0");
8828 }
8829
8830 #[test]
8831 fn getarg_scalar_n_flag_picks_second_match() {
8832 // C params.c:1929/1964 — `!--num` Nth-match counter on
8833 // scalar char-search. abcabc: 'a' at idx 0 and 3 → 2nd match
8834 // at byte position 4 (1-based).
8835 let out = getarg("(en.2.i)a", None, None, Some("abcabc")).expect("Some");
8836 assert_eq!(val_str(out), "4");
8837 }
8838
8839 #[test]
8840 fn getarg_scalar_b_flag_starts_from_offset() {
8841 // C params.c:1740-1742 — `(b.N.)` starts search from idx N-1.
8842 // abc bc abc: with b=4, skip first 3 chars; first 'b' at byte 5.
8843 let out = getarg("(b.4.ei)b", None, None, Some("abcbc")).expect("Some");
8844 assert_eq!(val_str(out), "4");
8845 }
8846
8847 #[test]
8848 fn getarg_scalar_re_n2_picks_second_substring() {
8849 let out = getarg("(en.2.r)b", None, None, Some("abab")).expect("Some");
8850 assert_eq!(val_str(out), "b");
8851 }
8852
8853 /// c:3076/3193 — assignsparam writes into paramtab; getsparam
8854 /// reads it back. The round-trip is the spine of every
8855 /// `foo=bar; print $foo` flow. Regression here would silently
8856 /// drop assignments.
8857 #[test]
8858 fn assignsparam_then_getsparam_round_trips() {
8859 let name = "ZSHRS_TEST_ASSIGN_GET";
8860 crate::ported::params::assignsparam(name, "test_value_42", 0);
8861 assert_eq!(
8862 crate::ported::params::getsparam(name).as_deref(),
8863 Some("test_value_42")
8864 );
8865 // Cleanup so other tests don't see leaked param.
8866 let _ = crate::ported::params::paramtab().write().unwrap().remove(name);
8867 }
8868
8869 /// c:3076 — getsparam on a non-existent param returns None.
8870 /// A regression returning Some("") would mask unset-param errors.
8871 #[test]
8872 fn getsparam_unknown_param_returns_none() {
8873 assert!(crate::ported::params::getsparam("ZSHRS_TEST_DEFINITELY_UNSET").is_none());
8874 }
8875
8876 /// c:3819 — direct paramtab.remove drops the entry; subsequent
8877 /// getsparam returns None. The set→remove→lookup gap verifies
8878 /// the canonical paramtab is actually backing both reads + writes.
8879 #[test]
8880 fn paramtab_remove_makes_getsparam_return_none() {
8881 let name = "ZSHRS_TEST_UNSET_FLOW";
8882 crate::ported::params::assignsparam(name, "to_be_removed", 0);
8883 assert!(crate::ported::params::getsparam(name).is_some(),
8884 "param must be set before remove path");
8885 let _ = crate::ported::params::paramtab().write().unwrap().remove(name);
8886 assert!(crate::ported::params::getsparam(name).is_none(),
8887 "after remove, getsparam must return None");
8888 }
8889
8890 /// c:3357 — assignaparam stores an array. getsparam on an array
8891 /// param returns the first element OR a join (depends on IFS).
8892 /// Verify the slot was populated AT ALL by querying paramtab.
8893 #[test]
8894 fn assignaparam_populates_paramtab_with_array() {
8895 let name = "ZSHRS_TEST_ARR_X";
8896 crate::ported::params::assignaparam(
8897 name, vec!["a".into(), "b".into(), "c".into()], 0);
8898 let tab = crate::ported::params::paramtab().read().expect("paramtab poisoned");
8899 let pm = tab.get(name).expect("param installed");
8900 assert_eq!(pm.u_arr.as_deref(), Some(&["a".to_string(), "b".to_string(), "c".to_string()][..]),
8901 "assignaparam stores all three elements");
8902 drop(tab);
8903 let _ = crate::ported::params::paramtab().write().unwrap().remove(name);
8904 }
8905
8906 // Use the module-scope HISTCHARS_TEST_LOCK_SHARED (declared
8907 // outside the test modules) so gsu_tests + tests serialise
8908 // against the same Mutex rather than two independent ones.
8909 use super::HISTCHARS_TEST_LOCK_SHARED as HISTCHARS_TEST_LOCK;
8910
8911 /// `Src/params.c:5095-5097` — `histcharssetfn` stores bangchar /
8912 /// hatchar / hashchar in the per-char globals. Pin the round-trip
8913 /// for ALL THREE: change HISTCHARS to a custom 3-char string,
8914 /// verify each atomic global reflects the new value, and verify
8915 /// the canonical default `"!^#"` restores on NULL.
8916 #[test]
8917 fn histcharssetfn_syncs_all_three_histchar_globals() {
8918 let _g = HISTCHARS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8919 use std::sync::atomic::Ordering;
8920 // Default state.
8921 crate::ported::params::histcharssetfn(None);
8922 assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'!' as i32);
8923 assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b'^' as i32);
8924 assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'#' as i32);
8925 // Set HISTCHARS to "@:%".
8926 crate::ported::params::histcharssetfn(Some("@:%".to_string()));
8927 assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'@' as i32,
8928 "c:5095 — bangchar = first byte of HISTCHARS");
8929 assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), b':' as i32,
8930 "c:5096 — hatchar = second byte of HISTCHARS");
8931 assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'%' as i32,
8932 "c:5097 — hashchar = third byte of HISTCHARS");
8933 // Restore.
8934 crate::ported::params::histcharssetfn(None);
8935 assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), b'!' as i32);
8936 assert_eq!(crate::ported::hist::hashchar.load(Ordering::SeqCst), b'#' as i32);
8937 }
8938
8939 /// `Src/params.c:5064-5074` — `histcharsgetfn` reads from the
8940 /// three atomic globals and returns a string of non-NUL bytes.
8941 /// Pin set→get symmetry: after `histcharssetfn(Some("@&%"))`,
8942 /// `histcharsgetfn()` returns `"@&%"`.
8943 #[test]
8944 fn histcharsgetfn_round_trips_with_histcharssetfn() {
8945 let _g = HISTCHARS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8946 crate::ported::params::histcharssetfn(Some("@&%".to_string()));
8947 assert_eq!(crate::ported::params::histcharsgetfn(), "@&%",
8948 "c:5068-5073 — getfn reads atomic globals setfn wrote");
8949 // Restore default and verify round-trip.
8950 crate::ported::params::histcharssetfn(None);
8951 assert_eq!(crate::ported::params::histcharsgetfn(), "!^#",
8952 "default `!^#` round-trips through atomics");
8953 }
8954
8955 /// `Src/params.c:5118-5128` — `homesetfn(x)` round-trip:
8956 /// `homesetfn(s); homegetfn() == s` for non-symlink paths and
8957 /// CHASELINKS-off. Pins the basic store-then-read contract.
8958 #[test]
8959 fn homesetfn_stores_value_for_getfn() {
8960 let saved = crate::ported::params::homegetfn();
8961 crate::ported::params::homesetfn("/tmp/zshrs_test_home".to_string());
8962 assert_eq!(crate::ported::params::homegetfn(), "/tmp/zshrs_test_home",
8963 "c:5121-5126 — homesetfn → homegetfn round-trip");
8964 // Restore.
8965 crate::ported::params::homesetfn(saved);
8966 }
8967
8968 /// `Src/params.c:5125-5126` — empty input becomes `ztrdup("")`.
8969 /// Pin empty-string handling.
8970 #[test]
8971 fn homesetfn_empty_input_stores_empty() {
8972 let saved = crate::ported::params::homegetfn();
8973 crate::ported::params::homesetfn(String::new());
8974 assert_eq!(crate::ported::params::homegetfn(), "",
8975 "c:5126 — empty x stores empty (no panic)");
8976 crate::ported::params::homesetfn(saved);
8977 }
8978
8979 /// `Src/params.c:5004-5011` — `errnosetfn(x)` writes errno
8980 /// unconditionally, then warns (NOT errors) on truncation. The
8981 /// store happens regardless of warning. Pin set→get round-trip.
8982 #[test]
8983 #[cfg(any(target_os = "macos", target_os = "linux"))]
8984 fn errnosetfn_writes_through_to_libc_errno_getfn() {
8985 // Set errno to a small int.
8986 crate::ported::params::errnosetfn(42);
8987 assert_eq!(crate::ported::params::errnogetfn(), 42,
8988 "c:5006 — errno = (int)x; subsequent getfn must read it back");
8989 crate::ported::params::errnosetfn(0);
8990 assert_eq!(crate::ported::params::errnogetfn(), 0);
8991 }
8992
8993 /// `Src/params.c:5008-5010` — truncation check fires when
8994 /// `(zlong)errno != x`. C also resets errno indirectly inside
8995 /// `zwarn` (libc calls touch errno) — so after the warning,
8996 /// the user's observed `$ERRNO` is the post-warning value, NOT
8997 /// the truncated cast. Faithful Rust port has the same behavior.
8998 /// Pin only that the function returns normally and doesn't crash;
8999 /// any specific post-call errno value is implementation-defined.
9000 #[test]
9001 #[cfg(any(target_os = "macos", target_os = "linux"))]
9002 fn errnosetfn_does_not_panic_on_truncation() {
9003 // i64::MAX → truncates to i32 = -1 → warning fires inside.
9004 // The store at c:5008 happens; whether the warning's libc
9005 // calls then overwrite errno is implementation-defined.
9006 crate::ported::params::errnosetfn(i64::MAX);
9007 // Just verify the call returned (no panic) and getfn works.
9008 let _ = crate::ported::params::errnogetfn();
9009 // Reset.
9010 crate::ported::params::errnosetfn(0);
9011 }
9012
9013 /// `Src/params.c:5090-5093` — non-ASCII chars in HISTCHARS
9014 /// produce a warning and the function returns WITHOUT updating
9015 /// any globals. Pin the rejection: state before == state after
9016 /// when a non-ASCII byte is in position 0/1/2.
9017 #[test]
9018 fn histcharssetfn_rejects_non_ascii_chars() {
9019 let _g = HISTCHARS_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9020 use std::sync::atomic::Ordering;
9021 // Reset to defaults.
9022 crate::ported::params::histcharssetfn(None);
9023 let bang_before = crate::ported::hist::bangchar.load(Ordering::SeqCst);
9024 let hat_before = crate::ported::hist::hatchar.load(Ordering::SeqCst);
9025 // Try to set HISTCHARS with non-ASCII char.
9026 crate::ported::params::histcharssetfn(Some("é".to_string()));
9027 // c:5092 — rejection returns BEFORE any state changes.
9028 assert_eq!(crate::ported::hist::bangchar.load(Ordering::SeqCst), bang_before,
9029 "c:5092 — bangchar unchanged after non-ASCII rejection");
9030 assert_eq!(crate::ported::hist::hatchar.load(Ordering::SeqCst), hat_before);
9031 }
9032
9033 /// Shared mutex for tests that mutate argzero/posixzero — both
9034 /// share global state and race when run in parallel.
9035 static ARGZERO_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
9036
9037 // HISTSIZ_TEST_LOCK is defined at module scope to share between
9038 // gsu_tests and tests submodules — both mutate histsiz.
9039
9040 /// `Src/params.c:4974-4977` — `histsizesetfn` floors at 1 then
9041 /// calls `resizehistents()` to prune the in-memory ring to the
9042 /// new cap. The previous Rust port skipped the resize call (and
9043 /// also failed to mirror the value into `hist::histsiz`), so
9044 /// HISTSIZE shrinks didn't take effect until the next implicit
9045 /// prune. Pin: setting HISTSIZE to N caps both the param store
9046 /// AND the hist::histsiz atomic used by resizehistents.
9047 #[test]
9048 fn histsizesetfn_floors_at_one_and_mirrors_to_hist_module() {
9049 let _g = HISTSIZ_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9050 use std::sync::atomic::Ordering;
9051 let saved_param = histsizegetfn();
9052 let saved_hist = crate::ported::hist::histsiz.load(Ordering::SeqCst);
9053
9054 // c:4976 — value < 1 floors at 1.
9055 crate::ported::params::histsizesetfn(0);
9056 assert_eq!(histsizegetfn(), 1,
9057 "c:4976 — HISTSIZE 0 must floor at 1");
9058 assert_eq!(crate::ported::hist::histsiz.load(Ordering::SeqCst), 1,
9059 "c:4977 — mirror into hist::histsiz so resizehistents sees it");
9060
9061 // Negative floors too.
9062 crate::ported::params::histsizesetfn(-5);
9063 assert_eq!(histsizegetfn(), 1, "c:4976 — negative floors at 1");
9064
9065 // Positive passes through.
9066 crate::ported::params::histsizesetfn(500);
9067 assert_eq!(histsizegetfn(), 500);
9068 assert_eq!(crate::ported::hist::histsiz.load(Ordering::SeqCst), 500);
9069
9070 // Restore.
9071 *crate::ported::params::histsiz_lock().lock().unwrap() = saved_param;
9072 crate::ported::hist::histsiz.store(saved_hist, Ordering::SeqCst);
9073 }
9074
9075 /// `Src/params.c:5152-5158` — `underscoregetfn` returns
9076 /// `dupstring(zunderscore)` then runs `untokenize(u)` on it.
9077 /// The Rust port previously skipped untokenize, exposing raw
9078 /// lexer-injected token bytes (Stringg, Equals, ...) in `$_`
9079 /// reads.
9080 #[test]
9081 fn underscoregetfn_runs_untokenize_on_zunderscore() {
9082 // Inject zunderscore containing a Pound token byte (\u{84})
9083 // and verify it gets stripped by untokenize in the return.
9084 let saved = crate::ported::params::zunderscore_lock()
9085 .lock().unwrap().clone();
9086
9087 // Set zunderscore to a string containing a Pound token byte
9088 // surrounded by literals.
9089 let pound = crate::ported::zsh_h::Pound;
9090 let mut s = String::new();
9091 s.push('a');
9092 s.push(pound);
9093 s.push('b');
9094 *crate::ported::params::zunderscore_lock().lock().unwrap() = s;
9095
9096 let result = crate::ported::params::underscoregetfn();
9097 // c:5156 — untokenize replaces Pound (ITOK) with '#'
9098 // (its ztokens entry). The raw \u{84} byte must NOT survive.
9099 assert!(!result.contains(pound),
9100 "c:5156 — untokenize must strip Pound token byte from $_");
9101 assert!(result.contains('#') || result.contains("a"),
9102 "c:5156 — Pound (ITOK) maps to '#' via ztokens[0]");
9103
9104 // Restore.
9105 *crate::ported::params::zunderscore_lock().lock().unwrap() = saved;
9106 }
9107
9108 /// `Src/params.c:4954-4961` — `argzerogetfn` returns `posixzero`
9109 /// when `isset(POSIXARGZERO)`, else `argzero`. The previous Rust
9110 /// port always returned `argzero`, defeating the POSIXARGZERO
9111 /// option entirely. After mutating argzero (e.g. `exec -a foo`),
9112 /// `$0` under POSIXARGZERO must report the ORIGINAL startup
9113 /// argv[0], not the rewritten name.
9114 #[test]
9115 fn argzerogetfn_respects_posixargzero_option() {
9116 let _g = ARGZERO_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9117 use crate::ported::options::{opt_state_get, opt_state_set};
9118 use crate::ported::utils::{set_argzero, set_posixzero, argzero, posixzero};
9119
9120 // Save state.
9121 let saved_argzero = argzero();
9122 let saved_posixzero = posixzero();
9123 let saved_pos_option = opt_state_get("posixargzero").unwrap_or(false);
9124
9125 // Set up: posixzero (original) ≠ argzero (rewritten).
9126 set_posixzero(Some("/bin/zsh".to_string()));
9127 set_argzero(Some("rewritten-name".to_string()));
9128 // The set_argzero call mirrors to posixzero only if unset,
9129 // and we set posixzero first → mirror skipped. Confirm separation.
9130
9131 // POSIXARGZERO off → returns argzero.
9132 opt_state_set("posixargzero", false);
9133 assert_eq!(crate::ported::params::argzerogetfn(), "rewritten-name",
9134 "c:4960 — !POSIXARGZERO returns argzero (current display name)");
9135
9136 // POSIXARGZERO on → returns posixzero (the preserved startup argv[0]).
9137 opt_state_set("posixargzero", true);
9138 assert_eq!(crate::ported::params::argzerogetfn(), "/bin/zsh",
9139 "c:4959 — POSIXARGZERO on returns posixzero (original startup argv[0])");
9140
9141 // Restore.
9142 set_argzero(saved_argzero);
9143 set_posixzero(saved_posixzero);
9144 opt_state_set("posixargzero", saved_pos_option);
9145 }
9146
9147 /// `Src/init.c:271` — `argv0 = argzero = posixzero = *argv++`.
9148 /// At shell init both share the same source. The Rust port
9149 /// preserves this contract by having `set_argzero` mirror to
9150 /// `posixzero` ONLY on first call (when posixzero is None).
9151 /// Subsequent argzero changes (function frames, exec -a) must
9152 /// NOT clobber posixzero.
9153 #[test]
9154 fn set_argzero_mirrors_to_posixzero_only_on_first_call() {
9155 let _g = ARGZERO_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9156 use crate::ported::utils::{set_argzero, set_posixzero, argzero, posixzero};
9157
9158 let saved_argzero = argzero();
9159 let saved_posixzero = posixzero();
9160
9161 // Reset both to None.
9162 set_argzero(None);
9163 set_posixzero(None);
9164 // First call: posixzero is None, so it should mirror.
9165 set_argzero(Some("/usr/local/bin/zsh".to_string()));
9166 assert_eq!(posixzero().as_deref(), Some("/usr/local/bin/zsh"),
9167 "c:271 — first set_argzero mirrors to posixzero (was None)");
9168 // Second call: posixzero now Some, so mirror is skipped.
9169 set_argzero(Some("function-name".to_string()));
9170 assert_eq!(posixzero().as_deref(), Some("/usr/local/bin/zsh"),
9171 "c:271 — second set_argzero does NOT clobber posixzero");
9172 assert_eq!(argzero().as_deref(), Some("function-name"),
9173 "argzero updated as normal");
9174
9175 // Restore.
9176 set_posixzero(saved_posixzero);
9177 set_argzero(saved_argzero);
9178 }
9179
9180 /// Locale-touching tests share process-wide env + libc state.
9181 /// Cargo runs tests in parallel by default, so without
9182 /// serialization a concurrent `env::set_var("LC_ALL")` can race
9183 /// a `env::remove_var("LC_ALL")` and corrupt assertions. Pin
9184 /// every locale test through this Mutex.
9185 fn locale_test_lock() -> &'static std::sync::Mutex<()> {
9186 static L: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
9187 L.get_or_init(|| std::sync::Mutex::new(()))
9188 }
9189
9190 /// Pin `LC_NAMES` to the canonical zsh `lc_names[]` table at
9191 /// `Src/params.c:4805-4825`. The five categories in entry order
9192 /// (LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_NUMERIC, LC_TIME) MUST
9193 /// match — `lcsetfn` walks this table by `strcmp(ln->name, pm->node.nam)`
9194 /// per c:4926 and dispatches to `setlocale(ln->category, ...)`.
9195 #[test]
9196 fn lc_names_match_zsh_canonical_table() {
9197 let names: Vec<&str> = LC_NAMES.iter().map(|(n, _)| *n).collect();
9198 assert_eq!(
9199 names,
9200 vec!["LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "LC_NUMERIC", "LC_TIME"],
9201 "Src/params.c:4805-4825 — lc_names entry order must be preserved"
9202 );
9203 // Verify each name maps to a distinct libc category — proves
9204 // we aren't aliasing LC_NUMERIC to LC_TIME etc.
9205 let cats: Vec<libc::c_int> = LC_NAMES.iter().map(|(_, c)| *c).collect();
9206 let mut sorted = cats.clone();
9207 sorted.sort();
9208 sorted.dedup();
9209 assert_eq!(sorted.len(), 5, "all five LC_* categories must be distinct");
9210 assert!(cats.contains(&libc::LC_COLLATE));
9211 assert!(cats.contains(&libc::LC_CTYPE));
9212 assert!(cats.contains(&libc::LC_MESSAGES));
9213 assert!(cats.contains(&libc::LC_NUMERIC));
9214 assert!(cats.contains(&libc::LC_TIME));
9215 }
9216
9217 /// Pin `lcsetfn` to the canonical `setlocale` invocation at
9218 /// `Src/params.c:4925-4927`. When LC_ALL is empty and pm matches
9219 /// an entry in `lc_names`, libc setlocale MUST be called with
9220 /// the corresponding category. Verified by reading libc state
9221 /// back via `setlocale(cat, NULL)` after the assignment.
9222 #[test]
9223 fn lcsetfn_invokes_libc_setlocale_for_matching_category() {
9224 let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
9225 // Save LC_ALL/LC_CTYPE state.
9226 let saved_lc_all = env::var("LC_ALL").ok();
9227 let saved_lc_ctype = env::var("LC_CTYPE").ok();
9228 env::remove_var("LC_ALL"); // c:4912 LC_ALL must be empty for body to run
9229
9230 // Read libc's current LC_CTYPE setting.
9231 let before = unsafe {
9232 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
9233 if p.is_null() {
9234 String::new()
9235 } else {
9236 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9237 }
9238 };
9239
9240 // Call lcsetfn with LC_CTYPE → "C" (universally available POSIX locale).
9241 lcsetfn("LC_CTYPE", Some("C".to_string()));
9242
9243 // Read it back — must report "C" since C invokes setlocale(LC_CTYPE, "C").
9244 let after = unsafe {
9245 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
9246 if p.is_null() {
9247 String::new()
9248 } else {
9249 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9250 }
9251 };
9252 assert_eq!(after, "C",
9253 "Src/params.c:4927 — lcsetfn must call setlocale(LC_CTYPE, \"C\")");
9254
9255 // Env mirror also set.
9256 assert_eq!(env::var("LC_CTYPE").unwrap_or_default(), "C");
9257
9258 // Restore libc + env state.
9259 let _ = unsafe {
9260 let c = std::ffi::CString::new(before.as_bytes()).unwrap_or_default();
9261 libc::setlocale(libc::LC_CTYPE, c.as_ptr())
9262 };
9263 match saved_lc_all {
9264 Some(v) => env::set_var("LC_ALL", v),
9265 None => env::remove_var("LC_ALL"),
9266 }
9267 match saved_lc_ctype {
9268 Some(v) => env::set_var("LC_CTYPE", v),
9269 None => env::remove_var("LC_CTYPE"),
9270 }
9271 }
9272
9273 /// Pin `lcsetfn`'s LC_ALL early-return per c:4912-4913: when
9274 /// LC_ALL is non-empty, lcsetfn must short-circuit BEFORE
9275 /// touching libc setlocale for the per-category override.
9276 #[test]
9277 fn lcsetfn_short_circuits_when_lc_all_set() {
9278 let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
9279 let saved_lc_all = env::var("LC_ALL").ok();
9280 let saved_lc_ctype = env::var("LC_CTYPE").ok();
9281 env::set_var("LC_ALL", "C"); // c:4912 non-empty LC_ALL
9282
9283 // Capture libc state before.
9284 let before = unsafe {
9285 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
9286 if p.is_null() {
9287 String::new()
9288 } else {
9289 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9290 }
9291 };
9292
9293 // Try to set LC_CTYPE; should NOT touch libc state.
9294 lcsetfn("LC_CTYPE", Some("POSIX".to_string()));
9295
9296 // libc state must be unchanged.
9297 let after = unsafe {
9298 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
9299 if p.is_null() {
9300 String::new()
9301 } else {
9302 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9303 }
9304 };
9305 assert_eq!(before, after,
9306 "c:4912-4913 — lcsetfn must early-return when LC_ALL is non-empty");
9307
9308 // Restore.
9309 match saved_lc_all {
9310 Some(v) => env::set_var("LC_ALL", v),
9311 None => env::remove_var("LC_ALL"),
9312 }
9313 match saved_lc_ctype {
9314 Some(v) => env::set_var("LC_CTYPE", v),
9315 None => env::remove_var("LC_CTYPE"),
9316 }
9317 }
9318
9319 /// Pin `getsparam_u` to its canonical C body at
9320 /// `Src/params.c:3089-3094`: returns `unmeta(getsparam(s))`,
9321 /// NOT a PM_SCALAR-checked `getstrvalue` wrapper.
9322 ///
9323 /// Before this fix, the Rust port took `Option<&mut value>`
9324 /// and gated on `PM_TYPE == PM_SCALAR` — a complete fabrication
9325 /// with no caller because no caller's type fit the bogus sig.
9326 #[test]
9327 fn getsparam_u_unmetas_getsparam_result() {
9328 let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
9329
9330 // Plain ASCII: getsparam_u returns the same content as
9331 // getsparam (no Meta bytes to strip).
9332 let saved = env::var("ZSHRS_TEST_LOCALE_GSU").ok();
9333 env::set_var("ZSHRS_TEST_LOCALE_GSU", "en_US.UTF-8");
9334 assert_eq!(
9335 getsparam_u("ZSHRS_TEST_LOCALE_GSU"),
9336 Some("en_US.UTF-8".to_string()),
9337 "Src/params.c:3092 — getsparam_u returns unmeta(getsparam(s)) for ASCII"
9338 );
9339
9340 // Missing param: returns None (matches C `if ((s = getsparam(s)))` false branch).
9341 env::remove_var("ZSHRS_TEST_LOCALE_GSU_MISSING");
9342 assert_eq!(
9343 getsparam_u("ZSHRS_TEST_LOCALE_GSU_MISSING"),
9344 None,
9345 "Src/params.c:3094 — getsparam_u returns NULL when getsparam returns NULL"
9346 );
9347
9348 // Restore.
9349 match saved {
9350 Some(v) => env::set_var("ZSHRS_TEST_LOCALE_GSU", v),
9351 None => env::remove_var("ZSHRS_TEST_LOCALE_GSU"),
9352 }
9353 }
9354
9355 /// Pin `setarrvalue` EXECOPT bail per `Src/params.c:2897-2898`.
9356 /// Same NO_EXEC semantic as setnumvalue: dry-run shell evaluation
9357 /// must not mutate array params.
9358 #[test]
9359 fn setarrvalue_bails_under_no_exec() {
9360 use crate::ported::zsh_h::{param, hashnode, value, PM_ARRAY};
9361
9362 let saved_exec = crate::ported::options::opt_state_get("exec")
9363 .unwrap_or(false);
9364
9365 crate::ported::options::opt_state_set("exec", false);
9366 let pm = Box::new(param {
9367 node: hashnode {
9368 next: None, nam: "noexec_arr".to_string(),
9369 flags: PM_ARRAY as i32,
9370 },
9371 u_data: 0,
9372 u_arr: Some(vec!["initial".to_string()]),
9373 u_str: None, u_val: 0, u_dval: 0.0, u_hash: None,
9374 gsu_s: None, gsu_i: None, gsu_f: None,
9375 gsu_a: None, gsu_h: None,
9376 base: 0, width: 0,
9377 env: None, ename: None, old: None, level: 0,
9378 });
9379 let mut v = value {
9380 pm: Some(pm),
9381 arr: Vec::new(),
9382 scanflags: 0, valflags: 0,
9383 start: 0, end: -1,
9384 };
9385 // Under NO_EXEC, the assign must be skipped.
9386 setarrvalue(&mut v, vec!["new1".to_string(), "new2".to_string()]);
9387 let arr = v.pm.as_ref().unwrap().u_arr.clone().unwrap_or_default();
9388 assert_eq!(arr, vec!["initial".to_string()],
9389 "c:2897 — NO_EXEC: setarrvalue must NOT replace u_arr");
9390
9391 // With exec=true, the same call replaces.
9392 crate::ported::options::opt_state_set("exec", true);
9393 setarrvalue(&mut v, vec!["new1".to_string(), "new2".to_string()]);
9394 let arr = v.pm.as_ref().unwrap().u_arr.clone().unwrap_or_default();
9395 assert_eq!(arr, vec!["new1".to_string(), "new2".to_string()],
9396 "with EXEC set, setarrvalue replaces u_arr");
9397
9398 crate::ported::options::opt_state_set("exec", saved_exec);
9399 }
9400
9401 /// Pin `setnumvalue` EXECOPT bail per `Src/params.c:2860`.
9402 /// When unset(EXECOPT) (i.e. NO_EXEC mode via `zsh -n` or
9403 /// `set -n`), param mutations MUST be skipped so dry-run shell
9404 /// evaluation doesn't leak state into the param table.
9405 #[test]
9406 fn setnumvalue_bails_under_no_exec() {
9407 use crate::ported::zsh_h::{param, hashnode, value, PM_INTEGER};
9408 use crate::ported::math::{mnumber, MN_INTEGER};
9409
9410 let saved_exec = crate::ported::options::opt_state_get("exec")
9411 .unwrap_or(false);
9412
9413 // c:2860 — NO_EXEC: setnumvalue must not mutate the param.
9414 crate::ported::options::opt_state_set("exec", false);
9415 let mut pm = Box::new(param {
9416 node: hashnode {
9417 next: None, nam: "ne".to_string(),
9418 flags: PM_INTEGER as i32,
9419 },
9420 u_data: 0, u_arr: None, u_str: None,
9421 u_val: 999, u_dval: 0.0, u_hash: None,
9422 gsu_s: None, gsu_i: None, gsu_f: None,
9423 gsu_a: None, gsu_h: None,
9424 base: 0, width: 0,
9425 env: None, ename: None, old: None, level: 0,
9426 });
9427 let mut v = value {
9428 pm: Some(pm.clone()),
9429 arr: Vec::new(),
9430 scanflags: 0, valflags: 0,
9431 start: 0, end: -1,
9432 };
9433 let val = mnumber { l: 42, d: 0.0, type_: MN_INTEGER };
9434 setnumvalue(Some(&mut v), val);
9435 // pm.u_val MUST still be 999 (the initial), not 42.
9436 let stored = v.pm.as_ref().unwrap().u_val;
9437 assert_eq!(stored, 999,
9438 "c:2860 — NO_EXEC: setnumvalue must NOT mutate pm.u_val \
9439 (was {} but should stay 999)", stored);
9440
9441 // With exec=true, the same call mutates.
9442 crate::ported::options::opt_state_set("exec", true);
9443 setnumvalue(Some(&mut v), val);
9444 let stored = v.pm.as_ref().unwrap().u_val;
9445 assert_eq!(stored, 42,
9446 "with EXEC set, setnumvalue stores u_val = 42");
9447
9448 let _ = pm;
9449 crate::ported::options::opt_state_set("exec", saved_exec);
9450 }
9451
9452 /// Pin `$-` rendering to honor `set -n` (noexec). The previous
9453 /// Rust port called `opt("noexec")` which isn't a real option
9454 /// name in zsh — the lookup always returned false so `$-` never
9455 /// included 'n' even when `set -n` was active.
9456 #[test]
9457 fn dash_param_rendering_honors_noexec_via_exec_negation() {
9458 let saved = crate::ported::options::opt_state_get("exec")
9459 .unwrap_or(false);
9460
9461 // With exec=true (default), $- should NOT include 'n'.
9462 crate::ported::options::opt_state_set("exec", true);
9463 let s = lookup_special_var("-").unwrap_or_default();
9464 assert!(!s.contains('n'),
9465 "exec=true → $-=`{}` must NOT include 'n'", s);
9466
9467 // With exec=false (`set -n`), $- SHOULD include 'n'.
9468 crate::ported::options::opt_state_set("exec", false);
9469 let s = lookup_special_var("-").unwrap_or_default();
9470 assert!(s.contains('n'),
9471 "exec=false → $-=`{}` MUST include 'n' (was silently dropped \
9472 when reading non-existent option name `noexec`)", s);
9473
9474 crate::ported::options::opt_state_set("exec", saved);
9475 }
9476
9477 /// Pin `TERM_UNKNOWN` bit value to the canonical C value at
9478 /// `Src/zsh.h:1986`. The previous params.rs duplicate had
9479 /// `1 << 0 = 0x01` which is actually C's TERM_BAD (Src/zsh.h:1985);
9480 /// the correct TERM_UNKNOWN value is 0x02. This single-byte
9481 /// drift caused the params.rs term-init code to silently set the
9482 /// TERM_BAD bit instead of TERM_UNKNOWN, while prompt.rs guards
9483 /// imported the correct 0x02 value from zsh_h.rs — the two
9484 /// paths disagreed about which bit means \"unknown terminal\".
9485 #[test]
9486 fn term_unknown_bit_value_matches_c() {
9487 use crate::ported::zsh_h::{TERM_BAD, TERM_UNKNOWN};
9488 assert_eq!(TERM_UNKNOWN, 0x02,
9489 "Src/zsh.h:1986 — TERM_UNKNOWN must be 0x02, got {:#x}", TERM_UNKNOWN);
9490 assert_eq!(TERM_BAD, 0x01,
9491 "Src/zsh.h:1985 — TERM_BAD must be 0x01 (and != TERM_UNKNOWN)");
9492 // Crucially: TERM_BAD and TERM_UNKNOWN must be DISTINCT bits.
9493 assert_ne!(TERM_BAD, TERM_UNKNOWN,
9494 "TERM_BAD and TERM_UNKNOWN must be distinct (caught the 1<<0 drift bug)");
9495 }
9496
9497 /// Pin `getstrvalue` PM_INTEGER branch to canonical C convbase
9498 /// dispatch at `Src/params.c:2373`. The previous Rust port used
9499 /// naked `.to_string()` (base-10) regardless of `pm.base`; C
9500 /// honors the param's stored base so `typeset -i 16 x=255` renders
9501 /// as `0xff` not `255`.
9502 #[test]
9503 fn getstrvalue_pm_integer_honors_pm_base() {
9504 use crate::ported::zsh_h::{value, param, hashnode, PM_INTEGER};
9505
9506 let saved_cbases_top = crate::ported::options::opt_state_get("cbases")
9507 .unwrap_or(false);
9508 crate::ported::options::opt_state_set("cbases", true);
9509
9510 // Build a PM_INTEGER param with u_val=255 and base=16.
9511 let mut pm = Box::new(param {
9512 node: hashnode {
9513 next: None,
9514 nam: "test_hex_var".to_string(),
9515 flags: PM_INTEGER as i32,
9516 },
9517 u_data: 0, u_arr: None, u_str: None,
9518 u_val: 255, u_dval: 0.0, u_hash: None,
9519 gsu_s: None, gsu_i: None, gsu_f: None,
9520 gsu_a: None, gsu_h: None,
9521 base: 16, width: 0,
9522 env: None, ename: None, old: None, level: 0,
9523 });
9524 let mut v = value {
9525 pm: Some(pm.clone()),
9526 arr: Vec::new(),
9527 scanflags: 0, valflags: 0,
9528 start: 0, end: -1,
9529 };
9530 let rendered = getstrvalue(Some(&mut v));
9531 assert_eq!(rendered, "0xFF",
9532 "c:2373 / c:5621 — PM_INTEGER base=16 + u_val=255 with CBASES \
9533 renders as `0xFF` (uppercase per C `dig - 10 + 'A'`), got {:?}",
9534 rendered);
9535
9536 // Base-8 (octal) with OCTALZEROES.
9537 let saved_oct = crate::ported::options::opt_state_get("octalzeroes")
9538 .unwrap_or(false);
9539 let saved_cbases = crate::ported::options::opt_state_get("cbases")
9540 .unwrap_or(false);
9541 crate::ported::options::opt_state_set("cbases", true);
9542 crate::ported::options::opt_state_set("octalzeroes", true);
9543 pm.base = 8;
9544 pm.u_val = 8;
9545 v.pm = Some(pm.clone());
9546 let rendered = getstrvalue(Some(&mut v));
9547 assert_eq!(rendered, "010",
9548 "c:2373 — PM_INTEGER base=8 with OCTALZEROES renders as `010`, got {:?}",
9549 rendered);
9550 crate::ported::options::opt_state_set("cbases", saved_cbases);
9551 crate::ported::options::opt_state_set("octalzeroes", saved_oct);
9552
9553 // Base=0 (default) → base-10.
9554 pm.base = 0;
9555 pm.u_val = 42;
9556 v.pm = Some(pm.clone());
9557 let rendered = getstrvalue(Some(&mut v));
9558 assert_eq!(rendered, "42",
9559 "c:2373 — PM_INTEGER base=0 defaults to base-10");
9560
9561 crate::ported::options::opt_state_set("cbases", saved_cbases_top);
9562 }
9563
9564 /// Pin `unsetparam` to its canonical C body at `Src/params.c:3819-3833`.
9565 /// Two guards the previous Rust port skipped:
9566 /// 1. PM_NAMEREF params are NOT removed by unsetparam (c:3830).
9567 /// 2. PM_READONLY rejection per unsetparam_pm c:3850 — readonly
9568 /// params survive the unset call.
9569 #[test]
9570 fn unsetparam_skips_nameref_and_readonly() {
9571 use crate::ported::zsh_h::{PM_NAMEREF, PM_READONLY, PM_SCALAR};
9572
9573 let saved_exec = crate::ported::options::opt_state_get("exec")
9574 .unwrap_or(false);
9575 crate::ported::options::opt_state_set("exec", true);
9576
9577 // Helper: install a scalar param with the given flag-set.
9578 fn install(name: &str, value: &str, flags: u32) {
9579 let mut tab = paramtab().write().unwrap();
9580 tab.insert(name.to_string(), Box::new(crate::ported::zsh_h::param {
9581 node: crate::ported::zsh_h::hashnode {
9582 next: None,
9583 nam: name.to_string(),
9584 flags: (PM_SCALAR | flags) as i32,
9585 },
9586 u_data: 0, u_arr: None, u_str: Some(value.to_string()),
9587 u_val: 0, u_dval: 0.0, u_hash: None,
9588 gsu_s: None, gsu_i: None, gsu_f: None,
9589 gsu_a: None, gsu_h: None,
9590 base: 0, width: 0,
9591 env: None, ename: None, old: None, level: 0,
9592 }));
9593 }
9594
9595 // c:3830 — nameref params skip the unset.
9596 let nameref_name = "zshrs_test_unsetparam_nameref";
9597 install(nameref_name, "target_var_name", PM_NAMEREF);
9598 unsetparam(nameref_name);
9599 {
9600 let tab = paramtab().read().unwrap();
9601 assert!(tab.contains_key(nameref_name),
9602 "c:3830 — PM_NAMEREF param survives unsetparam");
9603 }
9604
9605 // c:3850 (via unsetparam_pm) — readonly rejection.
9606 let ro_name = "zshrs_test_unsetparam_readonly";
9607 install(ro_name, "locked", PM_READONLY);
9608 unsetparam(ro_name);
9609 {
9610 let tab = paramtab().read().unwrap();
9611 assert!(tab.contains_key(ro_name),
9612 "c:3850 — PM_READONLY param survives unsetparam");
9613 }
9614
9615 // Plain scalar removed normally.
9616 let plain_name = "zshrs_test_unsetparam_plain";
9617 install(plain_name, "removable", 0);
9618 unsetparam(plain_name);
9619 {
9620 let tab = paramtab().read().unwrap();
9621 assert!(!tab.contains_key(plain_name),
9622 "plain scalar successfully removed");
9623 }
9624
9625 // Clean up.
9626 {
9627 let mut tab = paramtab().write().unwrap();
9628 tab.remove(nameref_name);
9629 tab.remove(ro_name);
9630 tab.remove(plain_name);
9631 }
9632 crate::ported::options::opt_state_set("exec", saved_exec);
9633 }
9634
9635 /// Pin `assigniparam` to its canonical C body at `Src/params.c:3754-3761`.
9636 /// Three-arg signature: `(s, val, flags)`. Previous Rust port
9637 /// dropped the flags arg AND returned void; this restores both.
9638 #[test]
9639 fn assigniparam_takes_flags_arg_and_returns_param() {
9640 use crate::ported::zsh_h::PM_INTEGER;
9641
9642 let saved_exec = crate::ported::options::opt_state_get("exec")
9643 .unwrap_or(false);
9644 crate::ported::options::opt_state_set("exec", true);
9645
9646 let name = "zshrs_test_assigniparam_x";
9647 {
9648 let mut tab = paramtab().write().unwrap();
9649 tab.remove(name);
9650 }
9651
9652 // c:3755-3760 — assigniparam returns Param and threads flags through.
9653 let r = assigniparam(name, 77, ASSPM_WARN as i32);
9654 assert!(r.is_some(), "c:3760 — returns Some(Param) for new int param");
9655 {
9656 let tab = paramtab().read().unwrap();
9657 let pm = tab.get(name).expect("integer param created");
9658 assert_ne!((pm.node.flags as u32) & PM_INTEGER, 0,
9659 "c:3757-3760 — PM_INTEGER flag set");
9660 assert_eq!(pm.u_val, 77,
9661 "c:3759 — value stored in u_val");
9662 }
9663
9664 // Reassign with a different flag value (0 — no warnings).
9665 let r = assigniparam(name, 88, 0);
9666 assert!(r.is_some(), "reassign returns Some");
9667 {
9668 let tab = paramtab().read().unwrap();
9669 let pm = tab.get(name).expect("param still present");
9670 assert_eq!(pm.u_val, 88, "reassign updates u_val");
9671 }
9672
9673 // Clean up.
9674 {
9675 let mut tab = paramtab().write().unwrap();
9676 tab.remove(name);
9677 }
9678 crate::ported::options::opt_state_set("exec", saved_exec);
9679 }
9680
9681 /// Pin `setnparam` to its canonical C body at `Src/params.c:3745-3749`.
9682 /// MUST accept `mnumber` (integer or float) and return Param.
9683 /// Previous Rust port took `f64` only and returned void — losing
9684 /// the integer side and the Param return entirely.
9685 #[test]
9686 fn setnparam_accepts_both_integer_and_float() {
9687 use crate::ported::math::{mnumber, MN_INTEGER as MN_INT, MN_FLOAT as MN_FLT};
9688 use crate::ported::zsh_h::{PM_INTEGER, PM_FFLOAT};
9689
9690 let saved_exec = crate::ported::options::opt_state_get("exec")
9691 .unwrap_or(false);
9692 crate::ported::options::opt_state_set("exec", true);
9693
9694 // Clean up any leftover.
9695 let int_name = "zshrs_test_setnparam_i";
9696 let flt_name = "zshrs_test_setnparam_f";
9697 {
9698 let mut tab = paramtab().write().unwrap();
9699 tab.remove(int_name);
9700 tab.remove(flt_name);
9701 }
9702
9703 // c:3748 — integer branch: setnparam returns Some(Param) with
9704 // PM_INTEGER flag and u_val set.
9705 let r = setnparam(int_name, mnumber { l: 999, d: 0.0, type_: MN_INT });
9706 assert!(r.is_some(), "setnparam returns Some for new param");
9707 {
9708 let tab = paramtab().read().unwrap();
9709 let pm = tab.get(int_name).expect("integer param created");
9710 assert_ne!((pm.node.flags as u32) & PM_INTEGER, 0,
9711 "c:3748 — PM_INTEGER flag set for integer mnumber");
9712 assert_eq!(pm.u_val, 999,
9713 "c:3748 — integer value stored in u_val");
9714 }
9715
9716 // c:3748 — float branch: setnparam with MN_FLOAT creates PM_FFLOAT.
9717 let r = setnparam(flt_name, mnumber { l: 0, d: 3.14, type_: MN_FLT });
9718 assert!(r.is_some(), "setnparam returns Some for new float param");
9719 {
9720 let tab = paramtab().read().unwrap();
9721 let pm = tab.get(flt_name).expect("float param created");
9722 assert_ne!((pm.node.flags as u32) & PM_FFLOAT, 0,
9723 "c:3748 — PM_FFLOAT flag set for float mnumber");
9724 assert!((pm.u_dval - 3.14).abs() < 1e-10,
9725 "c:3748 — float value stored in u_dval");
9726 }
9727
9728 // Clean up.
9729 {
9730 let mut tab = paramtab().write().unwrap();
9731 tab.remove(int_name);
9732 tab.remove(flt_name);
9733 }
9734 crate::ported::options::opt_state_set("exec", saved_exec);
9735 }
9736
9737 /// Pin `setiparam` to its canonical C body at `Src/params.c:3767-3773`.
9738 /// MUST create the param as PM_INTEGER via `assignnparam`, not as
9739 /// PM_SCALAR via `assignsparam` with a stringified value.
9740 #[test]
9741 fn setiparam_creates_pm_integer_param() {
9742 use crate::ported::zsh_h::PM_INTEGER;
9743 let name = "zshrs_test_setiparam_x";
9744
9745 // C: `assignnparam` bails when `unset(EXECOPT)` (Src/params.c:3679).
9746 // Real zsh startup sets exec=true; the unit-test env doesn't run
9747 // through `createoptiontable` so we set "exec" explicitly to
9748 // simulate normal runtime.
9749 let saved_exec = crate::ported::options::opt_state_get("exec")
9750 .unwrap_or(false);
9751 crate::ported::options::opt_state_set("exec", true);
9752
9753 // Clean up any leftover.
9754 {
9755 let mut tab = paramtab().write().unwrap();
9756 tab.remove(name);
9757 }
9758
9759 // Set integer value.
9760 setiparam(name, 42);
9761
9762 // Param should exist with PM_INTEGER flag set + u_val == 42.
9763 {
9764 let tab = paramtab().read().unwrap();
9765 let pm = tab.get(name).expect("setiparam must create the param");
9766 assert_ne!(
9767 (pm.node.flags as u32) & PM_INTEGER, 0,
9768 "c:3770-3772 — created param must have PM_INTEGER flag set, \
9769 got flags = {:#x}", pm.node.flags
9770 );
9771 assert_eq!(pm.u_val, 42,
9772 "c:3771 — integer value stored in pm.u_val");
9773 }
9774
9775 // Reassign to verify update path also keeps PM_INTEGER.
9776 setiparam(name, 100);
9777 {
9778 let tab = paramtab().read().unwrap();
9779 let pm = tab.get(name).expect("setiparam reassign must keep param");
9780 assert_eq!(pm.u_val, 100,
9781 "reassign updates the integer value");
9782 assert_ne!(
9783 (pm.node.flags as u32) & PM_INTEGER, 0,
9784 "reassign keeps PM_INTEGER flag"
9785 );
9786 }
9787
9788 // Clean up.
9789 {
9790 let mut tab = paramtab().write().unwrap();
9791 tab.remove(name);
9792 }
9793 // Restore EXECOPT.
9794 crate::ported::options::opt_state_set("exec", saved_exec);
9795 }
9796
9797 /// Pin `gethparam` / `gethkparam` to their canonical C bodies at
9798 /// `Src/params.c:3117-3140`. Same signature-fix family as `getaparam`:
9799 /// the `name: &str` path with digit-first reject + PM_HASHED check.
9800 #[test]
9801 fn gethparam_and_gethkparam_signature_matches_c() {
9802 // c:3122 / c:3136 — digit-first name reject.
9803 assert_eq!(gethparam("123abc"), None,
9804 "c:3122 — digit-first name rejected");
9805 assert_eq!(gethkparam("123abc"), None,
9806 "c:3136 — digit-first name rejected");
9807
9808 // Missing param → None.
9809 assert_eq!(gethparam("zshrs_test_hashparam_xyz"), None,
9810 "missing param returns None");
9811 assert_eq!(gethkparam("zshrs_test_hashparam_xyz"), None,
9812 "missing param returns None");
9813
9814 // PM_SCALAR param (not hashed) → None.
9815 {
9816 let mut tab = paramtab().write().unwrap();
9817 tab.insert("zshrs_test_gethp_scalar".to_string(),
9818 Box::new(crate::ported::zsh_h::param {
9819 node: crate::ported::zsh_h::hashnode {
9820 next: None,
9821 nam: "zshrs_test_gethp_scalar".to_string(),
9822 flags: crate::ported::zsh_h::PM_SCALAR as i32,
9823 },
9824 u_data: 0, u_arr: None,
9825 u_str: Some("scalar value".to_string()),
9826 u_val: 0, u_dval: 0.0, u_hash: None,
9827 gsu_s: None, gsu_i: None, gsu_f: None,
9828 gsu_a: None, gsu_h: None,
9829 base: 0, width: 0,
9830 env: None, ename: None, old: None, level: 0,
9831 }));
9832 }
9833 assert_eq!(gethparam("zshrs_test_gethp_scalar"), None,
9834 "c:3123 — non-PM_HASHED returns None");
9835 assert_eq!(gethkparam("zshrs_test_gethp_scalar"), None,
9836 "c:3137 — non-PM_HASHED returns None");
9837
9838 // PM_HASHED param → Some(Vec::new()) (backend not yet wired,
9839 // but signature should at least classify the type correctly).
9840 {
9841 let mut tab = paramtab().write().unwrap();
9842 tab.insert("zshrs_test_gethp_hash".to_string(),
9843 Box::new(crate::ported::zsh_h::param {
9844 node: crate::ported::zsh_h::hashnode {
9845 next: None,
9846 nam: "zshrs_test_gethp_hash".to_string(),
9847 flags: crate::ported::zsh_h::PM_HASHED as i32,
9848 },
9849 u_data: 0, u_arr: None, u_str: None,
9850 u_val: 0, u_dval: 0.0, u_hash: None,
9851 gsu_s: None, gsu_i: None, gsu_f: None,
9852 gsu_a: None, gsu_h: None,
9853 base: 0, width: 0,
9854 env: None, ename: None, old: None, level: 0,
9855 }));
9856 }
9857 assert_eq!(gethparam("zshrs_test_gethp_hash"), Some(Vec::new()),
9858 "c:3123-3124 — PM_HASHED returns Some(vec) (empty until backend wired)");
9859 assert_eq!(gethkparam("zshrs_test_gethp_hash"), Some(Vec::new()),
9860 "c:3137-3138 — PM_HASHED returns Some(vec) for keys");
9861
9862 // Clean up.
9863 {
9864 let mut tab = paramtab().write().unwrap();
9865 tab.remove("zshrs_test_gethp_scalar");
9866 tab.remove("zshrs_test_gethp_hash");
9867 }
9868 }
9869
9870 /// Pin `getaparam` to its canonical C body at `Src/params.c:3101-3110`.
9871 /// Three branches: digit-first reject (c:3107), PM_ARRAY return
9872 /// (c:3108-3109), non-array / missing-param return None (c:3110).
9873 #[test]
9874 fn getaparam_returns_array_for_pm_array_only() {
9875 // c:3107 — digit-first name → None (positional params reject).
9876 assert_eq!(getaparam("123abc"), None,
9877 "c:3107 — digit-first name rejected");
9878
9879 // c:3110 — missing param → None.
9880 assert_eq!(getaparam("zshrs_test_arr_nonexistent_xyz"), None,
9881 "c:3110 — missing param returns None");
9882
9883 // Helper that builds a Param via the canonical createparam
9884 // path so we don't reach into struct internals (param has
9885 // many fields and no Default).
9886 fn build_arr(name: &str, arr: Vec<String>) {
9887 let mut tab = paramtab().write().unwrap();
9888 tab.insert(name.to_string(), Box::new(crate::ported::zsh_h::param {
9889 node: crate::ported::zsh_h::hashnode {
9890 next: None,
9891 nam: name.to_string(),
9892 flags: crate::ported::zsh_h::PM_ARRAY as i32,
9893 },
9894 u_data: 0, u_arr: Some(arr), u_str: None,
9895 u_val: 0, u_dval: 0.0, u_hash: None,
9896 gsu_s: None, gsu_i: None, gsu_f: None,
9897 gsu_a: None, gsu_h: None,
9898 base: 0, width: 0,
9899 env: None, ename: None, old: None, level: 0,
9900 }));
9901 }
9902 fn build_scalar(name: &str, s: &str) {
9903 let mut tab = paramtab().write().unwrap();
9904 tab.insert(name.to_string(), Box::new(crate::ported::zsh_h::param {
9905 node: crate::ported::zsh_h::hashnode {
9906 next: None,
9907 nam: name.to_string(),
9908 flags: crate::ported::zsh_h::PM_SCALAR as i32,
9909 },
9910 u_data: 0, u_arr: None, u_str: Some(s.to_string()),
9911 u_val: 0, u_dval: 0.0, u_hash: None,
9912 gsu_s: None, gsu_i: None, gsu_f: None,
9913 gsu_a: None, gsu_h: None,
9914 base: 0, width: 0,
9915 env: None, ename: None, old: None, level: 0,
9916 }));
9917 }
9918
9919 // c:3108 — PM_ARRAY param returns the array contents.
9920 build_arr("zshrs_test_getaparam_arr",
9921 vec!["one".to_string(), "two".to_string(), "three".to_string()]);
9922 assert_eq!(
9923 getaparam("zshrs_test_getaparam_arr"),
9924 Some(vec!["one".to_string(), "two".to_string(), "three".to_string()]),
9925 "c:3108-3109 — PM_ARRAY param returns its array"
9926 );
9927
9928 // c:3108 — PM_SCALAR (non-array) param → None.
9929 build_scalar("zshrs_test_getaparam_scalar", "not an array");
9930 assert_eq!(getaparam("zshrs_test_getaparam_scalar"), None,
9931 "c:3108 — non-PM_ARRAY param returns None");
9932
9933 // Clean up.
9934 {
9935 let mut tab = paramtab().write().unwrap();
9936 tab.remove("zshrs_test_getaparam_arr");
9937 tab.remove("zshrs_test_getaparam_scalar");
9938 }
9939 }
9940}