use std::fmt::Write as _;
use serde::{Serialize, Serializer};
use crate::error::{MbrkitError, Result};
use crate::layout::PartitionLayout;
pub const SECTOR_SIZE: usize = 512;
pub const MBR_BOOTSTRAP_CODE_SIZE: usize = 440;
pub const DISK_SIGNATURE_SIZE: usize = 4;
pub const RESERVED_BYTES_SIZE: usize = 2;
pub const MBR_PREFIX_SIZE: usize =
MBR_BOOTSTRAP_CODE_SIZE + DISK_SIGNATURE_SIZE + RESERVED_BYTES_SIZE;
pub const DISK_SIGNATURE_OFFSET: usize = MBR_BOOTSTRAP_CODE_SIZE;
pub const RESERVED_BYTES_OFFSET: usize = DISK_SIGNATURE_OFFSET + DISK_SIGNATURE_SIZE;
pub const PARTITION_TABLE_OFFSET: usize = MBR_PREFIX_SIZE;
pub const PARTITION_ENTRY_COUNT: usize = 4;
pub const PARTITION_ENTRY_SIZE: usize = 16;
pub const MBR_SIGNATURE_OFFSET: usize = 510;
pub const MBR_SIGNATURE: u16 = 0xAA55;
#[derive(Clone, Debug, Serialize)]
pub struct MbrHeader {
#[serde(serialize_with = "serialize_bootstrap_code")]
pub bootstrap_code: [u8; MBR_BOOTSTRAP_CODE_SIZE],
pub disk_signature: u32,
pub reserved: [u8; 2],
pub partitions: [PartitionEntry; PARTITION_ENTRY_COUNT],
pub signature: u16,
}
#[derive(Clone, Copy, Debug, Default, Serialize)]
pub struct PartitionEntry {
pub boot_indicator: u8,
pub start_chs: [u8; 3],
pub partition_type: u8,
pub end_chs: [u8; 3],
pub starting_lba: u32,
pub sectors: u32,
}
#[derive(Clone, Copy, Debug)]
struct ChsAddress {
cylinder: u16,
head: u8,
sector: u8,
}
impl MbrHeader {
pub fn new(bootstrap_code: [u8; MBR_BOOTSTRAP_CODE_SIZE], disk_signature: u32) -> Self {
Self {
bootstrap_code,
disk_signature,
reserved: [0_u8; RESERVED_BYTES_SIZE],
partitions: [PartitionEntry::default(); PARTITION_ENTRY_COUNT],
signature: MBR_SIGNATURE,
}
}
pub fn from_sector(sector: &[u8]) -> Result<Self> {
if sector.len() < SECTOR_SIZE {
return Err(MbrkitError::InvalidMbr(
"disk image is smaller than one sector".into(),
));
}
let mut bootstrap_code = [0_u8; MBR_BOOTSTRAP_CODE_SIZE];
bootstrap_code.copy_from_slice(§or[..MBR_BOOTSTRAP_CODE_SIZE]);
let mut partitions = [PartitionEntry::default(); PARTITION_ENTRY_COUNT];
for (index, entry) in partitions.iter_mut().enumerate() {
let offset = PARTITION_TABLE_OFFSET + index * PARTITION_ENTRY_SIZE;
*entry = PartitionEntry::from_bytes(§or[offset..offset + PARTITION_ENTRY_SIZE]);
}
Ok(Self {
bootstrap_code,
disk_signature: u32::from_le_bytes([
sector[DISK_SIGNATURE_OFFSET],
sector[DISK_SIGNATURE_OFFSET + 1],
sector[DISK_SIGNATURE_OFFSET + 2],
sector[DISK_SIGNATURE_OFFSET + 3],
]),
reserved: [
sector[RESERVED_BYTES_OFFSET],
sector[RESERVED_BYTES_OFFSET + 1],
],
partitions,
signature: u16::from_le_bytes([
sector[MBR_SIGNATURE_OFFSET],
sector[MBR_SIGNATURE_OFFSET + 1],
]),
})
}
pub fn to_sector(&self) -> [u8; SECTOR_SIZE] {
let mut sector = [0_u8; SECTOR_SIZE];
sector[..MBR_BOOTSTRAP_CODE_SIZE].copy_from_slice(&self.bootstrap_code);
sector[DISK_SIGNATURE_OFFSET..DISK_SIGNATURE_OFFSET + 4]
.copy_from_slice(&self.disk_signature.to_le_bytes());
sector[RESERVED_BYTES_OFFSET..RESERVED_BYTES_OFFSET + 2].copy_from_slice(&self.reserved);
for (index, entry) in self.partitions.iter().enumerate() {
let offset = PARTITION_TABLE_OFFSET + index * PARTITION_ENTRY_SIZE;
sector[offset..offset + PARTITION_ENTRY_SIZE].copy_from_slice(&entry.to_bytes());
}
sector[MBR_SIGNATURE_OFFSET..MBR_SIGNATURE_OFFSET + 2]
.copy_from_slice(&self.signature.to_le_bytes());
sector
}
pub fn set_partition(&mut self, slot: usize, layout: &PartitionLayout) -> Result<()> {
if slot >= PARTITION_ENTRY_COUNT {
return Err(MbrkitError::InvalidArgument(format!(
"partition slot {} is out of range",
slot + 1
)));
}
self.partitions[slot] = PartitionEntry::from_layout(layout)?;
Ok(())
}
pub fn has_valid_signature(&self) -> bool {
self.signature == MBR_SIGNATURE
}
}
impl PartitionEntry {
pub fn from_bytes(bytes: &[u8]) -> Self {
Self {
boot_indicator: bytes[0],
start_chs: [bytes[1], bytes[2], bytes[3]],
partition_type: bytes[4],
end_chs: [bytes[5], bytes[6], bytes[7]],
starting_lba: u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
sectors: u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
}
}
pub fn to_bytes(self) -> [u8; PARTITION_ENTRY_SIZE] {
let mut bytes = [0_u8; PARTITION_ENTRY_SIZE];
bytes[0] = self.boot_indicator;
bytes[1..4].copy_from_slice(&self.start_chs);
bytes[4] = self.partition_type;
bytes[5..8].copy_from_slice(&self.end_chs);
bytes[8..12].copy_from_slice(&self.starting_lba.to_le_bytes());
bytes[12..16].copy_from_slice(&self.sectors.to_le_bytes());
bytes
}
pub fn from_layout(layout: &PartitionLayout) -> Result<Self> {
let starting_lba = u32::try_from(layout.start_lba).map_err(|_| {
MbrkitError::InvalidArgument(format!(
"partition {} start LBA exceeds MBR limits",
layout.slot
))
})?;
let sectors = u32::try_from(layout.sector_count).map_err(|_| {
MbrkitError::InvalidArgument(format!(
"partition {} size exceeds MBR limits",
layout.slot
))
})?;
let end_lba = layout.end_lba();
let start_chs = ChsAddress::from_lba(layout.start_lba).encode();
let end_chs = ChsAddress::from_lba(end_lba).encode();
Ok(Self {
boot_indicator: if layout.bootable { 0x80 } else { 0x00 },
start_chs,
partition_type: layout.partition_type.0,
end_chs,
starting_lba,
sectors,
})
}
pub fn is_empty(self) -> bool {
self.partition_type == 0 && self.starting_lba == 0 && self.sectors == 0
}
pub fn is_bootable(self) -> bool {
self.boot_indicator == 0x80
}
pub fn end_lba(self) -> Option<u64> {
if self.sectors == 0 {
return None;
}
Some(self.starting_lba as u64 + self.sectors as u64 - 1)
}
}
impl ChsAddress {
fn from_lba(lba: u64) -> Self {
let sectors_per_track = 63_u64;
let heads = 255_u64;
let max_lba = 1023_u64 * heads * sectors_per_track;
if lba >= max_lba {
return Self {
cylinder: 1023,
head: 254,
sector: 63,
};
}
let cylinder = lba / (heads * sectors_per_track);
let head = (lba / sectors_per_track) % heads;
let sector = (lba % sectors_per_track) + 1;
Self {
cylinder: cylinder as u16,
head: head as u8,
sector: sector as u8,
}
}
fn encode(self) -> [u8; 3] {
let cylinder_high = ((self.cylinder >> 8) & 0x03) as u8;
let cylinder_low = (self.cylinder & 0xff) as u8;
let sector = (self.sector & 0x3f) | (cylinder_high << 6);
[self.head, sector, cylinder_low]
}
}
fn serialize_bootstrap_code<S>(
bootstrap_code: &[u8; MBR_BOOTSTRAP_CODE_SIZE],
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut encoded = String::with_capacity(MBR_BOOTSTRAP_CODE_SIZE * 2);
for byte in bootstrap_code {
let _ = write!(&mut encoded, "{byte:02x}");
}
serializer.serialize_str(&encoded)
}
#[cfg(test)]
mod tests {
use crate::layout::{PartitionLayout, PartitionType};
use super::{MBR_SIGNATURE, MbrHeader, PartitionEntry};
#[test]
fn mbr_sector_round_trip_is_lossless() {
let mut header = MbrHeader::new([0x90; super::MBR_BOOTSTRAP_CODE_SIZE], 0x12345678);
let layout = PartitionLayout {
slot: 1,
file: "rootfs.img".into(),
partition_type: PartitionType(0x81),
bootable: true,
start_lba: 2048,
sector_count: 128,
file_size: 65_536,
};
header.set_partition(0, &layout).unwrap();
let sector = header.to_sector();
let decoded = MbrHeader::from_sector(§or).unwrap();
assert_eq!(decoded.signature, MBR_SIGNATURE);
assert_eq!(decoded.disk_signature, 0x12345678);
assert_eq!(decoded.partitions[0].partition_type, 0x81);
assert_eq!(decoded.partitions[0].starting_lba, 2048);
assert_eq!(decoded.partitions[0].sectors, 128);
}
#[test]
fn partition_entry_predicates_match_flags() {
let entry = PartitionEntry {
boot_indicator: 0x80,
partition_type: 0x83,
starting_lba: 63,
sectors: 1024,
..PartitionEntry::default()
};
assert!(entry.is_bootable());
assert!(!entry.is_empty());
assert_eq!(entry.end_lba(), Some(1086));
}
#[test]
fn bootstrap_code_serializes_as_hex_string() {
let mut header = MbrHeader::new([0_u8; super::MBR_BOOTSTRAP_CODE_SIZE], 0x12345678);
header.bootstrap_code[0] = 0x01;
header.bootstrap_code[1] = 0x23;
header.bootstrap_code[2] = 0xff;
let value = serde_json::to_value(&header).unwrap();
let bootstrap_code = value["bootstrap_code"].as_str().unwrap();
assert_eq!(bootstrap_code.len(), super::MBR_BOOTSTRAP_CODE_SIZE * 2);
assert!(bootstrap_code.starts_with("0123ff"));
}
}