use crate::error::{OpticaldiscsError, Result};
use crate::sector_reader::SectorReader;
const HFS_MDB_SIGNATURE: u16 = 0x4244;
const MDB_OFFSET: u64 = 1024;
#[derive(Debug, Clone)]
pub struct MasterDirectoryBlock {
pub creation_date: u32,
pub file_count: u16,
pub alloc_block_size: u32,
pub alloc_block_start: u16,
pub volume_name: String,
pub catalog_start_block: u16,
pub catalog_block_count: u16,
pub catalog_file_size: u32,
pub catalog_extents: [(u16, u16); 3],
}
impl MasterDirectoryBlock {
pub fn read_from(reader: &mut dyn SectorReader, partition_offset: u64) -> Result<Self> {
let data = reader.read_bytes(partition_offset + MDB_OFFSET, 256)?;
let signature = u16::from_be_bytes([data[0], data[1]]);
if signature != HFS_MDB_SIGNATURE {
return Err(OpticaldiscsError::Parse(format!(
"Invalid HFS MDB signature: expected 0x{HFS_MDB_SIGNATURE:04X}, \
got 0x{signature:04X}"
)));
}
let creation_date = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
let file_count = u16::from_be_bytes([data[10], data[11]]);
let alloc_block_size = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
let alloc_block_start = u16::from_be_bytes([data[28], data[29]]);
let name_len = data[36] as usize;
let name_end = (37 + name_len).min(64);
let volume_name = mac_roman_to_string(&data[37..name_end]);
let catalog_file_size = u32::from_be_bytes([data[146], data[147], data[148], data[149]]);
let mut catalog_extents = [(0u16, 0u16); 3];
for (i, ext) in catalog_extents.iter_mut().enumerate() {
let base = 150 + i * 4;
ext.0 = u16::from_be_bytes([data[base], data[base + 1]]);
ext.1 = u16::from_be_bytes([data[base + 2], data[base + 3]]);
}
let (catalog_start_block, catalog_block_count) = catalog_extents[0];
Ok(Self {
creation_date,
file_count,
alloc_block_size,
alloc_block_start,
volume_name,
catalog_start_block,
catalog_block_count,
catalog_file_size,
catalog_extents,
})
}
}
pub(crate) fn mac_roman_to_string(data: &[u8]) -> String {
data.iter().map(|&b| mac_roman_char(b)).collect()
}
fn mac_roman_char(b: u8) -> char {
if b < 0x80 {
return b as char;
}
MAC_ROMAN_TABLE[(b - 0x80) as usize]
}
static MAC_ROMAN_TABLE: [char; 128] = [
'\u{00C4}', '\u{00C5}', '\u{00C7}', '\u{00C9}', '\u{00D1}', '\u{00D6}', '\u{00DC}', '\u{00E1}',
'\u{00E0}', '\u{00E2}', '\u{00E4}', '\u{00E3}', '\u{00E5}', '\u{00E7}', '\u{00E9}', '\u{00E8}',
'\u{00EA}', '\u{00EB}', '\u{00ED}', '\u{00EC}', '\u{00EE}', '\u{00EF}', '\u{00F1}', '\u{00F3}',
'\u{00F2}', '\u{00F4}', '\u{00F6}', '\u{00FA}', '\u{00F9}', '\u{00FB}', '\u{00FC}', '\u{2020}',
'\u{00B0}', '\u{00A2}', '\u{00A3}', '\u{00A7}', '\u{2022}', '\u{00B6}', '\u{00DF}', '\u{00AE}',
'\u{00A9}', '\u{2122}', '\u{00B4}', '\u{00A8}', '\u{2260}', '\u{00C6}', '\u{00D8}', '\u{221E}',
'\u{00B1}', '\u{2264}', '\u{2265}', '\u{00A5}', '\u{00B5}', '\u{2202}', '\u{2211}', '\u{220F}',
'\u{03C0}', '\u{222B}', '\u{00AA}', '\u{00BA}', '\u{03A9}', '\u{00E6}', '\u{00F8}', '\u{00BF}',
'\u{00A1}', '\u{00AC}', '\u{221A}', '\u{0192}', '\u{2248}', '\u{2206}', '\u{00AB}', '\u{00BB}',
'\u{2026}', '\u{00A0}', '\u{00C0}', '\u{00C3}', '\u{00D5}', '\u{0152}', '\u{0153}', '\u{2013}',
'\u{2014}', '\u{201C}', '\u{201D}', '\u{2018}', '\u{2019}', '\u{00F7}', '\u{25CA}', '\u{00FF}',
'\u{0178}', '\u{2044}', '\u{20AC}', '\u{2039}', '\u{203A}', '\u{FB01}', '\u{FB02}', '\u{2021}',
'\u{00B7}', '\u{201A}', '\u{201E}', '\u{2030}', '\u{00C2}', '\u{00CA}', '\u{00C1}', '\u{00CB}',
'\u{00C8}', '\u{00CD}', '\u{00CE}', '\u{00CF}', '\u{00CC}', '\u{00D3}', '\u{00D4}', '\u{F8FF}',
'\u{00D2}', '\u{00DA}', '\u{00DB}', '\u{00D9}', '\u{0131}', '\u{02C6}', '\u{02DC}', '\u{00AF}',
'\u{02D8}', '\u{02D9}', '\u{02DA}', '\u{00B8}', '\u{02DD}', '\u{02DB}', '\u{02C7}', '\u{FFFD}',
];
#[cfg(test)]
mod tests {
use super::*;
use crate::sector_reader::SECTOR_SIZE;
use std::io::{Cursor, Read, Seek, SeekFrom};
struct CursorReader(Cursor<Vec<u8>>);
impl SectorReader for CursorReader {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
self.0
.seek(SeekFrom::Start(lba * SECTOR_SIZE))
.map_err(OpticaldiscsError::Io)?;
let mut buf = vec![0u8; SECTOR_SIZE as usize];
self.0.read_exact(&mut buf).map_err(OpticaldiscsError::Io)?;
Ok(buf)
}
}
fn make_mdb_image(
name: &str,
alloc_block_size: u32,
alloc_block_start: u16,
catalog_start: u16,
catalog_count: u16,
) -> Vec<u8> {
let mut img = vec![0u8; 2 * SECTOR_SIZE as usize];
let off = 1024usize;
img[off] = 0x42;
img[off + 1] = 0x44;
img[off + 2..off + 6].copy_from_slice(&0x8000_0000u32.to_be_bytes());
img[off + 10..off + 12].copy_from_slice(&5u16.to_be_bytes());
img[off + 20..off + 24].copy_from_slice(&alloc_block_size.to_be_bytes());
img[off + 28..off + 30].copy_from_slice(&alloc_block_start.to_be_bytes());
let nb = name.as_bytes();
img[off + 36] = nb.len() as u8;
img[off + 37..off + 37 + nb.len()].copy_from_slice(nb);
let cat_size = catalog_count as u32 * alloc_block_size;
img[off + 146..off + 150].copy_from_slice(&cat_size.to_be_bytes());
img[off + 150..off + 152].copy_from_slice(&catalog_start.to_be_bytes());
img[off + 152..off + 154].copy_from_slice(&catalog_count.to_be_bytes());
img
}
#[test]
fn read_mdb_all_fields() {
let img = make_mdb_image("TestVol", 4096, 3, 5, 10);
let mut reader = CursorReader(Cursor::new(img));
let mdb = MasterDirectoryBlock::read_from(&mut reader, 0).unwrap();
assert_eq!(mdb.volume_name, "TestVol");
assert_eq!(mdb.alloc_block_size, 4096);
assert_eq!(mdb.alloc_block_start, 3);
assert_eq!(mdb.catalog_start_block, 5);
assert_eq!(mdb.catalog_block_count, 10);
assert_eq!(mdb.catalog_extents[0], (5, 10));
assert_eq!(mdb.catalog_extents[1], (0, 0));
assert_eq!(mdb.catalog_file_size, 10 * 4096);
assert_eq!(mdb.file_count, 5);
assert_eq!(mdb.creation_date, 0x8000_0000);
}
#[test]
fn wrong_signature_is_parse_error() {
let mut img = make_mdb_image("X", 512, 0, 0, 0);
img[1024] = 0xFF; let mut reader = CursorReader(Cursor::new(img));
assert!(matches!(
MasterDirectoryBlock::read_from(&mut reader, 0),
Err(OpticaldiscsError::Parse(_))
));
}
#[test]
fn mac_roman_ascii_passthrough() {
assert_eq!(mac_roman_to_string(b"Hello"), "Hello");
}
#[test]
fn mac_roman_upper_range() {
assert_eq!(mac_roman_char(0x80), 'Ä');
assert_eq!(mac_roman_char(0xA9), '™');
}
}