Skip to main content

qssm_utils/
entropy_stats.rs

1//! Byte-level entropy quality checks (chi-square vs uniform + simple pattern guards).
2
3#![forbid(unsafe_code)]
4
5const MIN_BYTES_FOR_TEST: usize = 256;
6/// Approximate critical value for χ²(df=255) at **p ≈ 0.001** (upper tail); conservative gate.
7const CHI2_CRITICAL_P001: f64 = 340.0;
8/// Reject if fewer than this many distinct byte values appear (repeating / narrow alphabet).
9const 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
22/// Pearson χ² test for **256** byte categories vs uniform; also rejects very low **distinct** counts.
23///
24/// For `len < `[`MIN_BYTES_FOR_TEST`], returns **`Ok(())`** (no gate — avoid false positives on tiny buffers).
25pub 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}