Skip to main content

mbr_forensic/
partition.rs

1//! MBR partition entry types and partition-type-code semantics.
2
3/// Decoded CHS (Cylinder-Head-Sector) address.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize))]
6pub struct Chs {
7    pub cylinder: u16,
8    pub head: u8,
9    pub sector: u8,
10}
11
12impl Chs {
13    /// Decode a 3-byte MBR CHS field.
14    ///
15    /// Byte layout (packed):
16    /// ```text
17    /// byte 0 = head
18    /// byte 1 = [cyl_hi(7:6) | sector(5:0)]
19    /// byte 2 = cyl_lo(7:0)
20    /// ```
21    #[must_use]
22    pub fn from_bytes(b: [u8; 3]) -> Self {
23        let head = b[0];
24        let sector = b[1] & 0x3F;
25        let cylinder = ((b[1] as u16 & 0xC0) << 2) | b[2] as u16;
26        Chs {
27            cylinder,
28            head,
29            sector,
30        }
31    }
32
33    /// Convert to an approximate LBA (≤1023 cylinders, ≤255 heads, ≤63 sectors).
34    ///
35    /// Returns `None` when CHS indicates "not used" (all zeros or all ones).
36    #[must_use]
37    pub fn to_lba(self, heads_per_cylinder: u8, sectors_per_track: u8) -> Option<u32> {
38        if self.sector == 0 {
39            return None;
40        }
41        let hpc = heads_per_cylinder as u32;
42        let spt = sectors_per_track as u32;
43        if hpc == 0 || spt == 0 {
44            return None;
45        }
46        Some(
47            (self.cylinder as u32) * hpc * spt
48                + (self.head as u32) * spt
49                + (self.sector as u32 - 1),
50        )
51    }
52
53    /// `true` when the CHS field is the all-zero "unused" convention.
54    ///
55    /// LBA-only tooling routinely zeroes the CHS fields; this is never an
56    /// inconsistency.
57    #[must_use]
58    pub fn is_unused(self) -> bool {
59        self.cylinder == 0 && self.head == 0 && self.sector == 0
60    }
61
62    /// `true` when the CHS field is the maximum/overflow marker written when a
63    /// partition's start or end exceeds the CHS-addressable range.
64    ///
65    /// The canonical encoding is `FE FF FF` (head 254, sector 63, cylinder
66    /// 1023); some tools write `FF FF FF` (head 255). Both are accepted.
67    #[must_use]
68    pub fn is_overflow_marker(self) -> bool {
69        self.cylinder == CHS_MAX_CYLINDER
70            && self.sector == STD_SECTORS_PER_TRACK
71            && (self.head == 254 || self.head == 255)
72    }
73}
74
75/// De-facto standard LBA-assist geometry: 255 heads per cylinder. Virtually
76/// every modern MBR partitioning tool (fdisk, Windows, gparted) uses this when
77/// translating between CHS and LBA.
78pub const STD_HEADS_PER_CYL: u8 = 255;
79/// De-facto standard LBA-assist geometry: 63 sectors per track.
80pub const STD_SECTORS_PER_TRACK: u8 = 63;
81/// Highest cylinder representable in the packed 10-bit CHS cylinder field.
82const CHS_MAX_CYLINDER: u16 = 1023;
83
84/// Outcome of comparing a CHS address against its companion LBA value.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize))]
87pub enum ChsConsistency {
88    /// CHS translates to the LBA (or is the accepted overflow marker).
89    Consistent,
90    /// CHS is the all-zero "unused" convention — never an inconsistency.
91    Unused,
92    /// CHS contradicts the LBA — a hallmark of a hand-edited table.
93    Inconsistent,
94}
95
96/// Highest LBA addressable by the packed CHS scheme under the given geometry.
97///
98/// CHS covers cylinders `0..=1023`, heads `0..=hpc-1`, sectors `1..=spt`, so the
99/// addressable count is `1024 * hpc * spt` sectors (LBA `0` through count−1).
100#[must_use]
101fn max_chs_addressable_lba(hpc: u8, spt: u8) -> u64 {
102    1024u64 * hpc as u64 * spt as u64 - 1
103}
104
105/// Classify whether a packed CHS address is consistent with its LBA companion.
106///
107/// Conservative by design (per the no-false-positives mandate): the all-zero
108/// "unused" convention and the overflow marker are always accepted, and a CHS
109/// is only reported [`ChsConsistency::Inconsistent`] when it purports to be a
110/// real address that does not translate to the LBA under the given geometry.
111#[must_use]
112pub fn chs_consistency(chs: Chs, lba: u32, hpc: u8, spt: u8) -> ChsConsistency {
113    if chs.is_unused() {
114        return ChsConsistency::Unused;
115    }
116    if chs.is_overflow_marker() {
117        return ChsConsistency::Consistent;
118    }
119    if u64::from(lba) > max_chs_addressable_lba(hpc, spt) {
120        // Beyond CHS range, a well-formed table must use the overflow marker;
121        // a concrete small CHS here means the address was crafted by hand.
122        return ChsConsistency::Inconsistent;
123    }
124    match chs.to_lba(hpc, spt) {
125        Some(translated) if translated == lba => ChsConsistency::Consistent,
126        _ => ChsConsistency::Inconsistent,
127    }
128}
129
130/// A single 16-byte primary partition table entry.
131#[derive(Debug, Clone, PartialEq, Eq)]
132#[cfg_attr(feature = "serde", derive(serde::Serialize))]
133pub struct PartitionEntry {
134    /// `0x80` = bootable, `0x00` = inactive, other values are invalid.
135    pub status: u8,
136    /// CHS address of the partition's first sector.
137    pub chs_first: Chs,
138    /// Partition type code.
139    pub type_code: TypeCode,
140    /// CHS address of the partition's last sector.
141    pub chs_last: Chs,
142    /// LBA address of the partition's first sector.
143    pub lba_start: u32,
144    /// Number of sectors in the partition.
145    pub lba_count: u32,
146}
147
148impl PartitionEntry {
149    /// Decode a 16-byte partition entry slice.
150    #[must_use]
151    pub fn from_bytes(b: &[u8; 16]) -> Self {
152        PartitionEntry {
153            status: b[0],
154            chs_first: Chs::from_bytes([b[1], b[2], b[3]]),
155            type_code: TypeCode(b[4]),
156            chs_last: Chs::from_bytes([b[5], b[6], b[7]]),
157            lba_start: u32::from_le_bytes([b[8], b[9], b[10], b[11]]),
158            lba_count: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
159        }
160    }
161
162    /// Returns `true` if this entry is entirely zero (unused slot).
163    #[must_use]
164    pub fn is_empty(&self) -> bool {
165        self.type_code.is_empty() && self.lba_start == 0 && self.lba_count == 0
166    }
167
168    /// Returns `true` if the status byte marks this partition as bootable.
169    #[must_use]
170    pub fn is_bootable(&self) -> bool {
171        self.status == 0x80
172    }
173
174    /// Inclusive last LBA of this partition, saturating on overflow.
175    #[must_use]
176    pub fn lba_end(&self) -> u32 {
177        self.lba_start
178            .saturating_add(self.lba_count)
179            .saturating_sub(1)
180    }
181
182    /// `true` if this entry describes an extended partition container.
183    #[must_use]
184    pub fn is_extended(&self) -> bool {
185        self.type_code.is_extended()
186    }
187}
188
189/// Wrapper around an MBR partition type byte with semantic helpers.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
191#[cfg_attr(feature = "serde", derive(serde::Serialize))]
192pub struct TypeCode(pub u8);
193
194impl TypeCode {
195    /// Human-readable short name for the partition type.
196    ///
197    /// Sourced from the `forensicnomicon` knowledge base (Brouwer's partition
198    /// identifier list / Wikipedia); unrecognised codes return `"Unknown"`.
199    #[must_use]
200    pub fn name(self) -> &'static str {
201        forensicnomicon::partition_types::type_name(self.0).unwrap_or("Unknown")
202    }
203
204    /// High-level partition family classification.
205    #[must_use]
206    pub fn family(self) -> PartitionFamily {
207        match self.0 {
208            0x00 => PartitionFamily::Empty,
209            0x01 | 0x11 => PartitionFamily::Fat12,
210            0x04 | 0x06 | 0x0E | 0x14 | 0x16 | 0x1E => PartitionFamily::Fat16,
211            0x0B | 0x0C | 0x1B | 0x1C => PartitionFamily::Fat32,
212            0x07 | 0x17 | 0x87 => PartitionFamily::Ntfs,
213            0x05 | 0x0F | 0x85 => PartitionFamily::ExtendedMbr,
214            0x82 => PartitionFamily::LinuxSwap,
215            0x83 => PartitionFamily::Linux,
216            0x8E => PartitionFamily::LinuxLvm,
217            0xFD => PartitionFamily::LinuxRaid,
218            0x27 => PartitionFamily::WindowsRecovery,
219            0x42 => PartitionFamily::WindowsDynamic,
220            0xA5 => PartitionFamily::FreeBsd,
221            0xA6 => PartitionFamily::OpenBsd,
222            0xA9 => PartitionFamily::NetBsd,
223            0xAF | 0xAB => PartitionFamily::Hfs,
224            0xEE => PartitionFamily::GptProtective,
225            0xEF => PartitionFamily::EfiSystem,
226            0xFB | 0xFC => PartitionFamily::Vmware,
227            _ => PartitionFamily::Unknown(self.0),
228        }
229    }
230
231    /// `true` if this is an empty (unused) slot.
232    #[must_use]
233    pub fn is_empty(self) -> bool {
234        self.0 == 0x00
235    }
236
237    /// `true` if this type code marks an extended partition container.
238    #[must_use]
239    pub fn is_extended(self) -> bool {
240        matches!(self.0, 0x05 | 0x0F | 0x85)
241    }
242}
243
244/// High-level classification of a partition type.
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246#[cfg_attr(feature = "serde", derive(serde::Serialize))]
247pub enum PartitionFamily {
248    Empty,
249    Fat12,
250    Fat16,
251    Fat32,
252    Ntfs,
253    ExtendedMbr,
254    LinuxSwap,
255    Linux,
256    LinuxLvm,
257    LinuxRaid,
258    WindowsRecovery,
259    WindowsDynamic,
260    FreeBsd,
261    OpenBsd,
262    NetBsd,
263    Hfs,
264    GptProtective,
265    EfiSystem,
266    Vmware,
267    Unknown(u8),
268}