mod_rand/tier3.rs
1//! # Tier 3 — OS-backed cryptographic random
2//!
3//! Random values pulled directly from the operating system's secure
4//! random source. Suitable for tokens, API keys, password salts,
5//! session IDs, nonces — anything an attacker would benefit from
6//! predicting.
7//!
8//! ## Source per platform
9//!
10//! | Platform | Source |
11//! |----------|---------------------------------------------|
12//! | Linux | `getrandom(2)` syscall (via libc symbol) |
13//! | macOS | `getentropy(3)` from libSystem |
14//! | Windows | `BCryptGenRandom` from `bcrypt.dll` |
15//! | Other Unix | `/dev/urandom` read |
16//!
17//! On Linux, if `getrandom` is unavailable (kernel older than 3.17,
18//! which is older than every supported Rust target), the
19//! implementation falls back to reading `/dev/urandom`. **`/dev/urandom`
20//! is a fully-supported cryptographic source on all listed platforms
21//! — the fallback is not a security downgrade.** It is never used as
22//! a fallback from a *failed* syscall, only from an *unavailable* one.
23//!
24//! On every platform, if the OS RNG cannot service the request, the
25//! call returns `io::Error`. This module will **never** fall back to
26//! a non-cryptographic source.
27//!
28//! ## Failure modes
29//!
30//! - On sandboxed processes (Linux seccomp, macOS sandbox) where the
31//! syscall is filtered, you receive `io::Error`.
32//! - On Linux at very early boot before the entropy pool has been
33//! seeded, `getrandom` blocks (does not fail). This is the desired
34//! behaviour — predictable boot-time random is the classic
35//! real-world weakness this tier prevents.
36//! - On Windows, BCryptGenRandom failures surface the NTSTATUS code
37//! in the returned `io::Error`.
38//!
39//! ## Thread safety
40//!
41//! All functions in this module are thread-safe. The underlying
42//! syscalls (`getrandom`, `getentropy`, `BCryptGenRandom`) are
43//! documented thread-safe by their respective platforms, and the
44//! Rust-side wrappers hold no shared mutable state.
45//!
46//! ## Performance
47//!
48//! One syscall worth of overhead per call (typically 100–500ns).
49//! Amortize by reading 32 or 64 bytes at a time for token generation
50//! rather than one byte at a time.
51
52use core::ops::{Range, RangeInclusive};
53use std::io;
54
55mod sys {
56 //! Platform-specific entropy primitives.
57 //!
58 //! Each platform module exposes a single `fill(buf)` function that
59 //! either fills the buffer completely or returns `io::Error`.
60
61 use std::io;
62
63 /// Fill `buf` with cryptographically secure random bytes.
64 ///
65 /// Dispatches to the platform-specific implementation.
66 pub fn fill(buf: &mut [u8]) -> io::Result<()> {
67 platform::fill(buf)
68 }
69
70 #[cfg(target_os = "linux")]
71 mod platform {
72 use std::io;
73 use std::sync::atomic::{AtomicU8, Ordering};
74
75 // getrandom(2) — present in glibc >= 2.25 and musl >= 1.1.20.
76 // The symbol is resolved at link time; on systems lacking it
77 // we'd fail to link, but every supported Rust target ships a
78 // libc new enough to provide it.
79 //
80 // EINTR retry is required because getrandom can be interrupted
81 // by signal delivery when buflen > 256.
82 extern "C" {
83 fn getrandom(buf: *mut u8, buflen: usize, flags: u32) -> isize;
84 fn __errno_location() -> *mut i32;
85 }
86
87 const EINTR: i32 = 4;
88 const ENOSYS: i32 = 38;
89
90 // 0 = unknown, 1 = available, 2 = unavailable (use /dev/urandom).
91 static STATE: AtomicU8 = AtomicU8::new(0);
92
93 fn errno() -> i32 {
94 // SAFETY: __errno_location returns a valid pointer into
95 // thread-local storage in every supported libc. The
96 // pointer remains valid for the lifetime of the thread.
97 unsafe { *__errno_location() }
98 }
99
100 pub fn fill(buf: &mut [u8]) -> io::Result<()> {
101 if STATE.load(Ordering::Relaxed) == 2 {
102 return urandom_fallback(buf);
103 }
104
105 let mut pos = 0;
106 while pos < buf.len() {
107 // SAFETY: buf is a valid &mut [u8]; the pointer and
108 // length describe the unfilled tail. flags = 0 selects
109 // blocking, /dev/urandom-style behaviour.
110 let r = unsafe { getrandom(buf.as_mut_ptr().add(pos), buf.len() - pos, 0) };
111 if r > 0 {
112 pos += r as usize;
113 continue;
114 }
115 let e = errno();
116 if e == EINTR {
117 continue;
118 }
119 if e == ENOSYS {
120 STATE.store(2, Ordering::Relaxed);
121 // Re-attempt the whole fill via /dev/urandom from
122 // the start of the unfilled region.
123 return urandom_fallback(&mut buf[pos..]);
124 }
125 return Err(io::Error::from_raw_os_error(e));
126 }
127 STATE.store(1, Ordering::Relaxed);
128 Ok(())
129 }
130
131 fn urandom_fallback(buf: &mut [u8]) -> io::Result<()> {
132 use std::fs::File;
133 use std::io::Read;
134 let mut f = File::open("/dev/urandom")?;
135 f.read_exact(buf)
136 }
137 }
138
139 #[cfg(target_os = "macos")]
140 mod platform {
141 use std::io;
142
143 // getentropy(3) — present in macOS 10.12+ via libSystem.
144 // Capped at 256 bytes per call.
145 extern "C" {
146 fn getentropy(buf: *mut u8, buflen: usize) -> i32;
147 fn __error() -> *mut i32;
148 }
149
150 const EINTR: i32 = 4;
151 const MAX_PER_CALL: usize = 256;
152
153 fn errno() -> i32 {
154 // SAFETY: __error returns a pointer into thread-local
155 // storage; valid for the thread's lifetime.
156 unsafe { *__error() }
157 }
158
159 pub fn fill(buf: &mut [u8]) -> io::Result<()> {
160 for chunk in buf.chunks_mut(MAX_PER_CALL) {
161 loop {
162 // SAFETY: chunk is a valid &mut [u8] of length
163 // <= MAX_PER_CALL, satisfying getentropy's
164 // contract.
165 let r = unsafe { getentropy(chunk.as_mut_ptr(), chunk.len()) };
166 if r == 0 {
167 break;
168 }
169 let e = errno();
170 if e == EINTR {
171 continue;
172 }
173 return Err(io::Error::from_raw_os_error(e));
174 }
175 }
176 Ok(())
177 }
178 }
179
180 #[cfg(target_os = "windows")]
181 mod platform {
182 use std::io;
183
184 // BCryptGenRandom from bcrypt.dll. With a NULL algorithm
185 // handle plus BCRYPT_USE_SYSTEM_PREFERRED_RNG, the call uses
186 // the system's preferred CSPRNG without any caller setup. The
187 // call is documented thread-safe and reentrant.
188 //
189 // BCRYPT_USE_SYSTEM_PREFERRED_RNG is the only flag that
190 // permits a NULL algorithm handle. Available since
191 // Windows Vista / Server 2008.
192 #[link(name = "bcrypt")]
193 extern "system" {
194 fn BCryptGenRandom(
195 hAlgorithm: *mut core::ffi::c_void,
196 pbBuffer: *mut u8,
197 cbBuffer: u32,
198 dwFlags: u32,
199 ) -> i32;
200 }
201
202 const BCRYPT_USE_SYSTEM_PREFERRED_RNG: u32 = 0x0000_0002;
203 // STATUS_SUCCESS — every other NTSTATUS is an error in this API.
204 const STATUS_SUCCESS: i32 = 0;
205
206 pub fn fill(buf: &mut [u8]) -> io::Result<()> {
207 // cbBuffer is u32; if `buf` somehow exceeds u32::MAX, chunk it.
208 // In practice no caller approaches this limit; we handle it
209 // for safety rather than out of expected need.
210 for chunk in buf.chunks_mut(u32::MAX as usize) {
211 // SAFETY: chunk is a valid &mut [u8]; cbBuffer fits in
212 // u32 by construction.
213 let status = unsafe {
214 BCryptGenRandom(
215 core::ptr::null_mut(),
216 chunk.as_mut_ptr(),
217 chunk.len() as u32,
218 BCRYPT_USE_SYSTEM_PREFERRED_RNG,
219 )
220 };
221 if status != STATUS_SUCCESS {
222 return Err(io::Error::other(format!(
223 "BCryptGenRandom failed: NTSTATUS 0x{:08X}",
224 status as u32
225 )));
226 }
227 }
228 Ok(())
229 }
230 }
231
232 // Other Unix-like targets (FreeBSD, OpenBSD, NetBSD, illumos,
233 // etc.): /dev/urandom is universally available and is a
234 // cryptographic source on every one of these. Not a "fallback"
235 // from a stronger primitive — it IS the platform primitive here.
236 #[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
237 mod platform {
238 use std::fs::File;
239 use std::io::{self, Read};
240
241 pub fn fill(buf: &mut [u8]) -> io::Result<()> {
242 let mut f = File::open("/dev/urandom")?;
243 f.read_exact(buf)
244 }
245 }
246
247 #[cfg(not(any(unix, target_os = "windows")))]
248 mod platform {
249 use std::io;
250
251 pub fn fill(_buf: &mut [u8]) -> io::Result<()> {
252 Err(io::Error::new(
253 io::ErrorKind::Unsupported,
254 "mod-rand tier3 has no entropy source on this platform",
255 ))
256 }
257 }
258}
259
260/// Fill the given buffer with cryptographically secure random bytes.
261///
262/// Returns `Ok(())` only if the entire buffer was filled from the
263/// platform's secure random source. Returns `io::Error` if the OS
264/// random source is unavailable (sandbox, seccomp filter, missing
265/// device) — never falls back to a weaker source.
266///
267/// An empty buffer succeeds without making any syscall.
268///
269/// # Example
270///
271/// ```
272/// use mod_rand::tier3;
273///
274/// let mut buf = [0u8; 32];
275/// tier3::fill_bytes(&mut buf).unwrap();
276/// ```
277pub fn fill_bytes(buf: &mut [u8]) -> io::Result<()> {
278 if buf.is_empty() {
279 return Ok(());
280 }
281 sys::fill(buf)
282}
283
284/// Return a cryptographically secure random `u64`.
285///
286/// Convenience wrapper around [`fill_bytes`]. Uses little-endian byte
287/// order; consumers should not rely on this — for randomness, byte
288/// order is immaterial.
289///
290/// # Example
291///
292/// ```
293/// use mod_rand::tier3;
294///
295/// let n: u64 = tier3::random_u64().unwrap();
296/// # let _ = n;
297/// ```
298pub fn random_u64() -> io::Result<u64> {
299 let mut buf = [0u8; 8];
300 fill_bytes(&mut buf)?;
301 Ok(u64::from_le_bytes(buf))
302}
303
304/// Return a cryptographically secure random `u32`.
305///
306/// # Example
307///
308/// ```
309/// use mod_rand::tier3;
310///
311/// let n: u32 = tier3::random_u32().unwrap();
312/// # let _ = n;
313/// ```
314pub fn random_u32() -> io::Result<u32> {
315 let mut buf = [0u8; 4];
316 fill_bytes(&mut buf)?;
317 Ok(u32::from_le_bytes(buf))
318}
319
320/// Return a `Vec<u8>` of cryptographically secure random bytes.
321///
322/// Convenience for callers who don't already have a buffer.
323///
324/// # Example
325///
326/// ```
327/// use mod_rand::tier3;
328///
329/// let bytes = tier3::random_bytes(32).unwrap();
330/// assert_eq!(bytes.len(), 32);
331/// ```
332pub fn random_bytes(len: usize) -> io::Result<Vec<u8>> {
333 let mut v = vec![0u8; len];
334 fill_bytes(&mut v)?;
335 Ok(v)
336}
337
338/// Return a hex-encoded cryptographically secure random token.
339///
340/// `bytes` is the number of raw random bytes drawn; the resulting
341/// string is exactly `bytes * 2` lowercase hex characters long.
342///
343/// Common sizes:
344/// - 16 bytes (32 hex chars) — session tokens
345/// - 32 bytes (64 hex chars) — API keys, password reset tokens
346/// - 64 bytes (128 hex chars) — long-lived secrets
347///
348/// # Example
349///
350/// ```
351/// use mod_rand::tier3;
352///
353/// let token = tier3::random_hex(16).unwrap();
354/// assert_eq!(token.len(), 32);
355/// assert!(token.chars().all(|c| c.is_ascii_hexdigit()));
356/// ```
357pub fn random_hex(bytes: usize) -> io::Result<String> {
358 const HEX: &[u8; 16] = b"0123456789abcdef";
359 let mut buf = vec![0u8; bytes];
360 fill_bytes(&mut buf)?;
361 let mut out = String::with_capacity(bytes * 2);
362 for b in buf {
363 out.push(HEX[(b >> 4) as usize] as char);
364 out.push(HEX[(b & 0xF) as usize] as char);
365 }
366 Ok(out)
367}
368
369/// Return a Crockford base32-encoded cryptographically secure random
370/// token of exactly `chars` characters.
371///
372/// Each character contributes 5 bits of entropy; the function draws
373/// `ceil(chars * 5 / 8)` random bytes and encodes them. Suitable for
374/// case-insensitive secrets and filesystem-safe identifiers.
375///
376/// # Example
377///
378/// ```
379/// use mod_rand::tier3;
380///
381/// let s = tier3::random_base32(24).unwrap();
382/// assert_eq!(s.len(), 24);
383/// ```
384pub fn random_base32(chars: usize) -> io::Result<String> {
385 const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
386 let byte_count = (chars * 5).div_ceil(8);
387 let mut buf = vec![0u8; byte_count.max(1)];
388 fill_bytes(&mut buf)?;
389 let mut out = String::with_capacity(chars);
390 let mut acc: u64 = 0;
391 let mut bits: u32 = 0;
392 let mut idx = 0;
393 while out.len() < chars {
394 if bits < 5 {
395 acc |= (buf[idx] as u64) << bits;
396 bits += 8;
397 idx += 1;
398 if idx == buf.len() && bits < 5 && out.len() < chars {
399 // Should not happen with our byte_count math; guard
400 // defensively rather than panic.
401 let mut extra = [0u8; 8];
402 fill_bytes(&mut extra)?;
403 for &b in &extra {
404 acc |= (b as u64) << bits;
405 bits += 8;
406 if bits >= 5 + 56 {
407 break;
408 }
409 }
410 }
411 }
412 out.push(ALPHABET[(acc & 0x1F) as usize] as char);
413 acc >>= 5;
414 bits -= 5;
415 }
416 Ok(out)
417}
418
419// ------------------------------------------------------------
420// Bounded-range API
421//
422// Every bounded function below pulls a fresh `u64` from the OS CSPRNG
423// and reduces it with Lemire's "Nearly Divisionless" rejection
424// sampling. The result is uniformly distributed over the requested
425// range with no modulo bias.
426//
427// Each rejected draw is a fresh syscall (or a fresh `/dev/urandom`
428// read on the Linux ENOSYS fallback path). The rejection rate is
429// approximately `n / 2^64` per draw, so for any range smaller than
430// half the u64 space, the expected syscall count per call is
431// effectively 1.
432//
433// Invalid ranges (empty / reversed) return
434// `io::Error` with `ErrorKind::InvalidInput` rather than panicking,
435// matching the rest of the Tier 3 fallible API.
436// ------------------------------------------------------------
437
438/// Produce a uniformly-distributed `u64` in `[0, n)`.
439///
440/// Internal helper. `n` MUST be greater than zero; the public wrappers
441/// enforce this. Returns `io::Error` only if the underlying
442/// `random_u64()` call fails.
443#[inline]
444fn bounded_u64(n: u64) -> io::Result<u64> {
445 debug_assert!(n != 0, "bounded_u64 requires n > 0");
446 let mut x = random_u64()?;
447 let mut m: u128 = (x as u128).wrapping_mul(n as u128);
448 let mut l: u64 = m as u64;
449 if l < n {
450 let t: u64 = n.wrapping_neg() % n;
451 while l < t {
452 x = random_u64()?;
453 m = (x as u128).wrapping_mul(n as u128);
454 l = m as u64;
455 }
456 }
457 Ok((m >> 64) as u64)
458}
459
460/// Helper: build an `InvalidInput` error for empty ranges.
461#[inline]
462fn empty_range_error(msg: &'static str) -> io::Error {
463 io::Error::new(io::ErrorKind::InvalidInput, msg)
464}
465
466/// Generate a cryptographically-secure uniformly-distributed `u64` in
467/// the half-open range `[range.start, range.end)`.
468///
469/// Uses Lemire's "Nearly Divisionless" rejection sampling so the
470/// output is genuinely uniform — there is no modulo bias.
471///
472/// # Errors
473///
474/// - Returns `io::Error` with `ErrorKind::InvalidInput` if the range
475/// is empty (`range.start >= range.end`).
476/// - Returns `io::Error` if the OS CSPRNG is unavailable.
477///
478/// # Example
479///
480/// ```
481/// use mod_rand::tier3;
482///
483/// let n = tier3::random_range_u64(10..20)?;
484/// assert!((10..20).contains(&n));
485/// # Ok::<(), std::io::Error>(())
486/// ```
487pub fn random_range_u64(range: Range<u64>) -> io::Result<u64> {
488 let Range { start, end } = range;
489 if start >= end {
490 return Err(empty_range_error("random_range_u64: empty range"));
491 }
492 let span = end - start;
493 Ok(start + bounded_u64(span)?)
494}
495
496/// Generate a cryptographically-secure uniformly-distributed `u64` in
497/// the closed range `[range.start(), range.end()]`.
498///
499/// The full-width inclusive range `0..=u64::MAX` is supported and is
500/// equivalent to a raw `random_u64()` call.
501///
502/// # Errors
503///
504/// - Returns `io::Error` with `ErrorKind::InvalidInput` if the range
505/// is empty (`*range.start() > *range.end()`).
506/// - Returns `io::Error` if the OS CSPRNG is unavailable.
507///
508/// # Example
509///
510/// ```
511/// use mod_rand::tier3;
512///
513/// let d = tier3::random_range_inclusive_u64(1..=6)?;
514/// assert!((1..=6).contains(&d));
515/// # Ok::<(), std::io::Error>(())
516/// ```
517pub fn random_range_inclusive_u64(range: RangeInclusive<u64>) -> io::Result<u64> {
518 let (start, end) = range.into_inner();
519 if start > end {
520 return Err(empty_range_error("random_range_inclusive_u64: empty range"));
521 }
522 if start == 0 && end == u64::MAX {
523 return random_u64();
524 }
525 let span = end - start + 1;
526 Ok(start + bounded_u64(span)?)
527}
528
529/// Generate a cryptographically-secure uniformly-distributed `u32` in
530/// the half-open range `[range.start, range.end)`.
531///
532/// # Errors
533///
534/// - Returns `io::Error` with `ErrorKind::InvalidInput` if the range
535/// is empty.
536/// - Returns `io::Error` if the OS CSPRNG is unavailable.
537///
538/// # Example
539///
540/// ```
541/// use mod_rand::tier3;
542///
543/// let pct = tier3::random_range_u32(0..100)?;
544/// assert!(pct < 100);
545/// # Ok::<(), std::io::Error>(())
546/// ```
547pub fn random_range_u32(range: Range<u32>) -> io::Result<u32> {
548 let Range { start, end } = range;
549 if start >= end {
550 return Err(empty_range_error("random_range_u32: empty range"));
551 }
552 let span = (end - start) as u64;
553 Ok((start as u64 + bounded_u64(span)?) as u32)
554}
555
556/// Generate a cryptographically-secure uniformly-distributed `u32` in
557/// the closed range `[range.start(), range.end()]`.
558///
559/// The full-width inclusive range `0..=u32::MAX` is supported.
560///
561/// # Errors
562///
563/// - Returns `io::Error` with `ErrorKind::InvalidInput` if the range
564/// is empty.
565/// - Returns `io::Error` if the OS CSPRNG is unavailable.
566pub fn random_range_inclusive_u32(range: RangeInclusive<u32>) -> io::Result<u32> {
567 let (start, end) = range.into_inner();
568 if start > end {
569 return Err(empty_range_error("random_range_inclusive_u32: empty range"));
570 }
571 let span = (end as u64) - (start as u64) + 1;
572 Ok((start as u64 + bounded_u64(span)?) as u32)
573}
574
575/// Generate a cryptographically-secure uniformly-distributed `i64` in
576/// the half-open range `[range.start, range.end)`.
577///
578/// Negative bounds and mixed-sign ranges are supported.
579///
580/// # Errors
581///
582/// - Returns `io::Error` with `ErrorKind::InvalidInput` if the range
583/// is empty.
584/// - Returns `io::Error` if the OS CSPRNG is unavailable.
585///
586/// # Example
587///
588/// ```
589/// use mod_rand::tier3;
590///
591/// let n = tier3::random_range_i64(-50..50)?;
592/// assert!((-50..50).contains(&n));
593/// # Ok::<(), std::io::Error>(())
594/// ```
595pub fn random_range_i64(range: Range<i64>) -> io::Result<i64> {
596 let Range { start, end } = range;
597 if start >= end {
598 return Err(empty_range_error("random_range_i64: empty range"));
599 }
600 let span = (end as i128 - start as i128) as u64;
601 let offset = bounded_u64(span)?;
602 Ok(((start as i128) + (offset as i128)) as i64)
603}
604
605/// Generate a cryptographically-secure uniformly-distributed `i64` in
606/// the closed range `[range.start(), range.end()]`.
607///
608/// The full-width inclusive range `i64::MIN..=i64::MAX` is supported
609/// and is equivalent to reinterpreting a raw `random_u64()` draw as
610/// `i64`.
611///
612/// # Errors
613///
614/// - Returns `io::Error` with `ErrorKind::InvalidInput` if the range
615/// is empty.
616/// - Returns `io::Error` if the OS CSPRNG is unavailable.
617pub fn random_range_inclusive_i64(range: RangeInclusive<i64>) -> io::Result<i64> {
618 let (start, end) = range.into_inner();
619 if start > end {
620 return Err(empty_range_error("random_range_inclusive_i64: empty range"));
621 }
622 if start == i64::MIN && end == i64::MAX {
623 return random_u64().map(|u| u as i64);
624 }
625 let span = ((end as i128) - (start as i128) + 1) as u64;
626 let offset = bounded_u64(span)?;
627 Ok(((start as i128) + (offset as i128)) as i64)
628}
629
630/// Generate a cryptographically-secure uniformly-distributed `i32` in
631/// the half-open range `[range.start, range.end)`.
632///
633/// # Errors
634///
635/// - Returns `io::Error` with `ErrorKind::InvalidInput` if the range
636/// is empty.
637/// - Returns `io::Error` if the OS CSPRNG is unavailable.
638pub fn random_range_i32(range: Range<i32>) -> io::Result<i32> {
639 let Range { start, end } = range;
640 if start >= end {
641 return Err(empty_range_error("random_range_i32: empty range"));
642 }
643 let span = (end as i64 - start as i64) as u64;
644 let offset = bounded_u64(span)?;
645 Ok(((start as i64) + (offset as i64)) as i32)
646}
647
648/// Generate a cryptographically-secure uniformly-distributed `i32` in
649/// the closed range `[range.start(), range.end()]`.
650///
651/// The full-width inclusive range `i32::MIN..=i32::MAX` is supported.
652///
653/// # Errors
654///
655/// - Returns `io::Error` with `ErrorKind::InvalidInput` if the range
656/// is empty.
657/// - Returns `io::Error` if the OS CSPRNG is unavailable.
658pub fn random_range_inclusive_i32(range: RangeInclusive<i32>) -> io::Result<i32> {
659 let (start, end) = range.into_inner();
660 if start > end {
661 return Err(empty_range_error("random_range_inclusive_i32: empty range"));
662 }
663 let span = ((end as i64) - (start as i64) + 1) as u64;
664 let offset = bounded_u64(span)?;
665 Ok(((start as i64) + (offset as i64)) as i32)
666}
667
668// ------------------------------------------------------------
669// Bounded-range API — additional integer widths
670// ------------------------------------------------------------
671
672/// Produce a uniformly-distributed `u128` in `[0, n)`.
673///
674/// Internal helper. Lemire's "Nearly Divisionless" rejection sampling
675/// generalised to 128-bit width via a 256-bit intermediate. Two
676/// `random_u64` calls (two syscalls) per draw in the common case.
677#[inline]
678fn bounded_u128(n: u128) -> io::Result<u128> {
679 debug_assert!(n != 0, "bounded_u128 requires n > 0");
680 let mut x = draw_u128()?;
681 let (mut hi, mut lo) = mul_128_full(x, n);
682 if lo < n {
683 let t: u128 = n.wrapping_neg() % n;
684 while lo < t {
685 x = draw_u128()?;
686 let (h, l) = mul_128_full(x, n);
687 hi = h;
688 lo = l;
689 }
690 }
691 let _ = x;
692 Ok(hi)
693}
694
695#[inline]
696fn draw_u128() -> io::Result<u128> {
697 let mut buf = [0u8; 16];
698 fill_bytes(&mut buf)?;
699 Ok(u128::from_le_bytes(buf))
700}
701
702#[inline]
703fn mul_128_full(a: u128, b: u128) -> (u128, u128) {
704 let a_lo: u64 = a as u64;
705 let a_hi: u64 = (a >> 64) as u64;
706 let b_lo: u64 = b as u64;
707 let b_hi: u64 = (b >> 64) as u64;
708 let p00: u128 = (a_lo as u128) * (b_lo as u128);
709 let p01: u128 = (a_lo as u128) * (b_hi as u128);
710 let p10: u128 = (a_hi as u128) * (b_lo as u128);
711 let p11: u128 = (a_hi as u128) * (b_hi as u128);
712 let mask_lo: u128 = 0xFFFF_FFFF_FFFF_FFFF;
713 let mid: u128 = (p00 >> 64) + (p01 & mask_lo) + (p10 & mask_lo);
714 let low: u128 = (p00 & mask_lo) | (mid << 64);
715 let high: u128 = p11 + (p01 >> 64) + (p10 >> 64) + (mid >> 64);
716 (high, low)
717}
718
719/// Generate a cryptographically-secure uniformly-distributed `u8` in
720/// the half-open range `[range.start, range.end)`.
721///
722/// # Errors
723///
724/// - Returns `InvalidInput` if the range is empty.
725/// - Returns `io::Error` if the OS CSPRNG is unavailable.
726///
727/// # Example
728///
729/// ```
730/// use mod_rand::tier3;
731/// let n = tier3::random_range_u8(0..100)?;
732/// assert!(n < 100);
733/// # Ok::<(), std::io::Error>(())
734/// ```
735pub fn random_range_u8(range: Range<u8>) -> io::Result<u8> {
736 let Range { start, end } = range;
737 if start >= end {
738 return Err(empty_range_error("random_range_u8: empty range"));
739 }
740 let span = (end - start) as u64;
741 Ok((start as u64 + bounded_u64(span)?) as u8)
742}
743
744/// Generate a cryptographically-secure uniformly-distributed `u8` in
745/// the closed range `[range.start(), range.end()]`. The full-width
746/// `0..=u8::MAX` is supported.
747///
748/// # Errors
749///
750/// As for [`random_range_u8`].
751pub fn random_range_inclusive_u8(range: RangeInclusive<u8>) -> io::Result<u8> {
752 let (start, end) = range.into_inner();
753 if start > end {
754 return Err(empty_range_error("random_range_inclusive_u8: empty range"));
755 }
756 let span = (end as u64) - (start as u64) + 1;
757 Ok((start as u64 + bounded_u64(span)?) as u8)
758}
759
760/// Generate a cryptographically-secure uniformly-distributed `u16` in
761/// the half-open range `[range.start, range.end)`.
762///
763/// # Errors
764///
765/// As for [`random_range_u8`].
766pub fn random_range_u16(range: Range<u16>) -> io::Result<u16> {
767 let Range { start, end } = range;
768 if start >= end {
769 return Err(empty_range_error("random_range_u16: empty range"));
770 }
771 let span = (end - start) as u64;
772 Ok((start as u64 + bounded_u64(span)?) as u16)
773}
774
775/// Generate a cryptographically-secure uniformly-distributed `u16` in
776/// the closed range `[range.start(), range.end()]`. The full-width
777/// `0..=u16::MAX` is supported.
778///
779/// # Errors
780///
781/// As for [`random_range_u8`].
782pub fn random_range_inclusive_u16(range: RangeInclusive<u16>) -> io::Result<u16> {
783 let (start, end) = range.into_inner();
784 if start > end {
785 return Err(empty_range_error("random_range_inclusive_u16: empty range"));
786 }
787 let span = (end as u64) - (start as u64) + 1;
788 Ok((start as u64 + bounded_u64(span)?) as u16)
789}
790
791/// Generate a cryptographically-secure uniformly-distributed `u128` in
792/// the half-open range `[range.start, range.end)`.
793///
794/// Uses two `random_u64` calls per draw in the common case. Lemire
795/// rejection sampling at 128-bit width via a 256-bit intermediate.
796///
797/// # Errors
798///
799/// As for [`random_range_u8`].
800pub fn random_range_u128(range: Range<u128>) -> io::Result<u128> {
801 let Range { start, end } = range;
802 if start >= end {
803 return Err(empty_range_error("random_range_u128: empty range"));
804 }
805 let span = end - start;
806 Ok(start + bounded_u128(span)?)
807}
808
809/// Generate a cryptographically-secure uniformly-distributed `u128` in
810/// the closed range `[range.start(), range.end()]`. The full-width
811/// `0..=u128::MAX` is supported.
812///
813/// # Errors
814///
815/// As for [`random_range_u8`].
816pub fn random_range_inclusive_u128(range: RangeInclusive<u128>) -> io::Result<u128> {
817 let (start, end) = range.into_inner();
818 if start > end {
819 return Err(empty_range_error(
820 "random_range_inclusive_u128: empty range",
821 ));
822 }
823 if start == 0 && end == u128::MAX {
824 return draw_u128();
825 }
826 let span = end - start + 1;
827 Ok(start + bounded_u128(span)?)
828}
829
830/// Generate a cryptographically-secure uniformly-distributed `usize`
831/// in the half-open range `[range.start, range.end)`.
832///
833/// # Errors
834///
835/// As for [`random_range_u8`].
836pub fn random_range_usize(range: Range<usize>) -> io::Result<usize> {
837 let Range { start, end } = range;
838 if start >= end {
839 return Err(empty_range_error("random_range_usize: empty range"));
840 }
841 let span = (end - start) as u64;
842 Ok((start as u64 + bounded_u64(span)?) as usize)
843}
844
845/// Generate a cryptographically-secure uniformly-distributed `usize`
846/// in the closed range `[range.start(), range.end()]`. The full-width
847/// `0..=usize::MAX` is supported.
848///
849/// # Errors
850///
851/// As for [`random_range_u8`].
852pub fn random_range_inclusive_usize(range: RangeInclusive<usize>) -> io::Result<usize> {
853 let (start, end) = range.into_inner();
854 if start > end {
855 return Err(empty_range_error(
856 "random_range_inclusive_usize: empty range",
857 ));
858 }
859 if start == 0 && end == usize::MAX {
860 #[cfg(target_pointer_width = "64")]
861 {
862 return random_u64().map(|u| u as usize);
863 }
864 #[cfg(not(target_pointer_width = "64"))]
865 {
866 let span = (end as u64) - (start as u64) + 1;
867 return bounded_u64(span).map(|o| (start as u64 + o) as usize);
868 }
869 }
870 let span = (end - start) as u64 + 1;
871 Ok((start as u64 + bounded_u64(span)?) as usize)
872}
873
874/// Generate a cryptographically-secure uniformly-distributed `i8` in
875/// the half-open range `[range.start, range.end)`.
876///
877/// # Errors
878///
879/// As for [`random_range_u8`].
880pub fn random_range_i8(range: Range<i8>) -> io::Result<i8> {
881 let Range { start, end } = range;
882 if start >= end {
883 return Err(empty_range_error("random_range_i8: empty range"));
884 }
885 let span = (end as i64 - start as i64) as u64;
886 let offset = bounded_u64(span)?;
887 Ok(((start as i64) + (offset as i64)) as i8)
888}
889
890/// Generate a cryptographically-secure uniformly-distributed `i8` in
891/// the closed range `[range.start(), range.end()]`. The full-width
892/// `i8::MIN..=i8::MAX` is supported.
893///
894/// # Errors
895///
896/// As for [`random_range_u8`].
897pub fn random_range_inclusive_i8(range: RangeInclusive<i8>) -> io::Result<i8> {
898 let (start, end) = range.into_inner();
899 if start > end {
900 return Err(empty_range_error("random_range_inclusive_i8: empty range"));
901 }
902 let span = ((end as i64) - (start as i64) + 1) as u64;
903 let offset = bounded_u64(span)?;
904 Ok(((start as i64) + (offset as i64)) as i8)
905}
906
907/// Generate a cryptographically-secure uniformly-distributed `i16` in
908/// the half-open range `[range.start, range.end)`.
909///
910/// # Errors
911///
912/// As for [`random_range_u8`].
913pub fn random_range_i16(range: Range<i16>) -> io::Result<i16> {
914 let Range { start, end } = range;
915 if start >= end {
916 return Err(empty_range_error("random_range_i16: empty range"));
917 }
918 let span = (end as i64 - start as i64) as u64;
919 let offset = bounded_u64(span)?;
920 Ok(((start as i64) + (offset as i64)) as i16)
921}
922
923/// Generate a cryptographically-secure uniformly-distributed `i16` in
924/// the closed range `[range.start(), range.end()]`. The full-width
925/// `i16::MIN..=i16::MAX` is supported.
926///
927/// # Errors
928///
929/// As for [`random_range_u8`].
930pub fn random_range_inclusive_i16(range: RangeInclusive<i16>) -> io::Result<i16> {
931 let (start, end) = range.into_inner();
932 if start > end {
933 return Err(empty_range_error("random_range_inclusive_i16: empty range"));
934 }
935 let span = ((end as i64) - (start as i64) + 1) as u64;
936 let offset = bounded_u64(span)?;
937 Ok(((start as i64) + (offset as i64)) as i16)
938}
939
940/// Generate a cryptographically-secure uniformly-distributed `i128` in
941/// the half-open range `[range.start, range.end)`.
942///
943/// # Errors
944///
945/// As for [`random_range_u8`].
946pub fn random_range_i128(range: Range<i128>) -> io::Result<i128> {
947 let Range { start, end } = range;
948 if start >= end {
949 return Err(empty_range_error("random_range_i128: empty range"));
950 }
951 let span = (end as u128).wrapping_sub(start as u128);
952 let offset = bounded_u128(span)?;
953 Ok((start as u128).wrapping_add(offset) as i128)
954}
955
956/// Generate a cryptographically-secure uniformly-distributed `i128` in
957/// the closed range `[range.start(), range.end()]`. The full-width
958/// `i128::MIN..=i128::MAX` is supported.
959///
960/// # Errors
961///
962/// As for [`random_range_u8`].
963pub fn random_range_inclusive_i128(range: RangeInclusive<i128>) -> io::Result<i128> {
964 let (start, end) = range.into_inner();
965 if start > end {
966 return Err(empty_range_error(
967 "random_range_inclusive_i128: empty range",
968 ));
969 }
970 if start == i128::MIN && end == i128::MAX {
971 return draw_u128().map(|u| u as i128);
972 }
973 let span = (end as u128).wrapping_sub(start as u128) + 1;
974 let offset = bounded_u128(span)?;
975 Ok((start as u128).wrapping_add(offset) as i128)
976}
977
978/// Generate a cryptographically-secure uniformly-distributed `isize`
979/// in the half-open range `[range.start, range.end)`.
980///
981/// # Errors
982///
983/// As for [`random_range_u8`].
984pub fn random_range_isize(range: Range<isize>) -> io::Result<isize> {
985 let Range { start, end } = range;
986 if start >= end {
987 return Err(empty_range_error("random_range_isize: empty range"));
988 }
989 let span = (end as i128 - start as i128) as u64;
990 let offset = bounded_u64(span)?;
991 Ok(((start as i128) + (offset as i128)) as isize)
992}
993
994/// Generate a cryptographically-secure uniformly-distributed `isize`
995/// in the closed range `[range.start(), range.end()]`. The full-width
996/// `isize::MIN..=isize::MAX` is supported.
997///
998/// # Errors
999///
1000/// As for [`random_range_u8`].
1001pub fn random_range_inclusive_isize(range: RangeInclusive<isize>) -> io::Result<isize> {
1002 let (start, end) = range.into_inner();
1003 if start > end {
1004 return Err(empty_range_error(
1005 "random_range_inclusive_isize: empty range",
1006 ));
1007 }
1008 if start == isize::MIN && end == isize::MAX {
1009 #[cfg(target_pointer_width = "64")]
1010 {
1011 return random_u64().map(|u| u as isize);
1012 }
1013 #[cfg(not(target_pointer_width = "64"))]
1014 {
1015 let span = ((end as i128) - (start as i128) + 1) as u64;
1016 let offset = bounded_u64(span)?;
1017 return Ok(((start as i128) + (offset as i128)) as isize);
1018 }
1019 }
1020 let span = ((end as i128) - (start as i128) + 1) as u64;
1021 let offset = bounded_u64(span)?;
1022 Ok(((start as i128) + (offset as i128)) as isize)
1023}
1024
1025// ------------------------------------------------------------
1026// String generation
1027// ------------------------------------------------------------
1028
1029/// Helper: build an `InvalidInput` error for charset-validation
1030/// failures.
1031#[inline]
1032fn invalid_charset_error(msg: &'static str) -> io::Error {
1033 io::Error::new(io::ErrorKind::InvalidInput, msg)
1034}
1035
1036/// Generate a cryptographically-secure random string of exactly `len`
1037/// characters drawn from `charset` (uniformly).
1038///
1039/// Selection uses Lemire rejection sampling against `charset.len()`,
1040/// so the per-character distribution is uniform — no modulo bias.
1041///
1042/// Each character costs one rejection-sampled draw (typically one
1043/// `random_u64` syscall in the common case).
1044///
1045/// # Errors
1046///
1047/// - Returns `InvalidInput` if `charset` is empty or contains a
1048/// non-ASCII byte (byte >= 128).
1049/// - Returns `io::Error` if the OS CSPRNG is unavailable.
1050///
1051/// # Example
1052///
1053/// ```
1054/// use mod_rand::{tier3, charsets};
1055/// let s = tier3::random_string(16, charsets::ALPHANUMERIC)?;
1056/// assert_eq!(s.len(), 16);
1057/// # Ok::<(), std::io::Error>(())
1058/// ```
1059pub fn random_string(len: usize, charset: &[u8]) -> io::Result<String> {
1060 if charset.is_empty() {
1061 return Err(invalid_charset_error(
1062 "random_string: charset must be non-empty",
1063 ));
1064 }
1065 if !charset.iter().all(|&b| b < 128) {
1066 return Err(invalid_charset_error(
1067 "random_string: charset must be ASCII (every byte < 128)",
1068 ));
1069 }
1070 let n = charset.len() as u64;
1071 let mut out = String::with_capacity(len);
1072 for _ in 0..len {
1073 let idx = bounded_u64(n)? as usize;
1074 out.push(charset[idx] as char);
1075 }
1076 Ok(out)
1077}
1078
1079/// Generate a cryptographically-secure ASCII alphanumeric string
1080/// (`A-Z`, `a-z`, `0-9`) of exactly `len` characters.
1081///
1082/// Equivalent to `random_string(len, charsets::ALPHANUMERIC)`.
1083///
1084/// # Example
1085///
1086/// ```
1087/// use mod_rand::tier3;
1088/// let token = tier3::random_alphanumeric(32)?;
1089/// assert_eq!(token.len(), 32);
1090/// # Ok::<(), std::io::Error>(())
1091/// ```
1092#[inline]
1093pub fn random_alphanumeric(len: usize) -> io::Result<String> {
1094 random_string(len, crate::charsets::ALPHANUMERIC)
1095}
1096
1097/// Generate a cryptographically-secure ASCII alphabetic string
1098/// (`A-Z`, `a-z`) of exactly `len` characters.
1099///
1100/// # Example
1101///
1102/// ```
1103/// use mod_rand::tier3;
1104/// let s = tier3::random_alpha(12)?;
1105/// assert_eq!(s.len(), 12);
1106/// # Ok::<(), std::io::Error>(())
1107/// ```
1108#[inline]
1109pub fn random_alpha(len: usize) -> io::Result<String> {
1110 random_string(len, crate::charsets::ALPHA)
1111}
1112
1113/// Generate a cryptographically-secure ASCII numeric string (`0-9`)
1114/// of exactly `len` characters. Leading zeros may appear.
1115///
1116/// # Example
1117///
1118/// ```
1119/// use mod_rand::tier3;
1120/// let s = tier3::random_numeric(6)?;
1121/// assert_eq!(s.len(), 6);
1122/// # Ok::<(), std::io::Error>(())
1123/// ```
1124#[inline]
1125pub fn random_numeric(len: usize) -> io::Result<String> {
1126 random_string(len, crate::charsets::NUMERIC)
1127}
1128
1129/// Generate a cryptographically-secure lowercase hex string (`0-9`,
1130/// `a-f`) of exactly `len` characters.
1131///
1132/// **Distinct from [`random_hex`]**: [`random_hex`] takes a byte
1133/// count and returns `bytes * 2` hex characters; this function takes
1134/// a character count directly. Both produce identical entropy per
1135/// character but accept different argument units.
1136///
1137/// # Example
1138///
1139/// ```
1140/// use mod_rand::tier3;
1141/// let s = tier3::random_hex_string(40)?;
1142/// assert_eq!(s.len(), 40);
1143/// # Ok::<(), std::io::Error>(())
1144/// ```
1145#[inline]
1146pub fn random_hex_string(len: usize) -> io::Result<String> {
1147 random_string(len, crate::charsets::HEX_LOWER)
1148}
1149
1150// ------------------------------------------------------------
1151// Collection operations — cryptographic shuffle
1152// ------------------------------------------------------------
1153
1154/// In-place cryptographically-secure uniform Fisher-Yates shuffle.
1155///
1156/// Every permutation of `slice` is equally likely under the OS
1157/// CSPRNG. O(n) time, no allocation. Each swap consumes one
1158/// `random_u64` worth of entropy from Tier 3.
1159///
1160/// # Errors
1161///
1162/// Returns `io::Error` if the OS CSPRNG is unavailable. If the call
1163/// fails mid-shuffle, the slice is left in a partially-shuffled state
1164/// but still contains exactly its original elements.
1165///
1166/// # Example
1167///
1168/// ```
1169/// use mod_rand::tier3;
1170/// let mut deck: Vec<u8> = (0..52).collect();
1171/// tier3::shuffle(&mut deck)?;
1172/// // deck is now a uniformly-random permutation.
1173/// # Ok::<(), std::io::Error>(())
1174/// ```
1175pub fn shuffle<T>(slice: &mut [T]) -> io::Result<()> {
1176 let n = slice.len();
1177 if n < 2 {
1178 return Ok(());
1179 }
1180 for i in (1..n).rev() {
1181 // Pick j uniformly in [0, i]. i fits in u64 because n <= usize::MAX
1182 // and usize <= u64 on every supported target.
1183 let span = (i as u64) + 1;
1184 let j = bounded_u64(span)? as usize;
1185 slice.swap(i, j);
1186 }
1187 Ok(())
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192 use super::*;
1193 use std::collections::HashSet;
1194
1195 #[test]
1196 fn fill_bytes_produces_output() {
1197 let mut buf = [0u8; 32];
1198 fill_bytes(&mut buf).unwrap();
1199 assert!(buf.iter().any(|&b| b != 0));
1200 }
1201
1202 #[test]
1203 fn empty_buffer_succeeds_without_syscall() {
1204 let mut buf: [u8; 0] = [];
1205 fill_bytes(&mut buf).unwrap();
1206 }
1207
1208 #[test]
1209 fn random_u64_varies_within_small_sample() {
1210 let mut seen = HashSet::with_capacity(16);
1211 for _ in 0..16 {
1212 seen.insert(random_u64().unwrap());
1213 }
1214 assert!(seen.len() > 1, "u64 output appears constant in sample");
1215 }
1216
1217 #[test]
1218 fn random_u32_varies_within_small_sample() {
1219 let mut seen = HashSet::with_capacity(32);
1220 for _ in 0..32 {
1221 seen.insert(random_u32().unwrap());
1222 }
1223 assert!(seen.len() > 1, "u32 output appears constant in sample");
1224 }
1225
1226 #[test]
1227 fn random_hex_correct_length_and_alphabet() {
1228 let h = random_hex(16).unwrap();
1229 assert_eq!(h.len(), 32);
1230 assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
1231 assert!(h.chars().all(|c| !c.is_ascii_uppercase()));
1232 }
1233
1234 #[test]
1235 fn random_hex_zero_length() {
1236 let h = random_hex(0).unwrap();
1237 assert_eq!(h, "");
1238 }
1239
1240 #[test]
1241 fn random_base32_correct_length_and_alphabet() {
1242 const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1243 for len in [1, 5, 8, 16, 24, 32, 64] {
1244 let s = random_base32(len).unwrap();
1245 assert_eq!(s.len(), len, "length {len}");
1246 assert!(
1247 s.bytes().all(|b| ALPHABET.contains(&b)),
1248 "alphabet violation in {s}"
1249 );
1250 }
1251 }
1252
1253 #[test]
1254 fn random_bytes_is_correct_length() {
1255 let b = random_bytes(48).unwrap();
1256 assert_eq!(b.len(), 48);
1257 }
1258
1259 #[test]
1260 fn large_buffer_fill_succeeds() {
1261 // A buffer larger than macOS's 256-byte getentropy cap and
1262 // larger than typical syscall short-read thresholds. Verifies
1263 // the looping/chunking logic on every platform.
1264 let mut buf = vec![0u8; 4096];
1265 fill_bytes(&mut buf).unwrap();
1266 // The probability all 4096 bytes are zero is 2^-32768 — any
1267 // observation of that is a bug.
1268 assert!(buf.iter().any(|&b| b != 0));
1269 }
1270
1271 #[test]
1272 fn stress_many_small_calls() {
1273 // 1000 calls of 32 bytes each. Exercises syscall stability.
1274 for _ in 0..1000 {
1275 let mut buf = [0u8; 32];
1276 fill_bytes(&mut buf).unwrap();
1277 }
1278 }
1279
1280 #[test]
1281 fn byte_frequency_chi_squared() {
1282 // 1 048 576 bytes — 256 buckets, expected ~4096 per bucket.
1283 // Chi-squared critical value (255 d.f., alpha = 0.001) is
1284 // about 330. We use 500 to keep flake rate negligible.
1285 let mut buf = vec![0u8; 1 << 20];
1286 fill_bytes(&mut buf).unwrap();
1287
1288 let mut counts = [0u32; 256];
1289 for &b in &buf {
1290 counts[b as usize] += 1;
1291 }
1292 let n = buf.len() as f64;
1293 let expected = n / 256.0;
1294 let chi: f64 = counts
1295 .iter()
1296 .map(|&c| {
1297 let diff = c as f64 - expected;
1298 diff * diff / expected
1299 })
1300 .sum();
1301 assert!(chi < 500.0, "byte-frequency chi-squared {chi} too high");
1302 }
1303
1304 // ------------------------------------------------------------
1305 // Bounded-range tests
1306 // ------------------------------------------------------------
1307
1308 #[test]
1309 fn random_range_u64_bounds() {
1310 for _ in 0..1000 {
1311 let n = random_range_u64(100..200).unwrap();
1312 assert!((100..200).contains(&n));
1313 }
1314 }
1315
1316 #[test]
1317 fn random_range_u64_single_value_window() {
1318 // [start, start+1) — every draw lands on start. No syscalls
1319 // wasted on rejection in this case (span=1, l=0 is never < n).
1320 for _ in 0..100 {
1321 assert_eq!(random_range_u64(7..8).unwrap(), 7);
1322 }
1323 }
1324
1325 #[test]
1326 fn random_range_inclusive_u64_die_roll_visits_all_faces() {
1327 // 1000 cryptographic die rolls — all six faces must appear.
1328 let mut faces = [0u32; 6];
1329 for _ in 0..1000 {
1330 let d = random_range_inclusive_u64(1..=6).unwrap();
1331 assert!((1..=6).contains(&d));
1332 faces[(d - 1) as usize] += 1;
1333 }
1334 for (i, &c) in faces.iter().enumerate() {
1335 assert!(c > 0, "face {} never appeared in 1000 rolls", i + 1);
1336 }
1337 }
1338
1339 #[test]
1340 fn random_range_inclusive_u64_single_value() {
1341 for _ in 0..100 {
1342 assert_eq!(random_range_inclusive_u64(42..=42).unwrap(), 42);
1343 }
1344 }
1345
1346 #[test]
1347 fn random_range_inclusive_u64_full_width() {
1348 // 0..=u64::MAX — full-width path. Two draws differ.
1349 let a = random_range_inclusive_u64(0..=u64::MAX).unwrap();
1350 let b = random_range_inclusive_u64(0..=u64::MAX).unwrap();
1351 assert_ne!(a, b);
1352 }
1353
1354 #[test]
1355 fn random_range_u32_bounds() {
1356 for _ in 0..1000 {
1357 let n = random_range_u32(0..256).unwrap();
1358 assert!(n < 256);
1359 }
1360 }
1361
1362 #[test]
1363 fn random_range_inclusive_u32_full_width() {
1364 for _ in 0..100 {
1365 let _ = random_range_inclusive_u32(0..=u32::MAX).unwrap();
1366 }
1367 }
1368
1369 #[test]
1370 fn random_range_i64_negative() {
1371 for _ in 0..1000 {
1372 let n = random_range_i64(-100..-50).unwrap();
1373 assert!((-100..-50).contains(&n));
1374 }
1375 }
1376
1377 #[test]
1378 fn random_range_i64_mixed_sign() {
1379 let mut saw_neg = false;
1380 let mut saw_pos = false;
1381 for _ in 0..1000 {
1382 let n = random_range_i64(-100..100).unwrap();
1383 assert!((-100..100).contains(&n));
1384 if n < 0 {
1385 saw_neg = true;
1386 }
1387 if n >= 0 {
1388 saw_pos = true;
1389 }
1390 }
1391 assert!(saw_neg && saw_pos);
1392 }
1393
1394 #[test]
1395 fn random_range_inclusive_i64_full_width() {
1396 let _ = random_range_inclusive_i64(i64::MIN..=i64::MAX).unwrap();
1397 }
1398
1399 #[test]
1400 fn random_range_i32_bounds() {
1401 for _ in 0..1000 {
1402 let n = random_range_i32(-1000..1000).unwrap();
1403 assert!((-1000..1000).contains(&n));
1404 }
1405 }
1406
1407 #[test]
1408 fn random_range_inclusive_i32_full_width() {
1409 for _ in 0..100 {
1410 let _ = random_range_inclusive_i32(i32::MIN..=i32::MAX).unwrap();
1411 }
1412 }
1413
1414 #[test]
1415 fn random_range_u64_empty_returns_invalid_input() {
1416 let err = random_range_u64(10..10).unwrap_err();
1417 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1418 }
1419
1420 #[test]
1421 #[allow(clippy::reversed_empty_ranges)]
1422 fn random_range_u64_reverse_returns_invalid_input() {
1423 let err = random_range_u64(10..5).unwrap_err();
1424 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1425 }
1426
1427 #[test]
1428 #[allow(clippy::reversed_empty_ranges)]
1429 fn random_range_inclusive_u64_reverse_returns_invalid_input() {
1430 let err = random_range_inclusive_u64(10..=5).unwrap_err();
1431 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1432 }
1433
1434 #[test]
1435 #[allow(clippy::reversed_empty_ranges)]
1436 fn random_range_i64_reverse_returns_invalid_input() {
1437 let err = random_range_i64(5..-5).unwrap_err();
1438 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1439 }
1440
1441 #[test]
1442 fn random_range_uniformity_chi_squared() {
1443 // 50 000 cryptographic draws over 50 buckets. Expected count
1444 // per bucket: 1000. Chi-squared critical value (49 d.f.,
1445 // alpha=0.001) is ~85; we use 200 to keep flake rate
1446 // negligible. This is fewer draws than tier1/tier2 because
1447 // each draw is a syscall.
1448 let mut counts = [0u32; 50];
1449 for _ in 0..50_000 {
1450 let v = random_range_u32(0..50).unwrap();
1451 counts[v as usize] += 1;
1452 }
1453 let expected = 50_000.0 / 50.0;
1454 let chi: f64 = counts
1455 .iter()
1456 .map(|&c| {
1457 let diff = c as f64 - expected;
1458 diff * diff / expected
1459 })
1460 .sum();
1461 assert!(
1462 chi < 200.0,
1463 "tier3 chi-squared {chi} too high — bounded-range output is biased"
1464 );
1465 }
1466}