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}