zsh/ported/modules/ksh93.rs
1//! `zsh/ksh93` module — port of `Src/Modules/ksh93.c`.
2//!
3//! Implementing "namespace" requires creating a new keyword. // c:34
4//! Dummy to treat as NULL // c:105
5//!
6//! Top-level declaration order matches C source line-by-line:
7//! - `static struct builtin bintab[]` c:40
8//! - `edcharsetfn(pm, x)` c:46
9//! - `matchgetfn(pm)` c:60
10//! - 6× `static const struct gsu_*` c:91-103
11//! - `static char sh_unsetval[2]` c:105
12//! - `static char *sh_name = sh_unsetval;` c:106
13//! - `static char *sh_subscript = sh_unsetval;` c:107
14//! - `static char *sh_edchar = sh_unsetval;` c:108
15//! - `static char sh_edmode[2]` c:109
16//! - `static struct paramdef partab[]` c:116
17//! - `static struct features module_features` c:133
18//! - `ksh93_wrapper(prog, w, name)` c:142
19//! - `static struct funcwrap wrapper[]` c:230
20//! - `setup_(m)` / `features_(m, features)` /
21//! `enables_(m, enables)` / `boot_(m)` /
22//! `cleanup_(m)` / `finish_(m)` c:235-287
23
24#![allow(non_camel_case_types)]
25#![allow(non_upper_case_globals)]
26#![allow(non_snake_case)]
27
28use crate::ported::builtins::sched::zleactive;
29pub use crate::ported::options::emulation;
30pub use crate::ported::params::locallevel;
31use crate::ported::params::{createparam, paramtab, setiparam, setloopvar, setsparam};
32use crate::ported::signals_h::{queue_signals, unqueue_signals};
33use crate::ported::string::{dupstring, ztrdup};
34use crate::ported::zsh_h::{
35 eprog, features, funcstack, funcwrap, isset, module, param, paramdef, EMULATE_KSH, EMULATION,
36 KSHARRAYS, PARAMDEF, PM_LOCAL, PM_NAMEREF, PM_READONLY, PM_UNSET, VIMODE,
37};
38use crate::ported::ztype_h::INAMESPC;
39use crate::zsh_h::{PM_ARRAY, PM_SCALAR, PM_SPECIAL};
40use std::sync::atomic::{AtomicI64, Ordering};
41use std::sync::{Mutex, OnceLock};
42
43// =====================================================================
44// /* Implementing "namespace" requires creating a new keword. Hrm. */ c:34
45// /* Standard module configuration/linkage */ c:36-38
46// static struct builtin bintab[] = {
47// BUILTIN("nameref", BINF_ASSIGN, ..., 0, -1, 0, "gpru", "n")
48// }; c:40
49//
50// Static dispatch table consumed by C module loader. Static-link path:
51// the `nameref` builtin is dispatched directly via `bin_typeset` in
52// the typeset port. Table omitted from Rust port pending module-loader.
53// =====================================================================
54
55// =====================================================================
56// edcharsetfn(Param pm, char *x) c:46
57// =====================================================================
58
59/// Port of `edcharsetfn(Param pm, char *x)` from `Src/Modules/ksh93.c:47`.
60#[allow(unused_variables)]
61pub fn edcharsetfn(pm: *mut param, x: *mut libc::c_char) { // c:47
62 /*
63 * To make this work like ksh, we must intercept $KEYS before the widget
64 * is looked up, so that changing the key sequence causes a different
65 * widget to be substituted. Somewhat similar to "bindkey -s".
66 *
67 * Ksh93 adds SIGKEYBD to the trap list for this purpose.
68 */ // c:49-55
69 /* ; */ // c:56
70}
71
72// =====================================================================
73// matchgetfn(Param pm) c:60
74// =====================================================================
75
76/// Port of `matchgetfn(Param pm)` from `Src/Modules/ksh93.c:60`.
77///
78/// C signature mirrored verbatim:
79/// ```c
80/// static char **
81/// matchgetfn(Param pm)
82/// ```
83pub fn matchgetfn(pm: *mut param) -> Vec<String> {
84 // c:60
85 // c:Src/Modules/ksh93.c:60 — C's matchgetfn dereferences
86 // `pm->u.arr` directly with no null guard (the function is wired
87 // into a gsu_array vtable and never invoked with a NULL `pm`).
88 // The Rust port is callable from tests that pass `null_mut` to
89 // pin the safety boundary; preserve the convention by short-
90 // circuiting to an empty Vec when `pm` is null, BEFORE looking
91 // at the `match` paramtab entry. Without this, leftover `match`
92 // array values from a prior test (the global paramtab is shared
93 // across tests via the `global_state_lock`) leak into the null-pm
94 // call's return value, breaking the "null pm → empty vec" pin.
95 if pm.is_null() {
96 return Vec::new();
97 }
98 // c:60 — `char **zsh_match = getaparam("match");`
99 let zsh_match: Vec<String> = paramtab()
100 .read()
101 .ok()
102 .and_then(|t| t.get("match").and_then(|p| p.u_arr.clone()))
103 .unwrap_or_default();
104 /*
105 * For this to work accurately, ksh emulation should always imply
106 * that the (#m) and (#b) extendedglob operators are enabled.
107 *
108 * When we have a 0th element (ksharrays), it is $MATCH. Elements
109 * 1st and larger mirror the $match array.
110 */
111 // c:64-69
112 // c:71-72 — `if (pm->u.arr) freearray(pm->u.arr);`
113 if !pm.is_null() {
114 unsafe {
115 (*pm).u_arr = None;
116 } // c:71-72 freearray
117 }
118 if !zsh_match.is_empty() {
119 // c:73
120 if isset(KSHARRAYS) {
121 // c:74
122 // c:75-80 — char **ap = zalloc(...); pm->u.arr = ap;
123 // *ap++ = ztrdup(getsparam("MATCH"));
124 // while (*zsh_match) *ap = ztrdup(*zsh_match++);
125 let match_str: String = paramtab()
126 .read()
127 .ok()
128 .and_then(|t| t.get("MATCH").and_then(|p| p.u_str.clone()))
129 .unwrap_or_default();
130 let mut ap: Vec<String> = Vec::with_capacity(zsh_match.len() + 1);
131 ap.push(ztrdup(&match_str)); // c:78
132 for s in &zsh_match {
133 // c:79-80
134 ap.push(ztrdup(s));
135 }
136 if !pm.is_null() {
137 unsafe {
138 (*pm).u_arr = Some(ap.clone());
139 }
140 }
141 // c:88 — `return arrgetfn(pm);` — arrgetfn (params.c) reads
142 // pm->u.arr we just set, so return the same vec.
143 ap
144 } else {
145 // c:81-82 — pm->u.arr = zarrdup(zsh_match);
146 let dup: Vec<String> = zsh_match.iter().map(|s| ztrdup(s)).collect();
147 if !pm.is_null() {
148 unsafe {
149 (*pm).u_arr = Some(dup.clone());
150 }
151 }
152 dup // c:88 arrgetfn(pm)
153 }
154 } else if isset(KSHARRAYS) {
155 // c:83
156 // c:84 — pm->u.arr = mkarray(ztrdup(getsparam("MATCH")));
157 let match_str: String = paramtab()
158 .read()
159 .ok()
160 .and_then(|t| t.get("MATCH").and_then(|p| p.u_str.clone()))
161 .unwrap_or_default();
162 let one = vec![ztrdup(&match_str)];
163 if !pm.is_null() {
164 unsafe {
165 (*pm).u_arr = Some(one.clone());
166 }
167 }
168 one // c:88 arrgetfn(pm)
169 } else {
170 // c:86 — pm->u.arr = NULL;
171 if !pm.is_null() {
172 unsafe {
173 (*pm).u_arr = None;
174 }
175 }
176 Vec::new() // c:88 arrgetfn(pm) → NULL
177 }
178}
179
180// =====================================================================
181// static const struct gsu_scalar constant_gsu c:91
182// static const struct gsu_scalar sh_edchar_gsu c:94
183// static const struct gsu_scalar sh_edmode_gsu c:96
184// static const struct gsu_array sh_match_gsu c:98
185// static const struct gsu_scalar sh_name_gsu c:100
186// static const struct gsu_scalar sh_subscript_gsu c:102
187//
188// GSU vtables for the `.sh.*` parameters. Wired into `partab[]` below.
189// Static-link path: dispatcher invokes the getfn/setfn directly.
190// Tables omitted pending param-table port.
191// =====================================================================
192
193// =====================================================================
194// static char sh_unsetval[2]; /* Dummy to treat as NULL */ c:105
195// static char *sh_name = sh_unsetval; c:106
196// static char *sh_subscript = sh_unsetval; c:107
197// static char *sh_edchar = sh_unsetval; c:108
198// static char sh_edmode[2]; c:109
199// =====================================================================
200
201/// Port of `static char sh_unsetval[2];` from `ksh93.c:105`.
202/// /* Dummy to treat as NULL */
203pub static sh_unsetval: [u8; 2] = [0, 0]; // c:105
204
205/// Port of `static char *sh_name = sh_unsetval;` from `ksh93.c:106`.
206pub static sh_name: Mutex<String> = Mutex::new(String::new()); // c:106
207
208/// Port of `static char *sh_subscript = sh_unsetval;` from `ksh93.c:107`.
209pub static sh_subscript: Mutex<String> = Mutex::new(String::new()); // c:107
210
211/// Port of `static char *sh_edchar = sh_unsetval;` from `ksh93.c:108`.
212pub static sh_edchar: Mutex<String> = Mutex::new(String::new()); // c:108
213
214/// Port of `static char sh_edmode[2];` from `ksh93.c:109`.
215pub static sh_edmode: Mutex<[u8; 2]> = Mutex::new([0, 0]); // c:109
216
217/// Port of `ksh93_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/ksh93.c:143`.
218///
219/// C signature mirrored verbatim:
220/// ```c
221/// static int
222/// ksh93_wrapper(Eprog prog, FuncWrap w, char *name)
223/// ```
224#[allow(unused_variables)]
225pub fn ksh93_wrapper(prog: *const eprog, w: *const funcwrap, name: *mut libc::c_char) -> i32 {
226 // c:143
227 // c:143 — `Funcstack f;`
228 let mut f: *const funcstack;
229 // c:146 — `Param pm;`
230 let mut pm: *mut param;
231 // c:147 — `zlong num = funcstack->prev ? getiparam(".sh.level") : 0;`
232 // funcstack is the global from Src/exec.c:340; stub holds NULL so
233 // funcstack->prev is always NULL → branch picks 0.
234 let mut num: i64 = if (*funcstack.lock().unwrap()) != 0 {
235 paramtab()
236 .read()
237 .ok()
238 .and_then(|t| {
239 t.get(".sh.level")
240 .and_then(|p| p.u_str.as_ref().and_then(|s| s.parse::<i64>().ok()))
241 })
242 .unwrap_or(0)
243 } else {
244 0
245 };
246
247 if !EMULATION(EMULATE_KSH) {
248 // c:149
249 return 1; // c:150
250 }
251
252 if num == 0 {
253 // c:152
254 // c:153 — `for (f = funcstack; f; f = f->prev, num++);` — count
255 // function-call-stack depth. Route through the canonical
256 // FUNCSTACK Vec<funcstack> in modules::parameter (each push is
257 // a nested call). `len()` gives the depth directly.
258 let _ = f; // C iterates a linked list; Rust uses Vec depth.
259 if let Ok(stack) = crate::ported::modules::parameter::FUNCSTACK.lock() {
260 num = stack.len() as i64;
261 }
262 } else {
263 // c:154
264 num += 1; // c:155
265 }
266
267 queue_signals(); // c:157
268 locallevel.fetch_add(1, Ordering::SeqCst); // c:158 ++locallevel;
269 /* Make these local */ // c:158 trailing comment
270
271 // c:160-165 — .sh.command setup
272 pm = createparam(".sh.command", LOCAL_NAMEREF as i32)
273 .map(Box::into_raw)
274 .unwrap_or(std::ptr::null_mut());
275 if !pm.is_null() {
276 unsafe {
277 (*pm).level = locallevel.load(Ordering::Relaxed);
278 } // c:161 pm->level = locallevel;
279 // /* Why is this necessary? */
280 /* Force scoping by assignent hack */ // c:162
281 setloopvar(".sh.command", "ZSH_DEBUG_CMD"); // c:163
282 unsafe {
283 (*pm).node.flags |= PM_READONLY as i32;
284 } // c:164
285 }
286 /* .sh.edchar is in partab and below */
287 // c:166
288 if zleactive.load(Ordering::Relaxed) != 0 {
289 pm = createparam(".sh.edcol", LOCAL_NAMEREF as i32) // c:167
290 .map(Box::into_raw)
291 .unwrap_or(std::ptr::null_mut());
292 if !pm.is_null() {
293 unsafe {
294 (*pm).level = locallevel.load(Ordering::Relaxed);
295 } // c:168
296 setloopvar(".sh.edcol", "CURSOR"); // c:169
297 unsafe {
298 (*pm).node.flags |= (PM_NAMEREF | PM_READONLY) as i32;
299 } // c:170
300 }
301 }
302 /* .sh.edmode is in partab and below */
303 // c:172
304 if zleactive.load(Ordering::Relaxed) != 0 {
305 pm = createparam(".sh.edtext", LOCAL_NAMEREF as i32) // c:173
306 .map(Box::into_raw)
307 .unwrap_or(std::ptr::null_mut());
308 if !pm.is_null() {
309 unsafe {
310 (*pm).level = locallevel.load(Ordering::Relaxed);
311 } // c:174
312 setloopvar(".sh.edtext", "BUFFER"); // c:175
313 unsafe {
314 (*pm).node.flags |= PM_READONLY as i32;
315 } // c:176
316 }
317 }
318
319 pm = createparam(
320 ".sh.fun", // c:179
321 (PM_LOCAL | PM_UNSET) as i32,
322 )
323 .map(Box::into_raw)
324 .unwrap_or(std::ptr::null_mut());
325 if !pm.is_null() {
326 unsafe {
327 (*pm).level = locallevel.load(Ordering::Relaxed);
328 } // c:180
329 let name_str: String = if name.is_null() {
330 String::new()
331 } else {
332 unsafe {
333 std::ffi::CStr::from_ptr(name)
334 .to_string_lossy()
335 .into_owned()
336 }
337 };
338 setsparam(
339 ".sh.fun", // c:181
340 &ztrdup(&name_str),
341 );
342 unsafe {
343 (*pm).node.flags |= PM_READONLY as i32;
344 } // c:182
345 }
346 pm = createparam(
347 ".sh.level", // c:184
348 (PM_LOCAL | PM_UNSET) as i32,
349 )
350 .map(Box::into_raw)
351 .unwrap_or(std::ptr::null_mut());
352 if !pm.is_null() {
353 unsafe {
354 (*pm).level = locallevel.load(Ordering::Relaxed);
355 } // c:185
356 setiparam(".sh.level", num); // c:186
357 }
358 if zleactive.load(Ordering::Relaxed) != 0 {
359 // c:188
360 // c:189-190 — extern mod_import_variable char *curkeymapname / *varedarg;
361 // (extern declarations are at the locals-block level in C; ours
362 // are file-level statics below.)
363 /* bindkey -v forces VIMODE so this test is as good as any */ // c:191
364 let curkmap = curkeymapname.lock().unwrap().clone();
365 if !curkmap.is_empty() && isset(VIMODE) && curkmap == "main" {
366 // c:192-193
367 // c:194 — strcpy(sh_edmode, "\033");
368 let mut em = sh_edmode.lock().unwrap();
369 em[0] = 0o33;
370 em[1] = 0;
371 } else {
372 // c:196 — strcpy(sh_edmode, "");
373 let mut em = sh_edmode.lock().unwrap();
374 em[0] = 0;
375 em[1] = 0;
376 }
377 // c:197-198 — if (sh_edchar == sh_unsetval) sh_edchar = dupstring(getsparam("KEYS"));
378 let edch_unset = sh_edchar.lock().unwrap().is_empty();
379 if edch_unset {
380 let keys: String = paramtab()
381 .read()
382 .ok()
383 .and_then(|t| t.get("KEYS").and_then(|p| p.u_str.clone()))
384 .unwrap_or_default();
385 *sh_edchar.lock().unwrap() = dupstring(&keys);
386 }
387 let varedarg_val = varedarg.lock().unwrap().clone();
388 if !varedarg_val.is_empty() {
389 // c:199
390 // c:200 — char *ie = itype_end((sh_name = dupstring(varedarg)), INAMESPC, 0);
391 *sh_name.lock().unwrap() = dupstring(&varedarg_val);
392 let nm = sh_name.lock().unwrap().clone();
393 // c:200 — `char *ie = itype_end((sh_name=...), INAMESPC, 0);`
394 // itype_end returns a pointer past the run of chars matching
395 // the type-bits; INAMESPC = identifier-namespace chars.
396 // Rust port: utils::itype_end takes (s, allow_digits_start)
397 // — wrong signature for INAMESPC. Inline the byte-walk to
398 // mirror the C `for (;;) test bit; advance;` loop.
399 let _ = INAMESPC;
400 let ie_off = {
401 let mut k = 0;
402 for &b in nm.as_bytes() {
403 match b {
404 b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' => k += 1,
405 _ => break,
406 }
407 }
408 k
409 };
410 if ie_off < nm.len() {
411 // c:201 if (ie && *ie)
412 // c:202 — *ie++ = '\0';
413 let head = &nm[..ie_off];
414 let tail = &nm[ie_off + 1..]; // skip the bracket char
415 *sh_name.lock().unwrap() = head.to_string();
416 /* Assume bin_vared has validated subscript */
417 // c:203
418 // c:204 — sh_subscript = dupstring(ie);
419 *sh_subscript.lock().unwrap() = dupstring(tail);
420 // c:205-206 — ie = sh_subscript + strlen(sh_subscript); *--ie = '\0';
421 let mut sub = sh_subscript.lock().unwrap();
422 if sub.ends_with(']') {
423 sub.pop();
424 }
425 } else {
426 // c:208 — sh_subscript = sh_unsetval;
427 sh_subscript.lock().unwrap().clear();
428 }
429 pm = createparam(
430 ".sh.value", // c:209
431 LOCAL_NAMEREF as i32,
432 )
433 .map(Box::into_raw)
434 .unwrap_or(std::ptr::null_mut());
435 if !pm.is_null() {
436 unsafe {
437 (*pm).level = locallevel.load(Ordering::Relaxed);
438 } // c:210
439 setloopvar(".sh.value", "BUFFER"); /* Hack */
440 // c:211
441 unsafe {
442 (*pm).node.flags |= PM_READONLY as i32;
443 } // c:212
444 }
445 } else {
446 // c:215 — sh_name = sh_subscript = sh_unsetval;
447 sh_name.lock().unwrap().clear();
448 sh_subscript.lock().unwrap().clear();
449 }
450 } else {
451 // c:217 — sh_edchar = sh_name = sh_subscript = sh_unsetval;
452 sh_edchar.lock().unwrap().clear();
453 sh_name.lock().unwrap().clear();
454 sh_subscript.lock().unwrap().clear();
455 // c:218 — strcpy(sh_edmode, "");
456 let mut em = sh_edmode.lock().unwrap();
457 em[0] = 0;
458 em[1] = 0;
459 /* TODO: // c:219-222
460 * - disciplines
461 * - special handling of .sh.value in math
462 */
463 }
464 locallevel.fetch_sub(1, Ordering::SeqCst); // c:224 --locallevel;
465 unqueue_signals(); // c:225
466
467 1 // c:227
468}
469
470// =====================================================================
471/*
472 * Some parameters listed here do not appear in ksh93.mdd autofeatures
473 * because they are only instantiated by ksh93_wrapper() below. This
474 * obviously includes those commented out here.
475 */ // c:111-115
476// static struct paramdef partab[] c:116
477// static struct features module_features c:133
478//
479// Param/feature dispatch tables. Omitted pending module-loader port.
480// =====================================================================
481
482// =====================================================================
483// ksh93_wrapper(Eprog prog, FuncWrap w, char *name) c:142
484// =====================================================================
485
486/// `LOCAL_NAMEREF` — `#define LOCAL_NAMEREF (PM_LOCAL|PM_UNSET|PM_NAMEREF)`
487/// from `Src/Modules/ksh93.c:143`.
488#[allow(dead_code)]
489const LOCAL_NAMEREF: u32 = PM_LOCAL | PM_UNSET | PM_NAMEREF; // c:143
490
491// =====================================================================
492// static struct funcwrap wrapper[] c:230
493//
494// Per-function wrapper table consumed by `addwrapper(m, wrapper)` at
495// boot_. Omitted pending module-loader port.
496// =====================================================================
497
498// =====================================================================
499// setup_(UNUSED(Module m)) c:235
500// =====================================================================
501
502/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/ksh93.c:236`.
503#[allow(unused_variables)]
504pub fn setup_(m: *const module) -> i32 {
505 // c:236
506 // C body c:238-239 — `return 0`. Faithful empty-body port.
507 0
508}
509
510/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/ksh93.c:243`.
511/// C body c:245-247 — `*features = featuresarray(m, &module_features); return 0`.
512pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
513 // c:243
514 *features = featuresarray(m, module_features());
515 0 // c:258
516}
517
518/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/ksh93.c:251`.
519/// C body c:253-254 — `return handlefeatures(m, &module_features, enables)`.
520pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
521 // c:251
522 handlefeatures(m, module_features(), enables) // c:258
523}
524
525/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/ksh93.c:258`.
526/// C body: `return addwrapper(m, wrapper);`
527pub fn boot_(m: *const module) -> i32 {
528 // c:258 — addwrapper(m, wrapper); zshrs's fusevm doesn't run
529 // through C's wrapper-dispatch chain, no-op until wrapper
530 // machinery gets a Rust equivalent.
531 let _ = m;
532 0
533}
534
535/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/ksh93.c:265`.
536/// C body (c:267-278):
537/// ```c
538/// struct paramdef *p;
539/// deletewrapper(m, wrapper);
540/// for (p = partab; p < partab + sizeof(partab)/sizeof(*partab); ++p) {
541/// if (p->flags & PM_NAMEREF) {
542/// HashNode hn = gethashnode2(paramtab, p->name);
543/// if (hn)
544/// ((Param)hn)->node.flags &= ~PM_NAMEREF;
545/// }
546/// }
547/// return setfeatureenables(m, &module_features, NULL);
548/// ```
549pub fn cleanup_(m: *const module) -> i32 {
550 // c:267 — `struct paramdef *p;`
551 let mut p: usize; // c:267 (index over partab)
552 // c:269 — deletewrapper(m, wrapper); zshrs's fusevm wrapper
553 // machinery is a no-op (see boot_ note).
554
555 // c:116-131 — `static struct paramdef partab[]` inlined here
556 // because Rust statics can't hold String-typed paramdef.name.
557 let partab: [paramdef; 9] = [
558 PARAMDEF(".sh.edchar", (PM_SCALAR | PM_SPECIAL) as i32, 0, 0), // c:117
559 PARAMDEF(
560 ".sh.edmode",
561 (PM_SCALAR | PM_READONLY | PM_SPECIAL) as i32,
562 0,
563 0,
564 ), // c:119
565 PARAMDEF(".sh.file", (PM_NAMEREF | PM_READONLY) as i32, 0, 0), // c:121
566 PARAMDEF(".sh.lineno", (PM_NAMEREF | PM_READONLY) as i32, 0, 0), // c:122
567 PARAMDEF(".sh.match", (PM_ARRAY | PM_READONLY) as i32, 0, 0), // c:123
568 PARAMDEF(
569 ".sh.name",
570 (PM_SCALAR | PM_READONLY | PM_SPECIAL) as i32,
571 0,
572 0,
573 ), // c:124
574 PARAMDEF(
575 ".sh.subscript",
576 (PM_SCALAR | PM_READONLY | PM_SPECIAL) as i32,
577 0,
578 0,
579 ), // c:126
580 PARAMDEF(".sh.subshell", (PM_NAMEREF | PM_READONLY) as i32, 0, 0), // c:128
581 PARAMDEF(".sh.version", (PM_NAMEREF | PM_READONLY) as i32, 0, 0), // c:130
582 ];
583
584 /* Clean up namerefs, otherwise deleteparamdef() is confused */
585 // c:271
586 // c:272-277 — `for (p = partab; p < partab + ARRSZ; ++p) { ... }`
587 p = 0;
588 while p < partab.len() {
589 // c:272
590 let entry = &partab[p];
591 if (entry.flags as u32 & PM_NAMEREF) != 0 {
592 // c:273
593 // c:274-276 — `HashNode hn = gethashnode2(paramtab, p->name);`
594 // `if (hn) hn->flags &= ~PM_NAMEREF;`
595 if let Ok(mut t) = paramtab().write() {
596 if let Some(pm) = t.get_mut(&entry.name) {
597 pm.node.flags &= !(PM_NAMEREF as i32); // c:276
598 }
599 }
600 }
601 p += 1;
602 }
603 setfeatureenables(m, module_features(), None) // c:279
604}
605
606/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/ksh93.c:284`.
607#[allow(unused_variables)]
608pub fn finish_(m: *const module) -> i32 {
609 // c:284
610 // C body c:286-287 — `return 0`. Faithful empty-body port; the
611 // ksh93 wrapper unregisters in cleanup_ via
612 // deletewrapper.
613 0
614}
615
616static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
617
618// Local descriptor stub mirroring the C bintab + partab.
619// WARNING: NOT IN KSH93.C — Rust-only module-framework shim.
620// C uses generic featuresarray/handlefeatures/setfeatureenables from
621// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
622// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
623fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
624 vec![
625 "b:nameref".to_string(),
626 "p:.sh.edchar".to_string(),
627 "p:.sh.edmode".to_string(),
628 "p:.sh.file".to_string(),
629 "p:.sh.lineno".to_string(),
630 "p:.sh.match".to_string(),
631 "p:.sh.name".to_string(),
632 "p:.sh.subscript".to_string(),
633 "p:.sh.subshell".to_string(),
634 "p:.sh.version".to_string(),
635 ]
636}
637
638// WARNING: NOT IN KSH93.C — Rust-only module-framework shim.
639// C uses generic featuresarray/handlefeatures/setfeatureenables from
640// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
641// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
642fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
643 if enables.is_none() {
644 *enables = Some(vec![1; 10]);
645 }
646 0
647}
648
649// WARNING: NOT IN KSH93.C — Rust-only module-framework shim.
650// C uses generic featuresarray/handlefeatures/setfeatureenables from
651// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
652// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
653fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
654 0
655}
656
657// =====================================================================
658// External ported / globals from other Src/*.c files. Stubbed locally
659// pending the proper ports of their home files.
660// =====================================================================
661
662// `emulation` lives in `crate::ported::options::emulation` per Rule C
663// (its C definition is `Src/options.c:36`, not `ksh93.c`). The
664// `EMULATION(bits)` macro at zsh.h:2347 tests bits against it.
665
666// `locallevel` lives in `crate::ported::params::locallevel` per Rule C
667// (its C definition is `Src/params.c:54`, not `ksh93.c`). Bumped by
668// `startparamscope` on function entry, decremented by `endparamscope`
669// on return. `ksh93_wrapper` increments before `createparam()` and
670// decrements after.
671
672/// `curkeymapname` — `char *` global from `Src/Zle/zle_keymap.c`,
673/// declared `extern` at c:189. Holds the active keymap name.
674pub static curkeymapname: Mutex<String> = Mutex::new(String::new());
675
676/// `varedarg` — `char *` global from `Src/Zle/zle_misc.c`, declared
677/// `extern` at c:190. Holds the parameter name being edited by `vared`.
678pub static varedarg: Mutex<String> = Mutex::new(String::new());
679
680// `funcstack` — `Funcstack` global from `Src/exec.c:340`. Stubbed as
681// a Mutex-wrapped raw-pointer holder since pointers aren't `Sync`
682// without explicit handling. NULL by default — exec.c port wires the
683// real walk.
684static funcstack: Mutex<usize> = Mutex::new(0);
685
686// `param.u.arr` field — the C `union u` has `char **arr` at c:1835.
687// The Rust `param` struct exposes it as `pub u_arr: Option<Vec<String>>`
688// (zsh_h.rs:732), already accessible via `(*pm).u_arr`.
689
690// Suppress "unused" for the AtomicI64 import; we don't use it directly
691// (locallevel is AtomicI32 to match C `int` for that field).
692#[allow(dead_code)]
693const _: AtomicI64 = AtomicI64::new(0);
694
695// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
696// ─── RUST-ONLY ACCESSORS ───
697//
698// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
699// RwLock<T>>` globals declared above. C zsh uses direct global
700// access; Rust needs these wrappers because `OnceLock::get_or_init`
701// is the only way to lazily construct shared state. These ported sit
702// here so the body of this file reads in C source order without
703// the accessor wrappers interleaved between real port ported.
704// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
705
706// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
707// ─── RUST-ONLY ACCESSORS ───
708//
709// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
710// RwLock<T>>` globals declared above. C zsh uses direct global
711// access; Rust needs these wrappers because `OnceLock::get_or_init`
712// is the only way to lazily construct shared state. These ported sit
713// here so the body of this file reads in C source order without
714// the accessor wrappers interleaved between real port ported.
715// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
716
717/// Port of `ksh93_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/ksh93.c:143`.
718fn module_features() -> &'static Mutex<features> {
719 MODULE_FEATURES.get_or_init(|| {
720 Mutex::new(features {
721 bn_list: None,
722 bn_size: 1, // bintab: nameref (ksh93.c)
723 cd_list: None,
724 cd_size: 0,
725 mf_list: None,
726 mf_size: 0,
727 pd_list: None,
728 pd_size: 9, // partab: .sh.edchar/edmode/file/lineno/match/name/subscript/subshell/version
729 n_abstract: 0,
730 })
731 })
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use crate::zsh_h::hashnode;
738
739 /// Verifies `ksh93_wrapper` returns 1 in the !EMULATE_KSH branch
740 /// (c:149-150) when `emulation` global is 0 (default).
741 #[test]
742 fn ksh93_wrapper_returns_one_when_not_emulate_ksh() {
743 let _g = crate::test_util::global_state_lock();
744 emulation.store(0, Ordering::SeqCst);
745 let rc = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
746 assert_eq!(rc, 1);
747 }
748
749 /// Verifies `ksh93_wrapper` runs the full body (and still returns 1
750 /// per c:227) when EMULATE_KSH is set on the `emulation` global.
751 /// Body relies on stubbed externals so it can't validate the
752 /// param-creation side-effects yet, but it MUST not panic and MUST
753 /// terminate.
754 #[test]
755 fn ksh93_wrapper_runs_full_body_under_emulate_ksh() {
756 let _g = crate::test_util::global_state_lock();
757 let saved = emulation.load(Ordering::SeqCst);
758 emulation.store(EMULATE_KSH, Ordering::SeqCst);
759 let rc = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
760 assert_eq!(rc, 1);
761 // c:158 ++locallevel + c:224 --locallevel must net to 0.
762 assert_eq!(locallevel.load(Ordering::SeqCst), 0);
763 emulation.store(saved, Ordering::SeqCst);
764 }
765
766 /// Verifies `matchgetfn` returns empty Vec when `match` array is
767 /// unset and KSHARRAYS is off (c:86 NULL branch).
768 #[test]
769 fn matchgetfn_empty_returns_empty() {
770 let _g = crate::test_util::global_state_lock();
771 let v = matchgetfn(std::ptr::null_mut());
772 assert!(v.is_empty());
773 }
774
775 /// Verifies `edcharsetfn` is a no-op (c:56 `;`).
776 #[test]
777 fn edcharsetfn_noop() {
778 let _g = crate::test_util::global_state_lock();
779 edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
780 }
781
782 /// Verifies all module loaders return 0.
783 #[test]
784 fn module_loaders_return_zero() {
785 let _g = crate::test_util::global_state_lock();
786 let m: *const module = std::ptr::null();
787 assert_eq!(setup_(m), 0);
788 let mut features = Vec::new();
789 assert_eq!(features_(m, &mut features), 0);
790 let mut enables: Option<Vec<i32>> = None;
791 assert_eq!(enables_(m, &mut enables), 0);
792 assert_eq!(boot_(m), 0);
793 assert_eq!(cleanup_(m), 0);
794 assert_eq!(finish_(m), 0);
795 }
796
797 /// Port of `ksh93_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/ksh93.c:143`.
798 /// Verifies the C-faithful static globals are initialized empty
799 /// (sh_unsetval-equivalent) at module-load.
800 #[test]
801 fn statics_default_to_unsetval() {
802 let _g = crate::test_util::global_state_lock();
803 sh_name.lock().unwrap().clear();
804 sh_subscript.lock().unwrap().clear();
805 sh_edchar.lock().unwrap().clear();
806 *sh_edmode.lock().unwrap() = [0, 0];
807 assert!(sh_name.lock().unwrap().is_empty());
808 assert!(sh_subscript.lock().unwrap().is_empty());
809 assert!(sh_edchar.lock().unwrap().is_empty());
810 assert_eq!(*sh_edmode.lock().unwrap(), [0u8, 0u8]);
811 assert_eq!(sh_unsetval, [0u8, 0u8]);
812 }
813
814 /// Port of `ksh93_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/ksh93.c:143`.
815 /// Verifies `LOCAL_NAMEREF` matches the C `#define` at c:158.
816 #[test]
817 fn local_nameref_matches_c_define() {
818 let _g = crate::test_util::global_state_lock();
819 assert_eq!(LOCAL_NAMEREF, PM_LOCAL | PM_UNSET | PM_NAMEREF);
820 }
821
822 /// c:60 — `matchgetfn` on a non-null `Param` that has no array
823 /// data must still return an empty Vec. Builds a valid `param`
824 /// with `u_arr = None` to exercise the "has Param but no array"
825 /// branch — a regression that adds `.unwrap()` would SIGSEGV.
826 #[test]
827 fn matchgetfn_with_param_no_array_returns_empty() {
828 let _g = crate::test_util::global_state_lock();
829 // matchgetfn reads `match` from paramtab (ksh93.rs:60 — c:60).
830 // A prior subst-related test may have written to it; clear so
831 // the "no array data" branch isn't masked by leftover values.
832 crate::ported::params::unsetparam("match");
833 let mut pm = param {
834 node: hashnode {
835 next: None,
836 nam: "match".to_string(),
837 flags: 0,
838 },
839 u_data: 0,
840 u_tied: None,
841 u_arr: None,
842 u_str: None,
843 u_val: 0,
844 u_dval: 0.0,
845 u_hash: None,
846 gsu_s: None,
847 gsu_i: None,
848 gsu_f: None,
849 gsu_a: None,
850 gsu_h: None,
851 base: 0,
852 width: 0,
853 env: None,
854 ename: None,
855 old: None,
856 level: 0,
857 };
858 let v = matchgetfn(&mut pm as *mut _);
859 assert!(
860 v.is_empty(),
861 "param without array data must produce empty result"
862 );
863 }
864
865 /// c:158 — `LOCAL_NAMEREF` must include the three component flags
866 /// AND no others. Pin the exact bit set so a regen that adds
867 /// PM_READONLY silently changes the unset semantics.
868 #[test]
869 fn local_nameref_has_exactly_three_component_flags() {
870 let _g = crate::test_util::global_state_lock();
871 let expected = PM_LOCAL | PM_UNSET | PM_NAMEREF;
872 assert_eq!(LOCAL_NAMEREF, expected);
873 // Subtracting each in turn yields the other two
874 assert_eq!(LOCAL_NAMEREF & !PM_LOCAL, PM_UNSET | PM_NAMEREF);
875 assert_eq!(LOCAL_NAMEREF & !PM_UNSET, PM_LOCAL | PM_NAMEREF);
876 assert_eq!(LOCAL_NAMEREF & !PM_NAMEREF, PM_LOCAL | PM_UNSET);
877 }
878
879 /// c:50 — `sh_unsetval` is the byte sentinel for "unset" string
880 /// params (`{ 0, 0 }`). Pin the exact value so a regen flipping
881 /// it to `[b'\0']` or `[1, 0]` silently breaks unset detection
882 /// at every shXgetfn call site.
883 #[test]
884 fn sh_unsetval_is_two_zero_bytes() {
885 let _g = crate::test_util::global_state_lock();
886 assert_eq!(
887 sh_unsetval,
888 [0u8, 0u8],
889 "sh_unsetval must be the C-canonical [0,0] sentinel"
890 );
891 }
892
893 /// c:60 — `matchgetfn` with a null pointer must NOT dereference.
894 /// Pinning null-safety so a regen that adds `*pm` outside a
895 /// guard SIGSEGVs the test instead of shipping.
896 #[test]
897 fn matchgetfn_null_pointer_is_safe() {
898 let _g = crate::test_util::global_state_lock();
899 let _ = matchgetfn(std::ptr::null_mut());
900 }
901
902 /// c:47 — `edcharsetfn` accepts null param + null arg pointer
903 /// without dereferencing. The C body is `;` (empty) so the Rust
904 /// stub must mirror that no-op + re-entry safety.
905 #[test]
906 fn edcharsetfn_double_null_is_safe() {
907 let _g = crate::test_util::global_state_lock();
908 edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
909 edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
910 }
911
912 // ─── zsh-corpus pins for ksh93 ─────────────────────────────────
913
914 /// `matchgetfn(null)` returns empty vec without panic.
915 #[test]
916 fn ksh93_corpus_matchgetfn_null_returns_empty() {
917 let _g = crate::test_util::global_state_lock();
918 let r = matchgetfn(std::ptr::null_mut());
919 assert!(r.is_empty(), "null pm → empty vec, got {r:?}");
920 }
921
922 /// All four lifecycle shims return 0.
923 #[test]
924 fn ksh93_corpus_lifecycle_returns_zero() {
925 let _g = crate::test_util::global_state_lock();
926 let m: *const module = std::ptr::null();
927 assert_eq!(setup_(m), 0);
928 assert_eq!(boot_(m), 0);
929 assert_eq!(cleanup_(m), 0);
930 assert_eq!(finish_(m), 0);
931 }
932
933 /// `edcharsetfn` cycling null calls remains stable.
934 #[test]
935 fn ksh93_corpus_edcharsetfn_repeated_null_no_panic() {
936 let _g = crate::test_util::global_state_lock();
937 for _ in 0..100 {
938 edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
939 }
940 }
941
942 // ═══════════════════════════════════════════════════════════════════
943 // Additional C-parity tests for Src/Modules/ksh93.c.
944 // ═══════════════════════════════════════════════════════════════════
945
946 /// c:47 — `edcharsetfn(null, null)` is no-op (placeholder for the
947 /// SIGKEYBD trap interception; the body is just a comment in C).
948 /// Pin: single null call doesn't panic.
949 #[test]
950 fn edcharsetfn_single_null_call_no_panic() {
951 let _g = crate::test_util::global_state_lock();
952 edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
953 }
954
955 /// c:60 — `matchgetfn(NULL)` returns empty Vec when `match` array
956 /// is unset (no $match parameter present).
957 #[test]
958 fn matchgetfn_null_pm_unset_match_returns_empty() {
959 let _g = crate::test_util::global_state_lock();
960 // Without setting the "match" param, getaparam returns None →
961 // empty result.
962 crate::ported::params::unsetparam("match");
963 let r = matchgetfn(std::ptr::null_mut());
964 assert!(r.is_empty(), "no $match → empty vec");
965 }
966
967 /// c:212 — `ksh93_wrapper(null, null, null)` no panic on all-null.
968 #[test]
969 fn ksh93_wrapper_all_null_no_panic() {
970 let _g = crate::test_util::global_state_lock();
971 let _ = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
972 }
973
974 /// c:491 — `setup_(NULL)` returns 0 (split per-hook).
975 #[test]
976 fn ksh93_setup_returns_zero_pin() {
977 let _g = crate::test_util::global_state_lock();
978 assert_eq!(setup_(std::ptr::null()), 0);
979 }
980
981 /// c:514 — `boot_(NULL)` returns 0.
982 #[test]
983 fn ksh93_boot_returns_zero_pin() {
984 let _g = crate::test_util::global_state_lock();
985 assert_eq!(boot_(std::ptr::null()), 0);
986 }
987
988 /// c:536 — `cleanup_(NULL)` returns 0.
989 #[test]
990 fn ksh93_cleanup_returns_zero_pin() {
991 let _g = crate::test_util::global_state_lock();
992 assert_eq!(cleanup_(std::ptr::null()), 0);
993 }
994
995 /// c:595 — `finish_(NULL)` returns 0.
996 #[test]
997 fn ksh93_finish_returns_zero_pin() {
998 let _g = crate::test_util::global_state_lock();
999 assert_eq!(finish_(std::ptr::null()), 0);
1000 }
1001
1002 /// c:499 — `features_(NULL, _)` returns 0 with features populated.
1003 #[test]
1004 fn ksh93_features_returns_zero_pin() {
1005 let _g = crate::test_util::global_state_lock();
1006 let mut f = Vec::new();
1007 assert_eq!(features_(std::ptr::null(), &mut f), 0);
1008 }
1009
1010 /// c:507 — `enables_(NULL, _)` no panic.
1011 #[test]
1012 fn ksh93_enables_no_panic() {
1013 let _g = crate::test_util::global_state_lock();
1014 let mut e: Option<Vec<i32>> = None;
1015 let _ = enables_(std::ptr::null(), &mut e);
1016 }
1017
1018 // ═══════════════════════════════════════════════════════════════════
1019 // Additional C-parity tests for Src/Modules/ksh93.c
1020 // c:47 edcharsetfn / c:83 matchgetfn / c:212 ksh93_wrapper / lifecycle
1021 // ═══════════════════════════════════════════════════════════════════
1022
1023 /// c:47 — `edcharsetfn(null, null)` is idempotent (multiple calls safe).
1024 #[test]
1025 fn edcharsetfn_double_null_idempotent() {
1026 let _g = crate::test_util::global_state_lock();
1027 for _ in 0..10 {
1028 edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
1029 }
1030 }
1031
1032 /// c:83 — `matchgetfn(null)` returns Vec (not panic) — type pinning.
1033 #[test]
1034 fn matchgetfn_null_returns_vec_type() {
1035 let _g = crate::test_util::global_state_lock();
1036 let _: Vec<String> = matchgetfn(std::ptr::null_mut());
1037 }
1038
1039 /// c:83 — `matchgetfn(null)` is deterministic across repeated calls.
1040 #[test]
1041 fn matchgetfn_null_is_deterministic() {
1042 let _g = crate::test_util::global_state_lock();
1043 let first = matchgetfn(std::ptr::null_mut());
1044 for _ in 0..5 {
1045 assert_eq!(
1046 matchgetfn(std::ptr::null_mut()),
1047 first,
1048 "matchgetfn(null) must be deterministic"
1049 );
1050 }
1051 }
1052
1053 /// c:212 — `ksh93_wrapper(null, null, null)` is safe.
1054 #[test]
1055 fn ksh93_wrapper_all_null_no_panic_pin() {
1056 let _g = crate::test_util::global_state_lock();
1057 let _ = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1058 }
1059
1060 /// c:212 — `ksh93_wrapper` returns i32 (type pinning).
1061 #[test]
1062 fn ksh93_wrapper_returns_i32_type() {
1063 let _g = crate::test_util::global_state_lock();
1064 let _: i32 = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1065 }
1066
1067 /// c:491-595 — full lifecycle setup→features→enables→boot→cleanup→finish
1068 /// all return 0.
1069 #[test]
1070 fn ksh93_full_lifecycle_returns_zero_for_all() {
1071 let _g = crate::test_util::global_state_lock();
1072 let null = std::ptr::null();
1073 assert_eq!(setup_(null), 0);
1074 let mut feats = Vec::new();
1075 let _ = features_(null, &mut feats);
1076 let mut enables: Option<Vec<i32>> = None;
1077 let _ = enables_(null, &mut enables);
1078 assert_eq!(boot_(null), 0);
1079 assert_eq!(cleanup_(null), 0);
1080 assert_eq!(finish_(null), 0);
1081 }
1082
1083 /// c:491 — setup_ idempotent.
1084 #[test]
1085 fn ksh93_setup_idempotent() {
1086 let _g = crate::test_util::global_state_lock();
1087 for _ in 0..10 {
1088 assert_eq!(setup_(std::ptr::null()), 0);
1089 }
1090 }
1091
1092 /// c:595 — finish_ idempotent.
1093 #[test]
1094 fn ksh93_finish_idempotent() {
1095 let _g = crate::test_util::global_state_lock();
1096 for _ in 0..10 {
1097 assert_eq!(finish_(std::ptr::null()), 0);
1098 }
1099 }
1100
1101 /// c:536 — cleanup_ idempotent.
1102 #[test]
1103 fn ksh93_cleanup_idempotent() {
1104 let _g = crate::test_util::global_state_lock();
1105 for _ in 0..10 {
1106 assert_eq!(cleanup_(std::ptr::null()), 0);
1107 }
1108 }
1109
1110 /// c:514 — boot_ idempotent.
1111 #[test]
1112 fn ksh93_boot_idempotent() {
1113 let _g = crate::test_util::global_state_lock();
1114 for _ in 0..10 {
1115 assert_eq!(boot_(std::ptr::null()), 0);
1116 }
1117 }
1118
1119 /// c:212 — ksh93_wrapper return value in canonical exit-code range
1120 /// (signed-byte 0..256).
1121 #[test]
1122 fn ksh93_wrapper_return_in_exit_code_range() {
1123 let _g = crate::test_util::global_state_lock();
1124 let r = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1125 assert!(
1126 (0..256).contains(&r),
1127 "exit code must fit in u8 range, got {}",
1128 r
1129 );
1130 }
1131
1132 // ═══════════════════════════════════════════════════════════════════
1133 // Additional C-parity tests for Src/Modules/ksh93.c
1134 // c:47 edcharsetfn / c:83 matchgetfn / c:212 ksh93_wrapper
1135 // ═══════════════════════════════════════════════════════════════════
1136
1137 /// c:83 — `matchgetfn` returns Vec<String> (compile-time type pin).
1138 #[test]
1139 fn matchgetfn_returns_vec_string_type() {
1140 let _g = crate::test_util::global_state_lock();
1141 let _: Vec<String> = matchgetfn(std::ptr::null_mut());
1142 }
1143
1144 /// c:47 — `edcharsetfn` returns void (compile-time pin).
1145 #[test]
1146 fn edcharsetfn_returns_void() {
1147 let _g = crate::test_util::global_state_lock();
1148 let _: () = edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
1149 }
1150
1151 /// c:212 — `ksh93_wrapper` returns i32 (compile-time pin).
1152 #[test]
1153 fn ksh93_wrapper_returns_i32_type_pin() {
1154 let _g = crate::test_util::global_state_lock();
1155 let _: i32 = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1156 }
1157
1158 /// c:212 — `ksh93_wrapper` is deterministic for all-null args.
1159 #[test]
1160 fn ksh93_wrapper_all_null_is_deterministic() {
1161 let _g = crate::test_util::global_state_lock();
1162 let first = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1163 for _ in 0..5 {
1164 assert_eq!(
1165 ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut()),
1166 first,
1167 "ksh93_wrapper(all null) must be deterministic"
1168 );
1169 }
1170 }
1171
1172 /// c:83 — `matchgetfn(null)` returns empty Vec (no match available).
1173 #[test]
1174 fn matchgetfn_null_returns_empty_pin() {
1175 let _g = crate::test_util::global_state_lock();
1176 let v = matchgetfn(std::ptr::null_mut());
1177 assert!(v.is_empty(), "null pm → empty vec");
1178 }
1179
1180 /// c:491 — `setup_` is idempotent + returns void/i32.
1181 #[test]
1182 fn ksh93_setup_returns_i32_type() {
1183 let _g = crate::test_util::global_state_lock();
1184 let _: i32 = setup_(std::ptr::null());
1185 }
1186
1187 /// c:514 — `boot_` returns i32.
1188 #[test]
1189 fn ksh93_boot_returns_i32_type() {
1190 let _g = crate::test_util::global_state_lock();
1191 let _: i32 = boot_(std::ptr::null());
1192 }
1193
1194 /// c:536 — `cleanup_` returns i32.
1195 #[test]
1196 fn ksh93_cleanup_returns_i32_type() {
1197 let _g = crate::test_util::global_state_lock();
1198 let _: i32 = cleanup_(std::ptr::null());
1199 }
1200
1201 /// c:595 — `finish_` returns i32.
1202 #[test]
1203 fn ksh93_finish_returns_i32_type() {
1204 let _g = crate::test_util::global_state_lock();
1205 let _: i32 = finish_(std::ptr::null());
1206 }
1207
1208 /// c:47 — `edcharsetfn` idempotent for repeated null calls.
1209 #[test]
1210 fn edcharsetfn_repeated_null_calls_idempotent() {
1211 let _g = crate::test_util::global_state_lock();
1212 for _ in 0..20 {
1213 edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
1214 }
1215 }
1216
1217 // ═══════════════════════════════════════════════════════════════════
1218 // Additional C-parity tests for Src/Modules/ksh93.c
1219 // c:47 edcharsetfn / c:83 matchgetfn / c:212 ksh93_wrapper + lifecycle
1220 // ═══════════════════════════════════════════════════════════════════
1221
1222 /// c:83 — `matchgetfn(null)` is deterministic across calls (alt).
1223 #[test]
1224 fn matchgetfn_null_deterministic_alt() {
1225 let _g = crate::test_util::global_state_lock();
1226 let first = matchgetfn(std::ptr::null_mut());
1227 for _ in 0..5 {
1228 assert_eq!(
1229 matchgetfn(std::ptr::null_mut()),
1230 first,
1231 "matchgetfn(null) must be pure"
1232 );
1233 }
1234 }
1235
1236 /// c:83 — `matchgetfn(null)` length is exactly 0 (empty Vec).
1237 #[test]
1238 fn matchgetfn_null_length_is_zero() {
1239 let _g = crate::test_util::global_state_lock();
1240 let v = matchgetfn(std::ptr::null_mut());
1241 assert_eq!(v.len(), 0, "matchgetfn(null) must have len=0");
1242 }
1243
1244 /// c:212 — `ksh93_wrapper` exit code is non-negative.
1245 #[test]
1246 fn ksh93_wrapper_exit_code_non_negative() {
1247 let _g = crate::test_util::global_state_lock();
1248 let r = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1249 assert!(r >= 0, "exit code must be non-negative, got {}", r);
1250 }
1251
1252 /// c:212 — `ksh93_wrapper` exit code fits in u8 range (canonical
1253 /// shell exit code 0..256).
1254 #[test]
1255 fn ksh93_wrapper_exit_code_fits_u8() {
1256 let _g = crate::test_util::global_state_lock();
1257 let r = ksh93_wrapper(std::ptr::null(), std::ptr::null(), std::ptr::null_mut());
1258 assert!(
1259 (0..256).contains(&r),
1260 "exit code {} must fit in u8 range",
1261 r
1262 );
1263 }
1264
1265 /// c:499 — `features_` returns i32 (compile-time pin).
1266 #[test]
1267 fn ksh93_features_returns_i32_type() {
1268 let _g = crate::test_util::global_state_lock();
1269 let mut v: Vec<String> = Vec::new();
1270 let _: i32 = features_(std::ptr::null(), &mut v);
1271 }
1272
1273 /// c:507 — `enables_` returns i32 (compile-time pin).
1274 #[test]
1275 fn ksh93_enables_with_none_returns_i32() {
1276 let _g = crate::test_util::global_state_lock();
1277 let mut e: Option<Vec<i32>> = None;
1278 let _: i32 = enables_(std::ptr::null(), &mut e);
1279 }
1280
1281 /// c:491 — `setup_` is idempotent across many calls (alt).
1282 #[test]
1283 fn ksh93_setup_idempotent_many_call_alt() {
1284 let _g = crate::test_util::global_state_lock();
1285 for _ in 0..20 {
1286 assert_eq!(setup_(std::ptr::null()), 0);
1287 }
1288 }
1289
1290 /// c:514 — `boot_` is idempotent across many calls (alt).
1291 #[test]
1292 fn ksh93_boot_idempotent_many_call_alt() {
1293 let _g = crate::test_util::global_state_lock();
1294 for _ in 0..20 {
1295 assert_eq!(boot_(std::ptr::null()), 0);
1296 }
1297 }
1298
1299 /// c:536 — `cleanup_` is idempotent across many calls (alt).
1300 #[test]
1301 fn ksh93_cleanup_idempotent_many_call_alt() {
1302 let _g = crate::test_util::global_state_lock();
1303 for _ in 0..20 {
1304 assert_eq!(cleanup_(std::ptr::null()), 0);
1305 }
1306 }
1307
1308 /// c:595 — `finish_` is idempotent across many calls (alt).
1309 #[test]
1310 fn ksh93_finish_idempotent_many_call_alt() {
1311 let _g = crate::test_util::global_state_lock();
1312 for _ in 0..20 {
1313 assert_eq!(finish_(std::ptr::null()), 0);
1314 }
1315 }
1316
1317 /// c:491/499/507/514/536/595 — each lifecycle hook returns 0
1318 /// individually (tighter failure resolution).
1319 #[test]
1320 fn ksh93_each_lifecycle_hook_returns_zero_individually() {
1321 let _g = crate::test_util::global_state_lock();
1322 let null = std::ptr::null();
1323 let mut v: Vec<String> = Vec::new();
1324 let mut e: Option<Vec<i32>> = None;
1325 assert_eq!(setup_(null), 0, "c:491 setup_");
1326 assert_eq!(features_(null, &mut v), 0, "c:499 features_");
1327 assert_eq!(enables_(null, &mut e), 0, "c:507 enables_");
1328 assert_eq!(boot_(null), 0, "c:514 boot_");
1329 assert_eq!(cleanup_(null), 0, "c:536 cleanup_");
1330 assert_eq!(finish_(null), 0, "c:595 finish_");
1331 }
1332
1333 // ═══════════════════════════════════════════════════════════════════
1334 // Additional C-parity pins for Src/Modules/ksh93.c
1335 // c:47 edcharsetfn / c:83 matchgetfn / c:212 ksh93_wrapper /
1336 // c:491-595 lifecycle hooks
1337 // ═══════════════════════════════════════════════════════════════════
1338
1339 /// c:491 — `setup_` return type i32 (compile-time pin, alt).
1340 #[test]
1341 fn ksh93_setup_returns_i32_type_alt_pin() {
1342 let _g = crate::test_util::global_state_lock();
1343 let _: i32 = setup_(std::ptr::null());
1344 }
1345
1346 /// c:536 — `cleanup_` return type i32 (compile-time pin).
1347 #[test]
1348 fn ksh93_cleanup_returns_i32_type_alt_pin() {
1349 let _g = crate::test_util::global_state_lock();
1350 let _: i32 = cleanup_(std::ptr::null());
1351 }
1352
1353 /// c:595 — `finish_` return type i32 (compile-time pin).
1354 #[test]
1355 fn ksh93_finish_returns_i32_type_alt_pin() {
1356 let _g = crate::test_util::global_state_lock();
1357 let _: i32 = finish_(std::ptr::null());
1358 }
1359
1360 /// c:514 — `boot_` return type i32 (compile-time pin).
1361 #[test]
1362 fn ksh93_boot_returns_i32_type_alt_pin() {
1363 let _g = crate::test_util::global_state_lock();
1364 let _: i32 = boot_(std::ptr::null());
1365 }
1366
1367 /// c:83 — `matchgetfn(null)` returns Vec<String> safely.
1368 #[test]
1369 fn matchgetfn_null_pm_returns_vec_string() {
1370 let _g = crate::test_util::global_state_lock();
1371 let r = matchgetfn(std::ptr::null_mut());
1372 let _: Vec<String> = r;
1373 }
1374
1375 /// c:83 — `matchgetfn` is deterministic for null pm.
1376 #[test]
1377 fn matchgetfn_null_pm_deterministic() {
1378 let _g = crate::test_util::global_state_lock();
1379 let a = matchgetfn(std::ptr::null_mut());
1380 let b = matchgetfn(std::ptr::null_mut());
1381 assert_eq!(a, b, "matchgetfn(null) must be deterministic");
1382 }
1383
1384 /// c:47 — `edcharsetfn(null, null)` doesn't panic.
1385 #[test]
1386 fn edcharsetfn_null_inputs_no_panic() {
1387 let _g = crate::test_util::global_state_lock();
1388 edcharsetfn(std::ptr::null_mut(), std::ptr::null_mut());
1389 }
1390
1391 /// c:212 — `ksh93_wrapper` with all null inputs doesn't panic.
1392 #[test]
1393 fn ksh93_wrapper_all_null_inputs_no_panic() {
1394 let _g = crate::test_util::global_state_lock();
1395 let name = std::ffi::CString::new("test").unwrap();
1396 let _ = ksh93_wrapper(
1397 std::ptr::null(),
1398 std::ptr::null(),
1399 name.as_ptr() as *mut libc::c_char,
1400 );
1401 }
1402
1403 /// c:212 — `ksh93_wrapper` return type i32 (compile-time pin, alt).
1404 #[test]
1405 fn ksh93_wrapper_returns_i32_type_alt() {
1406 let _g = crate::test_util::global_state_lock();
1407 let name = std::ffi::CString::new("test").unwrap();
1408 let _: i32 = ksh93_wrapper(
1409 std::ptr::null(),
1410 std::ptr::null(),
1411 name.as_ptr() as *mut libc::c_char,
1412 );
1413 }
1414
1415 /// c:499 — `features_` deterministic on null module.
1416 #[test]
1417 fn ksh93_features_deterministic_alt_pin() {
1418 let _g = crate::test_util::global_state_lock();
1419 let mut v1: Vec<String> = Vec::new();
1420 let mut v2: Vec<String> = Vec::new();
1421 let _ = features_(std::ptr::null(), &mut v1);
1422 let _ = features_(std::ptr::null(), &mut v2);
1423 assert_eq!(v1, v2, "features_ must be deterministic");
1424 }
1425
1426 /// c:507 — `enables_` with Some(non-empty) doesn't panic.
1427 #[test]
1428 fn ksh93_enables_with_some_non_empty_no_panic() {
1429 let _g = crate::test_util::global_state_lock();
1430 let mut e: Option<Vec<i32>> = Some(vec![1, 2, 3]);
1431 let _ = enables_(std::ptr::null(), &mut e);
1432 }
1433
1434 /// c:491/514/595 — setup→boot→finish chain returns 0 each.
1435 #[test]
1436 fn ksh93_setup_boot_finish_chain_returns_zero_each() {
1437 let _g = crate::test_util::global_state_lock();
1438 let null = std::ptr::null();
1439 assert_eq!(setup_(null), 0);
1440 assert_eq!(boot_(null), 0);
1441 assert_eq!(finish_(null), 0);
1442 }
1443}