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}