const PARTITION_ENTRY_OFFSET: usize = 48;
const ENTRY_SIZE: usize = 144;
const STYLE_MBR: u32 = 0;
const STYLE_GPT: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ParsedPartition {
pub number: u32,
pub start_offset: u64,
pub length: u64,
pub type_desc: Option<String>,
pub name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ParsedLayout {
pub style: u32,
pub partitions: Vec<ParsedPartition>,
}
pub(super) fn parse_drive_layout(buf: &[u8]) -> ParsedLayout {
if buf.len() < 8 {
return ParsedLayout {
style: u32::MAX,
partitions: Vec::new(),
};
}
let style = u32_le(buf, 0);
let count = u32_le(buf, 4) as usize;
let mut partitions = Vec::new();
for i in 0..count {
let e = PARTITION_ENTRY_OFFSET + i * ENTRY_SIZE;
if e + ENTRY_SIZE > buf.len() {
break;
}
let entry = &buf[e..e + ENTRY_SIZE];
let length = i64_le(entry, 16).max(0) as u64;
if length == 0 {
continue;
}
let (type_desc, name) = match style {
STYLE_GPT => {
let name = utf16_name(&entry[72..144]);
(
Some(format_guid(&entry[32..48])),
(!name.is_empty()).then_some(name),
)
}
STYLE_MBR => {
let ty = entry[32];
if ty == 0 {
continue;
}
(Some(format!("0x{ty:02X}")), None)
}
_ => (None, None),
};
partitions.push(ParsedPartition {
number: u32_le(entry, 24),
start_offset: i64_le(entry, 8).max(0) as u64,
length,
type_desc,
name,
});
}
ParsedLayout { style, partitions }
}
fn format_guid(b: &[u8]) -> String {
format!(
"{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
u32_le(b, 0),
u16_le(b, 4),
u16_le(b, 6),
b[8],
b[9],
b[10],
b[11],
b[12],
b[13],
b[14],
b[15],
)
}
fn u16_le(b: &[u8], o: usize) -> u16 {
u16::from_le_bytes([b[o], b[o + 1]])
}
fn u32_le(b: &[u8], o: usize) -> u32 {
u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
}
fn i64_le(b: &[u8], o: usize) -> i64 {
i64::from_le_bytes(b[o..o + 8].try_into().expect("8 bytes"))
}
fn utf16_name(b: &[u8]) -> String {
let units: Vec<u16> = b
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.take_while(|&u| u != 0)
.collect();
String::from_utf16_lossy(&units).trim().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
const BASIC_DATA: [u8; 16] = [
0xA2, 0xA0, 0xD0, 0xEB, 0xE5, 0xB9, 0x33, 0x44, 0x87, 0xC0, 0x68, 0xB6, 0xB7, 0x26, 0x99,
0xC7,
];
fn put_u32(b: &mut [u8], o: usize, v: u32) {
b[o..o + 4].copy_from_slice(&v.to_le_bytes());
}
fn put_i64(b: &mut [u8], o: usize, v: i64) {
b[o..o + 8].copy_from_slice(&v.to_le_bytes());
}
fn one_entry_buf(style: u32) -> Vec<u8> {
let mut b = vec![0u8; PARTITION_ENTRY_OFFSET + ENTRY_SIZE];
put_u32(&mut b, 0, style); put_u32(&mut b, 4, 1); let e = PARTITION_ENTRY_OFFSET;
put_u32(&mut b, e, style); put_i64(&mut b, e + 8, 1_048_576); put_i64(&mut b, e + 16, 256 * 1024 * 1024); put_u32(&mut b, e + 24, 1); b
}
#[test]
fn format_guid_canonical_uppercase() {
assert_eq!(
format_guid(&BASIC_DATA),
"EBD0A0A2-B9E5-4433-87C0-68B6B72699C7"
);
}
#[test]
fn parse_gpt_partition_with_type_and_name() {
let mut b = one_entry_buf(STYLE_GPT);
let e = PARTITION_ENTRY_OFFSET;
b[e + 32..e + 48].copy_from_slice(&BASIC_DATA); for (i, u) in "Basic".encode_utf16().enumerate() {
let o = e + 72 + i * 2;
b[o..o + 2].copy_from_slice(&u.to_le_bytes());
}
let layout = parse_drive_layout(&b);
assert_eq!(layout.style, STYLE_GPT);
assert_eq!(layout.partitions.len(), 1);
let p = &layout.partitions[0];
assert_eq!(p.number, 1);
assert_eq!(p.start_offset, 1_048_576);
assert_eq!(p.length, 256 * 1024 * 1024);
assert_eq!(
p.type_desc.as_deref(),
Some("EBD0A0A2-B9E5-4433-87C0-68B6B72699C7")
);
assert_eq!(p.name.as_deref(), Some("Basic"));
}
#[test]
fn parse_mbr_partition_type_byte() {
let mut b = one_entry_buf(STYLE_MBR);
let e = PARTITION_ENTRY_OFFSET;
b[e + 32] = 0x07; let layout = parse_drive_layout(&b);
assert_eq!(layout.style, STYLE_MBR);
assert_eq!(layout.partitions.len(), 1);
assert_eq!(layout.partitions[0].type_desc.as_deref(), Some("0x07"));
assert_eq!(layout.partitions[0].name, None);
}
#[test]
fn parse_skips_empty_slots() {
let mut b = vec![0u8; PARTITION_ENTRY_OFFSET + 4 * ENTRY_SIZE];
put_u32(&mut b, 0, STYLE_MBR);
put_u32(&mut b, 4, 4);
let e = PARTITION_ENTRY_OFFSET;
put_i64(&mut b, e + 16, 100 * 1024 * 1024); b[e + 32] = 0x0c;
let layout = parse_drive_layout(&b);
assert_eq!(layout.partitions.len(), 1);
}
#[test]
fn parse_truncated_buffer_is_safe() {
let mut b = vec![0u8; PARTITION_ENTRY_OFFSET];
put_u32(&mut b, 0, STYLE_GPT);
put_u32(&mut b, 4, 4);
let layout = parse_drive_layout(&b);
assert!(layout.partitions.is_empty());
}
#[test]
fn utf16_name_stops_at_nul_and_trims() {
let mut bytes = Vec::new();
for u in "EFI ".encode_utf16() {
bytes.extend_from_slice(&u.to_le_bytes());
}
bytes.extend_from_slice(&[0, 0, 0x41, 0]); assert_eq!(utf16_name(&bytes), "EFI");
}
}