Skip to main content

zsh/ported/modules/
mapfile.rs

1//! `zsh/mapfile` module — port of `Src/Modules/mapfile.c`.
2//!
3//! Functions for the options special parameter.                             // c:64
4//! Here we scan the current directory, calling func() for each file        // c:254
5//!
6//! Provides the `$mapfile` magic associative array that exposes the
7//! filesystem as a hash:
8//!   - `$mapfile[fname]`        → reads the file's contents
9//!   - `mapfile[fname]=...`     → writes the value to that file
10//!   - `unset 'mapfile[fname]'` → unlinks the file
11//!   - `${(k)mapfile}`          → enumerates files in the cwd
12//!
13//! C source: 12 ported total — `setpmmapfile`, `unsetpmmapfile`,
14//! `setpmmapfiles`, `get_contents`, `getpmmapfile`, `scanpmmapfile`,
15//! `setup_`, `features_`, `enables_`, `boot_`, `cleanup_`, `finish_`.
16//! Zero structs/enums in mapfile.c (only `static const struct gsu_*`
17//! and `static struct paramdef partab[]` aggregates of pre-defined
18//! zsh-framework types).
19
20use crate::ported::utils::{metafy, unmeta, zwarn};
21use crate::ported::zsh_h::features;
22use crate::zsh_h::module;
23use std::fs::OpenOptions;
24use std::io;
25use std::io::{Read, Write};
26use std::os::unix::fs::MetadataExt;
27#[cfg(unix)]
28use std::os::unix::io::AsRawFd;
29use std::sync::{Mutex, OnceLock};
30
31// ---------------------------------------------------------------------------
32// File-scope statics (none in C body that need Rust mirrors —
33// gsu/paramdef tables are wired through zshrs's static-link
34// dispatch; they are not file-`static` storage in the bucket-1 sense).
35// ---------------------------------------------------------------------------
36
37/// Port of `setpmmapfile(Param pm, char *value)` from `Src/Modules/mapfile.c:68`. Writes
38/// `value` to a file named by the Param's name slot, using
39/// `mmap(MAP_SHARED)` + `msync` on `USE_MMAP` builds and `fopen("w")`
40/// + `putc` loop on the fallback path. Both `name` and `value` are
41/// `unmetafy`-ed (c:80-81). `PM_READONLY` params skip the write
42/// (c:84).
43///
44/// C signature: `static void setpmmapfile(Param pm, char *value)`.
45/// Rust port takes the param name + value + readonly flag directly;
46/// `Param` is a paramdef-table type and the magic-assoc dispatcher
47/// peels these out before the call. C silently no-ops on any
48/// `open`/`mmap` failure (the failure simply falls through past the
49/// inner block) — the Rust port mirrors that with `let _ = ...`
50/// where the C body discards the return.
51#[cfg(unix)]
52pub fn setpmmapfile(name: &str, value: &str, readonly: bool) {
53    // c:68
54    // c:68 — `char *name = ztrdup(pm->node.nam);`
55    let name_unmeta = unmeta(name); // c:71+82
56                                    // c:82-83 — `unmetafy(name, &len); unmetafy(value, &len);`
57    let value_unmeta = unmeta(value); // c:83
58    let value_bytes = value_unmeta.as_bytes();
59    let len = value_bytes.len(); // c:83 len out
60
61    // c:87 — `if (!(pm->node.flags & PM_READONLY) && ...`
62    // The whole mmap+memcpy+msync+ftruncate+munmap block is gated on
63    // !readonly && open success && mmap success. Any failure short-
64    // circuits past the inner body (no error reported).
65    if readonly {
66        return; // c:87 readonly skip
67    }
68
69    // c:88 — `(fd = open(name, O_RDWR|O_CREAT|O_NOCTTY, 0666)) >= 0`.
70    // O_NOCTTY matters when `name` happens to be a terminal device
71    // (`$mapfile[/dev/tty]=...` — unusual but legal). Without it,
72    // the open() call acquires /dev/tty as the process's controlling
73    // terminal if no controlling tty is currently set, which is
74    // never the intended effect of a mapfile assign. Prior port
75    // skipped the flag because std::fs::OpenOptions doesn't expose
76    // O_NOCTTY directly — but the OpenOptionsExt::custom_flags trait
77    // on Unix does.
78    use std::os::unix::fs::OpenOptionsExt;
79    let file = match OpenOptions::new()
80        .read(true)
81        .write(true)
82        .create(true)
83        .truncate(false)
84        .custom_flags(libc::O_NOCTTY)
85        .open(&name_unmeta)
86    {
87        Ok(f) => f,
88        Err(_) => return, // c:88 open fail → skip
89    };
90    let fd = file.as_raw_fd();
91
92    // c:89-90 — `mmptr = mmap((caddr_t)0, len, PROT_READ|PROT_WRITE,
93    //                          MMAP_ARGS, fd, (off_t)0)) != (caddr_t)-1`
94    // mmap on len=0 is invalid; for len=0 the whole mmap block is
95    // skipped (the && chain short-circuits) but the open already
96    // created/touched the file, matching C's observable behavior.
97    if len == 0 {
98        // No-op body match; close on drop. C would have skipped the
99        // inner block too because mmap(len=0) returns MAP_FAILED.
100        return;
101    }
102    // c:89-90 — `mmap((caddr_t)0, len, PROT_READ|PROT_WRITE,
103    //                   MMAP_ARGS, fd, (off_t)0)`. MMAP_ARGS at c:56
104    //                   is `MAP_FILE | MAP_VARIABLE | MAP_SHARED |
105    //                   MAP_NORESERVE`. MAP_FILE and MAP_VARIABLE are
106    //                   legacy aliases (`#define ... 0` on most modern
107    //                   platforms per c:47-52). MAP_NORESERVE matters
108    //                   on Linux — tells the kernel not to reserve
109    //                   swap for the mapping, avoiding OOM rejection
110    //                   on large $mapfile assigns. Prior port used
111    //                   only MAP_SHARED, missing MAP_NORESERVE.
112    let map_flags: libc::c_int = {
113        #[cfg(target_os = "linux")]
114        {
115            libc::MAP_SHARED | libc::MAP_NORESERVE
116        }
117        #[cfg(not(target_os = "linux"))]
118        {
119            libc::MAP_SHARED
120        }
121    };
122    let mmptr = unsafe {
123        libc::mmap(
124            std::ptr::null_mut(),
125            len,
126            libc::PROT_READ | libc::PROT_WRITE,
127            map_flags,
128            fd,
129            0,
130        )
131    };
132    if mmptr == libc::MAP_FAILED {
133        // c:110-117 — `#else /* don't USE_MMAP */` arm:
134        //   if ((fout = fopen(name, "w"))) {
135        //       while (len--) putc(*value++, fout);
136        //       fclose(fout);
137        //   }
138        if let Ok(mut fout) = OpenOptions::new()
139            .write(true)
140            .create(true)
141            .truncate(true)
142            .open(&name_unmeta)
143        {
144            let _ = fout.write_all(value_bytes); // c:113-114
145                                                 // fclose on drop                                            // c:115
146        }
147        return;
148    }
149
150    // c:91-94 — first ftruncate to grow the file before msync.
151    // Comment quoted: "we need to make sure the file is long enough
152    // for when we msync.  On AIX, at least, we just get zeroes
153    // otherwise."
154    unsafe {
155        if libc::ftruncate(fd, len as libc::off_t) < 0 {
156            // c:95
157            zwarn(&format!(
158                "ftruncate failed: {}", // c:96
159                io::Error::last_os_error()
160            ));
161        }
162        // c:97 — `memcpy(mmptr, value, len);`
163        std::ptr::copy_nonoverlapping(value_bytes.as_ptr(), mmptr as *mut u8, len);
164        // c:101 — `msync(mmptr, len, MS_SYNC);`
165        libc::msync(mmptr, len, libc::MS_SYNC);
166        // c:102-105 — second ftruncate. Comment quoted: "we need to
167        // truncate again, since mmap() always maps complete pages.
168        // Honestly, I tried it without, and you need both."
169        if libc::ftruncate(fd, len as libc::off_t) < 0 {
170            // c:106
171            zwarn(&format!(
172                "ftruncate failed: {}", // c:107
173                io::Error::last_os_error()
174            ));
175        }
176        // c:108 — `munmap(mmptr, len);`
177        libc::munmap(mmptr, len);
178    }
179
180    // c:118-121 — close(fd) (auto on drop), free(name), free(value)
181    // (both unmeta-allocated Strings, dropped at scope end).
182}
183
184/// Non-`USE_MMAP` build path (port of the `#else` arm at
185/// `Src/Modules/mapfile.c:68`): `fopen("w")` + `putc` loop +
186/// `fclose`. Used on platforms without mmap.
187#[cfg(not(unix))]
188pub fn setpmmapfile(name: &str, value: &str, readonly: bool) {
189    // c:68
190    if readonly {
191        return;
192    } // c:87 readonly skip
193    let name_unmeta = unmeta(name);
194    let value_unmeta = unmeta(value);
195    if let Ok(mut fout) = OpenOptions::new()
196        .write(true)
197        .create(true)
198        .truncate(true)
199        .open(&name_unmeta)
200    {
201        let _ = fout.write_all(value_unmeta.as_bytes()); // c:126-114
202    }
203}
204
205/// Port of `unsetpmmapfile(Param pm, UNUSED(int exp))` from `Src/Modules/mapfile.c:126`. Unset
206/// callback for an element of `$mapfile`: unlinks the file named by
207/// `pm->node.nam` (unless the param is readonly, c:133).
208///
209/// C signature: `static void unsetpmmapfile(Param pm, int exp)`. The
210/// `exp` arg is `UNUSED` (c:126) — it never gates anything.
211/// WARNING: param names don't match C — Rust=(pm, readonly) vs C=(pm, exp).
212/// The second Rust arg carries the `pm->node.flags & PM_READONLY` bit
213/// that C reads off the Param struct at c:133; zshrs's adapter doesn't
214/// thread the full Param here, so the caller passes the bit directly.
215/// It is NOT C's (unused) `exp`. A prior signature named it `exp`,
216/// which misread the C guard as explicit-unset gating.
217pub fn unsetpmmapfile(pm: &str, readonly: bool) {
218    // c:126
219    // c:126 — `char *fname = ztrdup(pm->node.nam);`
220    // c:131 — `unmetafy(fname, &dummy);`
221    let fname = unmeta(pm); // c:129+131
222                            // c:133-134 — `if (!(pm->node.flags & PM_READONLY)) unlink(fname);`
223    if !readonly {
224        // c:133
225        let _ = std::fs::remove_file(&fname); // c:134
226    }
227    // c:136 — free(fname); auto on drop.
228}
229
230/// Port of `setpmmapfiles(Param pm, HashTable ht)` from `Src/Modules/mapfile.c:141`. The
231/// bulk-set callback fired when `mapfile=( foo bar baz qux )` assigns
232/// a hashtable. For each (name, value) entry, calls
233/// `setpmmapfile(v.pm, ztrdup(getstrvalue(&v)))` (c:159).
234///
235/// C signature: `static void setpmmapfiles(Param pm, HashTable ht)`.
236/// Rust port takes a slice of `(name, value)` pairs since zshrs's
237/// magic-assoc dispatcher peels the HashTable into entries before
238/// the call; the per-entry writeback still goes through the real
239/// `setpmmapfile` so unmetafy + readonly + mmap path stays on the
240/// call chain.
241/// WARNING: param names don't match C — Rust=(entries, readonly) vs C=(pm, ht)
242pub fn setpmmapfiles(entries: &[(String, String)], readonly: bool) {
243    // c:141
244    // c:141-147 — `if (!ht) return;`
245    if entries.is_empty() {
246        // c:146
247        return; // c:147
248    }
249    // c:149-160 — `if (!(pm->node.flags & PM_READONLY))
250    //                  for (i = 0; i < ht->hsize; i++)
251    //                      for (hn = ht->nodes[i]; hn; hn = hn->next) { ... }`
252    if !readonly {
253        // c:149
254        for (name, value) in entries {
255            // c:150-151
256            // c:152-159 — set up `struct value v`, call setpmmapfile.
257            // Rust collapses the inline `struct value` setup since
258            // we already have the metafied string pair.
259            setpmmapfile(name, value, readonly); // c:159
260        }
261    }
262    // c:161-162 — `if (ht != pm->u.hash) deleteparamtable(ht);`
263    // No paramtable here in the slice-based Rust port; nothing to free.
264}
265
266/// Port of `get_contents(char *fname)` from `Src/Modules/mapfile.c:167`. Reads
267/// the file at `fname` and returns its contents.
268///
269/// DIVERGENCE from C: the c:195/202 `metafy(buf, size, META_HEAPDUP)`
270/// step is deliberately SKIPPED — zshrs param strings are native
271/// UTF-8 `String`s, and a metafied byte stream (Meta + byte^32 pairs,
272/// not valid UTF-8) can't round-trip through `String` without
273/// corruption. Binary file content goes through `from_utf8_lossy`
274/// instead (U+FFFD for invalid sequences). Revisit when a byte-string
275/// param substrate lands. Returns `None` on the C short-circuit
276/// failure paths (open, fstat, mmap all return NULL).
277///
278/// C signature: `static char *get_contents(char *fname)`. Returns
279/// `char *` (NULL on failure); Rust port returns `Option<String>`.
280#[cfg(unix)]
281pub fn get_contents(fname: &str) -> Option<String> {
282    // c:167
283    // c:177 — `unmetafy(fname = ztrdup(fname), &fd);`
284    let fname_unmeta = unmeta(fname);
285
286    // c:180 — `(fd = open(fname, O_RDONLY | O_NOCTTY)) < 0`.
287    // O_NOCTTY prevents `$mapfile[/dev/tty]` reads from acquiring
288    // /dev/tty as the controlling terminal — same fix as the
289    // setpmmapfile (write) path above. Prior port omitted the flag
290    // because std::fs::OpenOptions doesn't expose it directly;
291    // OpenOptionsExt::custom_flags adds it.
292    use std::os::unix::fs::OpenOptionsExt;
293    let file = match OpenOptions::new()
294        .read(true)
295        .custom_flags(libc::O_NOCTTY)
296        .open(&fname_unmeta)
297    {
298        Ok(f) => f,            // c:180 open ok
299        Err(_) => return None, // c:184-187 NULL return
300    };
301    // c:181 — `fstat(fd, &sbuf)`
302    let metadata = match file.metadata() {
303        Ok(m) => m,
304        Err(_) => return None, // c:184-187 NULL
305    };
306    let size = metadata.size() as usize;
307
308    if size == 0 {
309        // Match C: mmap(0, 0, ...) returns MAP_FAILED on most platforms,
310        // which falls through to the NULL-return branch at c:184-187.
311        // Empty-file behavior: return Some("") for usability — an
312        // empty regular file is a valid zsh string, not "unset".
313        // No metafy: zshrs strings are native UTF-8, not Meta-escaped
314        // — the C `metafy` step exists because zsh's internal
315        // representation is metafied and unmetafies on display, but
316        // the Rust port stores user strings as-is.
317        return Some(String::new());
318    }
319
320    let fd = file.as_raw_fd();
321    // c:182-183 — `mmap(NULL, sbuf.st_size, PROT_READ, MMAP_ARGS, fd, 0)`.
322    // MMAP_ARGS at c:56 expands to `MAP_FILE | MAP_VARIABLE | MAP_SHARED
323    // | MAP_NORESERVE`. MAP_FILE / MAP_VARIABLE are legacy 0s on modern
324    // platforms; the live bits are MAP_SHARED + MAP_NORESERVE.
325    //
326    // MAP_SHARED vs MAP_PRIVATE: for PROT_READ-only mappings both
327    // behave identically (no writes), but MAP_SHARED matches C
328    // byte-for-byte AND lets the kernel skip COW page-table setup.
329    //
330    // MAP_NORESERVE on Linux is what prevents OOM rejection on large
331    // reads — tells the kernel not to pre-reserve swap for the
332    // mapping. Same fix as setpmmapfile (mapfile.rs:113-115). Prior
333    // port used bare MAP_PRIVATE, so `$mapfile[/some/huge/file]` on a
334    // tight-swap Linux box could fail mmap and silently fall through
335    // to the read_to_end fallback (slower) or return None (data lost).
336    let map_flags: libc::c_int = {
337        #[cfg(target_os = "linux")]
338        {
339            libc::MAP_SHARED | libc::MAP_NORESERVE
340        }
341        #[cfg(not(target_os = "linux"))]
342        {
343            libc::MAP_SHARED
344        }
345    };
346    let mmptr = unsafe {
347        libc::mmap(
348            std::ptr::null_mut(),
349            size,
350            libc::PROT_READ,
351            map_flags,
352            fd,
353            0,
354        )
355    };
356
357    if mmptr == libc::MAP_FAILED {
358        // c:199-202 — `#else /* don't USE_MMAP */` arm. No metafy:
359        // zshrs strings are native UTF-8 (see empty-file comment).
360        let mut contents = Vec::new();
361        let mut file = file;
362        if file.read_to_end(&mut contents).is_err() {
363            return None;
364        }
365        return Some(String::from_utf8_lossy(&contents).into_owned()); // c:202
366    }
367
368    // c:190-195 — Comment quoted: "Sadly, we need to copy the thing
369    // even if metafying doesn't change it.  We just don't know when
370    // we might get a chance to munmap it, otherwise."
371    // val = metafy((char *)mmptr, sbuf.st_size, META_HEAPDUP);
372    // (Rust port: skip the metafy step per the empty-file comment.)
373    let slice = unsafe { std::slice::from_raw_parts(mmptr as *const u8, size) };
374    let val = String::from_utf8_lossy(slice).into_owned();
375
376    // c:197 — `munmap(mmptr, sbuf.st_size);`
377    unsafe {
378        libc::munmap(mmptr, size);
379    }
380    // c:198 — close(fd) auto on drop.
381    // c:204 — free(fname) auto on drop.
382    Some(val)
383}
384
385/// Non-Unix build path (port of the `#ifndef USE_MMAP` arm at
386/// `Src/Modules/mapfile.c:167`): plain read with metafy.
387#[cfg(not(unix))]
388pub fn get_contents(fname: &str) -> Option<String> {
389    // c:167. No metafy: zshrs strings are native UTF-8 (see the
390    // Unix arm's empty-file comment for the full rationale).
391    let fname_unmeta = unmeta(fname);
392    std::fs::read_to_string(&fname_unmeta).ok()
393}
394
395/// Port of `getpmmapfile(UNUSED(HashTable ht), const char *name)` from `Src/Modules/mapfile.c:217`. The
396/// magic-assoc lookup callback for `${mapfile[name]}`. C body
397/// allocates a `struct param` from the heap, sets `pm->node.nam`,
398/// `pm->node.flags = PM_SCALAR`, `pm->gsu.s = &mapfile_gsu`,
399/// inherits `PM_READONLY` from the partab entry, then either points
400/// `u.str` at `get_contents(name)` or sets `u.str = ""` + `PM_UNSET`.
401///
402/// Port of `static HashNode getpmmapfile(HashTable ht, const char *name)`
403/// from `Src/Modules/mapfile.c:217-237`. Signature matches C exactly
404/// so this can be registered as a `HashGetFn` in PARTAB.
405pub fn getpmmapfile(
406    _ht: *mut crate::ported::zsh_h::HashTable,
407    name: &str,
408) -> Option<crate::ported::zsh_h::Param> {
409    // c:217
410    use crate::ported::zsh_h::{hashnode, param, PM_SCALAR, PM_UNSET};
411    // c:220-221 — `pm = (Param) hcalloc(sizeof(struct param));
412    //              pm->node.nam = dupstring(name);`
413    let (str_val, is_unset) = match get_contents(name) {
414        // c:230 — `pm->u.str = contents;`
415        Some(s) => (s, false),
416        // c:233-234 — `pm->u.str = ""; pm->node.flags |= PM_UNSET;`
417        None => (String::new(), true),
418    };
419    let mut flags: i32 = PM_SCALAR as i32; // c:222 — `pm->node.flags = PM_SCALAR;`
420                                           // c:224 — `pm->node.flags |= (partab[0].pm->node.flags & PM_READONLY);`
421                                           // partab[0] is the mapfile entry itself; PM_READONLY isn't set on
422                                           // mapfile in C (it's writable via setpmmapfile), so this is a no-op.
423    if is_unset {
424        flags |= PM_UNSET as i32; // c:234
425    }
426    Some(Box::new(param {
427        node: hashnode {
428            next: None,
429            nam: name.to_string(),
430            flags,
431        },
432        u_data: 0,
433        u_tied: None,
434        u_arr: None,
435        u_str: Some(str_val),
436        u_val: 0,
437        u_dval: 0.0,
438        u_hash: None,
439        gsu_s: None, // c:223 — `pm->gsu.s = &mapfile_gsu;` (mapfile_gsu not yet wired)
440        gsu_i: None,
441        gsu_f: None,
442        gsu_a: None,
443        gsu_h: None,
444        base: 0,
445        width: 0,
446        env: None,
447        ename: None,
448        old: None,
449        level: 0,
450    }))
451}
452
453/// Port of `scanpmmapfile(UNUSED(HashTable ht), ScanFunc func, int flags)`
454/// from `Src/Modules/mapfile.c:241-266`. The magic-assoc scan callback
455/// for `${(k)mapfile}` / `${(kv)mapfile}`. Walks the cwd and yields
456/// one entry per file. C source quotes: "Hmmm, it's rather wasteful
457/// always to read the contents. In fact, it's grotesequely \[sic\]
458/// wasteful, since that would mean we always read the entire
459/// contents of every single file in the directory into memory.
460/// Hence just leave it empty." → values are always `""` (c:263).
461pub fn scanpmmapfile(
462    _ht: *mut crate::ported::zsh_h::HashTable,
463    func: Option<crate::ported::zsh_h::ScanFunc>,
464    flags: i32,
465) {
466    // c:241
467    use crate::ported::zsh_h::{hashnode, param, PM_SCALAR};
468    let dir = match std::fs::read_dir(".") {
469        Ok(d) => d,       // c:246
470        Err(_) => return, // c:247
471    };
472    let f = match func {
473        Some(f) => f,
474        None => return,
475    };
476    for entry in dir.flatten() {
477        // c:255
478        if let Some(n) = entry.file_name().to_str() {
479            if n == "." || n == ".." {
480                continue; // c:255 ignoredots=1
481            }
482            // c:258-263 — build a transient param + invoke func.
483            let pm = param {
484                node: hashnode {
485                    next: None,
486                    nam: n.to_string(),
487                    flags: PM_SCALAR as i32,
488                },
489                u_data: 0,
490                u_tied: None,
491                u_arr: None,
492                u_str: Some(String::new()), // c:263 `pm.u.str = "";`
493                u_val: 0,
494                u_dval: 0.0,
495                u_hash: None,
496                gsu_s: None,
497                gsu_i: None,
498                gsu_f: None,
499                gsu_a: None,
500                gsu_h: None,
501                base: 0,
502                width: 0,
503                env: None,
504                ename: None,
505                old: None,
506                level: 0,
507            };
508            let node_box = Box::new(pm.node.clone());
509            f(&node_box, flags); // c:264 `func(&pm.node, flags);`
510        }
511    }
512    // c:266 — `closedir(dir);` (auto on Drop)
513}
514
515// ---------------------------------------------------------------------------
516// Module loaders.
517// ---------------------------------------------------------------------------
518
519// =====================================================================
520// static struct paramdef partab[]                                   c:212
521// static struct features module_features                            c:267
522// =====================================================================
523
524// `partab` — port of `static struct paramdef partab[]` (mapfile.c:212).
525
526// `module_features` — port of `static struct features module_features`
527// from mapfile.c:267.
528
529/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/mapfile.c:279`.
530#[allow(unused_variables)]
531pub fn setup_(m: *const module) -> i32 {
532    // c:279
533    // C body c:280-281 — `return 0`. Faithful empty-body port.
534    0
535}
536
537/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/mapfile.c:286`.
538/// C body: `*features = featuresarray(m, &module_features); return 0;`
539pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
540    // c:286
541    *features = featuresarray(m, module_features());
542    0 // c:301
543}
544
545/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/mapfile.c:294`.
546/// C body: `return handlefeatures(m, &module_features, enables);`
547pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
548    // c:294
549    handlefeatures(m, module_features(), enables) // c:301
550}
551
552/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/mapfile.c:301`.
553#[allow(unused_variables)]
554pub fn boot_(m: *const module) -> i32 {
555    // c:301
556    // C body c:302-303 — `return 0`. Faithful empty-body port; the
557    //                    $mapfile assoc-param registers via pd_list.
558    0
559}
560
561/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/mapfile.c:308`.
562/// C body: `return setfeatureenables(m, &module_features, NULL);`
563pub fn cleanup_(m: *const module) -> i32 {
564    // c:308
565    setfeatureenables(m, module_features(), None) // c:315
566}
567
568/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/mapfile.c:315`.
569#[allow(unused_variables)]
570pub fn finish_(m: *const module) -> i32 {
571    // c:315
572    // C body c:316-317 — `return 0`. Faithful empty-body port.
573    0
574}
575
576static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
577
578// Local stubs for the per-module entry points. C uses generic
579// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
580// 3275/3370/3445) but those take `Builtin` + `Features` pointer
581// fields the Rust port doesn't carry. The hardcoded descriptor
582// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
583// WARNING: NOT IN MAPFILE.C — Rust-only module-framework shim.
584// C uses generic featuresarray/handlefeatures/setfeatureenables from
585// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
586// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
587fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
588    vec!["p:mapfile".to_string()]
589}
590
591// WARNING: NOT IN MAPFILE.C — Rust-only module-framework shim.
592// C uses generic featuresarray/handlefeatures/setfeatureenables from
593// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
594// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
595fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
596    if enables.is_none() {
597        *enables = Some(vec![1; 1]);
598    }
599    0
600}
601
602// WARNING: NOT IN MAPFILE.C — Rust-only module-framework shim.
603// C uses generic featuresarray/handlefeatures/setfeatureenables from
604// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
605// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
606fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
607    0
608}
609
610// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
611// ─── RUST-ONLY ACCESSORS ───
612//
613// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
614// RwLock<T>>` globals declared above. C zsh uses direct global
615// access; Rust needs these wrappers because `OnceLock::get_or_init`
616// is the only way to lazily construct shared state. These ported sit
617// here so the body of this file reads in C source order without
618// the accessor wrappers interleaved between real port ported.
619// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
620
621// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
622// ─── RUST-ONLY ACCESSORS ───
623//
624// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
625// RwLock<T>>` globals declared above. C zsh uses direct global
626// access; Rust needs these wrappers because `OnceLock::get_or_init`
627// is the only way to lazily construct shared state. These ported sit
628// here so the body of this file reads in C source order without
629// the accessor wrappers interleaved between real port ported.
630// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
631
632// WARNING: NOT IN MAPFILE.C — Rust-only module-framework shim.
633// C uses generic featuresarray/handlefeatures/setfeatureenables from
634// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
635// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
636fn module_features() -> &'static Mutex<features> {
637    MODULE_FEATURES.get_or_init(|| {
638        Mutex::new(features {
639            bn_list: None,
640            bn_size: 0,
641            cd_list: None,
642            cd_size: 0,
643            mf_list: None,
644            mf_size: 0,
645            pd_list: None,
646            pd_size: 1,
647            n_abstract: 0,
648        })
649    })
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655    use std::fs;
656    use std::path::Path;
657
658    /// Verifies the C c:228-234 `else` branch: `get_contents` returns
659    /// NULL → caller treats as PM_UNSET (Param with empty u_str + flag).
660    #[test]
661    fn getpmmapfile_nonexistent_returns_unset_param() {
662        let _g = crate::test_util::global_state_lock();
663        use crate::ported::zsh_h::PM_UNSET;
664        let pm = getpmmapfile(std::ptr::null_mut(), "/nonexistent/file/path/zshrs_mapfile")
665            .expect("getpmmapfile always returns Some(Param)");
666        assert!(
667            pm.u_str.as_deref() == Some(""),
668            "u_str should be empty for missing file"
669        );
670        assert!(
671            pm.node.flags & PM_UNSET as i32 != 0,
672            "PM_UNSET flag should be set"
673        );
674    }
675
676    /// Verifies the c:88 `open(O_RDWR|O_CREAT)` + c:97 `memcpy` +
677    /// c:101 `msync` + c:106 `ftruncate` write path: a file written
678    /// by `setpmmapfile` reads back identically through
679    /// `get_contents`.
680    #[test]
681    fn file_roundtrip() {
682        let _g = crate::test_util::global_state_lock();
683        let test_file = "/tmp/zshrs_mapfile_test_roundtrip.txt";
684        let content = "Hello, mapfile!";
685        let _ = fs::remove_file(test_file);
686        setpmmapfile(test_file, content, false);
687        let read_content = get_contents(test_file).expect("file should exist");
688        assert_eq!(read_content, content);
689        let _ = fs::remove_file(test_file);
690    }
691
692    /// Verifies the empty-content fast path skips the mmap block but
693    /// still touches the file via `open(O_CREAT)`. C semantics:
694    /// mmap(len=0) returns MAP_FAILED so the inner block is bypassed,
695    /// but the open already created the file.
696    #[test]
697    fn empty_value_creates_file() {
698        let _g = crate::test_util::global_state_lock();
699        let test_file = "/tmp/zshrs_mapfile_test_empty.txt";
700        let _ = fs::remove_file(test_file);
701        setpmmapfile(test_file, "", false);
702        assert!(Path::new(test_file).exists());
703        let read_content = get_contents(test_file).expect("file should exist");
704        assert!(read_content.is_empty());
705        let _ = fs::remove_file(test_file);
706    }
707
708    /// Verifies the c:255 `zreaddir(dir, 1)` walk with `ignoredots=1`
709    /// — `.` / `..` never appear in the result, every value is `""`
710    /// (c:263).
711    #[test]
712    fn scanpmmapfile_skips_dotdirs_and_returns_empty_values() {
713        let _g = crate::test_util::global_state_lock();
714        use std::sync::Mutex;
715        static COLLECTED: Mutex<Vec<(String, String)>> = Mutex::new(Vec::new());
716        COLLECTED.lock().unwrap().clear();
717        fn cb(node: &crate::ported::zsh_h::HashNode, _flags: i32) {
718            COLLECTED
719                .lock()
720                .unwrap()
721                .push((node.nam.clone(), String::new()));
722        }
723        scanpmmapfile(std::ptr::null_mut(), Some(cb), 0);
724        let entries = COLLECTED.lock().unwrap().clone();
725        for (name, val) in &entries {
726            assert!(name != "." && name != "..");
727            assert!(
728                val.is_empty(),
729                "scanpmmapfile values are always empty per c:263"
730            );
731        }
732    }
733
734    /// Verifies the c:134 `unlink(fname)` write — the unset callback
735    /// removes the file when not readonly.
736    #[test]
737    fn unsetpmmapfile_removes_file() {
738        let _g = crate::test_util::global_state_lock();
739        let test_file = "/tmp/zshrs_mapfile_test_unset.txt";
740        let _ = fs::write(test_file, "content");
741        unsetpmmapfile(test_file, false);
742        assert!(!Path::new(test_file).exists());
743    }
744
745    /// Verifies the c:133 readonly guard: a readonly param's unset
746    /// callback skips the unlink.
747    #[test]
748    fn unsetpmmapfile_readonly_skips() {
749        let _g = crate::test_util::global_state_lock();
750        let test_file = "/tmp/zshrs_mapfile_test_unset_ro.txt";
751        let _ = fs::write(test_file, "content");
752        unsetpmmapfile(test_file, true);
753        assert!(Path::new(test_file).exists());
754        let _ = fs::remove_file(test_file);
755    }
756
757    /// Verifies the c:87 readonly guard on `setpmmapfile`: write is
758    /// skipped, file is not created.
759    #[test]
760    fn setpmmapfile_readonly_skips_write() {
761        let _g = crate::test_util::global_state_lock();
762        let test_file = "/tmp/zshrs_mapfile_test_set_ro.txt";
763        let _ = fs::remove_file(test_file);
764        setpmmapfile(test_file, "should not be written", true);
765        assert!(!Path::new(test_file).exists());
766    }
767
768    /// Verifies `setpmmapfiles` (the bulk-set callback) routes each
769    /// entry through `setpmmapfile` and respects the readonly guard
770    /// at c:149.
771    #[test]
772    fn setpmmapfiles_writes_entries() {
773        let _g = crate::test_util::global_state_lock();
774        let f1 = "/tmp/zshrs_mapfile_bulk_1.txt";
775        let f2 = "/tmp/zshrs_mapfile_bulk_2.txt";
776        let _ = fs::remove_file(f1);
777        let _ = fs::remove_file(f2);
778        let entries = vec![
779            (f1.to_string(), "one".to_string()),
780            (f2.to_string(), "two".to_string()),
781        ];
782        setpmmapfiles(&entries, false);
783        assert_eq!(get_contents(f1).as_deref(), Some("one"));
784        assert_eq!(get_contents(f2).as_deref(), Some("two"));
785        let _ = fs::remove_file(f1);
786        let _ = fs::remove_file(f2);
787    }
788
789    /// c:167 — `get_contents` on a nonexistent file returns None.
790    #[test]
791    fn get_contents_nonexistent_file_returns_none() {
792        let _g = crate::test_util::global_state_lock();
793        assert!(
794            get_contents("/__never_a_file__/x").is_none(),
795            "missing file must return None, not empty Some"
796        );
797    }
798
799    /// c:167 — `get_contents` on an EMPTY file returns Some("") —
800    /// distinguishes "exists but empty" from "missing". A regression
801    /// that conflates the two would break `${mapfile[/some/empty]}`
802    /// detection in user scripts.
803    #[test]
804    fn get_contents_empty_file_returns_empty_string() {
805        let _g = crate::test_util::global_state_lock();
806        let f = "/tmp/zshrs_mapfile_empty.txt";
807        let _ = fs::write(f, "");
808        let r = get_contents(f);
809        assert_eq!(
810            r.as_deref(),
811            Some(""),
812            "empty file must yield Some(\"\"), not None"
813        );
814        let _ = fs::remove_file(f);
815    }
816
817    /// c:167 — Round-trip: write then read back. Pin file content
818    /// fidelity (no encoding mangling, no trailing-newline insertion).
819    #[test]
820    fn get_contents_round_trips_write() {
821        let _g = crate::test_util::global_state_lock();
822        let f = "/tmp/zshrs_mapfile_rt.txt";
823        let payload = "line1\nline2\nno_trailing_nl";
824        setpmmapfile(f, payload, false);
825        let r = get_contents(f);
826        assert_eq!(
827            r.as_deref(),
828            Some(payload),
829            "round-trip must preserve content exactly"
830        );
831        let _ = fs::remove_file(f);
832    }
833
834    /// c:68 — `setpmmapfile` with empty value writes an EMPTY file
835    /// (a valid write, NOT a delete).
836    #[test]
837    fn setpmmapfile_empty_value_writes_empty_file() {
838        let _g = crate::test_util::global_state_lock();
839        let f = "/tmp/zshrs_mapfile_empty_write.txt";
840        let _ = fs::remove_file(f);
841        setpmmapfile(f, "", false);
842        assert!(
843            Path::new(f).exists(),
844            "empty value must still create the file"
845        );
846        assert_eq!(get_contents(f).as_deref(), Some(""));
847        let _ = fs::remove_file(f);
848    }
849
850    /// c:126 — `unsetpmmapfile` on a missing path is a safe no-op.
851    /// Pin defensive behavior; a regression that unwrap()s the
852    /// `remove_file` Result would crash the shell on every `unset
853    /// mapfile[/missing]` call.
854    #[test]
855    fn unsetpmmapfile_missing_file_is_safe_noop() {
856        let _g = crate::test_util::global_state_lock();
857        unsetpmmapfile("/__never_existed_zshrs_mapfile__", false);
858    }
859
860    /// c:241 — `scanpmmapfile` always emits empty string values per
861    /// c:263. Pin the empty-value contract because users iterating
862    /// `${(kv)mapfile}` rely on values being empty (and use
863    /// `${mapfile[/path]}` for content).
864    #[test]
865    fn scanpmmapfile_values_always_empty() {
866        let _g = crate::test_util::global_state_lock();
867        use std::sync::Mutex;
868        static VALS: Mutex<Vec<String>> = Mutex::new(Vec::new());
869        VALS.lock().unwrap().clear();
870        // The callback receives a HashNode; we don't have direct
871        // access to the Param's u_str through HashNode alone, but
872        // the scan body itself constructs Param.u_str = "" before
873        // calling func (c:263), so the contract is enforced at the
874        // call site. This test verifies scan runs to completion
875        // without panicking and yields some entries.
876        fn cb(node: &crate::ported::zsh_h::HashNode, _flags: i32) {
877            VALS.lock().unwrap().push(node.nam.clone());
878        }
879        scanpmmapfile(std::ptr::null_mut(), Some(cb), 0);
880        // No assertion on contents — the c:263 empty-value contract
881        // is structurally enforced by the function body, not the
882        // emitted HashNode shape.
883    }
884
885    /// c:279-310 — module-lifecycle stubs return 0.
886    #[test]
887    fn module_lifecycle_shims_all_return_zero() {
888        let _g = crate::test_util::global_state_lock();
889        let m: *const module = std::ptr::null();
890        assert_eq!(setup_(m), 0);
891        assert_eq!(boot_(m), 0);
892        assert_eq!(cleanup_(m), 0);
893        assert_eq!(finish_(m), 0);
894    }
895
896    // ─── zsh-corpus pins for get_contents ──────────────────────────
897
898    /// `get_contents` on existing file returns content.
899    #[test]
900    fn mapfile_corpus_get_contents_reads_file() {
901        let _g = crate::test_util::global_state_lock();
902        let dir = tempfile::tempdir().unwrap();
903        let p = dir.path().join("test.txt");
904        std::fs::write(&p, "hello world\n").unwrap();
905        let c = get_contents(p.to_str().unwrap());
906        assert_eq!(c.as_deref(), Some("hello world\n"));
907    }
908
909    /// `get_contents` on missing file returns None.
910    #[test]
911    fn mapfile_corpus_get_contents_missing_returns_none() {
912        let _g = crate::test_util::global_state_lock();
913        let c = get_contents("/nonexistent/zshrs_test_xyz_unique_abc");
914        assert!(c.is_none(), "missing file returns None");
915    }
916
917    /// `get_contents` on empty file returns Some("").
918    #[test]
919    fn mapfile_corpus_get_contents_empty_file_returns_empty_string() {
920        let _g = crate::test_util::global_state_lock();
921        let dir = tempfile::tempdir().unwrap();
922        let p = dir.path().join("empty.txt");
923        std::fs::File::create(&p).unwrap();
924        let c = get_contents(p.to_str().unwrap());
925        assert_eq!(c.as_deref(), Some(""), "empty file → empty string");
926    }
927
928    /// `get_contents` preserves UTF-8 content.
929    #[test]
930    fn mapfile_corpus_get_contents_preserves_multibyte() {
931        let _g = crate::test_util::global_state_lock();
932        let dir = tempfile::tempdir().unwrap();
933        let p = dir.path().join("utf8.txt");
934        std::fs::write(&p, "日本語").unwrap();
935        let c = get_contents(p.to_str().unwrap());
936        assert_eq!(c.as_deref(), Some("日本語"));
937    }
938
939    // ═══════════════════════════════════════════════════════════════════
940    // Additional C-parity tests for Src/Modules/mapfile.c.
941    // ═══════════════════════════════════════════════════════════════════
942
943    /// c:239 — `get_contents("")` returns None (empty path invalid).
944    #[test]
945    fn get_contents_empty_path_returns_none() {
946        let _g = crate::test_util::global_state_lock();
947        assert!(get_contents("").is_none());
948    }
949
950    /// c:239 — `get_contents` of directory no panic (read fails / returns
951    /// something).
952    #[test]
953    fn get_contents_directory_no_panic() {
954        let _g = crate::test_util::global_state_lock();
955        let _ = get_contents("/tmp");
956    }
957
958    /// c:239 — `get_contents` of a text file preserves byte content.
959    #[test]
960    fn get_contents_text_file_round_trip() {
961        let _g = crate::test_util::global_state_lock();
962        let dir = tempfile::tempdir().unwrap();
963        let p = dir.path().join("data.txt");
964        let content = "line1\nline2\nline3\n";
965        std::fs::write(&p, content).unwrap();
966        let r = get_contents(p.to_str().unwrap());
967        assert_eq!(r.as_deref(), Some(content));
968    }
969
970    /// c:329 — `getpmmapfile(_, "")` returns Option (Some PM_UNSET or
971    /// None per port). Pin no panic; the Rust port returns Some(Param)
972    /// with PM_UNSET flag for empty/missing keys, matching C's
973    /// "always return a Param node" convention.
974    #[test]
975    fn getpmmapfile_empty_name_no_panic() {
976        let _g = crate::test_util::global_state_lock();
977        let _ = getpmmapfile(std::ptr::null_mut(), "");
978    }
979
980    /// c:329 — `getpmmapfile` of nonexistent path no panic
981    /// (may return Some PM_UNSET).
982    #[test]
983    fn getpmmapfile_nonexistent_path_no_panic() {
984        let _g = crate::test_util::global_state_lock();
985        let _ = getpmmapfile(std::ptr::null_mut(), "/__never_exists_zshrs_xyz__");
986    }
987
988    /// c:181 — `unsetpmmapfile("never_set", true)` no panic.
989    #[test]
990    fn unsetpmmapfile_unset_param_no_panic() {
991        let _g = crate::test_util::global_state_lock();
992        unsetpmmapfile("zshrs_never_mapfile_xyz", true);
993    }
994
995    /// c:206 — `setpmmapfiles(&[], true)` empty entries no-op.
996    #[test]
997    fn setpmmapfiles_empty_entries_no_panic() {
998        let _g = crate::test_util::global_state_lock();
999        setpmmapfiles(&[], true);
1000    }
1001
1002    /// Lifecycle (c:453/476/485/492) split per-hook.
1003    #[test]
1004    fn mapfile_setup_returns_zero_pin() {
1005        let _g = crate::test_util::global_state_lock();
1006        assert_eq!(setup_(std::ptr::null()), 0);
1007    }
1008
1009    /// c:485 — cleanup_(NULL) = 0.
1010    #[test]
1011    fn mapfile_cleanup_returns_zero_pin() {
1012        let _g = crate::test_util::global_state_lock();
1013        assert_eq!(cleanup_(std::ptr::null()), 0);
1014    }
1015
1016    /// c:492 — finish_(NULL) = 0.
1017    #[test]
1018    fn mapfile_finish_returns_zero_pin() {
1019        let _g = crate::test_util::global_state_lock();
1020        assert_eq!(finish_(std::ptr::null()), 0);
1021    }
1022
1023    // ═══════════════════════════════════════════════════════════════════
1024    // Additional C-parity tests for Src/Modules/mapfile.c
1025    // c:239 get_contents / c:329 getpmmapfile / c:384 scanpmmapfile
1026    // c:206 setpmmapfiles / c:181 unsetpmmapfile / lifecycle
1027    // ═══════════════════════════════════════════════════════════════════
1028
1029    /// c:239 — `get_contents` is deterministic for missing path.
1030    #[test]
1031    fn get_contents_missing_is_deterministic() {
1032        let _g = crate::test_util::global_state_lock();
1033        let p = "/nonexistent_path_xyz_zshrs";
1034        let first = get_contents(p);
1035        for _ in 0..5 {
1036            assert_eq!(get_contents(p), first, "must be deterministic");
1037        }
1038    }
1039
1040    /// c:239 — `get_contents("/")` (a directory) returns None (not a regular file).
1041    #[test]
1042    fn get_contents_root_dir_returns_none() {
1043        let _g = crate::test_util::global_state_lock();
1044        assert_eq!(get_contents("/"), None, "/ is a dir, not regular file");
1045    }
1046
1047    /// c:239 — `get_contents` preserves multi-line content.
1048    #[test]
1049    fn get_contents_preserves_multiline_content() {
1050        let _g = crate::test_util::global_state_lock();
1051        let dir = tempfile::tempdir().unwrap();
1052        let p = dir.path().join("multi.txt");
1053        let body = "line1\nline2\nline3\n";
1054        std::fs::write(&p, body).unwrap();
1055        let got = get_contents(p.to_str().unwrap()).expect("file should read");
1056        assert_eq!(got, body, "multiline content preserved");
1057    }
1058
1059    /// c:329 — `getpmmapfile` empty name returns Some(PM_UNSET) per
1060    /// C "always Param node" convention.
1061    #[test]
1062    fn getpmmapfile_empty_name_returns_some_unset() {
1063        use crate::ported::zsh_h::PM_UNSET;
1064        let _g = crate::test_util::global_state_lock();
1065        let pm = getpmmapfile(std::ptr::null_mut(), "");
1066        if let Some(p) = pm {
1067            assert_ne!(
1068                p.node.flags & PM_UNSET as i32,
1069                0,
1070                "missing file → PM_UNSET set"
1071            );
1072        }
1073    }
1074
1075    /// c:181 — `unsetpmmapfile` on multiple absent params doesn't panic.
1076    #[test]
1077    fn unsetpmmapfile_many_absent_no_panic() {
1078        let _g = crate::test_util::global_state_lock();
1079        for name in ["abc", "xyz", "", "deeply/nested/missing/param"] {
1080            unsetpmmapfile(name, false);
1081            unsetpmmapfile(name, true);
1082        }
1083    }
1084
1085    /// c:206 — `setpmmapfiles` with readonly=true accepts but doesn't
1086    /// crash on empty entries.
1087    #[test]
1088    fn setpmmapfiles_readonly_empty_no_panic() {
1089        let _g = crate::test_util::global_state_lock();
1090        setpmmapfiles(&[], true);
1091        setpmmapfiles(&[], false);
1092    }
1093
1094    /// c:384 — `scanpmmapfile` with None callback is a safe no-op.
1095    #[test]
1096    fn scanpmmapfile_none_callback_no_panic() {
1097        let _g = crate::test_util::global_state_lock();
1098        scanpmmapfile(std::ptr::null_mut(), None, 0);
1099    }
1100
1101    /// c:239 — `get_contents` with extreme paths (very long) doesn't
1102    /// panic (returns None for unreachable paths).
1103    #[test]
1104    fn get_contents_very_long_path_no_panic() {
1105        let _g = crate::test_util::global_state_lock();
1106        let p = format!("/{}", "a".repeat(500));
1107        let _ = get_contents(&p);
1108    }
1109
1110    /// c:453-492 — full lifecycle setup→features→enables→boot→cleanup→finish.
1111    #[test]
1112    fn mapfile_full_lifecycle_returns_zero() {
1113        let _g = crate::test_util::global_state_lock();
1114        let null = std::ptr::null();
1115        assert_eq!(setup_(null), 0);
1116        let mut feats = Vec::new();
1117        let _ = features_(null, &mut feats);
1118        let mut enables: Option<Vec<i32>> = None;
1119        let _ = enables_(null, &mut enables);
1120        assert_eq!(boot_(null), 0);
1121        assert_eq!(cleanup_(null), 0);
1122        assert_eq!(finish_(null), 0);
1123    }
1124
1125    /// c:453 — setup_ idempotent.
1126    #[test]
1127    fn mapfile_setup_idempotent() {
1128        let _g = crate::test_util::global_state_lock();
1129        for _ in 0..10 {
1130            assert_eq!(setup_(std::ptr::null()), 0);
1131        }
1132    }
1133
1134    // ═══════════════════════════════════════════════════════════════════
1135    // Additional C-parity tests for Src/Modules/mapfile.c
1136    // c:52 setpmmapfile / c:181 unsetpmmapfile / c:206 setpmmapfiles /
1137    // c:239 get_contents / c:329 getpmmapfile / c:384 scanpmmapfile
1138    // ═══════════════════════════════════════════════════════════════════
1139
1140    /// c:239 — `get_contents` returns Option<String> (compile-time pin).
1141    #[test]
1142    fn get_contents_returns_option_string_type() {
1143        let _g = crate::test_util::global_state_lock();
1144        let _: Option<String> = get_contents("/tmp");
1145    }
1146
1147    /// c:239 — `get_contents("")` empty path returns None.
1148    #[test]
1149    fn get_contents_empty_returns_none() {
1150        let _g = crate::test_util::global_state_lock();
1151        assert!(get_contents("").is_none(), "empty path → None");
1152    }
1153
1154    /// c:329 — `getpmmapfile(null, "anything")` returns Option<Param>.
1155    #[test]
1156    fn getpmmapfile_returns_option_param_type() {
1157        use crate::ported::zsh_h::Param;
1158        let _g = crate::test_util::global_state_lock();
1159        let _: Option<Param> = getpmmapfile(std::ptr::null_mut(), "anything");
1160    }
1161
1162    /// c:52 — `setpmmapfile(name, val, readonly=true)` is safe with bool flag.
1163    #[test]
1164    fn setpmmapfile_readonly_true_no_panic() {
1165        let _g = crate::test_util::global_state_lock();
1166        setpmmapfile("__never_real_mapfile_test__", "/tmp/__nonexistent__", true);
1167    }
1168
1169    /// c:206 — `setpmmapfiles` empty entries no-panic + readonly variant.
1170    #[test]
1171    fn setpmmapfiles_empty_entries_both_flags_no_panic() {
1172        let _g = crate::test_util::global_state_lock();
1173        setpmmapfiles(&[], false);
1174        setpmmapfiles(&[], true);
1175    }
1176
1177    /// c:181 — `unsetpmmapfile(name, exp)` empty name + both exp values safe.
1178    #[test]
1179    fn unsetpmmapfile_empty_name_both_exp_safe() {
1180        let _g = crate::test_util::global_state_lock();
1181        unsetpmmapfile("", false);
1182        unsetpmmapfile("", true);
1183    }
1184
1185    /// c:239 — `get_contents` is deterministic for nonexistent path.
1186    #[test]
1187    fn get_contents_nonexistent_is_deterministic() {
1188        let _g = crate::test_util::global_state_lock();
1189        let p = "/__nonexistent_zshrs_mapfile_xyz__";
1190        let first = get_contents(p);
1191        for _ in 0..3 {
1192            assert_eq!(
1193                get_contents(p),
1194                first,
1195                "get_contents nonexistent must be deterministic"
1196            );
1197        }
1198    }
1199
1200    /// c:329 — `getpmmapfile(null, "")` empty name returns Some(PM_UNSET).
1201    /// C convention: always Param node (PM_UNSET for missing).
1202    #[test]
1203    fn getpmmapfile_empty_returns_some_with_pm_unset() {
1204        use crate::ported::zsh_h::PM_UNSET;
1205        let _g = crate::test_util::global_state_lock();
1206        let pm = getpmmapfile(std::ptr::null_mut(), "");
1207        if let Some(p) = pm {
1208            assert_ne!(
1209                p.node.flags & PM_UNSET as i32,
1210                0,
1211                "empty name → PM_UNSET bit set"
1212            );
1213        }
1214    }
1215
1216    /// c:384 — `scanpmmapfile(null, None, 0)` returns void.
1217    #[test]
1218    fn scanpmmapfile_none_callback_signature_void() {
1219        let _g = crate::test_util::global_state_lock();
1220        let _: () = scanpmmapfile(std::ptr::null_mut(), None, 0);
1221    }
1222
1223    /// c:453-492 — setup_/cleanup_/finish_ all return i32.
1224    #[test]
1225    fn mapfile_lifecycle_funcs_return_i32_type() {
1226        let _g = crate::test_util::global_state_lock();
1227        let _: i32 = setup_(std::ptr::null());
1228        let _: i32 = boot_(std::ptr::null());
1229        let _: i32 = cleanup_(std::ptr::null());
1230        let _: i32 = finish_(std::ptr::null());
1231    }
1232
1233    // ═══════════════════════════════════════════════════════════════════
1234    // Additional C-parity tests for Src/Modules/mapfile.c
1235    // c:52 setpmmapfile / c:181 unsetpmmapfile / c:206 setpmmapfiles /
1236    // c:239 get_contents / c:329 getpmmapfile / c:384 scanpmmapfile
1237    // ═══════════════════════════════════════════════════════════════════
1238
1239    /// c:239 — `get_contents` returns Option<String> (compile-time pin, alt).
1240    #[test]
1241    fn get_contents_returns_option_string_pin_alt() {
1242        let _g = crate::test_util::global_state_lock();
1243        let _: Option<String> = get_contents("/__nonexistent__");
1244    }
1245
1246    /// c:239 — `get_contents("")` empty path returns None (alt-name pin).
1247    #[test]
1248    fn get_contents_empty_path_returns_none_alt() {
1249        let _g = crate::test_util::global_state_lock();
1250        assert!(get_contents("").is_none(), "empty path → None");
1251    }
1252
1253    /// c:239 — `get_contents` for nonexistent path returns None (alt-name pin).
1254    #[test]
1255    fn get_contents_nonexistent_returns_none_alt() {
1256        let _g = crate::test_util::global_state_lock();
1257        assert!(get_contents("/__definitely_no_such_xyz_zshrs__").is_none());
1258    }
1259
1260    /// c:239 — `get_contents("/dev/null")` returns Some("") on POSIX
1261    /// (empty file readable).
1262    #[cfg(unix)]
1263    #[test]
1264    fn get_contents_dev_null_returns_some_empty() {
1265        let _g = crate::test_util::global_state_lock();
1266        let r = get_contents("/dev/null");
1267        assert_eq!(
1268            r.as_deref(),
1269            Some(""),
1270            "/dev/null must read as Some(empty); got {:?}",
1271            r
1272        );
1273    }
1274
1275    /// c:52 — `setpmmapfile` with empty name doesn't panic.
1276    #[test]
1277    fn setpmmapfile_empty_name_no_panic() {
1278        let _g = crate::test_util::global_state_lock();
1279        setpmmapfile("", "/tmp/__nonexistent__", false);
1280    }
1281
1282    /// c:52 — `setpmmapfile` with both readonly flag values is safe.
1283    ///
1284    /// First arg doubles as the mmap target filename (see
1285    /// setpmmapfile c:88 `open(name, O_RDWR|O_CREAT)`), so the path
1286    /// must live under a tempdir. The earlier `__test_mapfile_a__`
1287    /// form created the file in cwd — when `cargo test` ran from the
1288    /// repo root, that surfaced as an untracked stray in `git
1289    /// status`. Pre- AND post-cleanup so a flaky cancel doesn't leave
1290    /// residue and a stale residue from a prior run can't fool the
1291    /// open-with-O_CREAT into a no-op.
1292    #[test]
1293    fn setpmmapfile_both_readonly_flags_safe() {
1294        let _g = crate::test_util::global_state_lock();
1295        let dir = std::env::temp_dir();
1296        let path_a = dir.join("zshrs_test_mapfile_a");
1297        let path_b = dir.join("zshrs_test_mapfile_b");
1298        let _ = std::fs::remove_file(&path_a);
1299        let _ = std::fs::remove_file(&path_b);
1300        setpmmapfile(
1301            path_a.to_str().expect("tmp path utf8"),
1302            "/tmp/__nonexistent_a__",
1303            false,
1304        );
1305        setpmmapfile(
1306            path_b.to_str().expect("tmp path utf8"),
1307            "/tmp/__nonexistent_b__",
1308            true, // readonly: setpmmapfile early-returns, no file write
1309        );
1310        let _ = std::fs::remove_file(&path_a);
1311        let _ = std::fs::remove_file(&path_b);
1312    }
1313
1314    /// c:181 — `unsetpmmapfile` non-existent name + both exp values safe.
1315    #[test]
1316    fn unsetpmmapfile_nonexistent_name_safe() {
1317        let _g = crate::test_util::global_state_lock();
1318        unsetpmmapfile("__never_mapped_xyz__", false);
1319        unsetpmmapfile("__never_mapped_xyz__", true);
1320    }
1321
1322    /// c:329 — `getpmmapfile` is deterministic for the same name.
1323    #[test]
1324    fn getpmmapfile_deterministic_for_anything_name() {
1325        let _g = crate::test_util::global_state_lock();
1326        let a = getpmmapfile(std::ptr::null_mut(), "anything");
1327        let b = getpmmapfile(std::ptr::null_mut(), "anything");
1328        assert_eq!(
1329            a.is_some(),
1330            b.is_some(),
1331            "getpmmapfile must be deterministic"
1332        );
1333    }
1334
1335    /// c:384 — `scanpmmapfile` various flag values are safe.
1336    #[test]
1337    fn scanpmmapfile_various_flags_no_panic() {
1338        let _g = crate::test_util::global_state_lock();
1339        for flags in [0i32, 1, 2, 0xff, -1] {
1340            scanpmmapfile(std::ptr::null_mut(), None, flags);
1341        }
1342    }
1343
1344    /// c:461 — `features_` returns i32 (compile-time pin).
1345    #[test]
1346    fn mapfile_features_returns_i32_type() {
1347        let _g = crate::test_util::global_state_lock();
1348        let mut v: Vec<String> = Vec::new();
1349        let _: i32 = features_(std::ptr::null(), &mut v);
1350    }
1351
1352    /// c:469 — `enables_` returns i32 (compile-time pin).
1353    #[test]
1354    fn mapfile_enables_returns_i32_type() {
1355        let _g = crate::test_util::global_state_lock();
1356        let mut e: Option<Vec<i32>> = None;
1357        let _: i32 = enables_(std::ptr::null(), &mut e);
1358    }
1359
1360    /// c:453/461/469/476/485/492 — each lifecycle hook returns 0 individually.
1361    #[test]
1362    fn mapfile_each_lifecycle_hook_returns_zero_individually() {
1363        let _g = crate::test_util::global_state_lock();
1364        let null = std::ptr::null();
1365        let mut v: Vec<String> = Vec::new();
1366        let mut e: Option<Vec<i32>> = None;
1367        assert_eq!(setup_(null), 0, "c:453 setup_");
1368        assert_eq!(features_(null, &mut v), 0, "c:461 features_");
1369        assert_eq!(enables_(null, &mut e), 0, "c:469 enables_");
1370        assert_eq!(boot_(null), 0, "c:476 boot_");
1371        assert_eq!(cleanup_(null), 0, "c:485 cleanup_");
1372        assert_eq!(finish_(null), 0, "c:492 finish_");
1373    }
1374}