1#[derive(Debug, Clone)]
35pub struct EntropyMetrics {
36 pub shannon_entropy: f64,
38 pub min_entropy: f64,
40 pub collision_entropy: f64,
42 pub chi_squared: f64,
44 pub chi_squared_p: f64,
46 pub serial_correlation: f64,
48 pub longest_run: usize,
50 pub bit_bias: f64,
52 pub unique_bytes: usize,
54}
55
56pub fn analyze(data: &[u8]) -> EntropyMetrics {
64 if data.is_empty() {
65 return EntropyMetrics {
66 shannon_entropy: 0.0,
67 min_entropy: 0.0,
68 collision_entropy: 0.0,
69 chi_squared: 0.0,
70 chi_squared_p: 1.0,
71 serial_correlation: 0.0,
72 longest_run: 0,
73 bit_bias: 0.0,
74 unique_bytes: 0,
75 };
76 }
77
78 let n = data.len();
79
80 let mut freq = [0usize; 256];
82 for &b in data {
83 freq[b as usize] += 1;
84 }
85
86 let unique_bytes = freq.iter().filter(|&&c| c > 0).count();
87
88 let mut shannon = 0.0_f64;
90 let mut max_freq = 0usize;
91 let mut collision_sum = 0.0_f64;
92 for &count in &freq {
93 if count > 0 {
94 let p = count as f64 / n as f64;
95 shannon -= p * p.log2();
96 collision_sum += p * p;
97 }
98 if count > max_freq {
99 max_freq = count;
100 }
101 }
102
103 let min_entropy = -((max_freq as f64) / (n as f64)).log2();
105
106 let collision_entropy = -collision_sum.log2();
108
109 let expected = n as f64 / 256.0;
111 let chi_squared = freq
112 .iter()
113 .map(|&count| {
114 let diff = count as f64 - expected;
115 diff * diff / expected
116 })
117 .sum::<f64>();
118 let chi_squared_p = chi2_p_value(chi_squared, 255);
119
120 let serial_correlation = compute_serial_correlation(data);
122
123 let longest_run = compute_longest_bit_run(data);
125
126 let total_bits = n * 8;
128 let ones: usize = data.iter().map(|b| b.count_ones() as usize).sum();
129 let bit_bias = ones as f64 / total_bits as f64;
130
131 EntropyMetrics {
132 shannon_entropy: shannon,
133 min_entropy,
134 collision_entropy,
135 chi_squared,
136 chi_squared_p,
137 serial_correlation,
138 longest_run,
139 bit_bias,
140 unique_bytes,
141 }
142}
143
144#[derive(Debug)]
150pub struct QualityRequirements {
151 pub min_shannon: f64,
153 pub min_min_entropy: f64,
155 pub min_collision: f64,
157 pub min_chi_squared_p: f64,
159 pub max_serial_correlation: f64,
161 pub max_bit_bias_deviation: f64,
163 pub max_longest_run: usize,
165}
166
167impl QualityRequirements {
168 pub fn for_bits(bits: u32) -> Self {
170 match bits {
171 128 => QualityRequirements {
172 min_shannon: 3.8,
173 min_min_entropy: 3.5,
174 min_collision: 3.8,
175 min_chi_squared_p: 0.0001,
176 max_serial_correlation: 0.7,
177 max_bit_bias_deviation: 0.2,
178 max_longest_run: 25,
179 },
180 192 => QualityRequirements {
181 min_shannon: 4.2,
182 min_min_entropy: 3.75,
183 min_collision: 4.2,
184 min_chi_squared_p: 0.0001,
185 max_serial_correlation: 0.6,
186 max_bit_bias_deviation: 0.15,
187 max_longest_run: 20,
188 },
189 _ => QualityRequirements {
191 min_shannon: 4.8,
192 min_min_entropy: 4.0,
193 min_collision: 4.8,
194 min_chi_squared_p: 0.0001,
195 max_serial_correlation: 0.5,
196 max_bit_bias_deviation: 0.12,
197 max_longest_run: 20,
198 },
199 }
200 }
201}
202
203pub fn check_quality(metrics: &EntropyMetrics, bits: u32) -> (bool, Vec<String>) {
207 let req = QualityRequirements::for_bits(bits);
208 let mut warnings = Vec::new();
209
210 if metrics.shannon_entropy < req.min_shannon {
211 warnings.push(format!(
212 "Shannon entropy ({:.2}) below minimum ({:.2})",
213 metrics.shannon_entropy, req.min_shannon
214 ));
215 }
216 if metrics.min_entropy < req.min_min_entropy {
217 warnings.push(format!(
218 "Min entropy ({:.2}) below minimum ({:.2})",
219 metrics.min_entropy, req.min_min_entropy
220 ));
221 }
222 if metrics.collision_entropy < req.min_collision {
223 warnings.push(format!(
224 "Collision entropy ({:.2}) below minimum ({:.2})",
225 metrics.collision_entropy, req.min_collision
226 ));
227 }
228 if metrics.chi_squared_p < req.min_chi_squared_p {
229 warnings.push(format!(
230 "Chi-squared p-value ({:.6}) below minimum ({:.6})",
231 metrics.chi_squared_p, req.min_chi_squared_p
232 ));
233 }
234 if metrics.serial_correlation.abs() > req.max_serial_correlation {
235 warnings.push(format!(
236 "Serial correlation ({:.4}) exceeds maximum ({:.1})",
237 metrics.serial_correlation, req.max_serial_correlation
238 ));
239 }
240 if (metrics.bit_bias - 0.5).abs() > req.max_bit_bias_deviation {
241 warnings.push(format!(
242 "Bit bias ({:.4}) deviates more than {:.2} from 0.5",
243 metrics.bit_bias, req.max_bit_bias_deviation
244 ));
245 }
246 if metrics.longest_run > req.max_longest_run {
247 warnings.push(format!(
248 "Longest run ({}) exceeds maximum ({})",
249 metrics.longest_run, req.max_longest_run
250 ));
251 }
252
253 (warnings.is_empty(), warnings)
254}
255
256fn chi2_p_value(chi2: f64, df: usize) -> f64 {
262 if df == 0 {
263 return 1.0;
264 }
265 let df_f = df as f64;
267 let x = (chi2 / df_f).powf(1.0 / 3.0);
268 let mean = 1.0 - 2.0 / (9.0 * df_f);
269 let std_dev = (2.0 / (9.0 * df_f)).sqrt();
270
271 if std_dev == 0.0 {
272 return 1.0;
273 }
274
275 let z = (x - mean) / std_dev;
276
277 normal_cdf(z)
279}
280
281fn normal_cdf(z: f64) -> f64 {
283 let t = 1.0 / (1.0 + 0.2316419 * z.abs());
284 let d = 0.3989422804014327; let prob = d
286 * (-z * z / 2.0).exp()
287 * t
288 * (0.319381530
289 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
290
291 if z > 0.0 {
292 1.0 - prob
293 } else {
294 prob
295 }
296}
297
298fn compute_serial_correlation(data: &[u8]) -> f64 {
300 if data.len() <= 1 {
301 return 0.0;
302 }
303
304 let n = data.len() as f64;
305 let mean: f64 = data.iter().map(|&b| b as f64).sum::<f64>() / n;
306
307 let mut numerator = 0.0_f64;
308 let mut denominator = 0.0_f64;
309
310 let deviations: Vec<f64> = data.iter().map(|&b| b as f64 - mean).collect();
311
312 for i in 0..deviations.len() {
313 denominator += deviations[i] * deviations[i];
314 if i > 0 {
315 numerator += deviations[i - 1] * deviations[i];
316 }
317 }
318
319 if denominator == 0.0 {
320 return 0.0;
321 }
322
323 let corr = numerator / denominator;
324 if corr.is_nan() {
325 0.0
326 } else {
327 corr
328 }
329}
330
331fn compute_longest_bit_run(data: &[u8]) -> usize {
333 if data.is_empty() {
334 return 0;
335 }
336
337 let mut longest = 1usize;
338 let mut current = 1usize;
339 let mut prev_bit: Option<bool> = None;
340
341 for &byte in data {
342 for shift in (0..8).rev() {
343 let bit = (byte >> shift) & 1 == 1;
344 if let Some(pb) = prev_bit {
345 if bit == pb {
346 current += 1;
347 if current > longest {
348 longest = current;
349 }
350 } else {
351 current = 1;
352 }
353 }
354 prev_bit = Some(bit);
355 }
356 }
357
358 longest
359}
360
361#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn perfect_random_has_high_entropy() {
371 let mut data = vec![0u8; 1024];
373 for i in 0..data.len() {
374 data[i] = ((i as u64 * 1103515245 + 12345) >> 16) as u8;
375 }
376 let m = analyze(&data);
377 assert!(
378 m.shannon_entropy > 5.0,
379 "Shannon too low: {}",
380 m.shannon_entropy
381 );
382 }
383
384 #[test]
385 fn constant_data_has_zero_entropy() {
386 let data = [0u8; 256];
387 let m = analyze(&data);
388 assert!(
389 m.shannon_entropy < 0.1,
390 "Shannon should be ~0: {}",
391 m.shannon_entropy
392 );
393 assert!(m.min_entropy < 0.1);
394 assert_eq!(m.unique_bytes, 1);
395 }
396
397 #[test]
398 fn alternating_bytes() {
399 let data: Vec<u8> = (0..256)
400 .map(|i| if i % 2 == 0 { 0xAA } else { 0x55 })
401 .collect();
402 let m = analyze(&data);
403 assert!(
404 m.shannon_entropy < 2.0,
405 "Alternating should have low entropy"
406 );
407 assert_eq!(m.unique_bytes, 2);
408 }
409
410 #[test]
411 fn quality_check_passes_for_good_data() {
412 let mut data = vec![0u8; 512];
413 for i in 0..data.len() {
414 data[i] = ((i as u64)
415 .wrapping_mul(6364136223846793005)
416 .wrapping_add(1442695040888963407)
417 >> 32) as u8;
418 }
419 let m = analyze(&data);
420 let (pass, warnings) = check_quality(&m, 256);
421 if !pass {
422 eprintln!("Warnings: {warnings:?}");
423 }
424 }
426
427 #[test]
428 fn quality_check_fails_for_constant() {
429 let data = [0u8; 256];
430 let m = analyze(&data);
431 let (pass, warnings) = check_quality(&m, 256);
432 assert!(!pass, "Constant data should fail quality check");
433 assert!(!warnings.is_empty());
434 }
435
436 #[test]
437 fn serial_correlation_zero_for_random() {
438 let mut data = vec![0u8; 1024];
439 for i in 0..data.len() {
440 data[i] = ((i as u64).wrapping_mul(6364136223846793005) >> 32) as u8;
441 }
442 let m = analyze(&data);
443 assert!(
444 m.serial_correlation.abs() < 0.3,
445 "Serial correlation too high: {}",
446 m.serial_correlation
447 );
448 }
449
450 #[test]
451 fn empty_input_no_panic() {
452 let m = analyze(&[]);
453 assert_eq!(m.shannon_entropy, 0.0);
454 }
455
456 #[test]
457 fn bit_bias_near_half() {
458 let mut data = vec![0u8; 1024];
459 for i in 0..data.len() {
460 data[i] = ((i as u64)
461 .wrapping_mul(6364136223846793005)
462 .wrapping_add(1442695040888963407)
463 >> 32) as u8;
464 }
465 let m = analyze(&data);
466 assert!(
467 (m.bit_bias - 0.5).abs() < 0.1,
468 "Bit bias too far from 0.5: {}",
469 m.bit_bias
470 );
471 }
472}