1use crate::error::{Error, Result};
13use crate::gpt::{self, type_guids};
14use crate::mbr::{self, types as mbr_types};
15use crate::probe::{Partition, PartitionKind, TableKind};
16use fs_core::{BlockDevice, BlockRead};
17
18const GPT_FIRST_USABLE_LBA: u64 = 34;
20const GPT_BACKUP_RESERVE_SECTORS: u64 = 33; const SECTOR_SIZE: u64 = 512;
22const ALIGNMENT_SECTORS: u64 = 2048;
25const MBR_LBA_MAX: u64 = 0xFFFF_FFFF;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum PartitionTypeId {
33 EfiSystem,
34 MicrosoftBasicData,
35 LinuxFilesystem,
36 LinuxSwap,
37 AppleHfsPlus,
38 AppleApfs,
39 GptCustom([u8; 16]),
42 MbrCustom(u8),
45}
46
47impl PartitionTypeId {
48 fn to_gpt_guid(self) -> Result<[u8; 16]> {
49 Ok(match self {
50 PartitionTypeId::EfiSystem => type_guids::EFI_SYSTEM,
51 PartitionTypeId::MicrosoftBasicData => type_guids::MICROSOFT_BASIC_DATA,
52 PartitionTypeId::LinuxFilesystem => type_guids::LINUX_FILESYSTEM,
53 PartitionTypeId::LinuxSwap => type_guids::LINUX_SWAP,
54 PartitionTypeId::AppleHfsPlus => type_guids::APPLE_HFS_PLUS,
55 PartitionTypeId::AppleApfs => type_guids::APPLE_APFS,
56 PartitionTypeId::GptCustom(g) => g,
57 PartitionTypeId::MbrCustom(_) => {
58 return Err(Error::Invalid("MBR type byte used in GPT context"))
59 }
60 })
61 }
62 fn to_mbr_byte(self) -> Result<u8> {
63 Ok(match self {
64 PartitionTypeId::EfiSystem => mbr_types::EFI_SYSTEM,
65 PartitionTypeId::MicrosoftBasicData => mbr_types::NTFS_OR_EXFAT,
66 PartitionTypeId::LinuxFilesystem => mbr_types::LINUX,
67 PartitionTypeId::LinuxSwap => mbr_types::LINUX_SWAP,
68 PartitionTypeId::AppleHfsPlus => mbr_types::HFS_PLUS,
69 PartitionTypeId::AppleApfs => {
71 return Err(Error::Invalid("APFS has no MBR type byte; use GPT"));
72 }
73 PartitionTypeId::MbrCustom(b) => b,
74 PartitionTypeId::GptCustom(_) => {
75 return Err(Error::Invalid("GPT type GUID used in MBR context"))
76 }
77 })
78 }
79}
80
81#[derive(Debug, Clone, Copy)]
84pub enum PartitionRef {
85 Index(usize),
86 Uuid([u8; 16]),
87}
88
89#[derive(Debug, Clone)]
92pub struct PartitionSet {
93 pub table_kind: TableKind,
94 pub partitions: Vec<Partition>,
95 pub disk_size: u64,
96 pub disk_guid: [u8; 16],
98}
99
100impl PartitionSet {
101 pub fn from_probe(dev: &dyn BlockRead) -> Result<Self> {
105 let (table_kind, partitions) = crate::probe(dev)?;
106 let disk_size = dev.size_bytes();
107 let disk_guid = if table_kind == TableKind::Gpt {
108 let mut sector = [0u8; 512];
110 dev.read_at(SECTOR_SIZE, &mut sector)?;
111 gpt::parse_header(§or)?.disk_guid
112 } else {
113 [0u8; 16]
114 };
115 Ok(PartitionSet {
116 table_kind,
117 partitions,
118 disk_size,
119 disk_guid,
120 })
121 }
122
123 pub fn empty_gpt(disk_size: u64) -> Self {
125 PartitionSet {
126 table_kind: TableKind::Gpt,
127 partitions: Vec::new(),
128 disk_size,
129 disk_guid: random_uuid(),
130 }
131 }
132
133 pub fn empty_mbr(disk_size: u64) -> Self {
135 PartitionSet {
136 table_kind: TableKind::Mbr,
137 partitions: Vec::new(),
138 disk_size,
139 disk_guid: [0u8; 16],
140 }
141 }
142
143 pub fn add(
150 &mut self,
151 start_hint: Option<u64>,
152 length: u64,
153 type_id: PartitionTypeId,
154 label: Option<String>,
155 ) -> Result<usize> {
156 if length == 0 {
157 return Err(Error::Invalid("zero length"));
158 }
159 if self.table_kind == TableKind::Mbr && self.partitions.len() >= 4 {
160 return Err(Error::Invalid("MBR primary table is full (4 entries)"));
161 }
162 if self.table_kind == TableKind::Gpt && self.partitions.len() >= 128 {
163 return Err(Error::Invalid("GPT canonical table is full (128 entries)"));
164 }
165
166 let length_sectors = length.div_ceil(SECTOR_SIZE);
168 let length_sectors = align_up(length_sectors, ALIGNMENT_SECTORS);
172
173 let (first_usable_lba, last_usable_lba) = self.usable_range();
174 if first_usable_lba > last_usable_lba {
175 return Err(Error::DeviceTooSmall);
176 }
177
178 let start_lba = match start_hint {
179 Some(byte) => {
180 let raw_sectors = byte / SECTOR_SIZE + if byte % SECTOR_SIZE != 0 { 1 } else { 0 };
181 let aligned = align_up(raw_sectors.max(first_usable_lba), ALIGNMENT_SECTORS);
182 if aligned < first_usable_lba {
183 return Err(Error::Invalid("hinted start before first usable LBA"));
184 }
185 aligned
186 }
187 None => self.find_free(length_sectors, first_usable_lba, last_usable_lba)?,
188 };
189
190 let end_lba = start_lba
191 .checked_add(length_sectors - 1)
192 .ok_or(Error::Invalid("partition end overflows"))?;
193 if end_lba > last_usable_lba {
194 return Err(Error::Invalid("partition extends past last usable LBA"));
195 }
196 if self.table_kind == TableKind::Mbr
197 && (start_lba > MBR_LBA_MAX || length_sectors > MBR_LBA_MAX)
198 {
199 return Err(Error::Invalid("partition exceeds MBR 32-bit LBA range"));
200 }
201
202 for p in &self.partitions {
204 let p_start = p.start / SECTOR_SIZE;
205 let p_end = (p.start + p.length) / SECTOR_SIZE - 1;
206 if start_lba <= p_end && end_lba >= p_start {
207 return Err(Error::Invalid("partition overlaps existing entry"));
208 }
209 }
210
211 let kind = match self.table_kind {
216 TableKind::Gpt => PartitionKind::Gpt {
217 type_guid: type_id.to_gpt_guid()?,
218 attributes: 0,
219 },
220 TableKind::Mbr => PartitionKind::Mbr {
221 type_byte: type_id.to_mbr_byte()?,
222 active: false,
223 },
224 };
225 let uuid = if self.table_kind == TableKind::Gpt {
226 Some(random_uuid())
227 } else {
228 None
229 };
230
231 let part = Partition {
232 start: start_lba * SECTOR_SIZE,
233 length: length_sectors * SECTOR_SIZE,
234 kind,
235 label,
236 uuid,
237 };
238 let idx = self.partitions.len();
239 self.partitions.push(part);
240 Ok(idx)
241 }
242
243 pub fn remove(&mut self, target: PartitionRef) -> Result<()> {
246 let idx = self.resolve(target)?;
247 self.partitions.remove(idx);
248 Ok(())
249 }
250
251 pub fn resize(&mut self, target: PartitionRef, new_length: u64) -> Result<()> {
254 if new_length == 0 {
255 return Err(Error::Invalid("zero length"));
256 }
257 let idx = self.resolve(target)?;
258 let new_length_sectors = align_up(new_length.div_ceil(SECTOR_SIZE), ALIGNMENT_SECTORS);
259 let (_, last_usable_lba) = self.usable_range();
260
261 let start_lba = self.partitions[idx].start / SECTOR_SIZE;
262 let new_end_lba = start_lba
263 .checked_add(new_length_sectors - 1)
264 .ok_or(Error::Invalid("partition end overflows"))?;
265 if new_end_lba > last_usable_lba {
266 return Err(Error::Invalid("resize extends past last usable LBA"));
267 }
268 if self.table_kind == TableKind::Mbr && new_length_sectors > MBR_LBA_MAX {
269 return Err(Error::Invalid("resize exceeds MBR 32-bit LBA range"));
270 }
271
272 for (j, p) in self.partitions.iter().enumerate() {
274 if j == idx {
275 continue;
276 }
277 let p_start = p.start / SECTOR_SIZE;
278 let p_end = (p.start + p.length) / SECTOR_SIZE - 1;
279 if start_lba <= p_end && new_end_lba >= p_start {
280 return Err(Error::Invalid("resize would overlap another partition"));
281 }
282 }
283 self.partitions[idx].length = new_length_sectors * SECTOR_SIZE;
284 Ok(())
285 }
286
287 pub fn commit(&self, dev: &dyn BlockDevice) -> Result<()> {
290 match self.table_kind {
291 TableKind::Gpt => {
292 crate::gpt_write::write_gpt(dev, &self.partitions, self.disk_guid)?;
293 }
294 TableKind::Mbr => {
295 mbr::write_mbr(dev, &self.partitions)?;
296 }
297 }
298 dev.flush()?;
299 Ok(())
300 }
301
302 fn usable_range(&self) -> (u64, u64) {
305 let total_sectors = self.disk_size / SECTOR_SIZE;
306 match self.table_kind {
307 TableKind::Gpt => {
308 if total_sectors < GPT_FIRST_USABLE_LBA + GPT_BACKUP_RESERVE_SECTORS + 1 {
309 return (GPT_FIRST_USABLE_LBA, GPT_FIRST_USABLE_LBA - 1);
310 }
311 let last = total_sectors - 1 - GPT_BACKUP_RESERVE_SECTORS;
312 (GPT_FIRST_USABLE_LBA, last)
313 }
314 TableKind::Mbr => {
315 if total_sectors < 2 {
316 return (1, 0);
317 }
318 let last = (total_sectors - 1).min(MBR_LBA_MAX);
319 (1, last)
320 }
321 }
322 }
323
324 fn resolve(&self, target: PartitionRef) -> Result<usize> {
325 match target {
326 PartitionRef::Index(i) => {
327 if i >= self.partitions.len() {
328 return Err(Error::Invalid("index out of range"));
329 }
330 Ok(i)
331 }
332 PartitionRef::Uuid(u) => self
333 .partitions
334 .iter()
335 .position(|p| p.uuid == Some(u))
336 .ok_or(Error::Invalid("uuid not found")),
337 }
338 }
339
340 fn find_free(&self, length_sectors: u64, first_usable: u64, last_usable: u64) -> Result<u64> {
342 let mut sorted: Vec<&Partition> = self.partitions.iter().collect();
343 sorted.sort_by_key(|p| p.start);
344
345 let mut cursor = align_up(first_usable, ALIGNMENT_SECTORS);
346 for p in &sorted {
347 let p_start = p.start / SECTOR_SIZE;
348 let p_end = (p.start + p.length) / SECTOR_SIZE - 1;
349 if cursor + length_sectors - 1 < p_start {
350 if cursor + length_sectors - 1 <= last_usable {
352 return Ok(cursor);
353 }
354 }
355 cursor = align_up(p_end + 1, ALIGNMENT_SECTORS);
357 }
358 if cursor + length_sectors - 1 <= last_usable {
359 Ok(cursor)
360 } else {
361 Err(Error::Invalid("no free space large enough"))
362 }
363 }
364}
365
366fn align_up(value: u64, align: u64) -> u64 {
368 debug_assert!(align > 0);
369 let rem = value % align;
370 if rem == 0 {
371 value
372 } else {
373 value + (align - rem)
374 }
375}
376
377pub(crate) fn random_uuid() -> [u8; 16] {
395 let mut bytes = [0u8; 16];
396 if !fill_from_urandom(&mut bytes) {
397 fill_from_prng(&mut bytes);
398 }
399 bytes[7] = (bytes[7] & 0x0F) | 0x40;
401 bytes[8] = (bytes[8] & 0x3F) | 0x80;
403 bytes
404}
405
406#[cfg(unix)]
407fn fill_from_urandom(buf: &mut [u8]) -> bool {
408 use std::io::Read;
409 match std::fs::File::open("/dev/urandom") {
410 Ok(mut f) => f.read_exact(buf).is_ok(),
411 Err(_) => false,
412 }
413}
414
415#[cfg(not(unix))]
416fn fill_from_urandom(_buf: &mut [u8]) -> bool {
417 false
418}
419
420fn fill_from_prng(buf: &mut [u8]) {
421 use std::time::{SystemTime, UNIX_EPOCH};
422 let nanos = SystemTime::now()
423 .duration_since(UNIX_EPOCH)
424 .map(|d| d.as_nanos() as u64)
425 .unwrap_or(0x9E37_79B9_7F4A_7C15);
426 let pid = std::process::id() as u64;
427 let mut state = nanos.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(pid);
428 if state == 0 {
429 state = 0x9E37_79B9_7F4A_7C15;
430 }
431 for chunk in buf.chunks_mut(8) {
432 state ^= state << 13;
434 state ^= state >> 7;
435 state ^= state << 17;
436 let b = state.to_le_bytes();
437 chunk.copy_from_slice(&b[..chunk.len()]);
438 }
439}