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