use crate::Result;
use crate::block::BlockDevice;
pub const VOLUME_HEADER_OFFSET: u64 = 1024;
pub const FORK_DATA_SIZE: usize = 80;
pub const FORK_EXTENT_COUNT: usize = 8;
pub const SIG_HFS_PLUS: [u8; 2] = *b"H+";
pub const SIG_HFSX: [u8; 2] = *b"HX";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ExtentDescriptor {
pub start_block: u32,
pub block_count: u32,
}
impl ExtentDescriptor {
pub fn decode(buf: &[u8; 8]) -> Self {
Self {
start_block: u32::from_be_bytes(buf[0..4].try_into().unwrap()),
block_count: u32::from_be_bytes(buf[4..8].try_into().unwrap()),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ForkData {
pub logical_size: u64,
pub clump_size: u32,
pub total_blocks: u32,
pub extents: [ExtentDescriptor; FORK_EXTENT_COUNT],
}
impl ForkData {
pub fn decode(buf: &[u8; FORK_DATA_SIZE]) -> Self {
let logical_size = u64::from_be_bytes(buf[0..8].try_into().unwrap());
let clump_size = u32::from_be_bytes(buf[8..12].try_into().unwrap());
let total_blocks = u32::from_be_bytes(buf[12..16].try_into().unwrap());
let mut extents = [ExtentDescriptor::default(); FORK_EXTENT_COUNT];
for (i, slot) in extents.iter_mut().enumerate() {
let off = 16 + i * 8;
let mut e = [0u8; 8];
e.copy_from_slice(&buf[off..off + 8]);
*slot = ExtentDescriptor::decode(&e);
}
Self {
logical_size,
clump_size,
total_blocks,
extents,
}
}
pub fn inline_blocks(&self) -> u64 {
self.extents.iter().map(|e| u64::from(e.block_count)).sum()
}
}
#[derive(Debug, Clone)]
pub struct VolumeHeader {
pub signature: [u8; 2],
pub version: u16,
pub attributes: u32,
pub journal_info_block: u32,
pub block_size: u32,
pub total_blocks: u32,
pub free_blocks: u32,
pub next_catalog_id: u32,
pub allocation_file: ForkData,
pub extents_file: ForkData,
pub catalog_file: ForkData,
pub attributes_file: ForkData,
pub startup_file: ForkData,
}
impl VolumeHeader {
pub const ENCODED_SIZE: usize = 512;
pub fn decode(buf: &[u8; Self::ENCODED_SIZE]) -> Result<Self> {
let signature = [buf[0], buf[1]];
if signature != SIG_HFS_PLUS && signature != SIG_HFSX {
return Err(crate::Error::InvalidImage(format!(
"hfs+: bad volume signature {:02x}{:02x} (expected 'H+' or 'HX')",
signature[0], signature[1]
)));
}
let version = u16::from_be_bytes(buf[2..4].try_into().unwrap());
let attributes = u32::from_be_bytes(buf[4..8].try_into().unwrap());
let journal_info_block = u32::from_be_bytes(buf[12..16].try_into().unwrap());
let block_size = u32::from_be_bytes(buf[40..44].try_into().unwrap());
let total_blocks = u32::from_be_bytes(buf[44..48].try_into().unwrap());
let free_blocks = u32::from_be_bytes(buf[48..52].try_into().unwrap());
let next_catalog_id = u32::from_be_bytes(buf[64..68].try_into().unwrap());
let fd = |off: usize| -> ForkData {
let mut tmp = [0u8; FORK_DATA_SIZE];
tmp.copy_from_slice(&buf[off..off + FORK_DATA_SIZE]);
ForkData::decode(&tmp)
};
let allocation_file = fd(0x070);
let extents_file = fd(0x0C0);
let catalog_file = fd(0x110);
let attributes_file = fd(0x160);
let startup_file = fd(0x1B0);
if block_size == 0 || !block_size.is_power_of_two() {
return Err(crate::Error::InvalidImage(format!(
"hfs+: block_size {block_size} is not a positive power of two"
)));
}
Ok(Self {
signature,
version,
attributes,
journal_info_block,
block_size,
total_blocks,
free_blocks,
next_catalog_id,
allocation_file,
extents_file,
catalog_file,
attributes_file,
startup_file,
})
}
pub fn is_hfsx(&self) -> bool {
self.signature == SIG_HFSX
}
}
pub fn read_volume_header(dev: &mut dyn BlockDevice) -> Result<VolumeHeader> {
let size = dev.total_size();
if size < VOLUME_HEADER_OFFSET + VolumeHeader::ENCODED_SIZE as u64 {
return Err(crate::Error::InvalidImage(format!(
"hfs+: device size {size} too small to hold a volume header"
)));
}
let mut buf = [0u8; VolumeHeader::ENCODED_SIZE];
dev.read_at(VOLUME_HEADER_OFFSET, &mut buf)?;
VolumeHeader::decode(&buf)
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_header(block_size: u32, total_blocks: u32) -> [u8; VolumeHeader::ENCODED_SIZE] {
let mut b = [0u8; VolumeHeader::ENCODED_SIZE];
b[0..2].copy_from_slice(&SIG_HFS_PLUS);
b[2..4].copy_from_slice(&4u16.to_be_bytes());
b[4..8].copy_from_slice(&0u32.to_be_bytes()); b[40..44].copy_from_slice(&block_size.to_be_bytes());
b[44..48].copy_from_slice(&total_blocks.to_be_bytes());
b[48..52].copy_from_slice(&0u32.to_be_bytes()); b[64..68].copy_from_slice(&100u32.to_be_bytes());
let cat = 0x110usize;
b[cat..cat + 8].copy_from_slice(&8192u64.to_be_bytes());
b[cat + 8..cat + 12].copy_from_slice(&0u32.to_be_bytes());
b[cat + 12..cat + 16].copy_from_slice(&2u32.to_be_bytes());
b[cat + 16..cat + 20].copy_from_slice(&5u32.to_be_bytes());
b[cat + 20..cat + 24].copy_from_slice(&2u32.to_be_bytes());
b
}
#[test]
fn decode_valid_header() {
let buf = synth_header(4096, 1024);
let vh = VolumeHeader::decode(&buf).unwrap();
assert_eq!(vh.signature, *b"H+");
assert_eq!(vh.version, 4);
assert_eq!(vh.block_size, 4096);
assert_eq!(vh.total_blocks, 1024);
assert_eq!(vh.next_catalog_id, 100);
assert_eq!(vh.catalog_file.logical_size, 8192);
assert_eq!(vh.catalog_file.total_blocks, 2);
assert_eq!(vh.catalog_file.extents[0].start_block, 5);
assert_eq!(vh.catalog_file.extents[0].block_count, 2);
assert!(!vh.is_hfsx());
}
#[test]
fn rejects_bad_signature() {
let mut buf = synth_header(4096, 1024);
buf[0] = b'X';
buf[1] = b'X';
assert!(VolumeHeader::decode(&buf).is_err());
}
#[test]
fn rejects_non_power_of_two_block_size() {
let buf = synth_header(3000, 1024);
assert!(VolumeHeader::decode(&buf).is_err());
}
#[test]
fn fork_data_decodes_inline_blocks() {
let mut buf = [0u8; FORK_DATA_SIZE];
buf[0..8].copy_from_slice(&1_048_576u64.to_be_bytes()); buf[12..16].copy_from_slice(&3u32.to_be_bytes()); buf[16..20].copy_from_slice(&100u32.to_be_bytes());
buf[20..24].copy_from_slice(&1u32.to_be_bytes());
buf[24..28].copy_from_slice(&200u32.to_be_bytes());
buf[28..32].copy_from_slice(&2u32.to_be_bytes());
let fd = ForkData::decode(&buf);
assert_eq!(fd.logical_size, 1_048_576);
assert_eq!(fd.total_blocks, 3);
assert_eq!(fd.extents[0].start_block, 100);
assert_eq!(fd.extents[0].block_count, 1);
assert_eq!(fd.extents[1].start_block, 200);
assert_eq!(fd.extents[1].block_count, 2);
assert_eq!(fd.inline_blocks(), 3);
}
}