pub fn shannon_entropy(bytes: &[u8]) -> f32 {
if bytes.is_empty() {
return 0.0;
}
let mut freq = [0u32; 256];
for b in bytes {
freq[*b as usize] += 1;
}
let len = bytes.len() as f32;
let mut h = 0.0f32;
for f in freq.iter() {
if *f == 0 {
continue;
}
let p = (*f as f32) / len;
h -= p * p.log2();
}
h
}
pub fn ks_d_against_uniform_base64(bytes: &[u8]) -> f32 {
if bytes.is_empty() {
return 1.0;
}
let mut counts = [0u32; 64];
let mut total = 0u32;
for b in bytes {
if let Some(idx) = base64url_index(*b) {
counts[idx] += 1;
total += 1;
}
}
if total == 0 {
return 1.0;
}
let mut emp_cdf = 0.0f32;
let mut max_d = 0.0f32;
let total_f = total as f32;
for (i, c) in counts.iter().enumerate() {
emp_cdf += (*c as f32) / total_f;
let uniform_cdf = ((i + 1) as f32) / 64.0;
let d = (emp_cdf - uniform_cdf).abs();
if d > max_d {
max_d = d;
}
}
max_d.clamp(0.0, 1.0)
}
fn base64url_index(b: u8) -> Option<usize> {
match b {
b'A'..=b'Z' => Some((b - b'A') as usize),
b'a'..=b'z' => Some(26 + (b - b'a') as usize),
b'0'..=b'9' => Some(52 + (b - b'0') as usize),
b'-' => Some(62),
b'_' => Some(63),
_ => None,
}
}
pub fn markov_transition_score(bytes: &[u8]) -> f32 {
if bytes.len() < 2 {
return 0.5;
}
const REAL_TRANS: [[f32; 4]; 4] = [
[0.10, 0.35, 0.25, 0.30],
[0.30, 0.20, 0.25, 0.25],
[0.25, 0.25, 0.20, 0.30],
[0.30, 0.30, 0.30, 0.10],
];
let mut log_sum = 0.0f64;
let mut n = 0;
let mut prev = class_of(bytes[0]);
for b in &bytes[1..] {
let cur = class_of(*b);
let p = REAL_TRANS[prev][cur].max(1e-6);
log_sum += (p as f64).ln();
prev = cur;
n += 1;
}
if n == 0 {
return 0.5;
}
let mean_log_p = log_sum / (n as f64);
let p_mean = mean_log_p.exp() as f32;
(p_mean * 2.0).clamp(0.0, 1.0)
}
fn class_of(b: u8) -> usize {
match b {
b'a' | b'e' | b'i' | b'o' | b'u' | b'A' | b'E' | b'I' | b'O' | b'U' => 0,
b'a'..=b'z' | b'A'..=b'Z' => 1,
b'0'..=b'9' => 2,
_ => 3,
}
}
pub fn chi_squared_normalised(bytes: &[u8]) -> f32 {
if bytes.is_empty() {
return 0.0;
}
let mut freq = [0u32; 256];
for b in bytes {
freq[*b as usize] += 1;
}
let len = bytes.len() as f32;
let expected = len / 256.0;
let mut chi2 = 0.0f32;
for f in freq.iter() {
let diff = (*f as f32) - expected;
chi2 += diff * diff / expected.max(1e-6);
}
let normalised = (chi2 / len).min(50.0);
1.0 - (normalised / 50.0).clamp(0.0, 1.0)
}
pub fn run_length_score(bytes: &[u8]) -> f32 {
if bytes.is_empty() {
return 0.0;
}
let mut run_hist = [0u32; 16];
let mut prev = bytes[0];
let mut cur_run = 1u32;
let mut runs = 0u32;
for b in &bytes[1..] {
if *b == prev {
cur_run += 1;
} else {
let idx = (cur_run.min(15)) as usize;
run_hist[idx] += 1;
runs += 1;
cur_run = 1;
prev = *b;
}
}
let idx = (cur_run.min(15)) as usize;
run_hist[idx] += 1;
runs += 1;
if runs == 0 {
return 0.0;
}
let p1 = (run_hist[1] as f32) / (runs as f32);
p1.clamp(0.0, 1.0)
}
pub fn compressibility_score(bytes: &[u8]) -> f32 {
if bytes.len() < 16 {
return 0.5;
}
let approx = lz_estimate(bytes);
let ratio = (approx as f32) / (bytes.len() as f32);
ratio.clamp(0.0, 1.0)
}
fn lz_estimate(bytes: &[u8]) -> usize {
let n = bytes.len();
let mut emitted = 0usize;
let mut i = 0usize;
let window = 64;
while i < n {
let start = i.saturating_sub(window);
let mut best_len = 0usize;
let mut j = start;
while j < i {
let mut k = 0usize;
while i + k < n && bytes[j + k] == bytes[i + k] && k < 16 {
k += 1;
}
if k > best_len {
best_len = k;
}
j += 1;
}
if best_len < 3 {
emitted += 1;
i += 1;
} else {
emitted += 1;
i += best_len;
}
}
emitted
}
pub fn ascii_concentration(bytes: &[u8]) -> f32 {
if bytes.is_empty() {
return 0.0;
}
let printable = bytes.iter().filter(|&&b| (32u8..=126).contains(&b)).count();
(printable as f32) / (bytes.len() as f32)
}
pub fn dot_segment_variation(token: &str) -> f32 {
let segs: Vec<usize> = token.split('.').map(|s| s.len()).collect();
if segs.len() < 2 {
return 0.5;
}
let n = segs.len() as f32;
let mean = segs.iter().map(|&l| l as f32).sum::<f32>() / n;
if mean <= 0.0 {
return 0.0;
}
let var = segs
.iter()
.map(|&l| {
let d = (l as f32) - mean;
d * d
})
.sum::<f32>()
/ n;
let cv = var.sqrt() / mean;
if cv <= 0.7 {
1.0
} else if cv < 1.5 {
1.0 - (cv - 0.7) / 0.8
} else {
0.0
}
.clamp(0.0, 1.0)
}
pub fn hex_ratio(bytes: &[u8]) -> f32 {
if bytes.is_empty() {
return 0.0;
}
let mut hex = 0usize;
let mut bu = 0usize;
for b in bytes {
if b.is_ascii_hexdigit() {
hex += 1;
}
if base64url_index(*b).is_some() {
bu += 1;
}
}
if bu == 0 {
return 0.0;
}
let r = (hex as f32) / (bu as f32);
if (0.2..=0.6).contains(&r) {
1.0
} else if r > 0.6 {
1.0 - (r - 0.6) / 0.4
} else {
r / 0.2
}
.clamp(0.0, 1.0)
}
pub fn bigram_coverage(bytes: &[u8]) -> f32 {
const REAL_BIGRAMS: &[[u8; 2]] = &[
*b"aB", *b"Bc", *b"cD", *b"De", *b"eF", *b"Fg", *b"gH", *b"Hi", *b"iJ", *b"Jk", *b"kL",
*b"Lm", *b"Mn", *b"No", *b"Op", *b"Pq", *b"qR", *b"Rs", *b"St", *b"Tu", *b"Uv", *b"Vw",
*b"Wx", *b"Xy", *b"Yz", *b"Z0", *b"01", *b"12", *b"23", *b"34", *b"45", *b"56",
];
if bytes.len() < 2 {
return 0.0;
}
let mut hits = 0u32;
let total = (bytes.len() - 1) as u32;
for i in 0..bytes.len() - 1 {
let pair = [bytes[i], bytes[i + 1]];
if REAL_BIGRAMS.contains(&pair) {
hits += 1;
}
}
let mut score = (hits as f32) / (total as f32);
if score > 0.05 {
score = (score - 0.05) / 0.10;
} else {
score = 0.0;
}
score.clamp(0.0, 1.0)
}