use core::{
cell::RefCell,
mem::{offset_of, MaybeUninit},
slice,
};
#[cfg(feature = "alloc")]
use alloc::{string::String, vec::Vec};
use crc32fast::Hasher;
use zerocopy::{FromBytes, Immutable, IntoBytes};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes};
use crate::{DiskRead, DiskSeek, DiskWrite, SeekFrom};
const GPT_HEADER_OFFSET: u64 = 0x200;
const GPT_SIG: &[u8; 8] = b"EFI PART";
const GPT_REVISION: u32 = 0x0001_0000;
const GPT_TABLE_SIZE: u32 = 1 << 17;
macro_rules! packed_field_accessors {
($field:ident, $type:ty) => {
#[must_use]
pub fn $field(&self) -> $type {
unsafe { core::ptr::read_unaligned(core::ptr::addr_of!(self.$field)) }
}
};
($field:ident, $type:ty, $accq:vis) => {
$accq fn $field(&self) -> $type {
unsafe { core::ptr::read_unaligned(core::ptr::addr_of!(self.$field)) }
}
};
($field:ident, $field_setter:ident, $type:ty) => {
#[must_use]
pub fn $field(&self) -> $type {
unsafe { core::ptr::read_unaligned(core::ptr::addr_of!(self.$field)) }
}
pub fn $field_setter(&mut self, value: $type) {
unsafe { core::ptr::write_unaligned(core::ptr::addr_of_mut!(self.$field), value) }
}
};
($field:ident, $field_setter:ident, $type:ty, $post_call:expr) => {
#[must_use]
pub fn $field(&self) -> $type {
unsafe { core::ptr::read_unaligned(core::ptr::addr_of!(self.$field)) }
}
pub fn $field_setter(&mut self, value: $type) {
unsafe { core::ptr::write_unaligned(core::ptr::addr_of_mut!(self.$field), value) }
$post_call(self);
}
};
($field:ident, $field_setter:ident, $type:ty, $accq:vis) => {
$accq fn $field(&self) -> $type {
unsafe { core::ptr::read_unaligned(core::ptr::addr_of!(self.$field)) }
}
$accq fn $field_setter(&mut self, value: $type) {
unsafe { core::ptr::write_unaligned(core::ptr::addr_of_mut!(self.$field), value) }
}
};
}
#[derive(Clone, Copy, Debug)]
pub enum GptError {
InvalidPartition(u32, GptPartitionError),
IoError,
InvalidHeaderChecksum {
expected: u32,
actual: u32,
},
InvalidPartitionsChecksum {
expected: u32,
actual: u32,
},
InvalidSignature,
NoSuchPartition,
OutOfBounds,
TableFull,
OverlappingPartitions,
}
#[derive(Clone, Copy, Debug)]
pub enum GptPartitionError {
InvalidLbaRange,
InvalidName,
}
#[derive(Clone, Debug, PartialEq, Eq, FromBytes, IntoBytes, Immutable)]
#[repr(packed(1), C)]
pub struct GptHeader {
signature: [u8; 8],
revision: u32,
size: u32,
checksum: u32,
reserved: [u8; 4],
lba: u64,
alternate_lba: u64,
first_usable_lba: u64,
last_usable_lba: u64,
guid: u128,
partition_start_lba: u64,
partition_entries_count: u32,
partition_entry_size: u32,
partition_entries_checksum: u32,
}
assert_eq_size!(GptHeader, [u8; 0x5C]);
assert_eq_align!(GptHeader, u8);
impl GptHeader {
#[must_use]
pub fn compute_checksum(&self) -> u32 {
let chksum_offset = offset_of!(GptHeader, checksum);
let bytes = self.as_bytes();
let mut crc32_hasher = Hasher::new();
crc32_hasher.update(&bytes[..chksum_offset]);
crc32_hasher.update(&[0u8; size_of::<u32>()]);
crc32_hasher.update(&bytes[chksum_offset + size_of::<u32>()..]);
crc32_hasher.finalize()
}
fn update_checksum(&mut self) {
self.checksum = self.compute_checksum();
}
fn is_valid(&self) -> Result<(), GptError> {
if &self.signature != GPT_SIG {
return Err(GptError::InvalidSignature);
}
let computed_checksum = self.compute_checksum();
if computed_checksum != self.checksum {
return Err(GptError::InvalidHeaderChecksum {
expected: self.checksum,
actual: computed_checksum,
});
}
Ok(())
}
packed_field_accessors!(
signature,
set_signature,
[u8; 8],
GptHeader::update_checksum
);
packed_field_accessors!(revision, set_revision, u32, GptHeader::update_checksum);
packed_field_accessors!(lba, set_lba, u64, GptHeader::update_checksum);
packed_field_accessors!(
alternate_lba,
set_alternate_lba,
u64,
GptHeader::update_checksum
);
packed_field_accessors!(
first_usable_lba,
set_first_usable_lba,
u64,
GptHeader::update_checksum
);
packed_field_accessors!(
last_usable_lba,
set_last_usable_lba,
u64,
GptHeader::update_checksum
);
packed_field_accessors!(guid, set_guid, u128, GptHeader::update_checksum);
packed_field_accessors!(checksum, u32);
packed_field_accessors!(partition_start_lba, u64);
packed_field_accessors!(partition_entries_count, u32);
packed_field_accessors!(partition_entry_size, u32);
packed_field_accessors!(partition_entries_checksum, u32, pub(self));
}
#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)]
#[repr(packed(1), C)]
pub struct GptPartition {
type_guid: u128,
partition_guid: u128,
start_lba: u64,
last_lba: u64,
attributes: u64,
partition_name: [u16; 36],
}
assert_eq_size!(GptPartition, [u8; 0x80]);
assert_eq_align!(GptPartition, u8);
impl GptPartition {
pub fn size(&self) -> u64 {
self.last_lba - self.start_lba
}
pub fn is_used(&self) -> bool {
self.type_guid != 0
}
pub fn check_correctness(&self) -> Result<(), GptPartitionError> {
if self.start_lba() > self.last_lba() {
return Err(GptPartitionError::InvalidLbaRange);
}
Ok(())
}
#[cfg(feature = "alloc")]
pub fn set_name(&mut self, name: &str) -> Result<(), GptPartitionError> {
let mut utf16_encoding = name.encode_utf16().collect::<Vec<_>>();
if utf16_encoding.len() >= 36 {
return Err(GptPartitionError::InvalidName);
}
utf16_encoding.resize(36, 0);
self.set_partition_name(utf16_encoding.try_into().unwrap());
Ok(())
}
#[cfg(feature = "alloc")]
pub fn name(&self) -> Result<String, GptPartitionError> {
let name_buf = self.partition_name();
let filtered_buf: Vec<u16> = name_buf.into_iter().take_while(|&c| c != 0).collect();
String::from_utf16(&filtered_buf).map_err(|_| GptPartitionError::InvalidName)
}
packed_field_accessors!(type_guid, set_type_guid, u128);
packed_field_accessors!(start_lba, set_start_lba, u64);
packed_field_accessors!(last_lba, set_last_lba, u64);
packed_field_accessors!(partition_name, set_partition_name, [u16; 36], pub(self));
}
impl Default for GptPartition {
fn default() -> Self {
Self {
type_guid: Default::default(),
partition_guid: Default::default(),
start_lba: Default::default(),
last_lba: Default::default(),
attributes: Default::default(),
partition_name: [0u16; 36],
}
}
}
pub struct Gpt<D, const S: u64 = 0x200>
where
D: DiskRead + DiskSeek,
{
pub header: GptHeader,
device_mapping: RefCell<D>,
}
impl<D, const S: u64> Gpt<D, S>
where
D: DiskRead + DiskSeek,
{
pub fn parse_from_device(mut device_mapping: D) -> Result<Self, GptError> {
device_mapping
.seek(SeekFrom::Start(GPT_HEADER_OFFSET))
.map_err(|_| GptError::IoError)?;
let mut buf = [0u8; size_of::<GptHeader>()];
device_mapping
.read(&mut buf)
.map_err(|_| GptError::IoError)?;
let header = GptHeader::read_from_bytes(&buf).map_err(|_| GptError::IoError)?;
header.is_valid()?;
let table = Gpt {
header,
device_mapping: RefCell::new(device_mapping),
};
Ok(table)
}
pub fn iter_partitions(&self) -> GptPartitionsIterator<'_, D, S> {
GptPartitionsIterator {
table: self,
cursor: 0,
skip_empty: true,
}
}
pub fn get_partition(&self, part_id: u32) -> Option<GptPartition> {
self.iter_partitions().nth(usize::try_from(part_id).ok()?)
}
pub fn check_table_correctness(&self) -> Result<(), GptError> {
self.header.is_valid()?;
let computed_checksum = self.compute_partitions_checksum();
if computed_checksum != self.header.partition_entries_checksum {
return Err(GptError::InvalidPartitionsChecksum {
expected: self.header.partition_entries_checksum(),
actual: computed_checksum,
});
}
#[cfg(feature = "alloc")]
{
let overlappings =
find_overlapping_partitions(self.iter_partitions().collect::<Vec<_>>());
if !overlappings.is_empty() {
return Err(GptError::OverlappingPartitions);
}
}
for (part, idx) in self.iter_partitions().zip(0u32..) {
part.check_correctness()
.map_err(|err| GptError::InvalidPartition(idx, err))?;
}
Ok(())
}
fn update_checksums(&mut self) {
let partitions_chksum = self.compute_partitions_checksum();
self.header.partition_entries_checksum = partitions_chksum;
let header_chksum = self.header.compute_checksum();
self.header.checksum = header_chksum;
}
fn compute_partitions_checksum(&self) -> u32 {
let mut crc32_hasher = Hasher::new();
self.iter_entries()
.for_each(|part| crc32_hasher.update(part.as_bytes()));
crc32_hasher.finalize()
}
fn read_from_device<T>(&self, offset: u64) -> Result<T, GptError>
where
T: FromBytes,
{
let mut device = self.device_mapping.borrow_mut();
device
.seek(SeekFrom::Start(offset))
.map_err(|_| GptError::IoError)?;
if cfg!(feature = "alloc") {
let mut buf = alloc::vec![0u8; size_of::<T>()];
let read = device.read(&mut buf).map_err(|_| GptError::IoError)?;
if read != size_of::<T>() {
return Err(GptError::IoError);
}
T::read_from_bytes(&buf).map_err(|_| GptError::IoError)
} else {
unsafe {
let mut uninit_data: MaybeUninit<T> = MaybeUninit::uninit();
let read = device
.read(slice::from_raw_parts_mut(
uninit_data.as_mut_ptr().cast(),
size_of::<T>(),
))
.map_err(|_| GptError::IoError)?;
if read != size_of::<T>() {
return Err(GptError::IoError);
}
Ok(uninit_data.assume_init())
}
}
}
fn read_alternate(&self) -> Result<GptHeader, GptError> {
self.read_from_device(S * self.header.alternate_lba())
}
#[cfg(feature = "alloc")]
fn would_new_partitions_overlap(&self, new_part: Vec<GptPartition>) -> bool {
let mut partitions = self.iter_partitions().collect::<Vec<_>>();
partitions.extend(new_part);
let overlappings = find_overlapping_partitions(partitions);
!overlappings.is_empty()
}
fn read_partition_entry(&self, part_id: u32) -> Result<GptPartition, GptError> {
if part_id >= self.header.partition_entry_size() {
return Err(GptError::OutOfBounds);
}
let (primary, _) = self.partition_offset(part_id);
self.read_from_device::<GptPartition>(primary)
}
fn iter_entries(&self) -> GptPartitionsIterator<'_, D, S> {
GptPartitionsIterator {
table: self,
cursor: 0,
skip_empty: false,
}
}
fn partition_offset(&self, part_id: u32) -> (u64, u64) {
let offset_in_table = u64::from(part_id * self.header.partition_entry_size());
let primary_offset = self.header.partition_start_lba() * S + offset_in_table;
let alternate_offset = S * self.header.alternate_lba()
- u64::from(self.header.partition_entries_count() * self.header.partition_entry_size())
+ offset_in_table;
(primary_offset, alternate_offset)
}
fn find_first_empty_slot(&self) -> Option<u32> {
self.iter_entries()
.position(|part| part.type_guid == 0)
.map(|idx| u32::try_from(idx).ok())?
}
}
impl<D, const S: u64> Gpt<D, S>
where
D: DiskRead + DiskWrite + DiskSeek,
{
#[cfg(feature = "std")]
pub fn new_on_device(device: D, header_lba: u64, alternate_lba: u64) -> Result<Self, GptError> {
use uuid::Uuid;
let first_usable_lba = 1 + header_lba + u64::from(GPT_TABLE_SIZE) / S;
let last_usable_lba = alternate_lba - u64::from(GPT_TABLE_SIZE) / S - 1;
if alternate_lba < 1 + u64::from(GPT_TABLE_SIZE) / S || first_usable_lba > last_usable_lba {
return Err(GptError::IoError);
}
let header = GptHeader {
signature: *GPT_SIG,
revision: GPT_REVISION,
size: u32::try_from(size_of::<GptHeader>()).expect("invalid gpt header size"),
checksum: 0,
reserved: [0u8; 4],
lba: header_lba,
alternate_lba,
first_usable_lba,
last_usable_lba,
guid: Uuid::new_v4().to_u128_le(),
partition_start_lba: header_lba + 1,
partition_entries_count: GPT_TABLE_SIZE
/ u32::try_from(size_of::<GptPartition>()).expect("invalid gpt partition size"),
partition_entry_size: u32::try_from(size_of::<GptPartition>())
.expect("invalid gpt partition size"),
partition_entries_checksum: 0,
};
let mut gpt = Self {
header,
device_mapping: RefCell::new(device),
};
gpt.update_checksums();
gpt.write_header()?;
gpt.write_alternate()?;
Ok(gpt)
}
pub fn restore_from_alternate(&mut self) -> Result<(), GptError> {
self.restore_main_header_from_alternate()?;
self.restore_partitions_from_alternate()?;
Ok(())
}
pub fn remove_partition(&mut self, part_id: u32) -> Result<(), GptError> {
self.get_partition(part_id)
.ok_or(GptError::NoSuchPartition)?;
let (primary, alternate) = self.partition_offset(part_id);
self.write_to_device(primary, &[0u8; size_of::<GptPartition>()])?;
self.write_to_device(alternate, &[0u8; size_of::<GptPartition>()])?;
self.update_checksums();
Ok(())
}
pub fn append_partition(&mut self, new_part: GptPartition) -> Result<(), GptError> {
let empty_idx = self.find_first_empty_slot().ok_or(GptError::TableFull)?;
#[cfg(feature = "alloc")]
{
if self.would_new_partitions_overlap(alloc::vec![new_part]) {
return Err(GptError::OverlappingPartitions);
}
}
self.write_partition_entry(empty_idx, &new_part)
}
pub fn update_partition(
&mut self,
part_id: u32,
new_part: GptPartition,
) -> Result<(), GptError> {
let (_, idx) = self
.iter_entries()
.zip(0u32..)
.filter(|(part, _)| part.is_used())
.nth(usize::try_from(part_id).map_err(|_| GptError::OutOfBounds)?)
.ok_or(GptError::NoSuchPartition)?;
new_part
.check_correctness()
.map_err(|err| GptError::InvalidPartition(part_id, err))?;
#[cfg(feature = "alloc")]
{
let mut partitions = self
.iter_partitions()
.enumerate()
.filter_map(|(idx, part)| {
if idx == usize::try_from(part_id).unwrap_or(0) {
None
} else {
Some(part)
}
})
.collect::<Vec<_>>();
partitions.push(new_part);
if !find_overlapping_partitions(partitions).is_empty() {
return Err(GptError::OverlappingPartitions);
}
}
self.write_partition_entry(idx, &new_part)
}
fn restore_partitions_from_alternate(&mut self) -> Result<(), GptError> {
let (_, alternate_start) = self.partition_offset(0);
for part_idx in 0..self.header.partition_entries_count() {
self.write_partition_entry(
part_idx,
&self.read_from_device::<GptPartition>(
alternate_start
+ u64::try_from(
size_of::<GptPartition>()
* usize::try_from(part_idx).map_err(|_| GptError::IoError)?,
)
.map_err(|_| GptError::IoError)?,
)?,
)?;
}
Ok(())
}
fn restore_main_header_from_alternate(&mut self) -> Result<(), GptError> {
let alternate = self.read_alternate()?;
alternate.is_valid()?;
self.header = alternate;
self.write_header()?;
Ok(())
}
fn write_header(&mut self) -> Result<(), GptError> {
self.write_to_device::<GptHeader>(S * self.header.lba(), &self.header.clone())
}
fn write_alternate(&mut self) -> Result<(), GptError> {
self.write_to_device(S * self.header.alternate_lba(), &self.header.clone())
}
fn write_partition_entry(
&mut self,
entry_id: u32,
new_part: &GptPartition,
) -> Result<(), GptError> {
let (primary, alternate) = self.partition_offset(entry_id);
self.write_to_device::<GptPartition>(primary, new_part)?;
self.write_to_device::<GptPartition>(alternate, new_part)?;
self.update_checksums();
self.write_alternate()?;
Ok(())
}
fn write_to_device<T>(&mut self, offset: u64, obj: &T) -> Result<(), GptError>
where
T: IntoBytes + Immutable,
{
self.device_mapping
.borrow_mut()
.seek(SeekFrom::Start(offset))
.map_err(|_| GptError::IoError)?;
let bytes_written = self
.device_mapping
.borrow_mut()
.write(obj.as_bytes())
.map_err(|_| GptError::IoError)?;
if bytes_written != size_of::<T>() {
return Err(GptError::IoError);
}
Ok(())
}
}
pub struct GptPartitionsIterator<'a, D, const S: u64>
where
D: DiskRead + DiskSeek,
{
table: &'a Gpt<D, S>,
cursor: u32,
skip_empty: bool,
}
impl<D, const S: u64> Iterator for GptPartitionsIterator<'_, D, S>
where
D: DiskRead + DiskSeek,
{
type Item = GptPartition;
fn next(&mut self) -> Option<Self::Item> {
let part = self.table.read_partition_entry(self.cursor).ok()?;
self.cursor += 1;
if self.skip_empty && part.type_guid == 0 {
return self.next();
}
Some(part)
}
}
#[cfg(feature = "alloc")]
fn find_overlapping_partitions(mut partitions: Vec<GptPartition>) -> Vec<Vec<GptPartition>> {
partitions.sort_by_key(GptPartition::start_lba);
let mut partitions_groups: Vec<Vec<GptPartition>> = Vec::with_capacity(partitions.len());
let mut curr_group_end = 0;
for part in partitions {
if part.start_lba() > curr_group_end {
curr_group_end = part.last_lba();
partitions_groups.push(alloc::vec![part]);
continue;
}
curr_group_end = core::cmp::max(part.last_lba(), curr_group_end);
partitions_groups.last_mut().unwrap().push(part);
}
partitions_groups.retain(|grp| grp.len() > 1);
partitions_groups
}
#[cfg(test)]
#[cfg(feature = "std")]
mod tests {
use std::{io::Cursor, vec::Vec};
use super::Gpt;
const GPT_DUMP: &[u8] = include_bytes!("../tests/dummy_gpt.bin");
#[test]
pub fn parse_gpt_from_buf() {
let gpt_dump = Cursor::new(GPT_DUMP);
let gpt: Gpt<Cursor<&[u8]>> = Gpt::parse_from_device(gpt_dump).unwrap();
assert_eq!(gpt.header.partition_entries_count(), 128);
assert_eq!(gpt.header.lba(), 1);
assert_eq!(gpt.header.last_usable_lba(), 264158);
}
#[test]
pub fn gpt_update_header() {
let gpt_dump = Cursor::new(GPT_DUMP);
let mut gpt: Gpt<Cursor<&[u8]>> = Gpt::parse_from_device(gpt_dump).unwrap();
gpt.header.set_last_usable_lba(27278);
assert_eq!(gpt.header.last_usable_lba(), 27278);
assert!(gpt.check_table_correctness().is_ok());
}
#[test]
pub fn new_gpt_in_buf() {
let mut buf = [0u8; 0x200 * 1024];
let device: Cursor<&mut [u8]> = Cursor::new(&mut buf);
let gpt_in_dev = Gpt::<Cursor<&mut [u8]>, 0x200>::new_on_device(device, 1, 1023).unwrap();
assert!(gpt_in_dev.check_table_correctness().is_ok());
let gpt =
Gpt::<Cursor<&mut [u8]>, 0x200>::parse_from_device(Cursor::new(&mut buf)).unwrap();
assert!(gpt.check_table_correctness().is_ok());
}
#[test]
pub fn parse_gpt_partitions() {
let gpt_dump = Cursor::new(GPT_DUMP);
let gpt: Gpt<Cursor<&[u8]>> = Gpt::parse_from_device(gpt_dump).unwrap();
for part in gpt.iter_partitions() {
assert!(part.is_used());
}
let partitions = gpt.iter_partitions().collect::<Vec<_>>();
assert_eq!(partitions[0].name().unwrap(), "part_a");
assert_eq!(partitions[1].name().unwrap(), "part_b");
assert_eq!(partitions[0].start_lba(), 40);
assert_eq!(partitions[1].start_lba(), 131112);
assert!(gpt.check_table_correctness().is_ok());
}
#[test]
pub fn gpt_partitions_checksum() {
let gpt_dump = Cursor::new(GPT_DUMP);
let gpt: Gpt<Cursor<&[u8]>> = Gpt::parse_from_device(gpt_dump).unwrap();
assert_eq!(
gpt.compute_partitions_checksum(),
gpt.header.partition_entries_checksum()
);
assert!(gpt.check_table_correctness().is_ok());
}
#[test]
pub fn gpt_partition_overlapping_check() {
let gpt_dump = Cursor::new(GPT_DUMP.iter().cloned().collect::<Vec<_>>());
let mut gpt: Gpt<Cursor<Vec<u8>>> = Gpt::parse_from_device(gpt_dump).unwrap();
let mut part_a = gpt.get_partition(0).unwrap();
part_a.set_start_lba(2727);
gpt.update_partition(0, part_a).unwrap();
assert!(gpt.check_table_correctness().is_ok());
}
#[test]
pub fn gpt_append_partition() {
let gpt_dump = Cursor::new(GPT_DUMP.iter().cloned().collect::<Vec<_>>());
let mut gpt: Gpt<Cursor<Vec<u8>>> = Gpt::parse_from_device(gpt_dump).unwrap();
let mut part_a = gpt.get_partition(0).unwrap();
part_a.set_start_lba(27272727);
part_a.set_last_lba(27272728);
gpt.append_partition(part_a).unwrap();
assert_eq!(gpt.iter_partitions().count(), 3);
let partitions = gpt.iter_partitions().collect::<Vec<_>>();
assert_eq!(partitions[2].name().unwrap(), "part_a");
assert_eq!(partitions[2].start_lba(), 27272727);
assert_eq!(gpt.read_alternate().unwrap(), gpt.header);
assert!(gpt.check_table_correctness().is_ok());
}
#[test]
pub fn gpt_update_partition() {
let gpt_dump = Cursor::new(GPT_DUMP.iter().cloned().collect::<Vec<_>>());
let mut gpt: Gpt<Cursor<Vec<u8>>> = Gpt::parse_from_device(gpt_dump).unwrap();
let mut part_a = gpt.get_partition(0).unwrap();
part_a.set_start_lba(2727);
part_a.set_name("frost").unwrap();
gpt.update_partition(0, part_a).unwrap();
let partitions = gpt.iter_partitions().collect::<Vec<_>>();
assert_eq!(partitions[0].name().unwrap(), "frost");
assert_eq!(partitions[0].start_lba(), 2727);
assert_eq!(gpt.read_alternate().unwrap(), gpt.header);
assert!(gpt.check_table_correctness().is_ok());
}
#[test]
pub fn gpt_remove_partition() {
let gpt_dump = Cursor::new(GPT_DUMP.iter().cloned().collect::<Vec<_>>());
let mut gpt: Gpt<Cursor<Vec<u8>>> = Gpt::parse_from_device(gpt_dump).unwrap();
gpt.remove_partition(0).unwrap();
let partitions = gpt.iter_partitions().collect::<Vec<_>>();
assert_eq!(partitions.len(), 1);
assert_eq!(partitions[0].name().unwrap(), "part_b");
assert!(gpt.check_table_correctness().is_ok());
}
#[test]
pub fn gpt_alternate_partition_offset() {
let gpt_dump = Cursor::new(GPT_DUMP);
let gpt: Gpt<Cursor<&[u8]>> = Gpt::parse_from_device(gpt_dump).unwrap();
let (primary, alternate) = gpt.partition_offset(0);
assert_eq!(
alternate - gpt.header.last_usable_lba() * 0x200,
primary - 0x200
);
let (primary, alternate) = gpt.partition_offset(1);
assert_eq!(
alternate - gpt.header.last_usable_lba() * 0x200,
primary - 0x200
);
}
}