use crate::error::{MfsError, Result};
use crate::util::{rd_u16, rd_u32, wr_u16, wr_u32};
pub(crate) const MFS_SIGNATURE: u16 = 0xD2D7;
pub(crate) const HFS_SIGNATURE: u16 = 0x4244;
pub(crate) const MDB_LEN: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Mdb {
pub sig_word: u16,
pub cr_date: u32,
pub ls_mod: u32,
pub atrb: u16,
pub nm_fls: u16,
pub dir_st: u16,
pub dir_len: u16,
pub nm_al_blks: u16,
pub al_blk_siz: u32,
pub clp_siz: u32,
pub al_bl_st: u16,
pub nxt_fnum: u32,
pub free_bks: u16,
pub name_raw: [u8; 28],
}
impl Mdb {
pub(crate) fn parse(region: &[u8]) -> Result<Mdb> {
if region.len() < MDB_LEN {
return Err(MfsError::CorruptVolume(format!(
"MDB region is {} bytes, need at least {MDB_LEN}",
region.len()
)));
}
let sig_word = rd_u16(region, 0);
if sig_word == HFS_SIGNATURE {
return Err(MfsError::UnsupportedHfs);
}
if sig_word != MFS_SIGNATURE {
return Err(MfsError::BadSignature { found: sig_word });
}
let al_blk_siz = rd_u32(region, 20);
if al_blk_siz == 0 || !al_blk_siz.is_multiple_of(512) {
return Err(MfsError::CorruptVolume(format!(
"drAlBlkSiz is {al_blk_siz}, expected a nonzero multiple of 512"
)));
}
let dir_st = rd_u16(region, 14);
if dir_st < 3 {
return Err(MfsError::CorruptVolume(format!(
"drDirSt is {dir_st}, expected at least 3 (boot blocks + MDB)"
)));
}
let dir_len = rd_u16(region, 16);
if dir_len < 1 {
return Err(MfsError::CorruptVolume(
"drDirLen is 0, expected at least 1 sector".to_string(),
));
}
let al_bl_st = rd_u16(region, 28);
let dir_end = dir_st as u32 + dir_len as u32;
if (al_bl_st as u32) < dir_end {
return Err(MfsError::CorruptVolume(format!(
"drAlBlSt is {al_bl_st}, expected at least {dir_end} \
(drDirSt {dir_st} + drDirLen {dir_len})"
)));
}
let mut name_raw = [0u8; 28];
name_raw.copy_from_slice(®ion[36..64]);
if name_raw[0] > 27 {
return Err(MfsError::CorruptVolume(format!(
"volume name length byte is {}, maximum is 27",
name_raw[0]
)));
}
Ok(Mdb {
sig_word,
cr_date: rd_u32(region, 2),
ls_mod: rd_u32(region, 6),
atrb: rd_u16(region, 10),
nm_fls: rd_u16(region, 12),
dir_st,
dir_len,
nm_al_blks: rd_u16(region, 18),
al_blk_siz,
clp_siz: rd_u32(region, 24),
al_bl_st,
nxt_fnum: rd_u32(region, 30),
free_bks: rd_u16(region, 34),
name_raw,
})
}
pub(crate) fn write_to(&self, out: &mut [u8]) {
assert!(
out.len() >= MDB_LEN,
"MDB output region must be at least {MDB_LEN} bytes"
);
wr_u16(out, 0, self.sig_word);
wr_u32(out, 2, self.cr_date);
wr_u32(out, 6, self.ls_mod);
wr_u16(out, 10, self.atrb);
wr_u16(out, 12, self.nm_fls);
wr_u16(out, 14, self.dir_st);
wr_u16(out, 16, self.dir_len);
wr_u16(out, 18, self.nm_al_blks);
wr_u32(out, 20, self.al_blk_siz);
wr_u32(out, 24, self.clp_siz);
wr_u16(out, 28, self.al_bl_st);
wr_u32(out, 30, self.nxt_fnum);
wr_u16(out, 34, self.free_bks);
out[36..64].copy_from_slice(&self.name_raw);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_bytes() -> [u8; 64] {
let mut b = [0u8; 64];
b[0..2].copy_from_slice(&0xD2D7u16.to_be_bytes()); b[2..6].copy_from_slice(&0xA1B2_C3D4u32.to_be_bytes()); b[6..10].copy_from_slice(&0xA1B2_C3E8u32.to_be_bytes()); b[10..12].copy_from_slice(&0x0080u16.to_be_bytes()); b[12..14].copy_from_slice(&7u16.to_be_bytes()); b[14..16].copy_from_slice(&4u16.to_be_bytes()); b[16..18].copy_from_slice(&12u16.to_be_bytes()); b[18..20].copy_from_slice(&391u16.to_be_bytes()); b[20..24].copy_from_slice(&1024u32.to_be_bytes()); b[24..28].copy_from_slice(&8192u32.to_be_bytes()); b[28..30].copy_from_slice(&16u16.to_be_bytes()); b[30..34].copy_from_slice(&23u32.to_be_bytes()); b[34..36].copy_from_slice(&300u16.to_be_bytes()); b[36] = 8;
b[37..45].copy_from_slice(b"Untitled");
b[45] = 0xEE;
b[63] = 0x11;
b
}
#[test]
fn parses_every_field() {
let m = Mdb::parse(&sample_bytes()).unwrap();
assert_eq!(m.sig_word, MFS_SIGNATURE);
assert_eq!(m.cr_date, 0xA1B2_C3D4);
assert_eq!(m.ls_mod, 0xA1B2_C3E8);
assert_eq!(m.atrb, 0x0080);
assert_eq!(m.nm_fls, 7);
assert_eq!(m.dir_st, 4);
assert_eq!(m.dir_len, 12);
assert_eq!(m.nm_al_blks, 391);
assert_eq!(m.al_blk_siz, 1024);
assert_eq!(m.clp_siz, 8192);
assert_eq!(m.al_bl_st, 16);
assert_eq!(m.nxt_fnum, 23);
assert_eq!(m.free_bks, 300);
assert_eq!(m.name_raw[0], 8);
assert_eq!(&m.name_raw[1..9], b"Untitled");
assert_eq!(m.name_raw[9], 0xEE);
assert_eq!(m.name_raw[27], 0x11);
}
#[test]
fn write_to_round_trips_byte_identically() {
let bytes = sample_bytes();
let m = Mdb::parse(&bytes).unwrap();
let mut out = [0xAAu8; 64];
m.write_to(&mut out);
assert_eq!(out, bytes);
assert_eq!(Mdb::parse(&out).unwrap(), m);
}
#[test]
fn parse_accepts_region_longer_than_64_bytes() {
let mut buf = vec![0u8; 512];
buf[..64].copy_from_slice(&sample_bytes());
buf[64] = 0xFF; assert!(Mdb::parse(&buf).is_ok());
}
#[test]
fn rejects_short_region() {
let bytes = sample_bytes();
assert!(matches!(
Mdb::parse(&bytes[..63]),
Err(MfsError::CorruptVolume(_))
));
}
#[test]
fn rejects_bad_signature() {
let mut b = sample_bytes();
b[0..2].copy_from_slice(&0xBEEFu16.to_be_bytes());
match Mdb::parse(&b) {
Err(MfsError::BadSignature { found }) => assert_eq!(found, 0xBEEF),
other => panic!("expected BadSignature, got {other:?}"),
}
}
#[test]
fn hfs_signature_is_unsupported() {
let mut b = sample_bytes();
b[0..2].copy_from_slice(&HFS_SIGNATURE.to_be_bytes());
assert!(matches!(Mdb::parse(&b), Err(MfsError::UnsupportedHfs)));
}
#[test]
fn rejects_bad_alloc_block_size() {
for bad in [0u32, 1, 511, 513, 1000] {
let mut b = sample_bytes();
b[20..24].copy_from_slice(&bad.to_be_bytes());
assert!(
matches!(Mdb::parse(&b), Err(MfsError::CorruptVolume(_))),
"drAlBlkSiz {bad} should be rejected"
);
}
for good in [512u32, 1024, 2048] {
let mut b = sample_bytes();
b[20..24].copy_from_slice(&good.to_be_bytes());
assert!(Mdb::parse(&b).is_ok(), "drAlBlkSiz {good} should be accepted");
}
}
#[test]
fn rejects_bad_directory_geometry() {
let mut b = sample_bytes();
b[14..16].copy_from_slice(&2u16.to_be_bytes());
assert!(matches!(Mdb::parse(&b), Err(MfsError::CorruptVolume(_))));
let mut b = sample_bytes();
b[16..18].copy_from_slice(&0u16.to_be_bytes());
assert!(matches!(Mdb::parse(&b), Err(MfsError::CorruptVolume(_))));
let mut b = sample_bytes();
b[28..30].copy_from_slice(&15u16.to_be_bytes()); assert!(matches!(Mdb::parse(&b), Err(MfsError::CorruptVolume(_))));
let mut b = sample_bytes();
b[28..30].copy_from_slice(&16u16.to_be_bytes());
assert!(Mdb::parse(&b).is_ok());
}
#[test]
fn rejects_overlong_volume_name() {
let mut b = sample_bytes();
b[36] = 28;
assert!(matches!(Mdb::parse(&b), Err(MfsError::CorruptVolume(_))));
b[36] = 27;
assert!(Mdb::parse(&b).is_ok());
}
}