Skip to main content

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}