blob: a1242ab4e8f50ea734511a3501ffe1fa1548aad7 [file] [log] [blame]
//! A pure-Rust library to work with GPT partition tables.
//!
//! It provides support for manipulating (R/W) GPT headers and partition
//! tables. Raw disk devices as well as disk images are supported.
//!
//! ```
//! extern crate gpt;
//! use std::convert::TryFrom;
//!
//! fn inspect_disk() {
//! let diskpath = std::path::Path::new("/dev/sdz");
//! let cfg = gpt::GptConfig::new().writable(false);
//!
//! let disk = cfg.open(diskpath).expect("failed to open disk");
//!
//! println!("Disk header: {:#?}", disk.primary_header());
//! println!("Partition layout: {:#?}", disk.partitions());
//! }
//!
//! fn create_partition() {
//! let diskpath = std::path::Path::new("/tmp/chris.img");
//! let cfg = gpt::GptConfig::new().writable(true).initialized(true);
//! let mut disk = cfg.open(diskpath).expect("failed to open disk");
//! let result = disk.add_partition(
//! "rust_partition",
//! 100,
//! gpt::partition_types::LINUX_FS,
//! 0,
//! );
//! disk.write().unwrap();
//! }
//!
//! /// Demonstrates how to create a new partition table without anything pre-existing
//! fn create_partition_in_ram() {
//! const TOTAL_BYTES: usize = 1024 * 64;
//! let mut mem_device = Box::new(std::io::Cursor::new(vec![0u8; TOTAL_BYTES]));
//!
//! // Create a protective MBR at LBA0
//! let mbr = gpt::mbr::ProtectiveMBR::with_lb_size(
//! u32::try_from((TOTAL_BYTES / 512) - 1).unwrap_or(0xFF_FF_FF_FF));
//! mbr.overwrite_lba0(&mut mem_device).expect("failed to write MBR");
//!
//! let mut gdisk = gpt::GptConfig::default()
//! .initialized(false)
//! .writable(true)
//! .logical_block_size(gpt::disk::LogicalBlockSize::Lb512)
//! .create_from_device(mem_device, None)
//! .expect("failed to crate GptDisk");
//!
//! // Initialize the headers using a blank partition table
//! gdisk.update_partitions(
//! std::collections::BTreeMap::<u32, gpt::partition::Partition>::new()
//! ).expect("failed to initialize blank partition table");
//!
//! // At this point, gdisk.primary_header() and gdisk.backup_header() are populated...
//! // Add a few partitions to demonstrate how...
//! gdisk.add_partition("test1", 1024 * 12, gpt::partition_types::BASIC, 0)
//! .expect("failed to add test1 partition");
//! gdisk.add_partition("test2", 1024 * 18, gpt::partition_types::LINUX_FS, 0)
//! .expect("failed to add test2 partition");
//! // Write the partition table and take ownership of
//! // the underlying memory buffer-backed block device
//! let mut mem_device = gdisk.write().expect("failed to write partition table");
//! // Read the written bytes out of the memory buffer device
//! mem_device.seek(std::io::SeekFrom::Start(0)).expect("failed to seek");
//! let mut final_bytes = vec![0u8; TOTAL_BYTES];
//! mem_device.read_exact(&mut final_bytes)
//! .expect("failed to read contents of memory device");
//! }
//!
//! // only manipulates memory buffers, so this can run on any system...
//! create_partition_in_ram();
//! ```
#![deny(missing_docs)]
use log::*;
use std::collections::BTreeMap;
use std::io::{Read, Seek, Write};
use std::{fs, io, path};
#[macro_use]
mod macros;
pub mod disk;
pub mod header;
pub mod mbr;
pub mod partition;
pub mod partition_types;
/// A generic device that we can read/write partitions from/to.
pub trait DiskDevice: Read + Write + Seek + std::fmt::Debug {}
/// Implement the DiskDevice trait for anything that meets the
/// requirements, e.g., `std::fs::File`
impl<T> DiskDevice for T where T: Read + Write + Seek + std::fmt::Debug {}
/// A dynamic trait object that is used by GptDisk for reading/writing/seeking.
pub type DiskDeviceObject<'a> = Box<dyn DiskDevice + 'a>;
/// Configuration options to open a GPT disk.
#[derive(Debug, Eq, PartialEq)]
pub struct GptConfig {
/// Logical block size.
lb_size: disk::LogicalBlockSize,
/// Whether to open a GPT partition table in writable mode.
writable: bool,
/// Whether to expect and parse an initialized disk image.
initialized: bool,
}
impl GptConfig {
// TODO(lucab): complete support for skipping backup
// header, etc, then expose all config knobs here.
/// Create a new default configuration.
pub fn new() -> Self {
GptConfig::default()
}
/// Whether to open a GPT partition table in writable mode.
pub fn writable(mut self, writable: bool) -> Self {
self.writable = writable;
self
}
/// Whether to assume an initialized GPT disk and read its
/// partition table on open.
pub fn initialized(mut self, initialized: bool) -> Self {
self.initialized = initialized;
self
}
/// Size of logical blocks (sectors) for this disk.
pub fn logical_block_size(mut self, lb_size: disk::LogicalBlockSize) -> Self {
self.lb_size = lb_size;
self
}
/// Open the GPT disk at the given path and inspect it according
/// to configuration options.
pub fn open(self, diskpath: &path::Path) -> io::Result<GptDisk> {
let file = Box::new(fs::OpenOptions::new()
.write(self.writable)
.read(true)
.open(diskpath)?);
self.open_from_device(file as DiskDeviceObject)
}
/// Open the GPT disk from the given DiskDeviceObject and
/// inspect it according to configuration options.
pub fn open_from_device(self, mut device: DiskDeviceObject) -> io::Result<GptDisk> {
// Uninitialized disk, no headers/table to parse.
if !self.initialized {
return self.create_from_device(device, Some(uuid::Uuid::new_v4()));
}
// Proper GPT disk, fully inspect its layout.
let h1 = header::read_primary_header(&mut device, self.lb_size)?;
let h2 = header::read_backup_header(&mut device, self.lb_size)?;
let table = partition::file_read_partitions(&mut device, &h1, self.lb_size)?;
let disk = GptDisk {
config: self,
device,
guid: h1.disk_guid,
primary_header: Some(h1),
backup_header: Some(h2),
partitions: table,
};
debug!("disk: {:?}", disk);
Ok(disk)
}
/// Create a GPTDisk with default headers and an empty partition table.
/// If guid is None then it will generate a new random guid.
pub fn create_from_device(
self,
device: DiskDeviceObject,
guid: Option<uuid::Uuid>
) -> io::Result<GptDisk> {
if self.initialized {
Err(io::Error::new(
io::ErrorKind::Other,
"we were expecting to read an existing partition table, but \
instead we're attempting to create a new blank table",
))
} else {
let empty = GptDisk {
config: self,
device,
guid: guid.unwrap_or_else(uuid::Uuid::new_v4),
primary_header: None,
backup_header: None,
partitions: BTreeMap::new(),
};
Ok(empty)
}
}
}
impl Default for GptConfig {
fn default() -> Self {
Self {
lb_size: disk::DEFAULT_SECTOR_SIZE,
initialized: true,
writable: false,
}
}
}
/// A GPT disk backed by an arbitrary device.
#[derive(Debug)]
pub struct GptDisk<'a> {
config: GptConfig,
device: DiskDeviceObject<'a>,
guid: uuid::Uuid,
primary_header: Option<header::Header>,
backup_header: Option<header::Header>,
partitions: BTreeMap<u32, partition::Partition>,
}
impl<'a> GptDisk<'a> {
/// Add another partition to this disk. This tries to find
/// the optimum partition location with the lowest block device.
/// Returns the new partition id if there was sufficient room
/// to add the partition. Size is specified in bytes.
pub fn add_partition(
&mut self,
name: &str,
size: u64,
part_type: partition_types::Type,
flags: u64,
) -> io::Result<u32> {
let size_lba = match size.checked_div(self.config.lb_size.into()) {
Some(s) => s,
None => {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"size must be greater than {} which is the logical block size.",
self.config.lb_size
),
));
}
};
// Find the lowest lba that is larger than size.
let free_sections = self.find_free_sectors();
for (starting_lba, length) in free_sections {
debug!("starting_lba {}, length {}", starting_lba, length);
if length >= (size_lba - 1) {
// Found our free slice.
let partition_id = self.find_next_partition_id();
debug!(
"Adding partition id: {} {:?}. first_lba: {} last_lba: {}",
partition_id,
part_type,
starting_lba,
starting_lba + size_lba - 1_u64
);
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);
}
return Ok(partition_id);
}
}
Err(io::Error::new(
io::ErrorKind::Other,
"Unable to find enough space on drive",
))
}
/// remove partition from this disk. This tries to find the partition based on either a
/// given partition number (id) or a partition guid. Returns the partition id if the
/// partition is removed
pub fn remove_partition(
&mut self,
id: Option<u32>,
partguid: Option<uuid::Uuid>,
) -> io::Result<u32> {
if let Some(part_id) = id {
if let Some(partition_id) = self.partitions.remove(&part_id) {
debug!("Removing partition number {}", partition_id);
}
return Ok(part_id);
}
if let Some(part_guid) = partguid {
for (key, partition) in &self.partitions.clone() {
if partition.part_guid == part_guid {
if let Some(partition_id) = self.partitions.remove(&key) {
debug!("Removing partition number {}", partition_id);
}
return Ok(*key);
}
}
}
Err(io::Error::new(
io::ErrorKind::Other,
"Unable to find partition to remove",
))
}
/// Find free space on the disk.
/// Returns a tuple of (starting_lba, length in lba's).
pub fn find_free_sectors(&self) -> Vec<(u64, u64)> {
if let Some(header) = self.primary_header().or_else(|| self.backup_header()) {
trace!("first_usable: {}", header.first_usable);
let mut disk_positions = vec![header.first_usable];
for part in self.partitions().iter().filter(|p| p.1.is_used()) {
trace!("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);
trace!("last_usable: {}", header.last_usable);
disk_positions.sort_unstable();
return disk_positions
// Walk through the LBA's in chunks of 2 (ending, starting).
.chunks(2)
// Add 1 to the ending and then subtract the starting if NOT the first usable sector
.map(|p| {
if p[0] != header.first_usable {
(p[0] + 1, p[1].saturating_sub(p[0] + 1))
} else {
(p[0], p[1].saturating_sub(p[0]))
}
})
.collect();
}
// No primary header. Return nothing.
vec![]
}
/// Find next highest partition id.
pub fn find_next_partition_id(&self) -> u32 {
let max = match self
.partitions()
.iter()
// Skip unused partitions.
.filter(|p| p.1.is_used())
// Find the maximum id.
.max_by_key(|x| x.0)
{
Some(i) => *i.0,
// Partitions start at 1.
None => return 1,
};
for i in 1..max {
if self.partitions().get(&i).is_none() {
return i;
}
}
max + 1
}
/// Retrieve primary header, if any.
pub fn primary_header(&self) -> Option<&header::Header> {
self.primary_header.as_ref()
}
/// Retrieve backup header, if any.
pub fn backup_header(&self) -> Option<&header::Header> {
self.backup_header.as_ref()
}
/// Retrieve partition entries.
pub fn partitions(&self) -> &BTreeMap<u32, partition::Partition> {
&self.partitions
}
/// Retrieve disk UUID.
pub fn guid(&self) -> &uuid::Uuid {
&self.guid
}
/// Retrieve disk logical block size.
pub fn logical_block_size(&self) -> &disk::LogicalBlockSize {
&self.config.lb_size
}
/// Change the disk device that we are reading/writing from/to.
/// Returns the previous disk device.
pub fn update_disk_device(
&mut self,
device: DiskDeviceObject<'a>,
writable: bool
) -> DiskDeviceObject {
self.config.writable = writable;
std::mem::replace(&mut self.device, device)
}
/// Update disk UUID.
///
/// If no UUID is specified, a new random one is generated.
/// No changes are recorded to disk until `write()` is called.
pub fn update_guid(&mut self, uuid: Option<uuid::Uuid>) -> io::Result<&Self> {
let guid = match uuid {
Some(u) => u,
None => {
let u = uuid::Uuid::new_v4();
debug!("Generated random uuid: {}", u);
u
}
};
self.guid = guid;
Ok(self)
}
/// Update current partition table.
///
/// No changes are recorded to disk until `write()` is called.
pub fn update_partitions(
&mut self,
pp: BTreeMap<u32, partition::Partition>,
) -> io::Result<&Self> {
// TODO(lucab): validate partitions.
let bak = header::find_backup_lba(&mut self.device, self.config.lb_size)?;
let h1 = header::Header::compute_new(
true, &pp, self.guid, bak, &self.primary_header, self.config.lb_size)?;
let h2 = header::Header::compute_new(
false, &pp, self.guid, bak, &self.backup_header, self.config.lb_size)?;
self.primary_header = Some(h1);
self.backup_header = Some(h2);
self.partitions = pp;
self.config.initialized = true;
Ok(self)
}
/// Persist state to disk, consuming this disk object.
///
/// This is a destructive action, as it overwrite headers and
/// partitions entries on disk. All writes are flushed to disk
/// before returning the underlying DiskDeviceObject.
pub fn write(mut self) -> io::Result<DiskDeviceObject<'a>> {
self.write_inplace()?;
Ok(self.device)
}
/// Persist state to disk, leaving this disk object intact.
///
/// This is a destructive action, as it overwrites headers
/// and partitions entries on disk. All writes are flushed
/// to disk before returning.
pub fn write_inplace(&mut self) -> io::Result<()> {
if !self.config.writable {
return Err(io::Error::new(
io::ErrorKind::Other,
"disk not opened in writable mode",
));
}
if !self.config.initialized {
return Err(io::Error::new(io::ErrorKind::Other, "disk not initialized"));
}
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 = self.primary_header.clone().unwrap();
let backup_header = self.backup_header.clone();
// Write all of the used partitions at the start of the partition array.
let mut next_partition_index = 0u64;
for partition in self.partitions().clone().iter().filter(|p| p.1.is_used()) {
// don't allow us to overflow partition array...
if next_partition_index >= u64::from(primary_header.num_parts) {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("attempting to write more than max of {} partitions in primary array",
primary_header.num_parts),
));
}
// Write to primary partition array
partition.1.write_to_device(
&mut self.device,
next_partition_index,
primary_header.part_start,
self.config.lb_size,
primary_header.part_size,
)?;
// IMPORTANT: must also write it to the backup header if it uses a different
// area to store the partition array; otherwise backup header will not point
// to an up to date partition array on disk.
if let Some(backup_header) = backup_header.as_ref() {
if next_partition_index >= u64::from(backup_header.num_parts) {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("attempting to write more than max of {} partitions in backup array",
backup_header.num_parts),
));
}
if primary_header.part_start != backup_header.part_start {
partition.1.write_to_device(
&mut self.device,
next_partition_index,
backup_header.part_start,
self.config.lb_size,
backup_header.part_size,
)?;
}
}
next_partition_index += 1;
}
// Next, write zeros to the rest of the primary/backup partition array
// (ensures any newly deleted partitions are truly removed from disk, etc.)
// NOTE: we should never underflow here because of boundary checking in loop above.
partition::Partition::write_zero_entries_to_device(
&mut self.device,
next_partition_index,
u64::from(primary_header.num_parts).checked_sub(next_partition_index).unwrap(),
primary_header.part_start,
self.config.lb_size,
primary_header.part_size,
)?;
if let Some(backup_header) = backup_header.as_ref() {
partition::Partition::write_zero_entries_to_device(
&mut self.device,
next_partition_index,
u64::from(backup_header.num_parts).checked_sub(next_partition_index).unwrap(),
backup_header.part_start,
self.config.lb_size,
backup_header.part_size,
)?;
}
let new_backup_header = header::Header::compute_new(
false,
&self.partitions,
self.guid,
bak,
&self.primary_header,
self.config.lb_size,
)?;
let new_primary_header = header::Header::compute_new(
true,
&self.partitions,
self.guid,
bak,
&self.backup_header,
self.config.lb_size,
)?;
debug!("Writing backup header");
new_backup_header.write_backup(&mut self.device, self.config.lb_size)?;
debug!("Writing primary header");
new_primary_header.write_primary(&mut self.device, self.config.lb_size)?;
trace!("new primary header: {:?}", new_primary_header);
trace!("new backup header: {:?}", new_backup_header);
self.device.flush()?;
self.primary_header = Some(new_primary_header);
self.backup_header = Some(new_backup_header);
Ok(())
}
}