#![deny(missing_docs)]
use std::collections::BTreeMap;
use std::io::{Read, Seek, Write};
use std::{fmt, fs, io, path};
#[macro_use]
mod macros;
#[macro_use]
mod logging;
pub mod disk;
pub mod header;
pub mod mbr;
pub mod partition;
pub mod partition_types;
use header::HeaderError;
use macros::ResultInsert;
pub trait DiskDevice: Read + Write + Seek + std::fmt::Debug {}
impl<T> DiskDevice for T where T: Read + Write + Seek + std::fmt::Debug {}
pub type DiskDeviceObject<'a> = Box<dyn DiskDevice + 'a>;
#[non_exhaustive]
#[derive(Debug)]
pub enum GptError {
Io(io::Error),
Header(HeaderError),
CreatingInitializedDisk,
Overflow(&'static str),
NotEnoughSpace,
ReadOnly,
OverflowPartitionCount,
PartitionCountWouldChange,
PartitionIdAlreadyUsed,
}
impl From<io::Error> for GptError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
impl From<HeaderError> for GptError {
fn from(e: HeaderError) -> Self {
Self::Header(e)
}
}
impl std::error::Error for GptError {}
impl fmt::Display for GptError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
use GptError::*;
let desc = match self {
Io(e) => return write!(fmt, "GPT IO Error: {e}"),
Header(e) => return write!(fmt, "GPT Header Error: {e}"),
CreatingInitializedDisk => {
"we were expecting to read an existing \
partition table, but instead we're attempting to create a \
new blank table"
}
Overflow(m) => return write!(fmt, "GTP error Overflow: {m}"),
NotEnoughSpace => "Unable to find enough space on drive",
ReadOnly => "disk not opened in writable mode",
OverflowPartitionCount => "not enough partition slots",
PartitionCountWouldChange => {
"partition would change but is not \
allowed"
}
PartitionIdAlreadyUsed => "partition id already used",
};
write!(fmt, "{desc}")
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct GptConfig {
lb_size: disk::LogicalBlockSize,
writable: bool,
only_valid_headers: bool,
readonly_backup: bool,
change_partition_count: bool,
}
impl GptConfig {
pub fn new() -> Self {
GptConfig::default()
}
pub fn writable(mut self, writable: bool) -> Self {
self.writable = writable;
self
}
pub fn logical_block_size(mut self, lb_size: disk::LogicalBlockSize) -> Self {
self.lb_size = lb_size;
self
}
pub fn only_valid_headers(mut self, only_valid_headers: bool) -> Self {
self.only_valid_headers = only_valid_headers;
self
}
pub fn readonly_backup(mut self, readonly_backup: bool) -> Self {
self.readonly_backup = readonly_backup;
self
}
pub fn change_partition_count(mut self, change_partition_count: bool) -> Self {
self.change_partition_count = change_partition_count;
self
}
pub fn open(self, diskpath: impl AsRef<path::Path>) -> Result<GptDisk<fs::File>, GptError> {
let file = fs::OpenOptions::new()
.write(self.writable)
.read(true)
.open(diskpath)?;
let mut gpt = self.open_from_device(file)?;
gpt.sync_all = Some(file_sync_all);
Ok(gpt)
}
pub fn create(self, diskpath: impl AsRef<path::Path>) -> Result<GptDisk<fs::File>, GptError> {
let file = fs::OpenOptions::new()
.write(self.writable)
.read(true)
.open(diskpath)?;
let mut gpt = self.create_from_device(file, None)?;
gpt.sync_all = Some(file_sync_all);
Ok(gpt)
}
pub fn open_from_device<D>(self, mut device: D) -> Result<GptDisk<D>, GptError>
where
D: DiskDevice,
{
let h1 = header::read_primary_header(&mut device, self.lb_size);
let h2 = header::read_backup_header(&mut device, self.lb_size);
let (h1, h2) = if self.only_valid_headers {
(Ok(h1?), Ok(h2?))
} else if h1.is_err() && h2.is_err() {
return Err(h1.unwrap_err().into());
} else {
(h1, h2)
};
let header = h1.as_ref().or(h2.as_ref()).unwrap();
let table = partition::file_read_partitions(&mut device, header, self.lb_size)?;
let disk = GptDisk {
config: self,
device,
guid: header.disk_guid,
primary_header: h1,
backup_header: h2,
partitions: table,
sync_all: None,
};
debug!("disk: {:?}", disk);
Ok(disk)
}
pub fn create_from_device<D>(
self,
device: D,
guid: Option<uuid::Uuid>,
) -> Result<GptDisk<D>, GptError>
where
D: DiskDevice,
{
let mut disk = GptDisk {
config: self,
device,
guid: guid.unwrap_or_else(uuid::Uuid::new_v4),
primary_header: Err(HeaderError::InvalidGptSignature),
backup_header: Err(HeaderError::InvalidGptSignature),
partitions: BTreeMap::new(),
sync_all: None,
};
disk.init_headers()?;
Ok(disk)
}
}
impl Default for GptConfig {
fn default() -> Self {
Self {
lb_size: disk::DEFAULT_SECTOR_SIZE,
writable: false,
only_valid_headers: false,
readonly_backup: false,
change_partition_count: false,
}
}
}
pub struct GptDisk<D> {
config: GptConfig,
device: D,
guid: uuid::Uuid,
primary_header: Result<header::Header, HeaderError>,
backup_header: Result<header::Header, HeaderError>,
partitions: BTreeMap<u32, partition::Partition>,
sync_all: Option<fn(&mut D) -> io::Result<()>>,
}
impl<D> fmt::Debug for GptDisk<D>
where
D: fmt::Debug,
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.debug_struct("GptDisk")
.field("config", &self.config)
.field("device", &self.device)
.field("guid", &self.guid)
.field("primary_header", &self.primary_header)
.field("backup_header", &self.backup_header)
.field("partitions", &self.partitions)
.finish()
}
}
impl<D: Clone> Clone for GptDisk<D> {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
device: self.device.clone(),
guid: self.guid,
primary_header: self
.primary_header
.as_ref()
.map_err(|e| e.lossy_clone())
.cloned(),
backup_header: self
.backup_header
.as_ref()
.map_err(|e| e.lossy_clone())
.cloned(),
partitions: self.partitions.clone(),
sync_all: self.sync_all,
}
}
}
impl<D> GptDisk<D> {
pub fn primary_header(&self) -> Result<&header::Header, HeaderError> {
self.primary_header.as_ref().map_err(|e| e.lossy_clone())
}
pub fn backup_header(&self) -> Result<&header::Header, HeaderError> {
self.backup_header.as_ref().map_err(|e| e.lossy_clone())
}
fn try_header(&self) -> Result<&header::Header, HeaderError> {
self.primary_header
.as_ref()
.or(self.backup_header.as_ref())
.map_err(|e| e.lossy_clone())
}
pub fn header(&self) -> &header::Header {
self.try_header().expect("no primary and no backup header")
}
pub fn partitions(&self) -> &BTreeMap<u32, partition::Partition> {
&self.partitions
}
pub fn guid(&self) -> &uuid::Uuid {
&self.guid
}
pub fn logical_block_size(&self) -> &disk::LogicalBlockSize {
&self.config.lb_size
}
pub fn update_disk_device(&mut self, device: D, writable: bool) -> D {
self.config.writable = writable;
std::mem::replace(&mut self.device, device)
}
pub fn with_disk_device<N>(&self, device: N, writable: bool) -> GptDisk<N> {
let mut n = GptDisk {
config: self.config.clone(),
device,
guid: self.guid,
primary_header: self
.primary_header
.as_ref()
.map_err(|e| e.lossy_clone())
.cloned(),
backup_header: self
.backup_header
.as_ref()
.map_err(|e| e.lossy_clone())
.cloned(),
partitions: self.partitions.clone(),
sync_all: None,
};
n.config.writable = writable;
n
}
pub fn device_ref(&self) -> &D {
&self.device
}
pub fn device_mut(&mut self) -> &mut D {
&mut self.device
}
pub fn take_device(self) -> D {
self.device
}
}
impl<D> GptDisk<D>
where
D: DiskDevice,
{
pub fn add_partition(
&mut self,
name: &str,
size: u64,
part_type: partition_types::Type,
flags: u64,
part_alignment: Option<u64>,
) -> Result<u32, GptError> {
assert!(size > 0, "size must be greater than zero");
let size_lba = (size - 1)
.checked_div(self.config.lb_size.into())
.ok_or(GptError::Overflow(
"invalid logical block size caused bad \
division when calculating size in blocks",
))?
+ 1;
let free_sections = self.find_free_sectors();
for (starting_lba, length) in free_sections {
let alignment_offset_lba = match part_alignment {
Some(alignment) => (alignment - (starting_lba % alignment)) % alignment,
None => 0_u64,
};
debug!(
"starting_lba {}, length {}, alignment_offset_lba {}",
starting_lba, length, alignment_offset_lba
);
if length >= (alignment_offset_lba + size_lba) {
let starting_lba = starting_lba + alignment_offset_lba;
let partition_id = self
.find_next_partition_id()
.unwrap_or_else(|| self.header().num_parts + 1);
debug!(
"Adding partition id: {} {:?}. first_lba: {} last_lba: {}",
partition_id,
part_type,
starting_lba,
starting_lba + size_lba - 1_u64
);
let num_parts_changes = self.header().num_parts_would_change(partition_id);
if num_parts_changes && !self.config.change_partition_count {
return Err(GptError::PartitionCountWouldChange);
}
let part = partition::Partition {
part_type_guid: part_type,
part_guid: uuid::Uuid::new_v4(),
first_lba: starting_lba,
last_lba: starting_lba + size_lba - 1_u64,
flags,
name: name.to_string(),
};
if let Some(p) = self.partitions.insert(partition_id, part.clone()) {
debug!("Replacing\n{}\nwith\n{}", p, part);
}
if num_parts_changes {
self.init_headers()?;
}
return Ok(partition_id);
}
}
Err(GptError::NotEnoughSpace)
}
pub fn add_partition_at(
&mut self,
name: &str,
id: u32,
first_lba: u64,
length_lba: u64,
part_type: partition_types::Type,
flags: u64,
) -> Result<u32, GptError> {
assert!(length_lba > 0, "length must be greater than zero");
assert!(id > 0, "id must be greater than zero");
match self.partitions.get(&id) {
Some(p) if p.is_used() => return Err(GptError::PartitionIdAlreadyUsed),
_ => {
}
}
for (starting_lba, length) in self.find_free_sectors() {
if !(first_lba >= starting_lba && length_lba <= length) {
continue;
}
debug!(
"starting_lba {}, length {}, id {}",
first_lba, length_lba, id
);
debug!(
"Adding partition id: {} {:?}. first_lba: {} last_lba: {}",
id,
part_type,
first_lba,
first_lba + length_lba - 1_u64
);
let num_parts_changes = self.header().num_parts_would_change(id);
if num_parts_changes && !self.config.change_partition_count {
return Err(GptError::PartitionCountWouldChange);
}
let part = partition::Partition {
part_type_guid: part_type,
part_guid: uuid::Uuid::new_v4(),
first_lba,
last_lba: first_lba + length_lba - 1_u64,
flags,
name: name.to_string(),
};
if let Some(p) = self.partitions.insert(id, part.clone()) {
debug!("Replacing\n{}\nwith\n{}", p, part);
}
if num_parts_changes {
self.init_headers()?;
}
return Ok(id);
}
Err(GptError::NotEnoughSpace)
}
pub fn calculate_alignment(&self) -> u64 {
if self.partitions.is_empty() {
return 0;
}
const MAX_ALIGN: u64 = 2048;
let mut align = MAX_ALIGN;
let mut exponent = (align as f64).log2() as u32;
for partition in self.partitions.values().filter(|p| p.is_used()) {
loop {
align = u64::pow(2, exponent);
if (partition.first_lba % align) == 0 {
break;
}
exponent -= 1;
}
}
align
}
pub fn remove_partition(&mut self, id: u32) -> Option<u32> {
self.partitions.remove(&id).map(|_| {
debug!("Removing partition number {id}");
id
})
}
pub fn remove_partition_by_guid(&mut self, guid: uuid::Uuid) -> Option<u32> {
let id = self
.partitions
.iter()
.find(|(_, v)| v.part_guid == guid)
.map(|(k, _)| *k)?;
debug!("Removing partition number {id}");
self.partitions.remove(&id);
Some(id)
}
pub fn find_free_sectors(&self) -> Vec<(u64, u64)> {
let header = self.header();
trace!("first_usable: {}", header.first_usable);
let mut disk_positions = vec![header.first_usable - 1];
for part in self.partitions().iter().filter(|p| p.1.is_used()) {
trace!(
"used partition: ({}, {})",
part.1.first_lba,
part.1.last_lba
);
disk_positions.push(part.1.first_lba);
disk_positions.push(part.1.last_lba);
}
disk_positions.push(header.last_usable + 1);
trace!("last_usable: {}", header.last_usable);
disk_positions.sort_unstable();
disk_positions
.chunks_exact(2)
.filter_map(|p| {
let start = p[0] + 1;
let end = p[1] - 1;
#[allow(clippy::unnecessary_lazy_evaluations)]
(start <= end).then(|| (start, end - start + 1))
})
.collect()
}
pub fn find_next_partition_id(&self) -> Option<u32> {
if self.partitions.is_empty() {
return Some(1);
}
for i in 1..=self.header().num_parts {
match self.partitions.get(&i) {
Some(p) if !p.is_used() => return Some(i),
None => return Some(i),
_ => {}
}
}
None
}
pub fn take_partitions(&mut self) -> BTreeMap<u32, partition::Partition> {
std::mem::take(&mut self.partitions)
}
pub fn update_guid(&mut self, uuid: Option<uuid::Uuid>) {
let guid = match uuid {
Some(u) => u,
None => {
let u = uuid::Uuid::new_v4();
debug!("Generated random uuid: {}", u);
u
}
};
self.guid = guid;
}
pub fn update_partitions(
&mut self,
pp: BTreeMap<u32, partition::Partition>,
) -> Result<(), GptError> {
assert!(!pp.contains_key(&0));
let num_parts = pp.len() as u32;
let num_parts_changes = self.header().num_parts_would_change(num_parts);
if num_parts_changes && !self.config.change_partition_count {
return Err(GptError::PartitionCountWouldChange);
}
self.partitions = pp;
self.init_headers()
}
pub(crate) fn init_headers(&mut self) -> Result<(), GptError> {
let bak = header::find_backup_lba(&mut self.device, self.config.lb_size)?;
let num_parts = self.partitions.len() as u32;
let h1 = header::HeaderBuilder::from_maybe_header(self.try_header())
.num_parts(num_parts)
.backup_lba(bak)
.disk_guid(self.guid)
.primary(true)
.build(self.config.lb_size)?;
let header = self.primary_header.insert_ok(h1);
if !self.config.readonly_backup {
let h2 = header::HeaderBuilder::from_header(header)
.primary(false)
.build(self.config.lb_size)?;
self.backup_header = Ok(h2);
}
Ok(())
}
pub fn write(mut self) -> Result<D, GptError> {
self.write_inplace()?;
Ok(self.device)
}
pub fn write_inplace(&mut self) -> Result<(), GptError> {
if !self.config.writable {
return Err(GptError::ReadOnly);
}
debug!("Computing new headers");
trace!("old primary header: {:?}", self.primary_header);
trace!("old backup header: {:?}", self.backup_header);
let bak = header::find_backup_lba(&mut self.device, self.config.lb_size)?;
trace!("old backup lba: {}", bak);
let primary_header = header::HeaderBuilder::from_header(self.header())
.primary(true)
.build(self.config.lb_size)?;
let primary_header = self.primary_header.insert_ok(primary_header);
let backup_header = if !self.config.readonly_backup {
let header = header::HeaderBuilder::from_header(primary_header)
.primary(false)
.build(self.config.lb_size)?;
Some(self.backup_header.insert_ok(header))
} else {
None
};
let mut next_partition_index = 0;
for (part_idx, partition) in self
.partitions
.clone()
.iter()
.filter(|p| p.1.is_used())
.enumerate()
{
if part_idx >= primary_header.num_parts as usize {
return Err(GptError::OverflowPartitionCount);
}
partition.1.write_to_device(
&mut self.device,
part_idx as u64,
primary_header.part_start,
self.config.lb_size,
primary_header.part_size,
)?;
if let Some(backup_header) = &backup_header {
if part_idx >= backup_header.num_parts as usize {
return Err(GptError::OverflowPartitionCount);
}
if primary_header.part_start != backup_header.part_start {
partition.1.write_to_device(
&mut self.device,
part_idx as u64,
backup_header.part_start,
self.config.lb_size,
backup_header.part_size,
)?;
}
}
next_partition_index = part_idx + 1;
}
partition::Partition::write_zero_entries_to_device(
&mut self.device,
next_partition_index as u64,
(primary_header.num_parts as u64)
.checked_sub(next_partition_index as u64)
.unwrap(),
primary_header.part_start,
self.config.lb_size,
primary_header.part_size,
)?;
if let Some(backup_header) = &backup_header {
partition::Partition::write_zero_entries_to_device(
&mut self.device,
next_partition_index as u64,
(backup_header.num_parts as u64)
.checked_sub(next_partition_index as u64)
.unwrap(),
backup_header.part_start,
self.config.lb_size,
backup_header.part_size,
)?;
}
if let Some(backup_header) = backup_header {
debug!("Writing backup header");
backup_header.write_backup(&mut self.device, self.config.lb_size)?;
}
debug!("Writing primary header");
primary_header.write_primary(&mut self.device, self.config.lb_size)?;
self.device.flush()?;
if let Some(sync_all) = self.sync_all {
sync_all(&mut self.device)?;
}
Ok(())
}
}
fn file_sync_all(device: &mut fs::File) -> io::Result<()> {
device.sync_all()
}