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}