Skip to main content

zsh/ported/modules/
random.rs

1//! Random number module - port of Modules/random.c
2//!
3//! Provides access to kernel random sources for cryptographically secure
4//! random number generation.
5
6use std::fs::metadata;
7use std::io;
8use std::io::Read;
9use std::os::fd::IntoRawFd;
10use std::os::unix::fs::FileTypeExt;
11use std::sync::atomic::Ordering;
12
13use crate::ported::utils::zwarn;
14use crate::random_real::random_real;
15use crate::zsh_h::{features, module};
16use std::sync::{Mutex, OnceLock};
17
18/// Fill a buffer with cryptographically random bytes.
19/// Port of `getrandom_buffer(void *buf, size_t len)` from Src/Modules/random.c:62 — the
20/// C source dispatches to `getentropy(3)` on BSD, `getrandom(2)` on
21/// Linux, or `/dev/urandom` as a portable fallback. We map onto
22/// `arc4random_buf(3)` for macOS (BSD-derived), `getrandom(2)` on
23/// Linux, and `/dev/urandom` everywhere else.
24#[cfg(target_os = "macos")]
25/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len)
26pub fn getrandom_buffer(buf: &mut [u8]) -> io::Result<()> {
27    // c:62
28    unsafe {
29        libc::arc4random_buf(buf.as_mut_ptr() as *mut libc::c_void, buf.len());
30    }
31    Ok(())
32}
33
34// Per-evaluator random-buffer state — bucket-1 dissolution per
35// C source has TWO file-statics at Src/Modules/random.c:50-51:
36//
37//     static uint32_t rand_buff[8];
38//     static int      buf_cnt = -1;
39//
40// Mirrored as two `thread_local!`s — each worker thread owns its
41// own buffer (file-static semantics preserve under threading per
42// PORT_PLAN bucket-1 rule).
43
44thread_local! {
45    /// Port of file-static `static uint32_t rand_buff[8];` at
46    /// `Src/Modules/random.c:50`. Pre-loaded buffer of u32s
47    /// drained one entry at a time by `get_srandom()`.
48    static RAND_BUFF: std::cell::RefCell<[u32; RAND_BUFF_SIZE]> = const {
49        std::cell::RefCell::new([0; RAND_BUFF_SIZE])
50    };
51    /// Port of file-static `static int buf_cnt = -1;` at
52    /// `Src/Modules/random.c:51`. Index of the next unread entry
53    /// in `RAND_BUFF`; zero triggers a refill via
54    /// `getrandom_buffer`.
55    static BUF_CNT: std::cell::Cell<usize> = const {
56        std::cell::Cell::new(0)
57    };
58}
59
60/// Port of `getrandom_buffer(void *buf, size_t len)` from `Src/Modules/random.c:62`,
61/// `#elif defined(HAVE_GETRANDOM)` branch (c:75-76):
62/// `ret = getrandom(bufptr, (len - val), 0);`
63/// with the C EINTR-retry loop at c:80-85.
64#[cfg(target_os = "linux")]
65/// WARNING: param names don't match C — Rust=(buf) vs C=(buf, len)
66pub fn getrandom_buffer(buf: &mut [u8]) -> io::Result<()> {
67    // c:62
68    let mut filled = 0;
69
70    while filled < buf.len() {
71        let ret = unsafe {
72            libc::getrandom(
73                buf[filled..].as_mut_ptr() as *mut libc::c_void,
74                buf.len() - filled,
75                0,
76            )
77        };
78
79        if ret < 0 {
80            let err = io::Error::last_os_error();
81            if err.kind() == io::ErrorKind::Interrupted {
82                continue;
83            }
84            return Err(err);
85        }
86
87        filled += ret as usize;
88    }
89
90    Ok(())
91}
92
93/// Port of `getrandom_buffer(void *buf, size_t len)` from `Src/Modules/random.c:62`.
94#[cfg(not(any(target_os = "macos", target_os = "linux")))]
95pub fn getrandom_buffer(m: &mut [u8]) -> io::Result<()> {
96    // c:62
97
98    let mut file = File::open("/dev/urandom")?;
99    file.read_exact(m)?;
100    Ok(())
101}
102
103/// Port of `void get_bound_random_buffer(uint32_t *buffer, size_t count,
104/// uint32_t max)` from `Src/Modules/random.c:104`. Lemire (2016)
105/// fast-random-shuffling: multiply, threshold = -max % max, rejection-
106/// sample only the rare `leftover < max` slot. `count` is folded into
107/// `buffer.len()` per Rust idiom.
108/// WARNING: param names don't match C — Rust=(buffer, max) vs C=(buffer, count, max)
109pub fn get_bound_random_buffer(buffer: &mut [u32], max: u32) {
110    // c:104
111    // c:112 getrandom_buffer(buffer, count*sizeof(uint32_t)) — fill u32s.
112    let mut bytes: Vec<u8> = vec![0u8; buffer.len() * 4];
113    let _ = getrandom_buffer(&mut bytes);
114    for (i, chunk) in bytes.chunks_exact(4).enumerate() {
115        buffer[i] = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
116    }
117    if max == u32::MAX {
118        // c:113 UINT32_MAX
119        return; // c:114
120    }
121    for i in 0..buffer.len() {
122        // c:116
123        let mut multi_result: u64 = (buffer[i] as u64) * (max as u64); // c:117
124        let mut leftover: u32 = multi_result as u32; // c:118
125        if leftover < max {
126            // c:124
127            let threshold: u32 = (max.wrapping_neg()) % max; // c:125 -max % max
128            while leftover < threshold {
129                // c:126
130                let j: u32 = get_srandom(); // c:127 get_srandom(NULL)
131                multi_result = (j as u64) * (max as u64); // c:128
132                leftover = multi_result as u32; // c:129
133            }
134        }
135        buffer[i] = (multi_result >> 32) as u32; // c:132
136    }
137}
138
139/// Port of `get_srandom(UNUSED(Param pm))` from `Src/Modules/random.c:58`. The
140/// `getfn` slot the C source wires for the `$SRANDOM` special
141/// parameter. Refills `rand_buff` via `getrandom_buffer()` when
142/// drained, then returns the next pre-loaded u32.
143/// WARNING: param names don't match C — Rust=() vs C=(pm)
144pub fn get_srandom() -> u32 {
145    // c:58
146    let cnt = BUF_CNT.with(|c| c.get());
147    if cnt == 0 {
148        // c:145
149        let mut bytes = [0u8; RAND_BUFF_SIZE * 4]; // c:143
150        if getrandom_buffer(&mut bytes).is_ok() {
151            // c:143
152            RAND_BUFF.with(|r| {
153                let mut buf = r.borrow_mut();
154                for (i, chunk) in bytes.chunks_exact(4).enumerate() {
155                    // c:143
156                    buf[i] = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
157                }
158            });
159        }
160        BUF_CNT.with(|c| c.set(RAND_BUFF_SIZE)); // c:145
161    }
162    let new_cnt = BUF_CNT.with(|c| c.get()) - 1; // c:145
163    BUF_CNT.with(|c| c.set(new_cnt));
164    RAND_BUFF.with(|r| r.borrow()[new_cnt]) // c:145
165}
166
167/// Port of `math_zrand_int(UNUSED(char *name), int argc, mnumber *argv, UNUSED(int id))` from Src/Modules/random.c:161 — the
168/// `zrand_int([upper [, lower [, inclusive]]])` math function.
169pub fn math_zrand_int(
170    _name: &str,
171    argc: i32,
172    argv: &[crate::ported::zsh_h::mnumber],
173    _id: i32,
174) -> crate::ported::zsh_h::mnumber {
175    // c:161
176    let mut ret = crate::ported::zsh_h::mnumber {
177        l: 0,
178        d: 0.0,
179        type_: crate::ported::zsh_h::MN_INTEGER, // c:168
180    };
181    // c:170-177 — `switch (argc)`, falling through from the most
182    // arguments to the fewest.
183    if argc == 0 {
184        ret.l = get_srandom() as i64; // c:171
185        return ret; // c:172
186    }
187    let inclusive = argc >= 3 && argv[2].l != 0; // c:173
188    let lower: i64 = if argc >= 2 { argv[1].l } else { 0 }; // c:174
189    let upper: i64 = argv[0].l; // c:175
190
191    // c:179-185 — bound checks are a WARN-ONLY if/else-if chain:
192    //
193    //     if (lower < 0 || lower >= UINT32_MAX) {
194    //         zwarn("Lower bound (%z) out of range: 0-4294967295",lower);
195    //     } else if (upper < lower) {
196    //         zwarn("Upper bound (%z) must be greater than Lower Bound (%z)",upper,lower);
197    //     } else if (upper < 0 || upper >= UINT32_MAX) {
198    //         zwarn("Upper bound (%z) out of range: 0-4294967295",upper);
199    //     }
200    //
201    //     if ( diff == 0 ) {
202    //         ret.u.l=upper; /* still not convinced this shouldn't be an error. */
203    //     } else {
204    //         get_bound_random_buffer(&i,1,(uint32_t) diff);
205    //         ret.u.l=i+lower;
206    //     }
207    //
208    // No return after any warning — C falls through to the diff
209    // computation and random draw, with `(uint32_t) diff` truncating
210    // wrapped values exactly as written (the c:188 comment shows the
211    // author knew and shipped it anyway). At most ONE warning fires
212    // (else-if chain). The prior Rust port aborted with Err on each
213    // check: `zrand_int(5, 10)` (upper<lower) errored out where zsh
214    // warns once and still yields a number from the wrapped range.
215    let incl: i64 = if inclusive { 1 } else { 0 }; // c:173
216    let diff: i64 = upper - lower + incl; // c:176
217
218    if lower < 0 || lower >= u32::MAX as i64 {
219        // c:179
220        crate::ported::utils::zwarn(&format!(
221            "Lower bound ({}) out of range: 0-4294967295",
222            lower
223        )); // c:180
224    } else if upper < lower {
225        // c:181
226        crate::ported::utils::zwarn(&format!(
227            "Upper bound ({}) must be greater than Lower Bound ({})",
228            upper, lower
229        )); // c:182
230    } else if upper < 0 || upper >= u32::MAX as i64 {
231        // c:183
232        crate::ported::utils::zwarn(&format!(
233            "Upper bound ({}) out of range: 0-4294967295",
234            upper
235        )); // c:184
236    }
237
238    if diff == 0 {
239        // c:187
240        /* still not convinced this shouldn't be an error. */
241        ret.l = upper; // c:188
242        return ret;
243    }
244    // c:190 — `get_bound_random_buffer(&i,1,(uint32_t) diff);` — the
245    // cast truncates exactly like C for warned out-of-range inputs.
246    let r = bounded(diff as u32);
247    ret.l = r as i64 + lower; // c:191
248    ret // c:193
249}
250
251/// `math_zrand_float()` math function.
252/// Port of `math_zrand_float(UNUSED(char *name), UNUSED(int argc), UNUSED(mnumber *argv), UNUSED(int id))` from Src/Modules/random.c:204 —
253/// the C source's math-function entry point that returns a
254/// uniform double in `[0, 1)`.
255///
256/// C body (verbatim):
257///   r = random_real();
258///   if (r < 0) {
259///       zwarnnam(name, "Failed to get sufficient random data.");
260///   }
261///   ret.type = MN_FLOAT;
262///   ret.u.d = r;
263///   return ret;
264///
265/// `random_real` returns -1 (via the thread_local sentinel set in
266/// random_real.rs) when the entropy syscall fails. C warns then still
267/// returns -1 as the math-function result; prior Rust port skipped
268/// the warn so callers had no signal that `[[ $((zrand_float())) ]]`
269/// produced a sentinel instead of a real probability.
270pub fn math_zrand_float(
271    name: &str,
272    _argc: i32,
273    _argv: &[crate::ported::zsh_h::mnumber],
274    _id: i32,
275) -> crate::ported::zsh_h::mnumber {
276    // c:204
277    let r = random_real(); // c:210
278    if r < 0.0 {
279        // c:211
280        crate::ported::utils::zwarnnam(name, "Failed to get sufficient random data."); // c:212
281    }
282    crate::ported::zsh_h::mnumber {
283        l: 0,
284        d: r,                                   // c:214
285        type_: crate::ported::zsh_h::MN_FLOAT, // c:213
286    }
287}
288
289/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/random.c:243`.
290#[allow(unused_variables)]
291pub fn setup_(m: *const module) -> i32 {
292    // c:243
293    // c:243-261 — USE_URANDOM block: stat /dev/urandom; verify
294    //              S_ISCHR. We probe via std::fs::metadata + file_type().
295    match metadata("/dev/urandom") {
296        // c:251
297        Ok(md) => {
298            if !md.file_type().is_char_device() {
299                // c:256
300                // c:257 — `zwarn("Error getting kernel random pool: %m");`
301                zwarn("Error getting kernel random pool: not a char device");
302                return 1;
303            }
304        }
305        Err(e) => {
306            zwarn(&format!("Error getting kernel random pool: {}", e));
307            return 1;
308        }
309    }
310    0 // c:275
311}
312
313/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/random.c:267`.
314pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
315    // c:267
316    *features = featuresarray(m, module_features());
317    0
318}
319
320/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/random.c:275`.
321pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
322    // c:275
323    handlefeatures(m, module_features(), enables)
324}
325
326/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/random.c:282-308`.
327#[allow(unused_variables)]
328pub fn boot_(m: *const module) -> i32 {
329    // c:282
330    // c:296 — `if ((tmpfd = open("/dev/urandom", O_RDONLY)) < 0)`
331    let f = match std::fs::OpenOptions::new().read(true).open("/dev/urandom") {
332        Ok(f) => f,
333        Err(e) => {
334            // c:297 — `zwarn("Could not access kernel random pool: %e.", errno);`
335            zwarn(&format!("Could not access kernel random pool: {}", e));
336            return 1; // c:298
337        }
338    };
339    // c:300 — `randfd = movefd(tmpfd);` — relocate to a high fd so the
340    // urandom handle doesn't clash with shell-side fd 3-9 use. Prior
341    // port skipped movefd, so randfd typically landed at fd 3 or 4 —
342    // colliding with redirect-save slots from `exec 3>file` style
343    // user commands. The shell's redirect machinery would then
344    // dup-over the urandom fd, leaving random_real reading from
345    // whatever the user redirected.
346    let tmpfd = f.into_raw_fd(); // c:293 `int tmpfd = -1`
347    let fd = crate::ported::utils::movefd(tmpfd);
348    // c:301 — `addmodulefd(randfd, FDT_MODULE);` — register the urandom
349    // fd in the global fdtable as FDT_MODULE so closem and exec
350    // redirect-save recognize it as module-owned. Same fix as the
351    // db_gdbm addmodulefd port (c0dad2eb83) but with FDT_MODULE
352    // (matches C: random.c uses FDT_MODULE, db_gdbm.c uses FDT_INTERNAL).
353    crate::ported::utils::addmodulefd(fd, crate::ported::zsh_h::FDT_MODULE);
354    // c:302-305 — `if (randfd < 0) { zwarn(...); return 1; }` — movefd
355    // failure (out of fd slots). Rust movefd returns -1 on failure
356    // matching C.
357    if fd < 0 {
358        zwarn("Could not access kernel random pool.");
359        return 1;
360    }
361    RANDFD.store(fd, Ordering::SeqCst);
362    0 // c:307
363}
364
365/// Re-export of the canonical `random_real()` from
366/// `Src/Modules/random_real.c:147` — Campbell's algorithm for
367/// distribution-correct uniform doubles in `[0, 1)`. The simpler
368/// "53-bit mantissa" approximation that previously lived here was
369/// removed because it biases ~3% of the interval; the C author
370/// (Taylor R. Campbell) explicitly warns against it in the random_real.c
371/// header comment.
372
373/// Generate a random integer in `[min, max]`.
374// =====================================================================
375// static struct features module_features                            c:255 (random.c)
376// =====================================================================
377
378/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/random.c:312`.
379pub fn cleanup_(m: *const module) -> i32 {
380    // c:312
381    setfeatureenables(m, module_features(), None)
382}
383
384/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/random.c:319-326`.
385#[allow(unused_variables)]
386pub fn finish_(m: *const module) -> i32 {
387    // c:319
388    // c:322-323 — `if (randfd >= 0) zclose(randfd);`
389    let fd = RANDFD.swap(-1, Ordering::SeqCst);
390    if fd >= 0 {
391        // Clear the fdtable entry BEFORE close so the post-close
392        // kernel-reuse of this fd number doesn't inherit the
393        // FDT_MODULE marker we set in boot_ (port c0dad2eb83 +
394        // c3a5125d9f pattern). C's zclose internally calls
395        // fdtable_set(fd, FDT_UNUSED) so the canonical path is safe;
396        // the raw libc::close below skips that step.
397        crate::ported::utils::fdtable_set(fd, crate::ported::zsh_h::FDT_UNUSED);
398        unsafe { libc::close(fd) }; // c:323 zclose
399    }
400    0 // c:325
401}
402
403/// Buffer size for pre-loading random integers
404// buffer to pre-load integers for SRANDOM to lessen the context switches  // c:49
405const RAND_BUFF_SIZE: usize = 8;
406
407// `mftab` — port of `static struct mathfunc mftab[]` (random.c).
408
409// `patab` — port of `static struct paramdef patab[]` (random.c).
410
411// `module_features` — port of `static struct features module_features`
412// from random.c:255.
413
414/// `RANDFD` — port of the file-static `int randfd` in
415/// `Src/Modules/random.c:243`. Holds the open fd for `/dev/urandom`.
416/// Set in `boot_()`, closed in `finish_()`.
417pub static RANDFD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1); // c:34
418
419// WARNING: NOT IN RANDOM.C — Rust-only convenience helpers.
420// C inlines the equivalent logic inside `get_bound_random_buffer()`
421// (Src/Modules/random.c:104) and the math-fn wrappers; the Rust
422// port factors them out because callers in this same file (the
423// Fisher-Yates shuffle in `bin_zshuffle`, the math ported
424// `math_zrand_int`/`math_zrand_real`, the `get_bound_random_buffer`
425// loop) would each inline identical 4-line getrandom-and-decode
426// blocks. Names are Rust-original; renaming to match a C name
427// would mislead.
428
429/// WARNING: NOT IN RANDOM.C — one-shot u32 helper; C inlines the 4-byte getrandom_buffer read
430/// (equivalent C logic at Src/Modules/random.c:79).
431/// One-shot u32 read. C inlines the equivalent at random.c:79
432/// (4-byte getrandom_buffer + decode).
433pub fn random_u32() -> u32 {
434    let mut buf = [0u8; 4];
435    let _ = getrandom_buffer(&mut buf);
436    u32::from_ne_bytes(buf)
437}
438
439/// WARNING: NOT IN RANDOM.C — two-word helper for random_real(); C inlines the 8-byte read
440/// (equivalent C logic at Src/Modules/random_real.c:158).
441/// Two-word read used by `random_real()` at random_real.c:158-175
442/// for uniform-real sampling. C reads 8 bytes inline.
443pub fn random_u64() -> u64 {
444    let mut buf = [0u8; 8];
445    let _ = getrandom_buffer(&mut buf);
446    u64::from_ne_bytes(buf)
447}
448
449/// Get a random integer in `[0, max)` using Lemire's unbiased
450/// rejection. Port of the inline bound-rejection logic inside
451/// `get_bound_random_buffer()` (random.c:104) — extracted as a
452/// per-element scalar helper since multiple callers need the
453/// single-value form.
454pub fn bounded(max: u32) -> u32 {
455    if max == 0 {
456        return 0;
457    }
458    if max == u32::MAX {
459        return random_u32();
460    }
461    let mut x = random_u32();
462    let mut m = (x as u64) * (max as u64);
463    let mut l = m as u32;
464    if l < max {
465        let threshold = (-(max as i64) as u64 % max as u64) as u32;
466        while l < threshold {
467            x = random_u32();
468            m = (x as u64) * (max as u64);
469            l = m as u32;
470        }
471    }
472    (m >> 32) as u32
473}
474
475static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
476
477// Local stubs for the per-module entry points. C uses generic
478// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
479// 3275/3370/3445) but those take `Builtin` + `Features` pointer
480// fields the Rust port doesn't carry. The hardcoded descriptor
481// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
482// WARNING: NOT IN RANDOM.C — Rust-only module-framework shim.
483// C uses generic featuresarray/handlefeatures/setfeatureenables from
484// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
485// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
486fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
487    vec![
488        "f:zrand_float".to_string(),
489        "f:zrand_int".to_string(),
490        "p:SRANDOM".to_string(),
491    ]
492}
493
494// WARNING: NOT IN RANDOM.C — Rust-only module-framework shim.
495// C uses generic featuresarray/handlefeatures/setfeatureenables from
496// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
497// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
498fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
499    // c:3394-3395 — `if (!enables || *enables) return
500    // setfeatureenables(m, f, enables ? *enables : NULL);`
501    if let Some(e) = enables.as_ref() {
502        let e = e.clone();
503        return setfeatureenables(m, f, Some(&e));
504    }
505    // c:3396 — `*enables = getfeatureenables(m, f);` read from the
506    // name-keyed ADDED ledger in src/ported/module.rs (this module ships
507    // no `Features` descriptor tables for the bit to live on; see
508    // MODULE_FEATURE_ENABLES there).
509    crate::ported::module::handlefeatures("zsh/random", &featuresarray(m, f), enables)
510}
511
512// WARNING: NOT IN RANDOM.C — Rust-only module-framework shim.
513// C uses generic featuresarray/handlefeatures/setfeatureenables from
514// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
515// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
516fn setfeatureenables(m: *const module, f: &Mutex<features>, e: Option<&[i32]>) -> i32 {
517    // c:3354-3382 — walk bn/cd/mf/pd in that order, each block taking its
518    // slice of the positional enables bitmap; `e == NULL` disables
519    // everything. random.c has no builtins or conditions (c:233-238).
520    let mut ret = crate::ported::module::setfeatureenables("zsh/random", &featuresarray(m, f), e);
521    let (mf_size, pd_size) = {
522        let g = f.lock().unwrap();
523        (g.mf_size as usize, g.pd_size as usize)
524    };
525    let block = |start: usize, len: usize| -> Option<Vec<i32>> {
526        e.map(|a| (0..len).map(|i| a.get(start + i).copied().unwrap_or(0)).collect())
527    };
528    // c:3368-3372 — setmathfuncs over `mftab` (c:228-231).
529    let mf_e = block(0, mf_size);
530    let mftab = MFTAB.get_or_init(|| {
531        Mutex::new(vec![
532            crate::ported::zsh_h::NUMMATHFUNC("zrand_float", math_zrand_float, 0, 0, 0), // c:229
533            crate::ported::zsh_h::NUMMATHFUNC("zrand_int", math_zrand_int, 0, 3, 0),    // c:230
534        ])
535    });
536    if crate::ported::module::setmathfuncs("zsh/random", &mut mftab.lock().unwrap(), mf_e.as_deref()) != 0 {
537        ret = 1; // c:3370
538    }
539    // c:3373-3378 — setparamdefs over `patab` (c:222-225).
540    let pd_e = block(mf_size, pd_size);
541    let patab = PATAB.get_or_init(|| {
542        Mutex::new(vec![crate::ported::zsh_h::PARAMDEF(
543            "SRANDOM", // c:223
544            (crate::ported::zsh_h::PM_INTEGER
545                | crate::ported::zsh_h::PM_READONLY_SPECIAL
546                | crate::ported::zsh_h::PM_HIDEVAL) as i32,
547            0,
548            &SRANDOM_GSU as *const crate::ported::zsh_h::gsu_integer as usize, // c:224
549        )])
550    });
551    if crate::ported::module::setparamdefs("zsh/random", &mut patab.lock().unwrap(), pd_e.as_deref()) != 0 {
552        ret = 1; // c:3377
553    }
554    ret // c:3382
555}
556
557/// `static const struct gsu_integer srandom_gsu` (random.c:219-220).
558static SRANDOM_GSU: crate::ported::zsh_h::gsu_integer = crate::ported::zsh_h::gsu_integer {
559    // c:219 `get_srandom` — its `Param` argument is unused.
560    getfn: |_pm| get_srandom() as i64,
561    setfn: crate::ported::params::nullintsetfn,
562    unsetfn: crate::ported::params::stdunsetfn,
563};
564
565
566/// `static struct mathfunc mftab[]` (random.c:228-231); the per-row
567/// `MFF_ADDED` bit setmathfuncs keeps lives here, as on C's static table.
568static MFTAB: OnceLock<Mutex<Vec<crate::ported::zsh_h::mathfunc>>> = OnceLock::new();
569
570/// `static struct paramdef patab[]` (random.c:222-225); `d->pm` is the
571/// per-parameter enable bit setparamdefs keeps.
572static PATAB: OnceLock<Mutex<Vec<crate::ported::zsh_h::paramdef>>> = OnceLock::new();
573
574
575// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
576// ─── RUST-ONLY ACCESSORS ───
577//
578// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
579// RwLock<T>>` globals declared above. C zsh uses direct global
580// access; Rust needs these wrappers because `OnceLock::get_or_init`
581// is the only way to lazily construct shared state. These ported sit
582// here so the body of this file reads in C source order without
583// the accessor wrappers interleaved between real port ported.
584// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
585
586// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
587// ─── RUST-ONLY ACCESSORS ───
588//
589// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
590// RwLock<T>>` globals declared above. C zsh uses direct global
591// access; Rust needs these wrappers because `OnceLock::get_or_init`
592// is the only way to lazily construct shared state. These ported sit
593// here so the body of this file reads in C source order without
594// the accessor wrappers interleaved between real port ported.
595// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
596
597// WARNING: NOT IN RANDOM.C — Rust-only module-framework shim.
598// C uses generic featuresarray/handlefeatures/setfeatureenables from
599// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
600// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
601fn module_features() -> &'static Mutex<features> {
602    MODULE_FEATURES.get_or_init(|| {
603        Mutex::new(features {
604            bn_list: None,
605            bn_size: 0,
606            cd_list: None,
607            cd_size: 0,
608            mf_list: None,
609            mf_size: 2,
610            pd_list: None,
611            pd_size: 1,
612            n_abstract: 0,
613        })
614    })
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620
621    /// `zrand_int(upper, lower, inclusive)` through the mathfunc entry.
622    fn zri(upper: Option<i64>, lower: Option<i64>, incl: bool) -> Result<i64, String> {
623        let mk = |l: i64| crate::ported::zsh_h::mnumber {
624            l,
625            d: 0.0,
626            type_: crate::ported::zsh_h::MN_INTEGER,
627        };
628        let argv = [mk(upper.unwrap_or(u32::MAX as i64)), mk(lower.unwrap_or(0)), mk(incl as i64)];
629        Ok(math_zrand_int("zrand_int", 3, &argv, 0).l)
630    }
631
632    /// `zrand_float()` through the mathfunc entry.
633    fn zrf() -> f64 {
634        math_zrand_float("zrand_float", 0, &[], 0).d
635    }
636
637    #[test]
638    fn test_random_state() {
639        let _g = crate::test_util::global_state_lock();
640
641        let r1 = get_srandom();
642        let r2 = get_srandom();
643        let r3 = get_srandom();
644        assert!(r1 != r2 || r2 != r3);
645    }
646
647    #[test]
648    fn test_get_random_u32() {
649        let _g = crate::test_util::global_state_lock();
650        let r1 = random_u32();
651        let r2 = random_u32();
652        let r3 = random_u32();
653        assert!(r1 != r2 || r2 != r3);
654    }
655
656    #[test]
657    fn test_get_random_u64() {
658        let _g = crate::test_util::global_state_lock();
659        let r1 = random_u64();
660        let r2 = random_u64();
661        assert_ne!(r1, r2);
662    }
663
664    #[test]
665    fn test_bounded_random() {
666        let _g = crate::test_util::global_state_lock();
667        for _ in 0..100 {
668            let r = bounded(10);
669            assert!(r < 10);
670        }
671    }
672
673    #[test]
674    fn test_bounded_random_one() {
675        let _g = crate::test_util::global_state_lock();
676        for _ in 0..10 {
677            let r = bounded(1);
678            assert_eq!(r, 0);
679        }
680    }
681
682    #[test]
683    fn test_zrand_int() {
684        let _g = crate::test_util::global_state_lock();
685        let r = zri(Some(100), Some(50), false).unwrap();
686        assert!((50..100).contains(&r));
687
688        let r = zri(Some(100), Some(50), true).unwrap();
689        assert!((50..=100).contains(&r));
690    }
691
692    #[test]
693    fn test_zrand_int_no_args() {
694        let _g = crate::test_util::global_state_lock();
695        let r = zri(None, None, false).unwrap();
696        assert!(r >= 0);
697    }
698
699    #[test]
700    fn test_zrand_int_bad_bounds_warn_and_continue() {
701        let _g = crate::test_util::global_state_lock();
702        // c:179-185 — bound violations WARN (zwarn, no return); C
703        // falls through to the (uint32_t)diff truncating draw, so the
704        // call still yields a value.
705        assert!(zri(Some(50), Some(100), false).is_ok());
706        assert!(zri(Some(-1), None, false).is_ok());
707    }
708
709    #[test]
710    fn test_zrand_float() {
711        let _g = crate::test_util::global_state_lock();
712        for _ in 0..100 {
713            let r = zrf();
714            assert!((0.0..1.0).contains(&r));
715        }
716    }
717
718    #[test]
719    fn test_random_real() {
720        let _g = crate::test_util::global_state_lock();
721        for _ in 0..100 {
722            let r = random_real();
723            assert!((0.0..1.0).contains(&r));
724        }
725    }
726
727    #[test]
728    fn test_shuffle() {
729        let _g = crate::test_util::global_state_lock();
730        // Fisher–Yates shuffle, inlined here since the helper is gone.
731        let mut arr = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
732        let original = arr.clone();
733        let n = arr.len();
734        for i in (1..n).rev() {
735            let j = bounded((i + 1) as u32) as usize;
736            arr.swap(i, j);
737        }
738        arr.sort();
739        assert_eq!(arr, original.to_vec());
740    }
741
742    #[test]
743    fn test_fill_random_bytes() {
744        let _g = crate::test_util::global_state_lock();
745        let mut buf = [0u8; 32];
746        getrandom_buffer(&mut buf).unwrap();
747        assert!(!buf.iter().all(|&b| b == 0));
748    }
749
750    /// c:161 — `zri(upper, lower, inclusive=true)` returns
751    /// values in `[lower, upper]`. 50 iterations to verify EVERY
752    /// returned value lies in range — regression returning out-of-
753    /// bounds values would silently corrupt arithmetic-driven scripts.
754    #[test]
755    fn math_zrand_int_inclusive_range_respects_bounds() {
756        let _g = crate::test_util::global_state_lock();
757        for _ in 0..50 {
758            let v = zri(Some(10), Some(5), true).unwrap();
759            assert!(
760                (5..=10).contains(&v),
761                "value {v} out of inclusive range [5, 10]"
762            );
763        }
764    }
765
766    /// c:161 — `inclusive=false` excludes upper bound. `random_int(0,10)`
767    /// must NEVER return 10 in this mode.
768    #[test]
769    fn math_zrand_int_exclusive_excludes_upper_bound() {
770        let _g = crate::test_util::global_state_lock();
771        for _ in 0..50 {
772            let v = zri(Some(10), Some(5), false).unwrap();
773            assert!(
774                (5..10).contains(&v),
775                "value {v} out of exclusive range [5, 10)"
776            );
777        }
778    }
779
780    /// c:204 — `math_zrand_float` returns a float in `[0.0, 1.0)`.
781    /// Regression returning negative or >=1.0 would break PRNGs users
782    /// layer on top.
783    #[test]
784    fn math_zrand_float_in_unit_interval() {
785        let _g = crate::test_util::global_state_lock();
786        for _ in 0..50 {
787            let v = zrf();
788            assert!(
789                (0.0..1.0).contains(&v),
790                "value {v} out of unit interval [0.0, 1.0)"
791            );
792        }
793    }
794
795    /// `getrandom_buffer` produces different output on successive
796    /// calls. Catches a regression where the RNG is fixed-seeded.
797    #[test]
798    fn getrandom_buffer_two_calls_differ() {
799        let _g = crate::test_util::global_state_lock();
800        let mut a = [0u8; 32];
801        let mut b = [0u8; 32];
802        getrandom_buffer(&mut a).unwrap();
803        getrandom_buffer(&mut b).unwrap();
804        assert_ne!(a, b, "two random reads must differ (or RNG is broken)");
805    }
806
807    // ─── zsh-corpus pins for random helpers ────────────────────────
808
809    /// `get_srandom` returns u32 with variance across calls.
810    #[test]
811    fn random_corpus_get_srandom_varies() {
812        let _g = crate::test_util::global_state_lock();
813        let a = get_srandom();
814        let b = get_srandom();
815        let c = get_srandom();
816        assert!(
817            a != b || b != c || a != c,
818            "3 srandom calls all returned same value: {a} {b} {c}"
819        );
820    }
821
822    /// `math_zrand_float` always in [0.0, 1.0).
823    #[test]
824    fn random_corpus_math_zrand_float_unit_interval() {
825        let _g = crate::test_util::global_state_lock();
826        for _ in 0..50 {
827            let v = zrf();
828            assert!((0.0..1.0).contains(&v), "value {v} out of [0.0, 1.0)");
829        }
830    }
831
832    /// `get_bound_random_buffer` with max=100 fills with values < 100.
833    #[test]
834    fn random_corpus_bound_random_under_max() {
835        let _g = crate::test_util::global_state_lock();
836        let mut buf = [0u32; 50];
837        get_bound_random_buffer(&mut buf, 100);
838        for &v in &buf {
839            assert!(v < 100, "{v} should be < 100");
840        }
841    }
842
843    /// `get_bound_random_buffer` with max=1 fills with all zeros.
844    #[test]
845    fn random_corpus_bound_random_max_one_all_zero() {
846        let _g = crate::test_util::global_state_lock();
847        let mut buf = [0u32; 20];
848        get_bound_random_buffer(&mut buf, 1);
849        for &v in &buf {
850            assert_eq!(v, 0, "max=1 → all values 0, got {v}");
851        }
852    }
853
854    /// `getrandom_buffer` with empty slice doesn't panic.
855    #[test]
856    fn random_corpus_getrandom_empty_buffer_no_panic() {
857        let _g = crate::test_util::global_state_lock();
858        let mut empty: [u8; 0] = [];
859        getrandom_buffer(&mut empty).unwrap();
860    }
861
862    /// `getrandom_buffer` fills 1-byte buffer.
863    #[test]
864    fn random_corpus_getrandom_single_byte() {
865        let _g = crate::test_util::global_state_lock();
866        let mut buf = [0u8; 1];
867        getrandom_buffer(&mut buf).unwrap();
868    }
869
870    // ═══════════════════════════════════════════════════════════════════
871    // Additional C-parity tests for Src/Modules/random.c.
872    // ═══════════════════════════════════════════════════════════════════
873
874    /// c:161 — `zri(None, None, false)` returns Ok value in
875    /// [0, u32::MAX) range (defaults: lower=0, upper=u32::MAX, exclusive).
876    #[test]
877    fn math_zrand_int_default_bounds_returns_ok() {
878        let _g = crate::test_util::global_state_lock();
879        let r = zri(None, None, false).expect("default bounds → Ok");
880        assert!(r >= 0, "result must be ≥ 0");
881        assert!(r <= u32::MAX as i64, "result must fit in u32 range");
882    }
883
884    /// c:179-180 — `lower < 0` fires the "Lower bound" zwarn but the
885    /// draw proceeds: diff = 100-(-1) = 101, result ∈ [-1, 99].
886    #[test]
887    fn math_zrand_int_negative_lower_warns_and_continues() {
888        let _g = crate::test_util::global_state_lock();
889        let r = zri(Some(100), Some(-1), false);
890        let v = r.expect("c:179 warns without returning");
891        assert!((-1..100).contains(&v), "result in wrapped range, got {}", v);
892    }
893
894    /// c:179-180 — `lower >= UINT32_MAX` warns; C still computes the
895    /// (negative) diff and truncates it through (uint32_t).
896    #[test]
897    fn math_zrand_int_lower_above_u32_max_warns_and_continues() {
898        let _g = crate::test_util::global_state_lock();
899        let r = zri(Some(100), Some((u32::MAX as i64) + 1), false);
900        assert!(r.is_ok(), "c:179 warns without returning");
901    }
902
903    /// c:181-182 — `upper < lower` fires the "must be greater" zwarn;
904    /// the wrapped-diff draw still happens (C's else-if chain has no
905    /// return).
906    #[test]
907    fn math_zrand_int_upper_below_lower_warns_and_continues() {
908        let _g = crate::test_util::global_state_lock();
909        let r = zri(Some(5), Some(10), false);
910        assert!(r.is_ok(), "c:181 warns without returning");
911    }
912
913    /// c:161 — `upper == lower` with exclusive returns the bound
914    /// (diff=0 short-circuit branch).
915    #[test]
916    fn math_zrand_int_upper_equals_lower_returns_bound() {
917        let _g = crate::test_util::global_state_lock();
918        let r = zri(Some(7), Some(7), false).expect("equal bounds OK");
919        assert_eq!(r, 7, "diff=0 → returns upper");
920    }
921
922    /// c:161 — `inclusive=true` with same lower=upper returns lower
923    /// (diff = 0 - 0 + 1 = 1, range [lower, upper]).
924    #[test]
925    fn math_zrand_int_inclusive_single_point_returns_lower() {
926        let _g = crate::test_util::global_state_lock();
927        let r = zri(Some(42), Some(42), true).expect("OK");
928        assert_eq!(r, 42, "inclusive single-point → that point");
929    }
930
931    /// c:161 — result always within [lower, upper] (or upper-1 if exclusive).
932    #[test]
933    fn math_zrand_int_result_in_range() {
934        let _g = crate::test_util::global_state_lock();
935        for _ in 0..50 {
936            let r = zri(Some(10), Some(0), false).unwrap();
937            assert!(r >= 0 && r < 10, "exclusive [0,10): got {}", r);
938        }
939        for _ in 0..50 {
940            let r = zri(Some(10), Some(0), true).unwrap();
941            assert!(r >= 0 && r <= 10, "inclusive [0,10]: got {}", r);
942        }
943    }
944
945    /// c:204 — `zrf()` returns value in [0.0, 1.0).
946    #[test]
947    fn math_zrand_float_in_zero_one_range() {
948        let _g = crate::test_util::global_state_lock();
949        for _ in 0..50 {
950            let r = zrf();
951            assert!(r >= 0.0 && r < 1.0, "must be in [0,1): got {}", r);
952        }
953    }
954
955    /// c:204 — `math_zrand_float` is not always the same value
956    /// (basic randomness sanity — 100 calls should produce ≥ 2
957    /// distinct values).
958    #[test]
959    fn math_zrand_float_produces_varied_output() {
960        let _g = crate::test_util::global_state_lock();
961        let first = zrf();
962        let any_different = (0..100).any(|_| zrf() != first);
963        assert!(
964            any_different,
965            "100 calls should produce ≥ 1 different value"
966        );
967    }
968
969    /// c:58 — `get_srandom()` returns u32, repeated calls produce
970    /// varied values (PRNG behavior pin).
971    #[test]
972    fn get_srandom_produces_varied_values() {
973        let _g = crate::test_util::global_state_lock();
974        let first = get_srandom();
975        let any_different = (0..100).any(|_| get_srandom() != first);
976        assert!(
977            any_different,
978            "100 calls should produce ≥ 1 different value"
979        );
980    }
981
982    // ═══════════════════════════════════════════════════════════════════
983    // Additional C-parity tests for Src/Modules/random.c
984    // c:26 getrandom_buffer / c:109 get_bound_random_buffer / c:144 get_srandom
985    // c:173 math_zrand_int / c:219 math_zrand_float / lifecycle
986    // ═══════════════════════════════════════════════════════════════════
987
988    /// c:26 — `getrandom_buffer(&mut [])` empty buf no-panic.
989    #[test]
990    fn getrandom_buffer_empty_buffer_returns_ok() {
991        let _g = crate::test_util::global_state_lock();
992        let mut buf: [u8; 0] = [];
993        assert!(getrandom_buffer(&mut buf).is_ok());
994    }
995
996    /// c:26 — `getrandom_buffer` returns Result type.
997    #[test]
998    fn getrandom_buffer_returns_io_result_type() {
999        let _g = crate::test_util::global_state_lock();
1000        let mut buf = [0u8; 1];
1001        let _: io::Result<()> = getrandom_buffer(&mut buf);
1002    }
1003
1004    /// c:26 — `getrandom_buffer` actually fills (high probability of
1005    /// at least one non-zero byte in 256 bytes).
1006    #[test]
1007    fn getrandom_buffer_fills_with_nonzero_bytes() {
1008        let _g = crate::test_util::global_state_lock();
1009        let mut buf = [0u8; 256];
1010        getrandom_buffer(&mut buf).unwrap();
1011        assert!(
1012            buf.iter().any(|&b| b != 0),
1013            "256 random bytes should have ≥ 1 non-zero"
1014        );
1015    }
1016
1017    /// c:109 — `get_bound_random_buffer(buf, 1)` fills all zeros (only
1018    /// value < 1 is 0).
1019    #[test]
1020    fn get_bound_random_buffer_max_one_all_zero() {
1021        let _g = crate::test_util::global_state_lock();
1022        let mut buf = [0u32; 100];
1023        get_bound_random_buffer(&mut buf, 1);
1024        for &v in &buf {
1025            assert_eq!(v, 0, "max=1 → all values must be 0");
1026        }
1027    }
1028
1029    /// c:109 — `get_bound_random_buffer(buf, max)` all values < max.
1030    #[test]
1031    fn get_bound_random_buffer_respects_max_bound() {
1032        let _g = crate::test_util::global_state_lock();
1033        let mut buf = [0u32; 1000];
1034        let max = 100u32;
1035        get_bound_random_buffer(&mut buf, max);
1036        for &v in &buf {
1037            assert!(v < max, "value {} must be < max {}", v, max);
1038        }
1039    }
1040
1041    /// c:144 — `get_srandom` returns u32 (compile-time type pin).
1042    #[test]
1043    fn get_srandom_returns_u32_type() {
1044        let _g = crate::test_util::global_state_lock();
1045        let _: u32 = get_srandom();
1046    }
1047
1048    /// c:219 — `zrf()` returns f64 strictly in [0.0, 1.0).
1049    #[test]
1050    fn math_zrand_float_strictly_in_half_open_unit() {
1051        let _g = crate::test_util::global_state_lock();
1052        for _ in 0..50 {
1053            let v = zrf();
1054            assert!(
1055                v >= 0.0 && v < 1.0,
1056                "math_zrand_float = {} must be in [0.0, 1.0)",
1057                v
1058            );
1059        }
1060    }
1061
1062    /// c:226-303 — full lifecycle setup→features→enables→boot→cleanup→finish.
1063    #[test]
1064    fn random_full_lifecycle_returns_zero_for_all() {
1065        let _g = crate::test_util::global_state_lock();
1066        let null = std::ptr::null();
1067        assert_eq!(setup_(null), 0);
1068        let mut feats = Vec::new();
1069        let _ = features_(null, &mut feats);
1070        let mut enables: Option<Vec<i32>> = None;
1071        let _ = enables_(null, &mut enables);
1072        assert_eq!(boot_(null), 0);
1073        assert_eq!(cleanup_(null), 0);
1074        assert_eq!(finish_(null), 0);
1075    }
1076
1077    /// c:343 — `random_u32` produces varied values.
1078    #[test]
1079    fn random_u32_produces_varied_values() {
1080        let _g = crate::test_util::global_state_lock();
1081        let first = random_u32();
1082        let any_diff = (0..100).any(|_| random_u32() != first);
1083        assert!(any_diff, "100 u32 randoms should differ from first");
1084    }
1085
1086    /// c:353 — `random_u64` produces varied values.
1087    #[test]
1088    fn random_u64_produces_varied_values() {
1089        let _g = crate::test_util::global_state_lock();
1090        let first = random_u64();
1091        let any_diff = (0..100).any(|_| random_u64() != first);
1092        assert!(any_diff, "100 u64 randoms should differ from first");
1093    }
1094
1095    // ═══════════════════════════════════════════════════════════════════
1096    // Additional C-parity tests for Src/Modules/random.c
1097    // c:343 random_u32 / c:353 random_u64 / c:364 bounded /
1098    // c:144 get_srandom + lifecycle type pins
1099    // ═══════════════════════════════════════════════════════════════════
1100
1101    /// c:343 — `random_u32` returns u32 (compile-time type pin).
1102    #[test]
1103    fn random_u32_returns_u32_type() {
1104        let _g = crate::test_util::global_state_lock();
1105        let _: u32 = random_u32();
1106    }
1107
1108    /// c:353 — `random_u64` returns u64 (compile-time type pin).
1109    #[test]
1110    fn random_u64_returns_u64_type() {
1111        let _g = crate::test_util::global_state_lock();
1112        let _: u64 = random_u64();
1113    }
1114
1115    /// c:364 — `bounded(0)` returns 0 (degenerate range).
1116    #[test]
1117    fn bounded_zero_max_returns_zero() {
1118        let _g = crate::test_util::global_state_lock();
1119        assert_eq!(bounded(0), 0, "max=0 always returns 0");
1120    }
1121
1122    /// c:364 — `bounded(1)` always returns 0 (only value < 1).
1123    #[test]
1124    fn bounded_one_max_always_zero() {
1125        let _g = crate::test_util::global_state_lock();
1126        for _ in 0..50 {
1127            assert_eq!(bounded(1), 0, "max=1 → result must be 0");
1128        }
1129    }
1130
1131    /// c:364 — `bounded(max)` result always strictly less than max.
1132    #[test]
1133    fn bounded_result_strictly_less_than_max() {
1134        let _g = crate::test_util::global_state_lock();
1135        for &max in &[2u32, 10, 100, 1000, 1_000_000] {
1136            for _ in 0..20 {
1137                let v = bounded(max);
1138                assert!(v < max, "bounded({}) = {} must be < max", max, v);
1139            }
1140        }
1141    }
1142
1143    /// c:364 — `bounded` returns u32 (compile-time type pin).
1144    #[test]
1145    fn bounded_returns_u32_type() {
1146        let _g = crate::test_util::global_state_lock();
1147        let _: u32 = bounded(100);
1148    }
1149
1150    /// c:364 — `bounded(u32::MAX)` short-circuit branch returns
1151    /// arbitrary u32 (degenerate special case at c:368).
1152    #[test]
1153    fn bounded_u32_max_returns_u32_range() {
1154        let _g = crate::test_util::global_state_lock();
1155        // No bound to check beyond "doesn't panic" + type.
1156        let _: u32 = bounded(u32::MAX);
1157    }
1158
1159    /// c:226 — `setup_` returns i32 (compile-time type pin).
1160    #[test]
1161    fn random_setup_returns_i32_type() {
1162        let _g = crate::test_util::global_state_lock();
1163        let _: i32 = setup_(std::ptr::null());
1164    }
1165
1166    /// c:249 — features list contains the canonical 3 entries
1167    /// (f:zrand_float, f:zrand_int, p:SRANDOM).
1168    #[test]
1169    fn random_features_canonical_three_entries() {
1170        let _g = crate::test_util::global_state_lock();
1171        let mut feats = Vec::new();
1172        features_(std::ptr::null(), &mut feats);
1173        assert_eq!(feats.len(), 3, "random advertises 3 features");
1174        assert!(
1175            feats.iter().any(|f| f == "f:zrand_float"),
1176            "must contain f:zrand_float"
1177        );
1178        assert!(
1179            feats.iter().any(|f| f == "f:zrand_int"),
1180            "must contain f:zrand_int"
1181        );
1182        assert!(
1183            feats.iter().any(|f| f == "p:SRANDOM"),
1184            "must contain p:SRANDOM"
1185        );
1186    }
1187
1188    /// c:296 — `cleanup_` idempotent.
1189    #[test]
1190    fn random_cleanup_idempotent() {
1191        let _g = crate::test_util::global_state_lock();
1192        for _ in 0..10 {
1193            assert_eq!(cleanup_(std::ptr::null()), 0);
1194        }
1195    }
1196
1197    /// c:303 — `finish_` idempotent.
1198    #[test]
1199    fn random_finish_idempotent() {
1200        let _g = crate::test_util::global_state_lock();
1201        for _ in 0..10 {
1202            assert_eq!(finish_(std::ptr::null()), 0);
1203        }
1204    }
1205
1206    /// c:263 — `boot_` idempotent.
1207    #[test]
1208    fn random_boot_idempotent() {
1209        let _g = crate::test_util::global_state_lock();
1210        for _ in 0..10 {
1211            assert_eq!(boot_(std::ptr::null()), 0);
1212        }
1213    }
1214
1215    // ═══════════════════════════════════════════════════════════════════
1216    // Additional C-parity tests for Src/Modules/random.c
1217    // c:144 get_srandom / c:219 math_zrand_float / c:343 random_u32 /
1218    // c:353 random_u64 / c:364 bounded / c:109 get_bound_random_buffer
1219    // ═══════════════════════════════════════════════════════════════════
1220
1221    /// c:144 — `get_srandom` returns u32 (compile-time pin, alt).
1222    #[test]
1223    fn get_srandom_returns_u32_pin_alt() {
1224        let _g = crate::test_util::global_state_lock();
1225        let _: u32 = get_srandom();
1226    }
1227
1228    /// c:144 — `get_srandom` is non-deterministic across two calls
1229    /// (probability of equal = 1/2^32 ≈ 2.3e-10 — cosmically unlikely).
1230    #[test]
1231    fn get_srandom_two_calls_differ() {
1232        let _g = crate::test_util::global_state_lock();
1233        let a = get_srandom();
1234        let b = get_srandom();
1235        assert_ne!(a, b, "two get_srandom() calls must differ");
1236    }
1237
1238    /// c:343 — `random_u32` returns u32 (compile-time pin, alt).
1239    #[test]
1240    fn random_u32_returns_u32_pin_alt() {
1241        let _g = crate::test_util::global_state_lock();
1242        let _: u32 = random_u32();
1243    }
1244
1245    /// c:353 — `random_u64` returns u64 (compile-time pin, alt).
1246    #[test]
1247    fn random_u64_returns_u64_pin_alt() {
1248        let _g = crate::test_util::global_state_lock();
1249        let _: u64 = random_u64();
1250    }
1251
1252    /// c:353 — `random_u64` eventually exceeds u32::MAX threshold
1253    /// (proves it's a full 64-bit value, not a u32 zero-extended).
1254    #[test]
1255    fn random_u64_eventually_exceeds_u32_max() {
1256        let _g = crate::test_util::global_state_lock();
1257        let any_large = (0..200).any(|_| random_u64() > (u32::MAX as u64));
1258        assert!(
1259            any_large,
1260            "200 random_u64 values must include ≥ 1 above u32::MAX"
1261        );
1262    }
1263
1264    /// c:219 — `math_zrand_float` returns f64 (compile-time pin).
1265    #[test]
1266    fn math_zrand_float_returns_f64_type() {
1267        let _g = crate::test_util::global_state_lock();
1268        let _: f64 = zrf();
1269    }
1270
1271    /// c:219 — `math_zrand_float` outputs always finite (no NaN/Inf).
1272    #[test]
1273    fn math_zrand_float_always_finite() {
1274        let _g = crate::test_util::global_state_lock();
1275        for _ in 0..500 {
1276            let v = zrf();
1277            assert!(
1278                v.is_finite(),
1279                "math_zrand_float must always be finite, got {}",
1280                v
1281            );
1282        }
1283    }
1284
1285    /// c:364 — `bounded(1)` always returns 0 (only one valid value).
1286    #[test]
1287    fn bounded_one_always_returns_zero() {
1288        let _g = crate::test_util::global_state_lock();
1289        for _ in 0..50 {
1290            assert_eq!(
1291                bounded(1),
1292                0,
1293                "bounded(1) must always return 0 (only valid value)"
1294            );
1295        }
1296    }
1297
1298    /// c:364 — `bounded(2)` returns only 0 or 1.
1299    #[test]
1300    fn bounded_two_returns_zero_or_one() {
1301        let _g = crate::test_util::global_state_lock();
1302        for _ in 0..50 {
1303            let v = bounded(2);
1304            assert!(v < 2, "bounded(2) must be 0 or 1; got {}", v);
1305        }
1306    }
1307
1308    /// c:109 — `get_bound_random_buffer` fills entire buffer with
1309    /// values strictly less than max.
1310    #[test]
1311    fn get_bound_random_buffer_all_under_max() {
1312        let _g = crate::test_util::global_state_lock();
1313        let mut buf = vec![0u32; 100];
1314        get_bound_random_buffer(&mut buf, 10);
1315        for &v in &buf {
1316            assert!(v < 10, "buffer value {} must be < max=10", v);
1317        }
1318    }
1319
1320    /// c:226/249/256/263/296/303 — each lifecycle hook returns 0 individually
1321    /// (tighter failure resolution).
1322    #[test]
1323    fn random_each_lifecycle_hook_returns_zero_individually() {
1324        let _g = crate::test_util::global_state_lock();
1325        let null = std::ptr::null();
1326        let mut v: Vec<String> = Vec::new();
1327        let mut e: Option<Vec<i32>> = None;
1328        assert_eq!(setup_(null), 0, "c:226 setup_");
1329        assert_eq!(features_(null, &mut v), 0, "c:249 features_");
1330        assert_eq!(enables_(null, &mut e), 0, "c:256 enables_");
1331        assert_eq!(boot_(null), 0, "c:263 boot_");
1332        assert_eq!(cleanup_(null), 0, "c:296 cleanup_");
1333        assert_eq!(finish_(null), 0, "c:303 finish_");
1334    }
1335}