qssm_utils/
entropy_stats.rs1#![forbid(unsafe_code)]
4
5const MIN_BYTES_FOR_TEST: usize = 256;
6const CHI2_CRITICAL_P001: f64 = 340.0;
8const MIN_DISTINCT_BYTES: usize = 16;
10
11#[non_exhaustive]
12#[derive(Debug, Clone, PartialEq, thiserror::Error)]
13pub enum EntropyStatsError {
14 #[error("need at least {need} bytes for entropy stats (have {have})")]
15 TooShort { have: usize, need: usize },
16 #[error("chi-square vs uniform failed: statistic={statistic:.2} (critical {critical:.2})")]
17 ChiSquareUniform { statistic: f64, critical: f64 },
18 #[error("too few distinct byte values: {distinct} (minimum {min} for non-degenerate source)")]
19 LowDistinctCount { distinct: usize, min: usize },
20}
21
22pub fn validate_entropy_distribution(bytes: &[u8]) -> Result<(), EntropyStatsError> {
26 if bytes.len() < MIN_BYTES_FOR_TEST {
27 return Ok(());
28 }
29
30 let mut hist = [0u64; 256];
31 for &b in bytes {
32 hist[usize::from(b)] += 1;
33 }
34
35 let distinct = hist.iter().filter(|&&c| c > 0).count();
36 if distinct < MIN_DISTINCT_BYTES {
37 return Err(EntropyStatsError::LowDistinctCount {
38 distinct,
39 min: MIN_DISTINCT_BYTES,
40 });
41 }
42
43 let n = bytes.len() as f64;
44 let exp = n / 256.0;
45 let mut chi2 = 0.0_f64;
46 for &c in &hist {
47 let o = c as f64;
48 let diff = o - exp;
49 chi2 += (diff * diff) / exp;
50 }
51
52 if chi2 > CHI2_CRITICAL_P001 {
53 return Err(EntropyStatsError::ChiSquareUniform {
54 statistic: chi2,
55 critical: CHI2_CRITICAL_P001,
56 });
57 }
58
59 Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn all_zero_fails_chi_or_distinct() {
68 let v = vec![0u8; 300];
69 let r = validate_entropy_distribution(&v);
70 assert!(r.is_err());
71 }
72
73 #[test]
74 fn alternating_two_bytes_fails_distinct() {
75 let v: Vec<u8> = (0..300).map(|i| if i % 2 == 0 { 0 } else { 1 }).collect();
76 let r = validate_entropy_distribution(&v);
77 assert!(matches!(r, Err(EntropyStatsError::LowDistinctCount { .. })));
78 }
79
80 #[test]
81 fn short_slice_skipped() {
82 assert!(validate_entropy_distribution(&[1, 2, 3]).is_ok());
83 }
84}