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