use std::{cell::RefCell, rc::Rc};
use sha2::{Digest, Sha256};
#[derive(Clone, Debug)]
pub struct TestEntropy {
seed: [u8; 32],
state: Rc<RefCell<EntropyState>>,
}
#[derive(Debug)]
struct EntropyState {
next_block: u64,
block: [u8; 32],
offset: usize,
}
impl TestEntropy {
pub fn seeded(seed: [u8; 32]) -> Self {
Self {
seed,
state: Rc::new(RefCell::new(EntropyState {
next_block: 0,
block: [0; 32],
offset: 32,
})),
}
}
pub fn fill(&self, output: &mut [u8]) {
let mut copied = 0;
let mut state = self.state.borrow_mut();
while copied < output.len() {
if state.offset == state.block.len() {
let mut hasher = Sha256::new();
hasher.update(b"lenso-test-entropy-v1\0");
hasher.update(self.seed);
hasher.update(state.next_block.to_be_bytes());
state.block.copy_from_slice(&hasher.finalize());
state.next_block = state
.next_block
.checked_add(1)
.expect("a test entropy stream cannot exceed u64 blocks");
state.offset = 0;
}
let remaining = output.len() - copied;
let available = state.block.len() - state.offset;
let count = remaining.min(available);
output[copied..copied + count]
.copy_from_slice(&state.block[state.offset..state.offset + count]);
copied += count;
state.offset += count;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn equal_seeds_produce_equal_bytes_across_multiple_blocks() {
let left = TestEntropy::seeded([7; 32]);
let right = TestEntropy::seeded([7; 32]);
let mut left_bytes = [0; 80];
let mut right_bytes = [0; 80];
left.fill(&mut left_bytes);
right.fill(&mut right_bytes);
assert_eq!(left_bytes, right_bytes);
}
#[test]
fn clones_share_one_explicit_stream() {
let entropy = TestEntropy::seeded([3; 32]);
let clone = entropy.clone();
let baseline = TestEntropy::seeded([3; 32]);
let mut first = [0; 16];
let mut second = [0; 16];
let mut expected = [0; 32];
entropy.fill(&mut first);
clone.fill(&mut second);
baseline.fill(&mut expected);
assert_eq!([first.as_slice(), second.as_slice()].concat(), expected);
}
}