use crate::error::{Error, Result};
use crate::gpt::{self, type_guids};
use crate::mbr::{self, types as mbr_types};
use crate::probe::{Partition, PartitionKind, TableKind};
use fs_core::{BlockDevice, BlockRead};
const GPT_FIRST_USABLE_LBA: u64 = 34;
const GPT_BACKUP_RESERVE_SECTORS: u64 = 33; const SECTOR_SIZE: u64 = 512;
const ALIGNMENT_SECTORS: u64 = 2048;
const MBR_LBA_MAX: u64 = 0xFFFF_FFFF;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PartitionTypeId {
EfiSystem,
MicrosoftBasicData,
LinuxFilesystem,
LinuxSwap,
AppleHfsPlus,
AppleApfs,
GptCustom([u8; 16]),
MbrCustom(u8),
}
impl PartitionTypeId {
fn to_gpt_guid(self) -> Result<[u8; 16]> {
Ok(match self {
PartitionTypeId::EfiSystem => type_guids::EFI_SYSTEM,
PartitionTypeId::MicrosoftBasicData => type_guids::MICROSOFT_BASIC_DATA,
PartitionTypeId::LinuxFilesystem => type_guids::LINUX_FILESYSTEM,
PartitionTypeId::LinuxSwap => type_guids::LINUX_SWAP,
PartitionTypeId::AppleHfsPlus => type_guids::APPLE_HFS_PLUS,
PartitionTypeId::AppleApfs => type_guids::APPLE_APFS,
PartitionTypeId::GptCustom(g) => g,
PartitionTypeId::MbrCustom(_) => {
return Err(Error::Invalid("MBR type byte used in GPT context"))
}
})
}
fn to_mbr_byte(self) -> Result<u8> {
Ok(match self {
PartitionTypeId::EfiSystem => mbr_types::EFI_SYSTEM,
PartitionTypeId::MicrosoftBasicData => mbr_types::NTFS_OR_EXFAT,
PartitionTypeId::LinuxFilesystem => mbr_types::LINUX,
PartitionTypeId::LinuxSwap => mbr_types::LINUX_SWAP,
PartitionTypeId::AppleHfsPlus => mbr_types::HFS_PLUS,
PartitionTypeId::AppleApfs => {
return Err(Error::Invalid("APFS has no MBR type byte; use GPT"));
}
PartitionTypeId::MbrCustom(b) => b,
PartitionTypeId::GptCustom(_) => {
return Err(Error::Invalid("GPT type GUID used in MBR context"))
}
})
}
}
#[derive(Debug, Clone, Copy)]
pub enum PartitionRef {
Index(usize),
Uuid([u8; 16]),
}
#[derive(Debug, Clone)]
pub struct PartitionSet {
pub table_kind: TableKind,
pub partitions: Vec<Partition>,
pub disk_size: u64,
pub disk_guid: [u8; 16],
}
impl PartitionSet {
pub fn from_probe(dev: &dyn BlockRead) -> Result<Self> {
let (table_kind, partitions) = crate::probe(dev)?;
let disk_size = dev.size_bytes();
let disk_guid = if table_kind == TableKind::Gpt {
let mut sector = [0u8; 512];
dev.read_at(SECTOR_SIZE, &mut sector)?;
gpt::parse_header(§or)?.disk_guid
} else {
[0u8; 16]
};
Ok(PartitionSet {
table_kind,
partitions,
disk_size,
disk_guid,
})
}
pub fn empty_gpt(disk_size: u64) -> Self {
PartitionSet {
table_kind: TableKind::Gpt,
partitions: Vec::new(),
disk_size,
disk_guid: random_uuid(),
}
}
pub fn empty_mbr(disk_size: u64) -> Self {
PartitionSet {
table_kind: TableKind::Mbr,
partitions: Vec::new(),
disk_size,
disk_guid: [0u8; 16],
}
}
pub fn add(
&mut self,
start_hint: Option<u64>,
length: u64,
type_id: PartitionTypeId,
label: Option<String>,
) -> Result<usize> {
if length == 0 {
return Err(Error::Invalid("zero length"));
}
if self.table_kind == TableKind::Mbr && self.partitions.len() >= 4 {
return Err(Error::Invalid("MBR primary table is full (4 entries)"));
}
if self.table_kind == TableKind::Gpt && self.partitions.len() >= 128 {
return Err(Error::Invalid("GPT canonical table is full (128 entries)"));
}
let length_sectors = length.div_ceil(SECTOR_SIZE);
let length_sectors = align_up(length_sectors, ALIGNMENT_SECTORS);
let (first_usable_lba, last_usable_lba) = self.usable_range();
if first_usable_lba > last_usable_lba {
return Err(Error::DeviceTooSmall);
}
let start_lba = match start_hint {
Some(byte) => {
let raw_sectors = byte / SECTOR_SIZE + if byte % SECTOR_SIZE != 0 { 1 } else { 0 };
let aligned = align_up(raw_sectors.max(first_usable_lba), ALIGNMENT_SECTORS);
if aligned < first_usable_lba {
return Err(Error::Invalid("hinted start before first usable LBA"));
}
aligned
}
None => self.find_free(length_sectors, first_usable_lba, last_usable_lba)?,
};
let end_lba = start_lba
.checked_add(length_sectors - 1)
.ok_or(Error::Invalid("partition end overflows"))?;
if end_lba > last_usable_lba {
return Err(Error::Invalid("partition extends past last usable LBA"));
}
if self.table_kind == TableKind::Mbr
&& (start_lba > MBR_LBA_MAX || length_sectors > MBR_LBA_MAX)
{
return Err(Error::Invalid("partition exceeds MBR 32-bit LBA range"));
}
for p in &self.partitions {
let p_start = p.start / SECTOR_SIZE;
let p_end = (p.start + p.length) / SECTOR_SIZE - 1;
if start_lba <= p_end && end_lba >= p_start {
return Err(Error::Invalid("partition overlaps existing entry"));
}
}
let kind = match self.table_kind {
TableKind::Gpt => PartitionKind::Gpt {
type_guid: type_id.to_gpt_guid()?,
attributes: 0,
},
TableKind::Mbr => PartitionKind::Mbr {
type_byte: type_id.to_mbr_byte()?,
active: false,
},
};
let uuid = if self.table_kind == TableKind::Gpt {
Some(random_uuid())
} else {
None
};
let part = Partition {
start: start_lba * SECTOR_SIZE,
length: length_sectors * SECTOR_SIZE,
kind,
label,
uuid,
};
let idx = self.partitions.len();
self.partitions.push(part);
Ok(idx)
}
pub fn remove(&mut self, target: PartitionRef) -> Result<()> {
let idx = self.resolve(target)?;
self.partitions.remove(idx);
Ok(())
}
pub fn resize(&mut self, target: PartitionRef, new_length: u64) -> Result<()> {
if new_length == 0 {
return Err(Error::Invalid("zero length"));
}
let idx = self.resolve(target)?;
let new_length_sectors = align_up(new_length.div_ceil(SECTOR_SIZE), ALIGNMENT_SECTORS);
let (_, last_usable_lba) = self.usable_range();
let start_lba = self.partitions[idx].start / SECTOR_SIZE;
let new_end_lba = start_lba
.checked_add(new_length_sectors - 1)
.ok_or(Error::Invalid("partition end overflows"))?;
if new_end_lba > last_usable_lba {
return Err(Error::Invalid("resize extends past last usable LBA"));
}
if self.table_kind == TableKind::Mbr && new_length_sectors > MBR_LBA_MAX {
return Err(Error::Invalid("resize exceeds MBR 32-bit LBA range"));
}
for (j, p) in self.partitions.iter().enumerate() {
if j == idx {
continue;
}
let p_start = p.start / SECTOR_SIZE;
let p_end = (p.start + p.length) / SECTOR_SIZE - 1;
if start_lba <= p_end && new_end_lba >= p_start {
return Err(Error::Invalid("resize would overlap another partition"));
}
}
self.partitions[idx].length = new_length_sectors * SECTOR_SIZE;
Ok(())
}
pub fn commit(&self, dev: &dyn BlockDevice) -> Result<()> {
match self.table_kind {
TableKind::Gpt => {
crate::gpt_write::write_gpt(dev, &self.partitions, self.disk_guid)?;
}
TableKind::Mbr => {
mbr::write_mbr(dev, &self.partitions)?;
}
}
dev.flush()?;
Ok(())
}
fn usable_range(&self) -> (u64, u64) {
let total_sectors = self.disk_size / SECTOR_SIZE;
match self.table_kind {
TableKind::Gpt => {
if total_sectors < GPT_FIRST_USABLE_LBA + GPT_BACKUP_RESERVE_SECTORS + 1 {
return (GPT_FIRST_USABLE_LBA, GPT_FIRST_USABLE_LBA - 1);
}
let last = total_sectors - 1 - GPT_BACKUP_RESERVE_SECTORS;
(GPT_FIRST_USABLE_LBA, last)
}
TableKind::Mbr => {
if total_sectors < 2 {
return (1, 0);
}
let last = (total_sectors - 1).min(MBR_LBA_MAX);
(1, last)
}
}
}
fn resolve(&self, target: PartitionRef) -> Result<usize> {
match target {
PartitionRef::Index(i) => {
if i >= self.partitions.len() {
return Err(Error::Invalid("index out of range"));
}
Ok(i)
}
PartitionRef::Uuid(u) => self
.partitions
.iter()
.position(|p| p.uuid == Some(u))
.ok_or(Error::Invalid("uuid not found")),
}
}
fn find_free(&self, length_sectors: u64, first_usable: u64, last_usable: u64) -> Result<u64> {
let mut sorted: Vec<&Partition> = self.partitions.iter().collect();
sorted.sort_by_key(|p| p.start);
let mut cursor = align_up(first_usable, ALIGNMENT_SECTORS);
for p in &sorted {
let p_start = p.start / SECTOR_SIZE;
let p_end = (p.start + p.length) / SECTOR_SIZE - 1;
if cursor + length_sectors - 1 < p_start {
if cursor + length_sectors - 1 <= last_usable {
return Ok(cursor);
}
}
cursor = align_up(p_end + 1, ALIGNMENT_SECTORS);
}
if cursor + length_sectors - 1 <= last_usable {
Ok(cursor)
} else {
Err(Error::Invalid("no free space large enough"))
}
}
}
fn align_up(value: u64, align: u64) -> u64 {
debug_assert!(align > 0);
let rem = value % align;
if rem == 0 {
value
} else {
value + (align - rem)
}
}
pub(crate) fn random_uuid() -> [u8; 16] {
let mut bytes = [0u8; 16];
if !fill_from_urandom(&mut bytes) {
fill_from_prng(&mut bytes);
}
bytes[7] = (bytes[7] & 0x0F) | 0x40;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
bytes
}
#[cfg(unix)]
fn fill_from_urandom(buf: &mut [u8]) -> bool {
use std::io::Read;
match std::fs::File::open("/dev/urandom") {
Ok(mut f) => f.read_exact(buf).is_ok(),
Err(_) => false,
}
}
#[cfg(not(unix))]
fn fill_from_urandom(_buf: &mut [u8]) -> bool {
false
}
fn fill_from_prng(buf: &mut [u8]) {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0x9E37_79B9_7F4A_7C15);
let pid = std::process::id() as u64;
let mut state = nanos.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(pid);
if state == 0 {
state = 0x9E37_79B9_7F4A_7C15;
}
for chunk in buf.chunks_mut(8) {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let b = state.to_le_bytes();
chunk.copy_from_slice(&b[..chunk.len()]);
}
}