mbr_forensic/wipe.rs
1//! Wipe-pattern classification for raw byte regions.
2//!
3//! Disk-wiping tools overwrite space with characteristic fills: all-zero,
4//! all-`0xFF`, a single repeated byte, an alternating two-byte pattern
5//! (`0x55`/`0xAA`), or pseudo-random data. On a static image only the final
6//! pass survives, but that pass still betrays a deliberate wipe — an
7//! anti-forensic / destruction trace. This module classifies a region's fill so
8//! the pipeline can distinguish *deliberately overwritten* space from ordinary
9//! unallocated (zero) space.
10
11use crate::entropy::{self, HIGH_ENTROPY_THRESHOLD};
12
13/// The dominant fill pattern of a byte region.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize))]
16pub enum FillPattern {
17 /// Entirely `0x00` — ordinary unallocated space (not a deliberate-wipe signal).
18 Zeros,
19 /// Entirely `0xFF`.
20 Ones,
21 /// Entirely one repeated byte value (other than `0x00` / `0xFF`).
22 Uniform(u8),
23 /// A repeating two-byte alternation `a, b, a, b, …` with `a != b`.
24 Alternating(u8, u8),
25 /// Near-maximal Shannon entropy — pseudo-random or encrypted fill.
26 HighEntropy,
27 /// No dominant pattern — ordinary structured data.
28 Mixed,
29}
30
31impl FillPattern {
32 /// `true` when this pattern is the signature of a *deliberate* overwrite.
33 ///
34 /// All-zero space is excluded: it is the default state of unallocated
35 /// sectors and carries no destruction signal on its own.
36 #[must_use]
37 pub fn is_deliberate_wipe(self) -> bool {
38 matches!(
39 self,
40 FillPattern::Ones | FillPattern::Uniform(_) | FillPattern::Alternating(_, _)
41 )
42 }
43
44 /// Short human-readable label, e.g. `"all 0xFF"` or `"alternating 0x55/0xAA"`.
45 #[must_use]
46 pub fn label(self) -> String {
47 match self {
48 FillPattern::Zeros => "all 0x00".to_string(),
49 FillPattern::Ones => "all 0xFF".to_string(),
50 FillPattern::Uniform(b) => format!("uniform {b:#04X}"),
51 FillPattern::Alternating(a, b) => format!("alternating {a:#04X}/{b:#04X}"),
52 FillPattern::HighEntropy => "high-entropy (random/encrypted)".to_string(),
53 FillPattern::Mixed => "mixed".to_string(),
54 }
55 }
56}
57
58/// Classify the dominant fill pattern of `data`.
59///
60/// An empty slice classifies as [`FillPattern::Mixed`] (nothing to judge).
61#[must_use]
62pub fn classify(data: &[u8]) -> FillPattern {
63 if data.is_empty() {
64 return FillPattern::Mixed;
65 }
66
67 let first = data[0];
68 if data.iter().all(|&b| b == first) {
69 return match first {
70 0x00 => FillPattern::Zeros,
71 0xFF => FillPattern::Ones,
72 other => FillPattern::Uniform(other),
73 };
74 }
75
76 if data.len() >= 2 {
77 let (a, b) = (data[0], data[1]);
78 if a != b
79 && data
80 .iter()
81 .enumerate()
82 .all(|(i, &x)| x == if i % 2 == 0 { a } else { b })
83 {
84 return FillPattern::Alternating(a, b);
85 }
86 }
87
88 if entropy::shannon(data) > HIGH_ENTROPY_THRESHOLD {
89 return FillPattern::HighEntropy;
90 }
91
92 FillPattern::Mixed
93}