#[derive(Clone)]
pub struct DetRng {
state: u64,
}
impl std::fmt::Debug for DetRng {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DetRng")
.field("state", &"<redacted>")
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeedQuality {
Excellent,
Good,
Acceptable,
Poor,
Dangerous {
reason: &'static str,
},
}
impl DetRng {
#[must_use]
#[inline]
pub const fn new(seed: u64) -> Self {
let validated_seed = Self::validate_and_correct_seed(seed);
Self {
state: validated_seed,
}
}
#[must_use]
pub fn new_with_quality(seed: u64) -> (Self, SeedQuality) {
let quality = Self::assess_seed_quality(seed);
let validated_seed = Self::validate_and_correct_seed(seed);
(
Self {
state: validated_seed,
},
quality,
)
}
#[must_use]
pub fn from_entropy() -> Self {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hash, Hasher};
let random_state = RandomState::new();
let mut hasher = random_state.build_hasher();
std::process::id().hash(&mut hasher);
std::thread::current().id().hash(&mut hasher);
#[cfg(not(feature = "test-internals"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.hash(&mut hasher);
}
let entropy_seed = hasher.finish();
Self::new(entropy_seed)
}
#[must_use]
const fn validate_and_correct_seed(seed: u64) -> u64 {
if seed == 0 {
return 1;
}
if Self::is_degenerate_seed(seed) {
return seed.wrapping_add(0x9e3779b97f4a7c15); }
seed
}
#[must_use]
const fn is_degenerate_seed(seed: u64) -> bool {
matches!(
seed,
0xFFFF_FFFF_FFFF_FFFF | 0x0000_0000_FFFF_FFFF | 0xFFFF_FFFF_0000_0000 | 0x5555_5555_5555_5555 | 0xAAAA_AAAA_AAAA_AAAA )
}
#[must_use]
pub fn assess_seed_quality(seed: u64) -> SeedQuality {
if seed == 0 {
return SeedQuality::Dangerous {
reason: "Zero seed causes xorshift64 to stick at zero",
};
}
if Self::is_degenerate_seed(seed) {
return SeedQuality::Dangerous {
reason: "Seed creates short cycles in xorshift64",
};
}
let popcount = seed.count_ones();
let leading_zeros = seed.leading_zeros();
let trailing_zeros = seed.trailing_zeros();
if popcount <= 8 || popcount >= 56 {
return SeedQuality::Poor;
}
if leading_zeros >= 32 || trailing_zeros >= 32 {
return SeedQuality::Poor;
}
if Self::has_simple_pattern(seed) {
return SeedQuality::Acceptable;
}
if (20..=44).contains(&popcount) && leading_zeros <= 8 && trailing_zeros <= 8 {
SeedQuality::Excellent
} else {
SeedQuality::Good
}
}
#[must_use]
const fn has_simple_pattern(seed: u64) -> bool {
let bytes = seed.to_le_bytes();
let b0 = bytes[0];
let mut all_same = true;
let mut idx = 1usize;
while idx < bytes.len() {
if bytes[idx] != b0 {
all_same = false;
break;
}
idx += 1;
}
if all_same {
return true;
}
let mut is_arithmetic = true;
if bytes.len() >= 3 {
let diff = bytes[1].wrapping_sub(bytes[0]);
let mut i = 2usize;
while i < bytes.len() {
if bytes[i].wrapping_sub(bytes[i - 1]) != diff {
is_arithmetic = false;
break;
}
i += 1;
}
}
is_arithmetic
}
pub fn verify_replay_consistency(seed: u64, steps: usize) -> Result<(), String> {
let mut rng1 = Self::new(seed);
let mut rng2 = Self::new(seed);
for step in 0..steps {
let val1 = rng1.next_u64();
let val2 = rng2.next_u64();
if val1 != val2 {
return Err(format!(
"Replay divergence at step {step}: rng1={val1:#x}, rng2={val2:#x}"
));
}
}
Ok(())
}
#[inline]
#[allow(clippy::missing_const_for_fn)] pub fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
#[inline]
pub fn next_u32(&mut self) -> u32 {
(self.next_u64() >> 32) as u32
}
#[inline]
#[allow(clippy::cast_possible_truncation)]
pub fn next_usize(&mut self, bound: usize) -> usize {
assert!(bound > 0, "bound must be non-zero");
let bound_u64 = bound as u64;
let threshold = u64::MAX - (u64::MAX % bound_u64);
loop {
let value = self.next_u64();
if value < threshold {
return (value % bound_u64) as usize;
}
}
}
#[inline]
pub fn next_bool(&mut self) -> bool {
self.next_u64() & 1 == 1
}
#[inline]
pub fn fill_bytes(&mut self, dest: &mut [u8]) {
let mut i = 0;
while i < dest.len() {
let rand = self.next_u64();
let bytes = rand.to_le_bytes();
let n = std::cmp::min(dest.len() - i, 8);
dest[i..i + n].copy_from_slice(&bytes[..n]);
i += n;
}
}
#[inline]
pub fn shuffle<T>(&mut self, slice: &mut [T]) {
for i in (1..slice.len()).rev() {
let j = self.next_usize(i + 1);
slice.swap(i, j);
}
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::pedantic,
clippy::nursery,
clippy::expect_fun_call,
clippy::map_unwrap_or,
clippy::cast_possible_wrap,
clippy::future_not_send
)]
use super::*;
#[test]
fn deterministic_sequence() {
let mut rng1 = DetRng::new(42);
let mut rng2 = DetRng::new(42);
for _ in 0..100 {
assert_eq!(rng1.next_u64(), rng2.next_u64());
}
}
#[test]
fn replay_different_seeds_different_sequences() {
let mut rng1 = DetRng::new(42);
let mut rng2 = DetRng::new(43);
assert_ne!(rng1.next_u64(), rng2.next_u64());
}
#[test]
fn zero_seed_handled() {
let mut rng = DetRng::new(0);
assert_ne!(rng.next_u64(), 0);
}
#[test]
fn det_rng_debug_clone() {
let mut rng = DetRng::new(42);
let dbg = format!("{rng:?}");
assert!(dbg.contains("DetRng"), "{dbg}");
let _ = rng.next_u64(); let mut forked = rng.clone();
assert_eq!(rng.next_u64(), forked.next_u64());
}
#[test]
fn debug_does_not_leak_state() {
let seeds: [u64; 8] = [
0xDEAD_BEEF_CAFE_BABE,
0x1234_5678_9ABC_DEF0,
42,
1,
u64::MAX,
0x0000_0001_0000_0001,
0xAAAA_AAAA_AAAA_AAAA,
0x5555_5555_5555_5555,
];
for &seed in &seeds {
let rng = DetRng::new(seed);
let dbg = format!("{rng:?}");
assert!(
dbg.contains("DetRng"),
"Debug must still identify the type, got {dbg:?}"
);
assert!(
dbg.contains("<redacted>"),
"Debug must mark redaction explicitly, got {dbg:?}"
);
let dec = format!("{seed}");
let lhex = format!("{seed:x}");
let uhex = format!("{seed:X}");
assert!(
!dbg.contains(&dec),
"decimal state {dec} leaked in Debug: {dbg}"
);
if lhex.len() >= 4 {
assert!(
!dbg.contains(&lhex),
"lower-hex state {lhex} leaked in Debug: {dbg}"
);
assert!(
!dbg.contains(&uhex),
"upper-hex state {uhex} leaked in Debug: {dbg}"
);
}
}
}
#[test]
fn debug_does_not_leak_state_after_advance() {
let mut rng = DetRng::new(0xDEAD_BEEF_CAFE_BABE);
for _ in 0..1000 {
let _ = rng.next_u64();
}
let mut probe = rng.clone();
let next = probe.next_u64();
let dbg = format!("{rng:?}");
let dec = format!("{next}");
let lhex = format!("{next:x}");
let uhex = format!("{next:X}");
assert!(
!dbg.contains(&dec),
"post-advance decimal state leaked: {dbg}"
);
if lhex.len() >= 4 {
assert!(!dbg.contains(&lhex), "post-advance lhex leaked: {dbg}");
assert!(!dbg.contains(&uhex), "post-advance uhex leaked: {dbg}");
}
}
#[test]
fn seed_quality_assessment_dangerous_seeds() {
assert_eq!(
DetRng::assess_seed_quality(0),
SeedQuality::Dangerous {
reason: "Zero seed causes xorshift64 to stick at zero"
}
);
assert_eq!(
DetRng::assess_seed_quality(0xFFFF_FFFF_FFFF_FFFF),
SeedQuality::Dangerous {
reason: "Seed creates short cycles in xorshift64"
}
);
assert_eq!(
DetRng::assess_seed_quality(0x5555_5555_5555_5555),
SeedQuality::Dangerous {
reason: "Seed creates short cycles in xorshift64"
}
);
}
#[test]
fn seed_quality_assessment_poor_seeds() {
assert_eq!(
DetRng::assess_seed_quality(0x0000_0000_0000_0007),
SeedQuality::Poor
);
assert_eq!(
DetRng::assess_seed_quality(0xFFFF_FFFF_FFFF_FFF8),
SeedQuality::Poor
);
assert_eq!(
DetRng::assess_seed_quality(0x0000_0000_1234_5678),
SeedQuality::Poor
);
assert_eq!(
DetRng::assess_seed_quality(0x1234_5678_0000_0000),
SeedQuality::Poor
);
}
#[test]
fn seed_quality_assessment_good_seeds() {
assert_eq!(
DetRng::assess_seed_quality(0x9e37_79b9_7f4a_7c15),
SeedQuality::Excellent
);
assert_eq!(
DetRng::assess_seed_quality(0x1234_5678_9abc_def0),
SeedQuality::Excellent
);
let seed_with_pattern = 0x1111_2222_3333_4444;
let quality = DetRng::assess_seed_quality(seed_with_pattern);
assert!(matches!(
quality,
SeedQuality::Acceptable | SeedQuality::Good
));
}
#[test]
fn new_with_quality_validation() {
let (rng, quality) = DetRng::new_with_quality(0);
assert!(matches!(quality, SeedQuality::Dangerous { .. }));
let mut rng = rng;
let first_val = rng.next_u64();
assert_ne!(first_val, 0, "Zero seed should be corrected");
let (_, quality) = DetRng::new_with_quality(0x9e37_79b9_7f4a_7c15);
assert_eq!(quality, SeedQuality::Excellent);
}
#[test]
fn entropy_based_rng_quality() {
let rng = DetRng::from_entropy();
let mut rng = rng;
let vals: Vec<_> = (0..10).map(|_| rng.next_u64()).collect();
let mut has_variation = false;
for i in 1..vals.len() {
if vals[i] != vals[i - 1] {
has_variation = true;
break;
}
}
assert!(has_variation, "Entropy RNG should produce varied output");
}
#[test]
fn replay_consistency_verification() {
let seed = 0x1234_5678_9abc_def0;
DetRng::verify_replay_consistency(seed, 100).expect("Short replay should be consistent");
DetRng::verify_replay_consistency(seed, 10000).expect("Long replay should be consistent");
}
#[test]
fn replay_consistency_across_instances() {
let seed = 0xdeadbeefcafebabe;
let mut rng1 = DetRng::new(seed);
let mut rng2 = DetRng::new(seed);
for step in 0..1000 {
let val1 = rng1.next_u64();
let val2 = rng2.next_u64();
assert_eq!(val1, val2, "Replay divergence at step {step}");
}
}
#[test]
fn different_seeds_different_sequences() {
let mut rng1 = DetRng::new(12345);
let mut rng2 = DetRng::new(54321);
let seq1: Vec<_> = (0..100).map(|_| rng1.next_u64()).collect();
let seq2: Vec<_> = (0..100).map(|_| rng2.next_u64()).collect();
assert_ne!(
seq1, seq2,
"Different seeds must produce different sequences"
);
}
#[test]
fn zero_seed_auto_correction() {
let rng = DetRng::new(0);
let mut rng = rng;
let first = rng.next_u64();
let second = rng.next_u64();
let third = rng.next_u64();
assert!(
first != 0 || second != 0 || third != 0,
"Auto-corrected seed should not stick at zero"
);
assert!(
first != second || second != third,
"Auto-corrected RNG should show state progression"
);
}
#[test]
fn degenerate_seed_correction() {
let degenerate_seeds = [
0xFFFF_FFFF_FFFF_FFFF,
0x5555_5555_5555_5555,
0xAAAA_AAAA_AAAA_AAAA,
];
for &seed in °enerate_seeds {
let rng = DetRng::new(seed);
let mut rng = rng;
let sequence: Vec<_> = (0..100).map(|_| rng.next_u64()).collect();
let unique_count = sequence
.iter()
.collect::<std::collections::BTreeSet<_>>()
.len();
assert!(
unique_count > 80,
"Corrected degenerate seed should produce varied output, got {unique_count}/100 unique values"
);
}
}
#[test]
fn seed_validation_preserves_determinism() {
let seed = 0x9999_8888_7777_6666;
let mut rng1 = DetRng::new(seed);
let mut rng2 = DetRng::new(seed);
let mut rng3 = DetRng::new(seed);
for _ in 0..500 {
let val1 = rng1.next_u64();
let val2 = rng2.next_u64();
let val3 = rng3.next_u64();
assert_eq!(val1, val2);
assert_eq!(val2, val3);
}
}
#[test]
fn pattern_detection_works() {
assert!(DetRng::has_simple_pattern(0x1111_1111_1111_1111));
assert!(DetRng::has_simple_pattern(0x0001_0203_0405_0607));
assert!(!DetRng::has_simple_pattern(0x9e37_79b9_7f4a_7c15));
}
#[test]
fn seed_quality_debug_clone() {
let quality = SeedQuality::Excellent;
let dbg = format!("{quality:?}");
assert!(dbg.contains("Excellent"));
let quality2 = quality.clone();
assert_eq!(quality, quality2);
let dangerous = SeedQuality::Dangerous { reason: "test" };
let dbg2 = format!("{dangerous:?}");
assert!(dbg2.contains("Dangerous"));
assert!(dbg2.contains("test"));
}
}