Skip to main content

hadris_part/
geometry.rs

1//! Disk geometry and alignment utilities.
2//!
3//! This module provides types and utilities for working with disk geometry
4//! and partition alignment, including support for modern Advanced Format disks.
5
6use crate::PartitionInfoTrait;
7use crate::error::{Error, Result};
8
9/// Disk geometry information.
10///
11/// Contains information about the disk's logical and physical block sizes,
12/// total capacity, and provides utilities for calculating alignment.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct DiskGeometry {
15    /// Logical block (sector) size in bytes.
16    pub block_size: u32,
17    /// Total number of blocks on the disk.
18    pub total_blocks: u64,
19    /// Physical sector size (for alignment), if different from logical.
20    ///
21    /// Modern Advanced Format drives have 4096-byte physical sectors
22    /// but may present 512-byte logical sectors for compatibility.
23    pub physical_block_size: Option<u32>,
24}
25
26impl DiskGeometry {
27    /// Standard logical block size (512 bytes).
28    pub const STANDARD_BLOCK_SIZE: u32 = 512;
29
30    /// Advanced Format block size (4096 bytes).
31    pub const ADVANCED_FORMAT_BLOCK_SIZE: u32 = 4096;
32
33    /// Default partition alignment (1 MiB in bytes).
34    ///
35    /// 1 MiB alignment is the modern standard for optimal performance
36    /// on both traditional and Advanced Format drives.
37    pub const DEFAULT_ALIGNMENT_BYTES: u64 = 1024 * 1024;
38
39    /// Creates geometry for a standard 512-byte sector disk.
40    ///
41    /// # Arguments
42    ///
43    /// * `total_blocks` - Total number of 512-byte sectors on the disk
44    pub const fn standard(total_blocks: u64) -> Self {
45        Self {
46            block_size: Self::STANDARD_BLOCK_SIZE,
47            total_blocks,
48            physical_block_size: None,
49        }
50    }
51
52    /// Creates geometry for a 4K sector disk (Advanced Format).
53    ///
54    /// # Arguments
55    ///
56    /// * `total_blocks` - Total number of 4096-byte sectors on the disk
57    pub const fn advanced_format(total_blocks: u64) -> Self {
58        Self {
59            block_size: Self::ADVANCED_FORMAT_BLOCK_SIZE,
60            total_blocks,
61            physical_block_size: None,
62        }
63    }
64
65    /// Creates geometry for a 512e disk (512-byte logical, 4K physical).
66    ///
67    /// These disks present 512-byte logical sectors for compatibility
68    /// but have 4096-byte physical sectors internally.
69    ///
70    /// # Arguments
71    ///
72    /// * `total_blocks` - Total number of 512-byte logical sectors
73    pub const fn emulated_512(total_blocks: u64) -> Self {
74        Self {
75            block_size: Self::STANDARD_BLOCK_SIZE,
76            total_blocks,
77            physical_block_size: Some(Self::ADVANCED_FORMAT_BLOCK_SIZE),
78        }
79    }
80
81    /// Creates a new DiskGeometry with custom parameters.
82    pub const fn new(block_size: u32, total_blocks: u64, physical_block_size: Option<u32>) -> Self {
83        Self {
84            block_size,
85            total_blocks,
86            physical_block_size,
87        }
88    }
89
90    /// Returns the total disk size in bytes.
91    pub const fn total_bytes(&self) -> u64 {
92        self.total_blocks * self.block_size as u64
93    }
94
95    /// Returns the effective alignment boundary in bytes.
96    ///
97    /// This is the physical block size if set, otherwise the logical block size.
98    pub const fn alignment_boundary(&self) -> u32 {
99        match self.physical_block_size {
100            Some(size) => size,
101            None => self.block_size,
102        }
103    }
104
105    /// Returns the default partition alignment in sectors.
106    ///
107    /// This is 1 MiB expressed in sectors for the current block size.
108    /// For 512-byte sectors, this is 2048 sectors.
109    /// For 4096-byte sectors, this is 256 sectors.
110    pub const fn default_alignment(&self) -> u64 {
111        Self::DEFAULT_ALIGNMENT_BYTES / self.block_size as u64
112    }
113
114    /// Aligns an LBA up to the next alignment boundary.
115    ///
116    /// # Arguments
117    ///
118    /// * `lba` - The LBA to align
119    /// * `alignment_sectors` - The alignment boundary in sectors
120    ///
121    /// # Returns
122    ///
123    /// The LBA rounded up to the next multiple of `alignment_sectors`.
124    pub const fn align_up(&self, lba: u64, alignment_sectors: u64) -> u64 {
125        if alignment_sectors == 0 {
126            return lba;
127        }
128        let mask = alignment_sectors - 1;
129        (lba.saturating_add(mask)) & !mask
130    }
131
132    /// Aligns an LBA down to the previous alignment boundary.
133    ///
134    /// # Arguments
135    ///
136    /// * `lba` - The LBA to align
137    /// * `alignment_sectors` - The alignment boundary in sectors
138    ///
139    /// # Returns
140    ///
141    /// The LBA rounded down to the previous multiple of `alignment_sectors`.
142    pub const fn align_down(&self, lba: u64, alignment_sectors: u64) -> u64 {
143        if alignment_sectors == 0 {
144            return lba;
145        }
146        let mask = alignment_sectors - 1;
147        lba & !mask
148    }
149
150    /// Checks if an LBA is properly aligned.
151    ///
152    /// # Arguments
153    ///
154    /// * `lba` - The LBA to check
155    /// * `alignment_sectors` - The alignment boundary in sectors
156    pub const fn is_aligned(&self, lba: u64, alignment_sectors: u64) -> bool {
157        if alignment_sectors == 0 {
158            return true;
159        }
160        lba.is_multiple_of(alignment_sectors)
161    }
162
163    /// Calculates the first usable LBA for GPT partitions.
164    ///
165    /// This accounts for:
166    /// - MBR (1 sector)
167    /// - GPT header (1 sector)
168    /// - Partition entry array
169    ///
170    /// # Arguments
171    ///
172    /// * `num_entries` - Number of partition entries (typically 128)
173    /// * `entry_size` - Size of each entry in bytes (typically 128)
174    pub const fn gpt_first_usable_lba(&self, num_entries: u32, entry_size: u32) -> u64 {
175        let entry_bytes = num_entries as u64 * entry_size as u64;
176        let entry_sectors = entry_bytes.div_ceil(self.block_size as u64);
177        // MBR (LBA 0) + GPT header (LBA 1) + partition entries
178        2 + entry_sectors
179    }
180
181    /// Calculates the first usable LBA aligned to the default alignment.
182    ///
183    /// # Arguments
184    ///
185    /// * `num_entries` - Number of partition entries (typically 128)
186    /// * `entry_size` - Size of each entry in bytes (typically 128)
187    pub const fn gpt_first_usable_lba_aligned(&self, num_entries: u32, entry_size: u32) -> u64 {
188        let first = self.gpt_first_usable_lba(num_entries, entry_size);
189        self.align_up(first, self.default_alignment())
190    }
191
192    /// Calculates the last usable LBA for GPT partitions.
193    ///
194    /// This accounts for:
195    /// - Backup partition entry array
196    /// - Backup GPT header (1 sector at the last LBA)
197    ///
198    /// # Arguments
199    ///
200    /// * `num_entries` - Number of partition entries (typically 128)
201    /// * `entry_size` - Size of each entry in bytes (typically 128)
202    pub const fn gpt_last_usable_lba(&self, num_entries: u32, entry_size: u32) -> u64 {
203        let entry_bytes = num_entries as u64 * entry_size as u64;
204        let entry_sectors = entry_bytes.div_ceil(self.block_size as u64);
205        // Last LBA is total_blocks - 1
206        // Backup header at last LBA, backup entries before that
207        self.total_blocks.saturating_sub(entry_sectors + 2)
208    }
209
210    /// Calculates the last usable LBA aligned down to the default alignment.
211    ///
212    /// # Arguments
213    ///
214    /// * `num_entries` - Number of partition entries (typically 128)
215    /// * `entry_size` - Size of each entry in bytes (typically 128)
216    pub const fn gpt_last_usable_lba_aligned(&self, num_entries: u32, entry_size: u32) -> u64 {
217        let last = self.gpt_last_usable_lba(num_entries, entry_size);
218        // Align down and subtract 1 to stay within bounds
219        self.align_down(last.saturating_add(1), self.default_alignment())
220            .saturating_sub(1)
221    }
222
223    /// Calculates the usable space in sectors for GPT partitions.
224    ///
225    /// # Arguments
226    ///
227    /// * `num_entries` - Number of partition entries (typically 128)
228    /// * `entry_size` - Size of each entry in bytes (typically 128)
229    pub const fn gpt_usable_sectors(&self, num_entries: u32, entry_size: u32) -> u64 {
230        let first = self.gpt_first_usable_lba(num_entries, entry_size);
231        let last = self.gpt_last_usable_lba(num_entries, entry_size);
232        if last > first { last - first + 1 } else { 0 }
233    }
234}
235
236/// Validates that a partition is properly aligned.
237///
238/// # Arguments
239///
240/// * `partition` - The partition to validate
241/// * `geometry` - The disk geometry
242/// * `alignment` - The required alignment in sectors
243///
244/// # Errors
245///
246/// Returns `Error::MisalignedPartition` if the partition is not aligned.
247pub fn validate_partition_alignment<P: PartitionInfoTrait>(
248    partition: &P,
249    geometry: &DiskGeometry,
250    alignment: u64,
251) -> Result<()> {
252    if !geometry.is_aligned(partition.start_lba(), alignment) {
253        return Err(Error::MisalignedPartition {
254            lba: partition.start_lba(),
255            required_alignment: alignment,
256        });
257    }
258    Ok(())
259}
260
261/// Validates that all partitions in a slice are properly aligned.
262///
263/// # Arguments
264///
265/// * `partitions` - The partitions to validate
266/// * `geometry` - The disk geometry
267/// * `alignment` - The required alignment in sectors
268///
269/// # Errors
270///
271/// Returns `Error::MisalignedPartition` for the first misaligned partition found.
272pub fn validate_all_partitions_aligned<P: PartitionInfoTrait>(
273    partitions: &[P],
274    geometry: &DiskGeometry,
275    alignment: u64,
276) -> Result<()> {
277    for partition in partitions {
278        validate_partition_alignment(partition, geometry, alignment)?;
279    }
280    Ok(())
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_standard_geometry() {
289        let geom = DiskGeometry::standard(2_097_152); // 1 GiB
290        assert_eq!(geom.block_size, 512);
291        assert_eq!(geom.total_bytes(), 1024 * 1024 * 1024);
292        assert_eq!(geom.default_alignment(), 2048); // 1 MiB / 512 = 2048
293    }
294
295    #[test]
296    fn test_advanced_format_geometry() {
297        let geom = DiskGeometry::advanced_format(262_144); // 1 GiB
298        assert_eq!(geom.block_size, 4096);
299        assert_eq!(geom.total_bytes(), 1024 * 1024 * 1024);
300        assert_eq!(geom.default_alignment(), 256); // 1 MiB / 4096 = 256
301    }
302
303    #[test]
304    fn test_emulated_512_geometry() {
305        let geom = DiskGeometry::emulated_512(2_097_152);
306        assert_eq!(geom.block_size, 512);
307        assert_eq!(geom.physical_block_size, Some(4096));
308        assert_eq!(geom.alignment_boundary(), 4096);
309    }
310
311    #[test]
312    fn test_align_up() {
313        let geom = DiskGeometry::standard(1000000);
314
315        assert_eq!(geom.align_up(0, 2048), 0);
316        assert_eq!(geom.align_up(1, 2048), 2048);
317        assert_eq!(geom.align_up(2047, 2048), 2048);
318        assert_eq!(geom.align_up(2048, 2048), 2048);
319        assert_eq!(geom.align_up(2049, 2048), 4096);
320    }
321
322    #[test]
323    fn test_align_down() {
324        let geom = DiskGeometry::standard(1000000);
325
326        assert_eq!(geom.align_down(0, 2048), 0);
327        assert_eq!(geom.align_down(1, 2048), 0);
328        assert_eq!(geom.align_down(2047, 2048), 0);
329        assert_eq!(geom.align_down(2048, 2048), 2048);
330        assert_eq!(geom.align_down(4095, 2048), 2048);
331        assert_eq!(geom.align_down(4096, 2048), 4096);
332    }
333
334    #[test]
335    fn test_is_aligned() {
336        let geom = DiskGeometry::standard(1000000);
337
338        assert!(geom.is_aligned(0, 2048));
339        assert!(!geom.is_aligned(1, 2048));
340        assert!(geom.is_aligned(2048, 2048));
341        assert!(geom.is_aligned(4096, 2048));
342        assert!(!geom.is_aligned(4097, 2048));
343    }
344
345    #[test]
346    fn test_gpt_usable_lba() {
347        // 100 MiB disk with 512-byte sectors
348        let geom = DiskGeometry::standard(204800);
349
350        // 128 entries * 128 bytes = 16384 bytes = 32 sectors
351        let first = geom.gpt_first_usable_lba(128, 128);
352        let last = geom.gpt_last_usable_lba(128, 128);
353
354        // First usable: 2 (MBR + header) + 32 (entries) = 34
355        assert_eq!(first, 34);
356
357        // Last usable: total - 1 - 32 (backup entries) - 1 (backup header)
358        // = 204800 - 1 - 32 - 1 = 204766
359        assert_eq!(last, 204766);
360
361        // First aligned to 1 MiB (2048 sectors)
362        let first_aligned = geom.gpt_first_usable_lba_aligned(128, 128);
363        assert_eq!(first_aligned, 2048);
364    }
365
366    #[test]
367    fn test_gpt_usable_sectors() {
368        let geom = DiskGeometry::standard(204800);
369        let usable = geom.gpt_usable_sectors(128, 128);
370
371        // 204766 - 34 + 1 = 204733
372        assert_eq!(usable, 204733);
373    }
374}