Skip to main content

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}