use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use crate::tree::TreeNode;
pub const SECTOR_SIZE: u64 = 512;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct Partition {
pub index: u8,
pub status: u8,
pub type_code: u8,
pub start: u64,
pub length: u64,
}
#[derive(Debug)]
pub enum Error {
TooShort,
BadSignature,
ProtectiveMbr,
Io(std::io::Error),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::TooShort => write!(f, "image is shorter than one MBR sector (512 bytes)"),
Error::BadSignature => write!(f, "MBR boot signature 0x55AA missing"),
Error::ProtectiveMbr => write!(f, "protective MBR (GPT disk)"),
Error::Io(e) => write!(f, "MBR I/O error: {e}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
pub fn parse(file: &mut File) -> Result<Vec<Partition>, Error> {
file.seek(SeekFrom::Start(0))?;
let mut sector = [0u8; SECTOR_SIZE as usize];
if file.read(&mut sector)? < SECTOR_SIZE as usize {
return Err(Error::TooShort);
}
parse_sector(§or)
}
pub fn parse_sector(sector: &[u8]) -> Result<Vec<Partition>, Error> {
if sector.len() < SECTOR_SIZE as usize {
return Err(Error::TooShort);
}
if sector[0x1FE] != 0x55 || sector[0x1FF] != 0xAA {
return Err(Error::BadSignature);
}
let mut partitions = Vec::with_capacity(4);
let mut all_ee = true;
let mut had_any = false;
for i in 0..4 {
let off = 0x1BE + 16 * i;
let entry = §or[off..off + 16];
let status = entry[0];
let type_code = entry[4];
let lba_start = u32::from_le_bytes([entry[8], entry[9], entry[10], entry[11]]);
let num_sectors = u32::from_le_bytes([entry[12], entry[13], entry[14], entry[15]]);
if type_code == 0 && num_sectors == 0 {
continue;
}
had_any = true;
if type_code != 0xEE {
all_ee = false;
}
partitions.push(Partition {
index: i as u8,
status,
type_code,
start: (lba_start as u64) * SECTOR_SIZE,
length: (num_sectors as u64) * SECTOR_SIZE,
});
}
if had_any && all_ee && partitions.len() == 1 {
return Err(Error::ProtectiveMbr);
}
Ok(partitions)
}
pub fn to_tree(partitions: &[Partition]) -> TreeNode {
let mut root = TreeNode::new_directory("/".to_string());
for p in partitions {
let name = format!("partition-{}-type-{:02x}", p.index, p.type_code);
let node = if p.length == 0 {
TreeNode::new_file(name, 0)
} else {
TreeNode::new_file_with_location(name, p.length, p.start, p.length)
};
root.add_child(node);
}
root.calculate_directory_size();
root
}
pub fn detect_and_parse(file: &mut File) -> Result<TreeNode, Error> {
let parts = parse(file)?;
Ok(to_tree(&parts))
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_sector() -> [u8; 512] {
let mut s = [0u8; 512];
s[0x1FE] = 0x55;
s[0x1FF] = 0xAA;
s
}
fn write_entry(
sector: &mut [u8; 512],
slot: usize,
status: u8,
type_code: u8,
lba_start: u32,
num_sectors: u32,
) {
let off = 0x1BE + 16 * slot;
sector[off] = status;
sector[off + 4] = type_code;
sector[off + 8..off + 12].copy_from_slice(&lba_start.to_le_bytes());
sector[off + 12..off + 16].copy_from_slice(&num_sectors.to_le_bytes());
}
#[test]
fn rejects_missing_signature() {
let s = [0u8; 512];
assert!(matches!(parse_sector(&s), Err(Error::BadSignature)));
}
#[test]
fn empty_partition_table_ok() {
let s = empty_sector();
let parts = parse_sector(&s).unwrap();
assert!(parts.is_empty());
}
#[test]
fn one_linux_partition() {
let mut s = empty_sector();
write_entry(&mut s, 0, 0x80, 0x83, 2048, 100 * 1024 * 2);
let parts = parse_sector(&s).unwrap();
assert_eq!(parts.len(), 1);
assert_eq!(parts[0].status, 0x80);
assert_eq!(parts[0].type_code, 0x83);
assert_eq!(parts[0].start, 2048 * 512);
assert_eq!(parts[0].length, 100 * 1024 * 1024);
}
#[test]
fn three_partitions_one_empty() {
let mut s = empty_sector();
write_entry(&mut s, 0, 0x00, 0x07, 2048, 1024);
write_entry(&mut s, 1, 0x00, 0x83, 4096, 2048);
write_entry(&mut s, 3, 0x00, 0x82, 8192, 512);
let parts = parse_sector(&s).unwrap();
assert_eq!(parts.len(), 3);
assert_eq!(
parts.iter().map(|p| p.index).collect::<Vec<_>>(),
vec![0, 1, 3]
);
}
#[test]
fn protective_mbr_detected() {
let mut s = empty_sector();
write_entry(&mut s, 0, 0x00, 0xEE, 1, u32::MAX);
assert!(matches!(parse_sector(&s), Err(Error::ProtectiveMbr)));
}
#[test]
fn to_tree_shapes_children() {
let mut s = empty_sector();
write_entry(&mut s, 0, 0x00, 0x07, 2048, 1024);
write_entry(&mut s, 1, 0x00, 0x83, 4096, 2048);
let parts = parse_sector(&s).unwrap();
let root = to_tree(&parts);
assert_eq!(root.name, "/");
assert!(root.is_directory);
assert_eq!(root.children.len(), 2);
assert!(root.children[0].name.starts_with("partition-0-type-07"));
assert_eq!(root.children[0].size, 1024 * 512);
assert_eq!(root.children[0].file_location, Some(2048 * 512));
}
#[test]
fn error_display_too_short() {
let msg = format!("{}", Error::TooShort);
assert!(msg.contains("512") || msg.contains("short"), "got: {msg}");
}
#[test]
fn error_display_bad_signature() {
let msg = format!("{}", Error::BadSignature);
assert!(
msg.contains("55AA") || msg.contains("signature"),
"got: {msg}"
);
}
#[test]
fn error_display_protective_mbr() {
let msg = format!("{}", Error::ProtectiveMbr);
assert!(
msg.contains("GPT") || msg.contains("protective"),
"got: {msg}"
);
}
#[test]
fn error_display_io() {
let io = std::io::Error::other("disk");
let msg = format!("{}", Error::Io(io));
assert!(msg.contains("disk"), "got: {msg}");
}
#[test]
fn error_source_io() {
use std::error::Error as StdError;
assert!(Error::Io(std::io::Error::other("s")).source().is_some());
}
#[test]
fn error_source_non_io() {
use std::error::Error as StdError;
assert!(Error::TooShort.source().is_none());
assert!(Error::BadSignature.source().is_none());
assert!(Error::ProtectiveMbr.source().is_none());
}
#[test]
fn error_from_io_error() {
let e = Error::from(std::io::Error::other("mbr test"));
assert!(matches!(e, Error::Io(_)));
}
#[test]
fn parse_sector_too_short_returns_error() {
let short = vec![0u8; 100];
assert!(matches!(parse_sector(&short), Err(Error::TooShort)));
}
#[test]
fn to_tree_zero_length_partition_has_no_location() {
let parts = vec![Partition {
index: 0,
status: 0,
type_code: 0x83,
start: 512,
length: 0, }];
let tree = to_tree(&parts);
assert!(
tree.children[0].file_location.is_none(),
"zero-length partition should have file_location=None"
);
}
}