Skip to main content

hadris_part/
scheme.rs

1//! Unified partition scheme handling.
2//!
3//! This module provides a unified API for working with different partition schemes:
4//! - MBR (Master Boot Record)
5//! - GPT (GUID Partition Table)
6//! - Hybrid MBR (GPT with MBR entries for BIOS compatibility)
7
8#[cfg(feature = "alloc")]
9extern crate alloc;
10
11#[cfg(feature = "alloc")]
12use alloc::vec::Vec;
13
14#[cfg(feature = "alloc")]
15use crate::error::{Error, Result};
16use crate::gpt::Guid;
17#[cfg(feature = "alloc")]
18use crate::gpt::{GptHeader, GptPartitionEntry};
19use crate::hybrid::is_hybrid_mbr;
20use crate::mbr::MasterBootRecord;
21#[cfg(feature = "alloc")]
22use endian_num::Le;
23
24/// The type of partition scheme detected or to be created.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PartitionSchemeType {
27    /// Master Boot Record (legacy BIOS).
28    Mbr,
29    /// GUID Partition Table (UEFI).
30    Gpt,
31    /// Hybrid MBR (GPT with MBR entries for dual BIOS/UEFI boot).
32    Hybrid,
33}
34
35impl core::fmt::Display for PartitionSchemeType {
36    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37        match self {
38            Self::Mbr => write!(f, "MBR"),
39            Self::Gpt => write!(f, "GPT"),
40            Self::Hybrid => write!(f, "Hybrid MBR"),
41        }
42    }
43}
44
45/// Information about a partition, independent of the underlying scheme.
46#[derive(Debug, Clone, Copy)]
47pub struct PartitionInfo {
48    /// Partition index (0-based).
49    pub index: usize,
50    /// Starting LBA.
51    pub start_lba: u64,
52    /// Ending LBA (inclusive).
53    pub end_lba: u64,
54    /// Size in sectors.
55    pub size_sectors: u64,
56    /// Whether the partition is bootable/active.
57    pub bootable: bool,
58    /// Partition type (MBR type code or GPT type GUID).
59    pub partition_type: PartitionType,
60}
61
62/// Partition type information.
63#[derive(Debug, Clone, Copy)]
64pub enum PartitionType {
65    /// MBR partition type code.
66    Mbr(u8),
67    /// GPT partition type GUID.
68    Gpt(Guid),
69}
70
71impl PartitionInfo {
72    /// Returns the size in bytes (assuming 512-byte sectors).
73    ///
74    /// Saturates to `u64::MAX` on corrupt inputs whose sector count is
75    /// not representable in bytes.
76    pub const fn size_bytes(&self) -> u64 {
77        self.size_sectors.saturating_mul(512)
78    }
79
80    /// Returns the size in bytes for a given sector size.
81    ///
82    /// Saturates to `u64::MAX` on corrupt inputs whose sector count is
83    /// not representable in bytes.
84    pub const fn size_bytes_with_sector_size(&self, sector_size: u32) -> u64 {
85        self.size_sectors.saturating_mul(sector_size as u64)
86    }
87}
88
89/// A complete GPT disk structure.
90#[cfg(feature = "alloc")]
91#[derive(Debug, Clone)]
92pub struct GptDisk {
93    /// The primary GPT header (at LBA 1).
94    pub primary_header: GptHeader,
95    /// The backup GPT header (at last LBA).
96    pub backup_header: GptHeader,
97    /// Partition entries.
98    pub entries: Vec<GptPartitionEntry>,
99    /// Logical block size in bytes.
100    pub block_size: u32,
101}
102
103#[cfg(feature = "alloc")]
104impl GptDisk {
105    /// Default number of partition entries.
106    pub const DEFAULT_ENTRY_COUNT: u32 = 128;
107
108    /// Creates a new empty GPT disk structure.
109    pub fn new(disk_sectors: u64, block_size: u32) -> Self {
110        let entry_count = Self::DEFAULT_ENTRY_COUNT;
111        let entry_size = core::mem::size_of::<GptPartitionEntry>() as u32;
112        let entries_per_sector = (block_size / entry_size).max(1);
113        let entry_sectors = entry_count.div_ceil(entries_per_sector);
114
115        // First usable LBA is after: MBR (1) + GPT header (1) + entries
116        let first_usable = 2 + entry_sectors as u64;
117        // Last usable LBA is before: backup entries + backup header (1)
118        let last_usable = disk_sectors.saturating_sub(2 + entry_sectors as u64);
119
120        let disk_guid = {
121            #[cfg(feature = "rand")]
122            {
123                Guid::generate_v4()
124            }
125            #[cfg(not(feature = "rand"))]
126            {
127                Guid::UNUSED
128            }
129        };
130
131        #[cfg_attr(not(feature = "crc"), allow(unused_mut))]
132        let mut primary_header = GptHeader {
133            signature: GptHeader::SIGNATURE,
134            revision: Le::<u32>::from_ne(GptHeader::REVISION_1_0),
135            header_size: Le::<u32>::from_ne(GptHeader::STANDARD_HEADER_SIZE),
136            header_crc32: Le::<u32>::from_ne(0),
137            reserved: Le::<u32>::from_ne(0),
138            my_lba: Le::<u64>::from_ne(1),
139            alternate_lba: Le::<u64>::from_ne(disk_sectors.saturating_sub(1)),
140            first_usable_lba: Le::<u64>::from_ne(first_usable),
141            last_usable_lba: Le::<u64>::from_ne(last_usable),
142            disk_guid,
143            partition_entry_lba: Le::<u64>::from_ne(2),
144            num_partition_entries: Le::<u32>::from_ne(entry_count),
145            size_of_partition_entry: Le::<u32>::from_ne(entry_size),
146            partition_entry_array_crc32: Le::<u32>::from_ne(0),
147        };
148
149        #[cfg_attr(not(feature = "crc"), allow(unused_mut))]
150        let mut backup_header = GptHeader {
151            my_lba: Le::<u64>::from_ne(disk_sectors.saturating_sub(1)),
152            alternate_lba: Le::<u64>::from_ne(1),
153            partition_entry_lba: Le::<u64>::from_ne(
154                disk_sectors.saturating_sub(1 + entry_sectors as u64),
155            ),
156            ..primary_header
157        };
158
159        let entries = alloc::vec![GptPartitionEntry::default(); entry_count as usize];
160
161        // Update CRCs
162        #[cfg(feature = "crc")]
163        {
164            let entries_crc = crate::gpt::calculate_partition_array_crc32(&entries);
165            primary_header.partition_entry_array_crc32 = Le::<u32>::from_ne(entries_crc);
166            backup_header.partition_entry_array_crc32 = Le::<u32>::from_ne(entries_crc);
167            primary_header.update_crc32();
168            backup_header.update_crc32();
169        }
170
171        Self {
172            primary_header,
173            backup_header,
174            entries,
175            block_size,
176        }
177    }
178
179    /// Returns the number of used (non-empty) partition entries.
180    pub fn partition_count(&self) -> usize {
181        self.entries.iter().filter(|e| !e.is_unused()).count()
182    }
183
184    /// Returns an iterator over non-empty partition entries.
185    pub fn partitions(&self) -> impl Iterator<Item = (usize, &GptPartitionEntry)> {
186        self.entries
187            .iter()
188            .enumerate()
189            .filter(|(_, e)| !e.is_unused())
190    }
191
192    /// Adds a partition to the first available slot.
193    ///
194    /// Returns the index of the new partition, or an error if no slots are available.
195    pub fn add_partition(&mut self, entry: GptPartitionEntry) -> Result<usize> {
196        for (i, slot) in self.entries.iter_mut().enumerate() {
197            if slot.is_unused() {
198                *slot = entry;
199                self.update_crcs();
200                return Ok(i);
201            }
202        }
203        Err(Error::TooManyPartitions {
204            max: self.entries.len(),
205            requested: self.entries.len() + 1,
206        })
207    }
208
209    /// Validates the GPT structure.
210    pub fn validate(&self) -> Result<()> {
211        // Check signature
212        if !self.primary_header.has_valid_signature() {
213            return Err(Error::InvalidGptSignature {
214                found: self.primary_header.signature,
215            });
216        }
217
218        // Check CRCs
219        #[cfg(feature = "crc")]
220        {
221            if !self.primary_header.verify_crc32() {
222                return Err(Error::GptHeaderCrcMismatch {
223                    expected: self.primary_header.header_crc32.to_ne(),
224                    actual: self.primary_header.calculate_crc32(),
225                });
226            }
227
228            let entries_crc = crate::gpt::calculate_partition_array_crc32(&self.entries);
229            if self.primary_header.partition_entry_array_crc32.to_ne() != entries_crc {
230                return Err(Error::GptEntriesCrcMismatch {
231                    expected: self.primary_header.partition_entry_array_crc32.to_ne(),
232                    actual: entries_crc,
233                });
234            }
235        }
236
237        // Check for overlapping partitions
238        let used: Vec<_> = self.partitions().collect();
239        for i in 0..used.len() {
240            for j in (i + 1)..used.len() {
241                let (idx1, p1) = used[i];
242                let (idx2, p2) = used[j];
243                if p1.first_lba.to_ne() <= p2.last_lba.to_ne()
244                    && p2.first_lba.to_ne() <= p1.last_lba.to_ne()
245                {
246                    let overlap_start = p1.first_lba.to_ne().max(p2.first_lba.to_ne());
247                    let overlap_end = p1.last_lba.to_ne().min(p2.last_lba.to_ne());
248                    return Err(Error::PartitionOverlap {
249                        index1: idx1,
250                        index2: idx2,
251                        overlap_start,
252                        overlap_end,
253                    });
254                }
255            }
256        }
257
258        // Check partitions are within usable area
259        for (idx, entry) in self.partitions() {
260            if entry.first_lba.to_ne() < self.primary_header.first_usable_lba.to_ne()
261                || entry.last_lba.to_ne() > self.primary_header.last_usable_lba.to_ne()
262            {
263                return Err(Error::PartitionOutOfBounds {
264                    index: idx,
265                    partition_end: entry.last_lba.to_ne(),
266                    disk_end: self.primary_header.last_usable_lba.to_ne(),
267                });
268            }
269        }
270
271        Ok(())
272    }
273
274    /// Updates all CRCs in the headers.
275    #[cfg(feature = "crc")]
276    pub fn update_crcs(&mut self) {
277        let entries_crc = crate::gpt::calculate_partition_array_crc32(&self.entries);
278        self.primary_header.partition_entry_array_crc32 = Le::<u32>::from_ne(entries_crc);
279        self.backup_header.partition_entry_array_crc32 = Le::<u32>::from_ne(entries_crc);
280        self.primary_header.update_crc32();
281        self.backup_header.update_crc32();
282    }
283
284    #[cfg(not(feature = "crc"))]
285    /// Leaves the headers unchanged when CRC support is disabled.
286    ///
287    /// Enable the `crc` feature to calculate and update the partition-array
288    /// and header checksums.
289    pub fn update_crcs(&mut self) {
290        // No-op without CRC feature
291    }
292
293    /// Creates a protective MBR for this GPT disk.
294    pub fn create_protective_mbr(&self) -> MasterBootRecord {
295        let disk_sectors = self.backup_header.my_lba.to_ne().saturating_add(1);
296        MasterBootRecord::protective(disk_sectors)
297    }
298}
299
300/// A unified partition scheme that can be MBR, GPT, or Hybrid.
301#[cfg(feature = "alloc")]
302#[derive(Debug, Clone)]
303pub enum PartitionTable {
304    /// Pure MBR partitioning.
305    Mbr(MasterBootRecord),
306    /// GPT partitioning with protective MBR.
307    Gpt {
308        /// The protective MBR.
309        protective_mbr: MasterBootRecord,
310        /// The GPT disk structure.
311        gpt: GptDisk,
312    },
313    /// Hybrid MBR + GPT partitioning.
314    Hybrid {
315        /// The hybrid MBR.
316        hybrid_mbr: MasterBootRecord,
317        /// The GPT disk structure.
318        gpt: GptDisk,
319    },
320}
321
322#[cfg(feature = "alloc")]
323impl PartitionTable {
324    /// Creates a new MBR-only partition scheme.
325    pub fn new_mbr() -> Self {
326        Self::Mbr(MasterBootRecord::default())
327    }
328
329    /// Creates a new GPT partition scheme.
330    pub fn new_gpt(disk_sectors: u64, block_size: u32) -> Self {
331        let gpt = GptDisk::new(disk_sectors, block_size);
332        let protective_mbr = gpt.create_protective_mbr();
333        Self::Gpt {
334            protective_mbr,
335            gpt,
336        }
337    }
338
339    /// Returns the partition scheme type.
340    pub fn scheme_type(&self) -> PartitionSchemeType {
341        match self {
342            Self::Mbr(_) => PartitionSchemeType::Mbr,
343            Self::Gpt { .. } => PartitionSchemeType::Gpt,
344            Self::Hybrid { .. } => PartitionSchemeType::Hybrid,
345        }
346    }
347
348    /// Returns partition information for all partitions.
349    pub fn partitions(&self) -> Vec<PartitionInfo> {
350        match self {
351            Self::Mbr(mbr) => {
352                let pt = mbr.get_partition_table();
353                pt.partitions
354                    .iter()
355                    .enumerate()
356                    .filter(|(_, p)| !p.is_empty())
357                    .map(|(i, p)| PartitionInfo {
358                        index: i,
359                        start_lba: p.start_lba.to_ne() as u64,
360                        end_lba: p.end_lba() as u64,
361                        size_sectors: p.sector_count.to_ne() as u64,
362                        bootable: p.is_bootable(),
363                        partition_type: PartitionType::Mbr(p.part_type),
364                    })
365                    .collect()
366            }
367            Self::Gpt { gpt, .. } | Self::Hybrid { gpt, .. } => gpt
368                .partitions()
369                .map(|(i, e)| PartitionInfo {
370                    index: i,
371                    start_lba: e.first_lba.to_ne(),
372                    end_lba: e.last_lba.to_ne(),
373                    size_sectors: e.size_sectors(),
374                    bootable: e.attributes.is_legacy_bios_bootable(),
375                    partition_type: PartitionType::Gpt(e.type_guid),
376                })
377                .collect(),
378        }
379    }
380
381    /// Validates the partition scheme.
382    pub fn validate(&self) -> Result<()> {
383        match self {
384            Self::Mbr(mbr) => {
385                if !mbr.has_valid_signature() {
386                    return Err(Error::InvalidMbrSignature {
387                        found: mbr.signature,
388                    });
389                }
390                let pt = mbr.get_partition_table();
391                if !pt.is_valid() {
392                    return Err(Error::InvalidHybridMbr {
393                        reason: "invalid MBR partition table",
394                    });
395                }
396                Ok(())
397            }
398            Self::Gpt {
399                protective_mbr,
400                gpt,
401            } => {
402                if !protective_mbr.has_valid_signature() {
403                    return Err(Error::InvalidMbrSignature {
404                        found: protective_mbr.signature,
405                    });
406                }
407                let pt = protective_mbr.get_partition_table();
408                if !pt.is_protective() {
409                    return Err(Error::NoProtectiveMbr);
410                }
411                gpt.validate()
412            }
413            Self::Hybrid { hybrid_mbr, gpt } => {
414                if !hybrid_mbr.has_valid_signature() {
415                    return Err(Error::InvalidMbrSignature {
416                        found: hybrid_mbr.signature,
417                    });
418                }
419                if !is_hybrid_mbr(hybrid_mbr) {
420                    return Err(Error::InvalidHybridMbr {
421                        reason: "not a valid hybrid MBR",
422                    });
423                }
424                gpt.validate()
425            }
426        }
427    }
428}
429
430/// Detects the partition scheme type from an MBR.
431///
432/// This is a preliminary detection based only on the MBR.
433/// To fully detect GPT, you need to also read and validate the GPT header.
434pub fn detect_scheme_from_mbr(mbr: &MasterBootRecord) -> PartitionSchemeType {
435    if !mbr.has_valid_signature() {
436        return PartitionSchemeType::Mbr;
437    }
438
439    let pt = mbr.get_partition_table();
440    if is_hybrid_mbr(mbr) {
441        PartitionSchemeType::Hybrid
442    } else if pt.is_protective() {
443        PartitionSchemeType::Gpt
444    } else {
445        PartitionSchemeType::Mbr
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use crate::mbr::MbrPartition;
453
454    #[cfg(feature = "alloc")]
455    #[test]
456    fn test_gpt_disk_creation() {
457        let disk = GptDisk::new(1000000, 512);
458        assert!(disk.primary_header.has_valid_signature());
459        assert_eq!(disk.entries.len(), 128);
460        assert_eq!(disk.partition_count(), 0);
461    }
462
463    #[cfg(feature = "alloc")]
464    #[test]
465    fn test_scheme_detection() {
466        // MBR
467        let mbr = MasterBootRecord::default();
468        assert_eq!(detect_scheme_from_mbr(&mbr), PartitionSchemeType::Mbr);
469
470        // Protective MBR (GPT)
471        let protective = MasterBootRecord::protective(1000000);
472        assert_eq!(
473            detect_scheme_from_mbr(&protective),
474            PartitionSchemeType::Gpt
475        );
476
477        // Hybrid MBR
478        let mut hybrid = protective;
479        hybrid.with_partition_table(|pt| {
480            pt[1] = MbrPartition::new(crate::mbr::MbrPartitionType::Fat32, 2048, 100000);
481        });
482        assert_eq!(detect_scheme_from_mbr(&hybrid), PartitionSchemeType::Hybrid);
483    }
484
485    #[cfg(feature = "alloc")]
486    #[test]
487    fn test_partition_scheme_new_gpt() {
488        let scheme = PartitionTable::new_gpt(1000000, 512);
489        assert_eq!(scheme.scheme_type(), PartitionSchemeType::Gpt);
490        assert!(scheme.validate().is_ok());
491        assert!(scheme.partitions().is_empty());
492    }
493}