use thiserror::Error;
#[derive(Debug, Error)]
pub enum MbrError {
#[error("MBR boot code is {got} bytes; expected at least 440")]
BootCodeTooShort { got: usize },
#[error("disk too small for partition: {disk_sectors} sectors, need > {partition_start}")]
DiskTooSmall { disk_sectors: u64, partition_start: u64 },
#[error("existing MBR sector is {got} bytes; expected 512")]
ExistingTooShort { got: usize },
#[error("MBR boot blobs were not embedded; rebuild with --features embed-boot-asm")]
NotEmbedded,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
pub enum PartitionType {
Fat32Lba = 0x0C,
#[allow(dead_code)]
Fat32Chs = 0x0B,
#[allow(dead_code)]
Ntfs = 0x07,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct PartitionEntry {
pub bootable: bool,
pub partition_type: PartitionType,
pub lba_start: u32,
pub sector_count: u32,
}
impl PartitionEntry {
pub fn encode(&self) -> [u8; 16] {
let mut bytes = [0u8; 16];
bytes[0] = if self.bootable { 0x80 } else { 0x00 };
bytes[1] = 0xFE; bytes[2] = 0xFF; bytes[3] = 0xFF; bytes[4] = self.partition_type as u8;
bytes[5] = 0xFE; bytes[6] = 0xFF; bytes[7] = 0xFF; bytes[8..12].copy_from_slice(&self.lba_start.to_le_bytes());
bytes[12..16].copy_from_slice(&self.sector_count.to_le_bytes());
bytes
}
}
pub const PARTITION_START_LBA: u32 = 2048;
pub fn mbr_xp(disk_sectors: u64) -> Result<[u8; 512], MbrError> {
build_mbr(crate::MBR_XP_BOOT, disk_sectors)
}
pub fn mbr_win7(disk_sectors: u64) -> Result<[u8; 512], MbrError> {
build_mbr(crate::MBR_WIN7_BOOT, disk_sectors)
}
pub fn build_mbr(boot_code: &[u8], disk_sectors: u64) -> Result<[u8; 512], MbrError> {
if boot_code.is_empty() {
return Err(MbrError::NotEmbedded);
}
if boot_code.len() < 440 {
return Err(MbrError::BootCodeTooShort {
got: boot_code.len(),
});
}
let partition_start = PARTITION_START_LBA as u64;
if disk_sectors <= partition_start {
return Err(MbrError::DiskTooSmall {
disk_sectors,
partition_start,
});
}
let sector_count_u64 = disk_sectors - partition_start;
let sector_count: u32 = sector_count_u64.try_into().unwrap_or(u32::MAX);
let mut mbr = [0u8; 512];
mbr[0..440].copy_from_slice(&boot_code[..440]);
mbr[0x1B8..0x1BC].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
let active = PartitionEntry {
bootable: true,
partition_type: PartitionType::Fat32Lba,
lba_start: PARTITION_START_LBA,
sector_count,
};
mbr[0x1BE..0x1CE].copy_from_slice(&active.encode());
mbr[0x1FE] = 0x55;
mbr[0x1FF] = 0xAA;
Ok(mbr)
}
pub fn splice_mbr(existing: &[u8], boot: &[u8]) -> Result<[u8; 512], MbrError> {
if boot.is_empty() {
return Err(MbrError::NotEmbedded);
}
if boot.len() < 440 {
return Err(MbrError::BootCodeTooShort { got: boot.len() });
}
if existing.len() < 512 {
return Err(MbrError::ExistingTooShort { got: existing.len() });
}
let mut out = [0u8; 512];
out[..440].copy_from_slice(&boot[..440]);
out[440..510].copy_from_slice(&existing[440..510]);
out[510] = 0x55;
out[511] = 0xAA;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_boot() -> Vec<u8> {
vec![0xCC; 440]
}
#[test]
fn entry_encodes_active_fat32_with_lba_only() {
let e = PartitionEntry {
bootable: true,
partition_type: PartitionType::Fat32Lba,
lba_start: 2048,
sector_count: 131072,
};
let b = e.encode();
assert_eq!(b[0], 0x80, "bootable flag");
assert_eq!(b[1..4], [0xFE, 0xFF, 0xFF], "CHS first = LBA-marker");
assert_eq!(b[4], 0x0C, "FAT32 LBA type");
assert_eq!(b[5..8], [0xFE, 0xFF, 0xFF], "CHS last = LBA-marker");
assert_eq!(&b[8..12], &2048u32.to_le_bytes());
assert_eq!(&b[12..16], &131072u32.to_le_bytes());
}
#[test]
fn entry_inactive_clears_boot_flag() {
let e = PartitionEntry {
bootable: false,
partition_type: PartitionType::Fat32Lba,
lba_start: 0,
sector_count: 0,
};
assert_eq!(e.encode()[0], 0x00);
}
#[test]
fn mbr_has_signature_and_active_partition() {
let mbr = build_mbr(&fake_boot(), 131072).unwrap();
assert_eq!(mbr.len(), 512);
assert_eq!(&mbr[0x1FE..], &[0x55, 0xAA], "boot signature");
assert_eq!(mbr[0x1BE], 0x80, "partition 1 active");
assert_eq!(mbr[0x1BE + 4], 0x0C, "FAT32 LBA");
let lba = u32::from_le_bytes(mbr[0x1C6..0x1CA].try_into().unwrap());
assert_eq!(lba, 2048);
let count = u32::from_le_bytes(mbr[0x1CA..0x1CE].try_into().unwrap());
assert_eq!(count, 131072 - 2048);
}
#[test]
fn mbr_preserves_boot_code() {
let mbr = build_mbr(&fake_boot(), 131072).unwrap();
assert!(mbr[0..440].iter().all(|&b| b == 0xCC), "boot code from input");
}
#[test]
fn mbr_zeros_unused_partition_slots() {
let mbr = build_mbr(&fake_boot(), 131072).unwrap();
for slot in 1..4 {
let offset = 0x1BE + 16 * slot;
for i in 0..16 {
assert_eq!(mbr[offset + i], 0, "slot {slot} byte {i} should be 0");
}
}
}
#[test]
fn mbr_rejects_short_boot_code() {
let short = vec![0u8; 100];
assert!(matches!(
build_mbr(&short, 131072),
Err(MbrError::BootCodeTooShort { got: 100 })
));
}
#[test]
fn mbr_rejects_empty_boot_code() {
assert!(matches!(build_mbr(&[], 131072), Err(MbrError::NotEmbedded)));
}
#[test]
fn mbr_rejects_disk_too_small() {
let err = build_mbr(&fake_boot(), 1024).unwrap_err();
assert!(matches!(err, MbrError::DiskTooSmall { .. }));
}
#[test]
fn splice_mbr_replaces_boot_code_only() {
let mut existing = [0u8; 512];
for (i, b) in existing[440..510].iter_mut().enumerate() {
*b = (i as u8).wrapping_add(0x40);
}
existing[510] = 0x55;
existing[511] = 0xAA;
let boot: Vec<u8> = (0..440).map(|i| (i & 0xFF) as u8).collect();
let out = splice_mbr(&existing, &boot).unwrap();
assert_eq!(&out[..440], &boot[..]);
assert_eq!(&out[440..510], &existing[440..510]);
assert_eq!(out[510], 0x55);
assert_eq!(out[511], 0xAA);
}
#[test]
fn splice_mbr_forces_signature_even_if_missing() {
let mut existing = [0u8; 512];
for b in existing[440..510].iter_mut() {
*b = 0x42;
}
let out = splice_mbr(&existing, &fake_boot()).unwrap();
assert_eq!(out[510], 0x55);
assert_eq!(out[511], 0xAA);
}
#[test]
fn splice_mbr_rejects_short_existing() {
assert!(matches!(
splice_mbr(&[0u8; 100], &fake_boot()),
Err(MbrError::ExistingTooShort { got: 100 })
));
}
#[test]
fn splice_mbr_rejects_short_boot() {
let existing = [0u8; 512];
assert!(matches!(
splice_mbr(&existing, &[0u8; 100]),
Err(MbrError::BootCodeTooShort { got: 100 })
));
}
#[test]
fn splice_mbr_rejects_empty_boot() {
let existing = [0u8; 512];
assert!(matches!(splice_mbr(&existing, &[]), Err(MbrError::NotEmbedded)));
}
#[test]
fn mbr_clamps_huge_disks_to_u32_max() {
let mbr = build_mbr(&fake_boot(), 9_765_625_000).unwrap();
let count = u32::from_le_bytes(mbr[0x1CA..0x1CE].try_into().unwrap());
assert_eq!(count, u32::MAX);
}
}