Skip to main content

lenso_test/
entropy.rs

1//! Explicit deterministic entropy for test-support seams only.
2
3use std::{cell::RefCell, rc::Rc};
4
5use sha2::{Digest, Sha256};
6
7/// A reproducible byte stream derived from an explicitly supplied test seed.
8///
9/// This type is not a cryptographic random-number generator for production
10/// work. It never installs a fallback into a Driver, Plugin, or security path;
11/// production code must continue to obtain its entropy from its production
12/// provider. A test-support seam must opt in by receiving this exact value.
13#[derive(Clone, Debug)]
14pub struct TestEntropy {
15    seed: [u8; 32],
16    state: Rc<RefCell<EntropyState>>,
17}
18
19#[derive(Debug)]
20struct EntropyState {
21    next_block: u64,
22    block: [u8; 32],
23    offset: usize,
24}
25
26impl TestEntropy {
27    /// Creates a new deterministic stream beginning at the first block.
28    pub fn seeded(seed: [u8; 32]) -> Self {
29        Self {
30            seed,
31            state: Rc::new(RefCell::new(EntropyState {
32                next_block: 0,
33                block: [0; 32],
34                offset: 32,
35            })),
36        }
37    }
38
39    /// Fills `output` with the next stable test-only bytes.
40    pub fn fill(&self, output: &mut [u8]) {
41        let mut copied = 0;
42        let mut state = self.state.borrow_mut();
43        while copied < output.len() {
44            if state.offset == state.block.len() {
45                let mut hasher = Sha256::new();
46                hasher.update(b"lenso-test-entropy-v1\0");
47                hasher.update(self.seed);
48                hasher.update(state.next_block.to_be_bytes());
49                state.block.copy_from_slice(&hasher.finalize());
50                state.next_block = state
51                    .next_block
52                    .checked_add(1)
53                    .expect("a test entropy stream cannot exceed u64 blocks");
54                state.offset = 0;
55            }
56            let remaining = output.len() - copied;
57            let available = state.block.len() - state.offset;
58            let count = remaining.min(available);
59            output[copied..copied + count]
60                .copy_from_slice(&state.block[state.offset..state.offset + count]);
61            copied += count;
62            state.offset += count;
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn equal_seeds_produce_equal_bytes_across_multiple_blocks() {
73        let left = TestEntropy::seeded([7; 32]);
74        let right = TestEntropy::seeded([7; 32]);
75        let mut left_bytes = [0; 80];
76        let mut right_bytes = [0; 80];
77
78        left.fill(&mut left_bytes);
79        right.fill(&mut right_bytes);
80
81        assert_eq!(left_bytes, right_bytes);
82    }
83
84    #[test]
85    fn clones_share_one_explicit_stream() {
86        let entropy = TestEntropy::seeded([3; 32]);
87        let clone = entropy.clone();
88        let baseline = TestEntropy::seeded([3; 32]);
89        let mut first = [0; 16];
90        let mut second = [0; 16];
91        let mut expected = [0; 32];
92
93        entropy.fill(&mut first);
94        clone.fill(&mut second);
95        baseline.fill(&mut expected);
96
97        assert_eq!([first.as_slice(), second.as_slice()].concat(), expected);
98    }
99}