use crate::schema::{EntropyResult, EntropyWindow};
fn round6(x: f64) -> f64 {
let r = (x * 1_000_000.0).round() / 1_000_000.0;
if r == 0.0 {
0.0
} else {
r
}
}
pub fn shannon_entropy(data: &[u8]) -> f64 {
if data.is_empty() {
return 0.0;
}
let mut counts = [0u64; 256];
for &b in data {
counts[b as usize] += 1;
}
let len = data.len() as f64;
counts
.iter()
.filter(|&&c| c > 0)
.map(|&c| {
let p = c as f64 / len;
-p * p.log2()
})
.sum()
}
pub fn build_entropy_result(
data: &[u8],
file: &str,
window_size: usize,
high_threshold: f64,
) -> EntropyResult {
let mut windows = Vec::new();
if window_size > 0 && !data.is_empty() {
let mut index = 0u32;
let mut start = 0usize;
while start < data.len() {
let end = (start + window_size).min(data.len()); let length = end - start;
let entropy = round6(shannon_entropy(&data[start..end]));
windows.push(EntropyWindow {
index,
start_offset: start as u64,
start_hex: format!("0x{:X}", start),
end_offset: (start + length - 1) as u64, length: length as u32,
entropy,
is_high: entropy >= high_threshold,
});
index += 1;
start += length;
}
}
let high_window_count = windows.iter().filter(|w| w.is_high).count() as u32;
EntropyResult {
file: file.to_string(),
file_size: data.len() as u64,
window_size: window_size as u32,
window_count: windows.len() as u32,
overall_entropy: round6(shannon_entropy(data)),
high_entropy_threshold: high_threshold,
high_window_count,
windows,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entropy_of_a_single_repeated_byte_is_zero() {
assert_eq!(shannon_entropy(&[0xAA; 64]), 0.0);
}
#[test]
fn zero_entropy_serializes_as_positive_zero() {
assert!(round6(shannon_entropy(&[0x00; 256])).is_sign_positive());
let r = build_entropy_result(&[0u8; 512], "x", 256, 7.0);
assert!(
r.overall_entropy.is_sign_positive(),
"overall entropy must be +0.0, got {:?}",
r.overall_entropy
);
assert!(
r.windows.iter().all(|w| w.entropy.is_sign_positive()),
"every window entropy must be +0.0"
);
}
#[test]
fn entropy_of_a_balanced_two_value_buffer_is_one_bit() {
let data: Vec<u8> = (0..64u8).map(|i| i % 2).collect();
assert!((shannon_entropy(&data) - 1.0).abs() < 1e-9);
}
#[test]
fn entropy_of_a_uniform_distribution_is_eight_bits() {
let data: Vec<u8> = (0..=255u8).collect();
assert!((shannon_entropy(&data) - 8.0).abs() < 1e-9);
}
#[test]
fn windows_are_contiguous_with_inclusive_end_offsets() {
let data = vec![0u8; 300];
let r = build_entropy_result(&data, "x", 256, 7.0);
assert_eq!(r.window_count, 2);
assert_eq!(r.windows[0].start_offset, 0);
assert_eq!(r.windows[0].end_offset, 255); assert_eq!(r.windows[1].start_offset, 256);
assert_eq!(r.windows[1].end_offset, 299);
assert_eq!(r.windows[1].length, 44);
}
#[test]
fn high_entropy_windows_are_flagged_at_the_threshold() {
let data: Vec<u8> = (0..=255u8).collect(); let r = build_entropy_result(&data, "x", 256, 7.0);
assert_eq!(r.high_window_count, 1);
assert!(r.windows[0].is_high);
}
}