use uuid::Uuid;
use super::mbr::write_protective_mbr;
use super::{Partition, PartitionKind, PartitionTable, uuid_from_gpt_bytes, uuid_to_gpt_bytes};
use crate::Result;
use crate::block::BlockDevice;
pub const SIGNATURE: &[u8; 8] = b"EFI PART";
pub const REVISION: u32 = 0x0001_0000;
pub const HEADER_SIZE: u32 = 92;
pub const NUM_ENTRIES: u32 = 128;
pub const ENTRY_SIZE: u32 = 128;
pub const ENTRIES_LBAS: u64 = (NUM_ENTRIES as u64 * ENTRY_SIZE as u64) / 512;
pub const FIRST_USABLE_LBA: u64 = 1 + 1 + ENTRIES_LBAS;
pub const REQUIRED_BLOCK_SIZE: u32 = 512;
#[derive(Debug, Clone)]
pub struct Gpt {
pub disk_guid: Uuid,
pub partitions: Vec<Partition>,
}
impl Gpt {
pub fn build(mut partitions: Vec<Partition>) -> Result<Self> {
if partitions.len() > NUM_ENTRIES as usize {
return Err(crate::Error::InvalidArgument(format!(
"GPT supports up to {NUM_ENTRIES} entries, got {}",
partitions.len()
)));
}
for p in &mut partitions {
if p.kind == PartitionKind::Empty {
continue;
}
if p.size_lba == 0 {
return Err(crate::Error::InvalidArgument(
"GPT partition has zero size".into(),
));
}
if p.uuid.is_none() {
p.uuid = Some(Uuid::new_v4());
}
if let Some(ref n) = p.name {
if n.encode_utf16().count() > 36 {
return Err(crate::Error::InvalidArgument(format!(
"GPT partition name exceeds 36 UTF-16 code units: {n:?}"
)));
}
}
}
check_no_overlap(&partitions)?;
Ok(Self {
disk_guid: Uuid::new_v4(),
partitions,
})
}
pub fn read(dev: &mut dyn BlockDevice) -> Result<Self> {
require_block_size(dev)?;
let total_lba = dev.total_size() / 512;
if total_lba < FIRST_USABLE_LBA + ENTRIES_LBAS + 1 {
return Err(crate::Error::InvalidImage(
"device too small for a GPT".into(),
));
}
let mut hdr_buf = [0u8; 512];
dev.read_at(512, &mut hdr_buf)?;
let hdr = parse_header(&hdr_buf)?;
if hdr.my_lba != 1 {
return Err(crate::Error::InvalidImage(format!(
"GPT primary header has unexpected my_lba {}",
hdr.my_lba
)));
}
let entries_bytes = hdr.num_entries as usize * hdr.entry_size as usize;
let mut entries_buf = vec![0u8; entries_bytes];
dev.read_at(hdr.entries_start_lba * 512, &mut entries_buf)?;
let computed = crc32fast::hash(&entries_buf);
if computed != hdr.entries_crc {
return Err(crate::Error::InvalidImage(format!(
"GPT entries CRC mismatch: stored {:#010x}, computed {:#010x}",
hdr.entries_crc, computed
)));
}
let mut partitions = Vec::new();
for i in 0..hdr.num_entries as usize {
let off = i * hdr.entry_size as usize;
let ent = &entries_buf[off..off + 128];
if let Some(p) = decode_entry(ent) {
partitions.push(p);
}
}
Ok(Self {
disk_guid: hdr.disk_guid,
partitions,
})
}
fn header_for(&self, is_primary: bool, total_lba: u64, entries_crc: u32) -> Header {
let primary_lba = 1;
let backup_lba = total_lba - 1;
let (my_lba, alt_lba, entries_start_lba) = if is_primary {
(primary_lba, backup_lba, 2)
} else {
(backup_lba, primary_lba, total_lba - 1 - ENTRIES_LBAS)
};
Header {
my_lba,
alternate_lba: alt_lba,
first_usable_lba: FIRST_USABLE_LBA,
last_usable_lba: total_lba - 1 - ENTRIES_LBAS - 1,
disk_guid: self.disk_guid,
entries_start_lba,
num_entries: NUM_ENTRIES,
entry_size: ENTRY_SIZE,
entries_crc,
}
}
}
impl PartitionTable for Gpt {
fn write(&self, dev: &mut dyn BlockDevice) -> Result<()> {
require_block_size(dev)?;
let total_lba = dev.total_size() / 512;
let min_required = FIRST_USABLE_LBA + ENTRIES_LBAS + 1;
if total_lba < min_required {
return Err(crate::Error::InvalidArgument(format!(
"device has {total_lba} LBAs, need at least {min_required} for a GPT"
)));
}
let last_usable = total_lba - 1 - ENTRIES_LBAS - 1;
for p in &self.partitions {
if p.kind == PartitionKind::Empty {
continue;
}
if p.start_lba < FIRST_USABLE_LBA {
return Err(crate::Error::InvalidArgument(format!(
"partition starts at LBA {} (before first usable LBA {FIRST_USABLE_LBA})",
p.start_lba
)));
}
if p.end_lba() > last_usable {
return Err(crate::Error::InvalidArgument(format!(
"partition ends at LBA {} (past last usable LBA {last_usable})",
p.end_lba()
)));
}
}
write_protective_mbr(dev)?;
let mut entries = vec![0u8; (NUM_ENTRIES * ENTRY_SIZE) as usize];
for (i, p) in self.partitions.iter().enumerate() {
let off = i * ENTRY_SIZE as usize;
encode_entry(p, &mut entries[off..off + ENTRY_SIZE as usize]);
}
let entries_crc = crc32fast::hash(&entries);
dev.write_at(2 * 512, &entries)?;
let backup_entries_lba = total_lba - 1 - ENTRIES_LBAS;
dev.write_at(backup_entries_lba * 512, &entries)?;
let primary_hdr = self.header_for(true, total_lba, entries_crc);
let primary_buf = encode_header(&primary_hdr);
dev.write_at(512, &primary_buf)?;
let backup_hdr = self.header_for(false, total_lba, entries_crc);
let backup_buf = encode_header(&backup_hdr);
dev.write_at((total_lba - 1) * 512, &backup_buf)?;
Ok(())
}
fn partitions(&self) -> &[Partition] {
&self.partitions
}
}
#[derive(Debug)]
struct Header {
my_lba: u64,
alternate_lba: u64,
first_usable_lba: u64,
last_usable_lba: u64,
disk_guid: Uuid,
entries_start_lba: u64,
num_entries: u32,
entry_size: u32,
entries_crc: u32,
}
fn require_block_size(dev: &dyn BlockDevice) -> Result<()> {
if dev.block_size() != REQUIRED_BLOCK_SIZE {
return Err(crate::Error::Unsupported(format!(
"GPT in v1 requires {REQUIRED_BLOCK_SIZE}-byte sectors, device reports {}",
dev.block_size()
)));
}
Ok(())
}
fn check_no_overlap(parts: &[Partition]) -> Result<()> {
let mut spans: Vec<(u64, u64)> = parts
.iter()
.filter(|p| p.kind != PartitionKind::Empty)
.map(|p| (p.start_lba, p.end_lba()))
.collect();
spans.sort_unstable_by_key(|(s, _)| *s);
for w in spans.windows(2) {
if w[1].0 <= w[0].1 {
return Err(crate::Error::InvalidArgument(format!(
"partitions overlap: [{}-{}] vs [{}-{}]",
w[0].0, w[0].1, w[1].0, w[1].1
)));
}
}
Ok(())
}
fn encode_header(h: &Header) -> [u8; 512] {
let mut buf = [0u8; 512];
buf[0..8].copy_from_slice(SIGNATURE);
buf[8..12].copy_from_slice(&REVISION.to_le_bytes());
buf[12..16].copy_from_slice(&HEADER_SIZE.to_le_bytes());
buf[24..32].copy_from_slice(&h.my_lba.to_le_bytes());
buf[32..40].copy_from_slice(&h.alternate_lba.to_le_bytes());
buf[40..48].copy_from_slice(&h.first_usable_lba.to_le_bytes());
buf[48..56].copy_from_slice(&h.last_usable_lba.to_le_bytes());
buf[56..72].copy_from_slice(&uuid_to_gpt_bytes(h.disk_guid));
buf[72..80].copy_from_slice(&h.entries_start_lba.to_le_bytes());
buf[80..84].copy_from_slice(&h.num_entries.to_le_bytes());
buf[84..88].copy_from_slice(&h.entry_size.to_le_bytes());
buf[88..92].copy_from_slice(&h.entries_crc.to_le_bytes());
let crc = crc32fast::hash(&buf[..HEADER_SIZE as usize]);
buf[16..20].copy_from_slice(&crc.to_le_bytes());
buf
}
fn parse_header(buf: &[u8; 512]) -> Result<Header> {
if &buf[0..8] != SIGNATURE {
return Err(crate::Error::InvalidImage(
"GPT signature \"EFI PART\" not found".into(),
));
}
let revision = u32::from_le_bytes(buf[8..12].try_into().unwrap());
if revision != REVISION {
return Err(crate::Error::Unsupported(format!(
"GPT revision {revision:#010x} not supported"
)));
}
let header_size = u32::from_le_bytes(buf[12..16].try_into().unwrap());
if header_size as usize > buf.len() || header_size < 92 {
return Err(crate::Error::InvalidImage(format!(
"GPT header_size {header_size} out of range"
)));
}
let stored_crc = u32::from_le_bytes(buf[16..20].try_into().unwrap());
let mut tmp = *buf;
tmp[16..20].fill(0);
let computed = crc32fast::hash(&tmp[..header_size as usize]);
if stored_crc != computed {
return Err(crate::Error::InvalidImage(format!(
"GPT header CRC mismatch: stored {stored_crc:#010x}, computed {computed:#010x}"
)));
}
let my_lba = u64::from_le_bytes(buf[24..32].try_into().unwrap());
let alternate_lba = u64::from_le_bytes(buf[32..40].try_into().unwrap());
let first_usable_lba = u64::from_le_bytes(buf[40..48].try_into().unwrap());
let last_usable_lba = u64::from_le_bytes(buf[48..56].try_into().unwrap());
let disk_guid = uuid_from_gpt_bytes(buf[56..72].try_into().unwrap());
let entries_start_lba = u64::from_le_bytes(buf[72..80].try_into().unwrap());
let num_entries = u32::from_le_bytes(buf[80..84].try_into().unwrap());
let entry_size = u32::from_le_bytes(buf[84..88].try_into().unwrap());
let entries_crc = u32::from_le_bytes(buf[88..92].try_into().unwrap());
Ok(Header {
my_lba,
alternate_lba,
first_usable_lba,
last_usable_lba,
disk_guid,
entries_start_lba,
num_entries,
entry_size,
entries_crc,
})
}
fn encode_entry(p: &Partition, out: &mut [u8]) {
debug_assert_eq!(out.len(), 128);
if p.kind == PartitionKind::Empty {
return;
}
let type_uuid = p.kind.as_gpt_uuid();
let part_uuid = p.uuid.unwrap_or(Uuid::nil());
out[0..16].copy_from_slice(&uuid_to_gpt_bytes(type_uuid));
out[16..32].copy_from_slice(&uuid_to_gpt_bytes(part_uuid));
out[32..40].copy_from_slice(&p.start_lba.to_le_bytes());
out[40..48].copy_from_slice(&p.end_lba().to_le_bytes());
let mut attrs = p.attributes;
if p.bootable {
attrs |= 1 << 2; }
out[48..56].copy_from_slice(&attrs.to_le_bytes());
if let Some(name) = &p.name {
for (i, cu) in name.encode_utf16().take(36).enumerate() {
let off = 56 + i * 2;
out[off..off + 2].copy_from_slice(&cu.to_le_bytes());
}
}
}
fn decode_entry(b: &[u8]) -> Option<Partition> {
debug_assert_eq!(b.len(), 128);
let type_bytes: [u8; 16] = b[0..16].try_into().unwrap();
let uuid_bytes: [u8; 16] = b[16..32].try_into().unwrap();
let type_uuid = uuid_from_gpt_bytes(&type_bytes);
if type_uuid.is_nil() {
return None;
}
let part_uuid = uuid_from_gpt_bytes(&uuid_bytes);
let start = u64::from_le_bytes(b[32..40].try_into().unwrap());
let end = u64::from_le_bytes(b[40..48].try_into().unwrap());
let attrs = u64::from_le_bytes(b[48..56].try_into().unwrap());
let mut name = String::new();
let mut name_units = [0u16; 36];
for (i, slot) in name_units.iter_mut().enumerate() {
let off = 56 + i * 2;
*slot = u16::from_le_bytes(b[off..off + 2].try_into().unwrap());
}
let len = name_units
.iter()
.position(|&c| c == 0)
.unwrap_or(name_units.len());
if let Ok(s) = String::from_utf16(&name_units[..len]) {
name = s;
}
Some(Partition {
start_lba: start,
size_lba: end - start + 1,
kind: PartitionKind::from_gpt_uuid(type_uuid),
uuid: Some(part_uuid),
name: if name.is_empty() { None } else { Some(name) },
bootable: attrs & (1 << 2) != 0,
attributes: attrs & !(1 << 2),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
fn mb(n: u64) -> u64 {
n * 1024 * 1024
}
#[test]
fn roundtrip_two_partitions() {
let mut dev = MemoryBackend::new(mb(64));
let parts = vec![
Partition {
start_lba: 2048,
size_lba: 2048,
kind: PartitionKind::EfiSystem,
name: Some("EFI System Partition".into()),
..Partition::new(0, 0, PartitionKind::EfiSystem)
},
Partition {
start_lba: 4096,
size_lba: 100_000,
kind: PartitionKind::LinuxFilesystem,
name: Some("root".into()),
bootable: false,
..Partition::new(0, 0, PartitionKind::LinuxFilesystem)
},
];
let gpt = Gpt::build(parts.clone()).unwrap();
gpt.write(&mut dev).unwrap();
let parsed = Gpt::read(&mut dev).unwrap();
assert_eq!(parsed.disk_guid, gpt.disk_guid);
assert_eq!(parsed.partitions.len(), 2);
for (a, b) in gpt.partitions.iter().zip(parsed.partitions.iter()) {
assert_eq!(a.start_lba, b.start_lba);
assert_eq!(a.size_lba, b.size_lba);
assert_eq!(a.kind, b.kind);
assert_eq!(a.name, b.name);
assert_eq!(a.uuid, b.uuid);
}
}
#[test]
fn backup_header_present() {
let mut dev = MemoryBackend::new(mb(64));
let gpt = Gpt::build(vec![Partition::new(
2048,
1024,
PartitionKind::LinuxFilesystem,
)])
.unwrap();
gpt.write(&mut dev).unwrap();
let total_lba = dev.total_size() / 512;
let mut backup = [0u8; 512];
dev.read_at((total_lba - 1) * 512, &mut backup).unwrap();
let hdr = parse_header(&backup).unwrap();
assert_eq!(hdr.my_lba, total_lba - 1);
assert_eq!(hdr.alternate_lba, 1);
assert_eq!(hdr.entries_start_lba, total_lba - 1 - ENTRIES_LBAS);
}
#[test]
fn header_crc_catches_corruption() {
let mut dev = MemoryBackend::new(mb(64));
let gpt = Gpt::build(vec![Partition::new(
2048,
1024,
PartitionKind::LinuxFilesystem,
)])
.unwrap();
gpt.write(&mut dev).unwrap();
let mut byte = [0u8; 1];
dev.read_at(2 * 512 + 60, &mut byte).unwrap();
byte[0] ^= 0x01;
dev.write_at(2 * 512 + 60, &byte).unwrap();
let err = Gpt::read(&mut dev).unwrap_err();
assert!(matches!(err, crate::Error::InvalidImage(_)));
}
#[test]
fn protective_mbr_present_after_write() {
let mut dev = MemoryBackend::new(mb(64));
let gpt = Gpt::build(vec![Partition::new(
2048,
1024,
PartitionKind::LinuxFilesystem,
)])
.unwrap();
gpt.write(&mut dev).unwrap();
let mut sector = [0u8; 512];
dev.read_at(0, &mut sector).unwrap();
assert_eq!(sector[510..512], [0x55, 0xAA]);
assert_eq!(sector[446 + 4], 0xEE);
}
#[test]
fn rejects_overlapping_partitions() {
let parts = vec![
Partition::new(2048, 1024, PartitionKind::LinuxFilesystem),
Partition::new(2500, 1024, PartitionKind::LinuxFilesystem),
];
let err = Gpt::build(parts).unwrap_err();
assert!(matches!(err, crate::Error::InvalidArgument(_)));
}
#[test]
fn rejects_too_small_device() {
let mut dev = MemoryBackend::new(64 * 1024); let gpt = Gpt::build(vec![Partition::new(34, 32, PartitionKind::LinuxFilesystem)]).unwrap();
gpt.write(&mut dev).unwrap();
let parsed = Gpt::read(&mut dev).unwrap();
assert_eq!(parsed.partitions.len(), 1);
}
}