use super::entry::{EntryType, FileEntry};
use super::filesystem::{Filesystem, FilesystemError};
use crate::hfs::{mac_roman_to_string, MasterDirectoryBlock};
use crate::sector_reader::SectorReader;
const HFS_ROOT_DIR_ID: u32 = 2;
const HFS_FOLDER_RECORD: i8 = 1;
const HFS_FILE_RECORD: i8 = 2;
pub struct HfsFilesystem {
reader: Box<dyn SectorReader>,
partition_offset: u64,
alloc_block_size: u32,
alloc_block_start: u32,
catalog_data: Vec<u8>,
node_size: u16,
first_leaf_node: u32,
volume_name: String,
}
impl HfsFilesystem {
pub fn new(
mut reader: Box<dyn SectorReader>,
partition_offset: u64,
) -> Result<Self, FilesystemError> {
let mdb = MasterDirectoryBlock::read_from(reader.as_mut(), partition_offset)
.map_err(hfs_disc_err)?;
let catalog_extents: Vec<HfsExtent> = mdb
.catalog_extents
.iter()
.filter(|(_, count)| *count != 0)
.map(|&(start, count)| HfsExtent {
start_block: start,
block_count: count,
})
.collect();
let catalog_data = read_extents_into_vec(
reader.as_mut(),
partition_offset,
mdb.alloc_block_start as u32,
mdb.alloc_block_size,
&catalog_extents,
mdb.catalog_file_size as usize,
)?;
if catalog_data.len() < 34 {
return Err(FilesystemError::Parse(
"HFS catalog too small to contain B-tree header".into(),
));
}
let first_leaf_node = u32::from_be_bytes([
catalog_data[24],
catalog_data[25],
catalog_data[26],
catalog_data[27],
]);
let node_size = u16::from_be_bytes([catalog_data[32], catalog_data[33]]);
Ok(Self {
reader,
partition_offset,
alloc_block_size: mdb.alloc_block_size,
alloc_block_start: mdb.alloc_block_start as u32,
catalog_data,
node_size,
first_leaf_node,
volume_name: mdb.volume_name,
})
}
fn read_node(&self, node_num: u32) -> Result<&[u8], FilesystemError> {
let node_size = self.node_size as usize;
let start = node_num as usize * node_size;
let end = start + node_size;
self.catalog_data.get(start..end).ok_or_else(|| {
FilesystemError::Parse(format!(
"HFS B-tree node {node_num} out of bounds (catalog has {} bytes)",
self.catalog_data.len()
))
})
}
fn list_by_id(
&mut self,
parent_cnid: u32,
parent_path: &str,
) -> Result<Vec<FileEntry>, FilesystemError> {
let mut entries: Vec<FileEntry> = Vec::new();
let mut metas: Vec<Option<HfsFileMeta>> = Vec::new();
let mut current = self.first_leaf_node;
let mut attempts = 0u32;
const MAX: u32 = 10_000;
while current != 0 && attempts < MAX {
attempts += 1;
let node = self.read_node(current)?;
let next = u32::from_be_bytes([node[0], node[1], node[2], node[3]]);
let kind = node[8] as i8;
let num_rec = u16::from_be_bytes([node[10], node[11]]);
if kind != -1 {
current = next;
continue;
}
process_leaf_node(
node,
self.node_size as usize,
num_rec,
parent_cnid,
parent_path,
&mut entries,
&mut metas,
);
current = next;
}
for (i, meta) in metas.iter().enumerate() {
if let Some(meta) = meta {
if meta.finder_flags & super::mac_alias::IS_ALIAS_FLAG != 0 && meta.rsrc_size > 0 {
if let Ok(rsrc) =
self.read_extents_range(&meta.rsrc_extents, 0, meta.rsrc_size as usize)
{
if let Some(target) = super::mac_alias::resolve_alias_target(&rsrc) {
entries[i].symlink_target = Some(target);
}
}
}
}
}
entries.sort_by(|a, b| match (a.entry_type, b.entry_type) {
(EntryType::Directory, EntryType::File) => std::cmp::Ordering::Less,
(EntryType::File, EntryType::Directory) => std::cmp::Ordering::Greater,
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
});
Ok(entries)
}
fn find_file_record(&mut self, cnid: u32) -> Result<HfsFileRecord, FilesystemError> {
let mut current = self.first_leaf_node;
let mut attempts = 0u32;
const MAX: u32 = 10_000;
while current != 0 && attempts < MAX {
attempts += 1;
let node = self.read_node(current)?;
let next = u32::from_be_bytes([node[0], node[1], node[2], node[3]]);
let kind = node[8] as i8;
let num_rec = u16::from_be_bytes([node[10], node[11]]);
if kind != -1 {
current = next;
continue;
}
if let Some(rec) = search_node_for_file(node, self.node_size as usize, num_rec, cnid) {
return Ok(rec);
}
current = next;
}
Err(FilesystemError::NotFound(format!(
"File CNID {cnid} not found in catalog"
)))
}
fn read_extents_range(
&mut self,
extents: &[HfsExtent],
range_offset: u64,
range_length: usize,
) -> Result<Vec<u8>, FilesystemError> {
let first_alloc_offset = self.partition_offset + self.alloc_block_start as u64 * 512;
let mut result = Vec::with_capacity(range_length);
let mut logical_pos: u64 = 0;
let end = range_offset + range_length as u64;
for ext in extents {
if ext.block_count == 0 {
break;
}
let ext_size = ext.block_count as u64 * self.alloc_block_size as u64;
let ext_end = logical_pos + ext_size;
if ext_end <= range_offset {
logical_pos = ext_end;
continue;
}
if logical_pos >= end {
break;
}
let read_start = range_offset.max(logical_pos);
let read_end = end.min(ext_end);
let read_len = (read_end - read_start) as usize;
let offset_in_ext = read_start - logical_pos;
let phys_off = first_alloc_offset
+ ext.start_block as u64 * self.alloc_block_size as u64
+ offset_in_ext;
let chunk = self
.reader
.read_bytes(phys_off, read_len)
.map_err(hfs_disc_err)?;
result.extend_from_slice(&chunk);
logical_pos = ext_end;
}
Ok(result)
}
}
impl Filesystem for HfsFilesystem {
fn root(&mut self) -> Result<FileEntry, FilesystemError> {
Ok(FileEntry::root(HFS_ROOT_DIR_ID as u64))
}
fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
if entry.entry_type != EntryType::Directory {
return Err(FilesystemError::NotADirectory(entry.path.clone()));
}
let cnid = entry.location as u32;
self.list_by_id(cnid, &entry.path)
}
fn read_file(&mut self, entry: &FileEntry) -> Result<Vec<u8>, FilesystemError> {
if entry.entry_type != EntryType::File {
return Err(FilesystemError::NotADirectory(format!(
"{} is not a file",
entry.path
)));
}
let rec = self.find_file_record(entry.location as u32)?;
self.read_extents_range(&rec.data_extents, 0, entry.size as usize)
}
fn read_file_range(
&mut self,
entry: &FileEntry,
offset: u64,
length: usize,
) -> Result<Vec<u8>, FilesystemError> {
if entry.entry_type != EntryType::File {
return Err(FilesystemError::NotADirectory(format!(
"{} is not a file",
entry.path
)));
}
let actual_len = length.min(entry.size.saturating_sub(offset) as usize);
if actual_len == 0 {
return Ok(Vec::new());
}
let rec = self.find_file_record(entry.location as u32)?;
self.read_extents_range(&rec.data_extents, offset, actual_len)
}
fn read_resource_fork(
&mut self,
entry: &FileEntry,
) -> Result<Option<Vec<u8>>, FilesystemError> {
if entry.entry_type != EntryType::File {
return Err(FilesystemError::NotADirectory(format!(
"{} is not a file",
entry.path
)));
}
let rec = self.find_file_record(entry.location as u32)?;
if rec.resource_size == 0 {
return Ok(None);
}
let bytes =
self.read_extents_range(&rec.resource_extents, 0, rec.resource_size as usize)?;
Ok(Some(bytes))
}
fn read_resource_fork_range(
&mut self,
entry: &FileEntry,
offset: u64,
length: usize,
) -> Result<Option<Vec<u8>>, FilesystemError> {
if entry.entry_type != EntryType::File {
return Err(FilesystemError::NotADirectory(format!(
"{} is not a file",
entry.path
)));
}
let rec = self.find_file_record(entry.location as u32)?;
if rec.resource_size == 0 {
return Ok(None);
}
let actual_len = length.min(rec.resource_size.saturating_sub(offset) as usize);
if actual_len == 0 {
return Ok(Some(Vec::new()));
}
let bytes = self.read_extents_range(&rec.resource_extents, offset, actual_len)?;
Ok(Some(bytes))
}
fn volume_name(&self) -> Option<&str> {
if self.volume_name.is_empty() {
None
} else {
Some(&self.volume_name)
}
}
}
#[derive(Debug, Clone, Copy)]
struct HfsExtent {
start_block: u16,
block_count: u16,
}
fn read_extents_into_vec(
reader: &mut dyn SectorReader,
partition_offset: u64,
alloc_block_start: u32,
alloc_block_size: u32,
extents: &[HfsExtent],
total_size: usize,
) -> Result<Vec<u8>, FilesystemError> {
let first_alloc_offset = partition_offset + alloc_block_start as u64 * 512;
let mut data = Vec::with_capacity(total_size);
for ext in extents {
if ext.block_count == 0 || data.len() >= total_size {
break;
}
let phys_off = first_alloc_offset + ext.start_block as u64 * alloc_block_size as u64;
let ext_len = ext.block_count as usize * alloc_block_size as usize;
let want = ext_len.min(total_size - data.len());
let chunk = reader.read_bytes(phys_off, want).map_err(hfs_disc_err)?;
data.extend_from_slice(&chunk);
}
data.truncate(total_size);
Ok(data)
}
#[derive(Debug, Clone)]
struct HfsFileRecord {
data_extents: Vec<HfsExtent>,
resource_extents: Vec<HfsExtent>,
resource_size: u64,
}
#[derive(Debug, Clone)]
struct HfsFileMeta {
finder_flags: u16,
rsrc_extents: Vec<HfsExtent>,
rsrc_size: u64,
}
fn process_leaf_node(
node: &[u8],
node_size: usize,
num_rec: u16,
parent_cnid: u32,
parent_path: &str,
entries: &mut Vec<FileEntry>,
metas: &mut Vec<Option<HfsFileMeta>>,
) {
let offsets_base = node_size - 2;
for i in 0..num_rec {
let off_pos = offsets_base - i as usize * 2;
if off_pos + 2 > node.len() {
continue;
}
let rec_off = u16::from_be_bytes([node[off_pos], node[off_pos + 1]]) as usize;
if rec_off + 8 > node.len() {
continue;
}
let key_len = node[rec_off] as usize;
if key_len < 6 || rec_off + 1 + key_len > node.len() {
continue;
}
let pid = u32::from_be_bytes([
node[rec_off + 2],
node[rec_off + 3],
node[rec_off + 4],
node[rec_off + 5],
]);
if pid != parent_cnid {
continue;
}
let name_len = node[rec_off + 6] as usize;
if name_len == 0 {
continue;
}
let name_start = rec_off + 7;
let name_end = name_start + name_len;
if name_end > node.len() {
continue;
}
let name = mac_roman_to_string(&node[name_start..name_end]);
let data_off = rec_off + ((key_len + 2) & !1usize);
if data_off + 2 > node.len() {
continue;
}
let rec_type = node[data_off] as i8;
let path = if parent_path == "/" {
format!("/{name}")
} else {
format!("{parent_path}/{name}")
};
match rec_type {
HFS_FOLDER_RECORD => {
if data_off + 10 > node.len() {
continue;
}
let dir_id = u32::from_be_bytes([
node[data_off + 6],
node[data_off + 7],
node[data_off + 8],
node[data_off + 9],
]);
entries.push(FileEntry::new_directory(name, path, dir_id as u64));
metas.push(None);
}
HFS_FILE_RECORD => {
if data_off + 98 > node.len() {
continue;
}
let type_code = [
node[data_off + 4],
node[data_off + 5],
node[data_off + 6],
node[data_off + 7],
];
let creator_code = [
node[data_off + 8],
node[data_off + 9],
node[data_off + 10],
node[data_off + 11],
];
let file_id = u32::from_be_bytes([
node[data_off + 20],
node[data_off + 21],
node[data_off + 22],
node[data_off + 23],
]);
let data_logical_size = u32::from_be_bytes([
node[data_off + 26],
node[data_off + 27],
node[data_off + 28],
node[data_off + 29],
]);
let rsrc_logical_size = u32::from_be_bytes([
node[data_off + 36],
node[data_off + 37],
node[data_off + 38],
node[data_off + 39],
]);
entries.push(FileEntry::new_hfs_file(
name,
path,
data_logical_size as u64,
file_id as u64,
rsrc_logical_size as u64,
type_code,
creator_code,
));
let finder_flags = u16::from_be_bytes([node[data_off + 12], node[data_off + 13]]);
let mut rsrc_extents = Vec::with_capacity(3);
for j in 0..3 {
let base = data_off + 86 + j * 4;
rsrc_extents.push(HfsExtent {
start_block: u16::from_be_bytes([node[base], node[base + 1]]),
block_count: u16::from_be_bytes([node[base + 2], node[base + 3]]),
});
}
metas.push(Some(HfsFileMeta {
finder_flags,
rsrc_extents,
rsrc_size: rsrc_logical_size as u64,
}));
}
_ => {}
}
}
}
fn search_node_for_file(
node: &[u8],
node_size: usize,
num_rec: u16,
target_cnid: u32,
) -> Option<HfsFileRecord> {
let offsets_base = node_size - 2;
for i in 0..num_rec {
let off_pos = offsets_base - i as usize * 2;
if off_pos + 2 > node.len() {
continue;
}
let rec_off = u16::from_be_bytes([node[off_pos], node[off_pos + 1]]) as usize;
if rec_off + 8 > node.len() {
continue;
}
let key_len = node[rec_off] as usize;
if key_len < 6 {
continue;
}
let data_off = rec_off + ((key_len + 2) & !1usize);
if data_off + 2 > node.len() {
continue;
}
let rec_type = node[data_off] as i8;
if rec_type != HFS_FILE_RECORD {
continue;
}
if data_off + 98 > node.len() {
continue;
}
let file_id = u32::from_be_bytes([
node[data_off + 20],
node[data_off + 21],
node[data_off + 22],
node[data_off + 23],
]);
if file_id != target_cnid {
continue;
}
let resource_size = u32::from_be_bytes([
node[data_off + 36],
node[data_off + 37],
node[data_off + 38],
node[data_off + 39],
]) as u64;
let read_three_extents = |base_off: usize| -> Vec<HfsExtent> {
(0..3)
.map(|j| {
let base = base_off + j * 4;
HfsExtent {
start_block: u16::from_be_bytes([node[base], node[base + 1]]),
block_count: u16::from_be_bytes([node[base + 2], node[base + 3]]),
}
})
.collect()
};
let data_extents = read_three_extents(data_off + 74);
let resource_extents = read_three_extents(data_off + 86);
return Some(HfsFileRecord {
data_extents,
resource_extents,
resource_size,
});
}
None
}
fn hfs_disc_err(e: crate::error::OpticaldiscsError) -> FilesystemError {
match e {
crate::error::OpticaldiscsError::Io(io) => FilesystemError::Io(io),
e => FilesystemError::Parse(e.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hfs_extent_fields() {
let ext = HfsExtent {
start_block: 10,
block_count: 5,
};
assert_eq!(ext.start_block, 10);
assert_eq!(ext.block_count, 5);
}
#[test]
fn process_leaf_node_empty_returns_nothing() {
let node = vec![0u8; 512];
let mut entries = Vec::new();
let mut metas = Vec::new();
process_leaf_node(&node, 512, 0, 2, "/", &mut entries, &mut metas);
assert!(entries.is_empty());
}
#[test]
fn process_leaf_node_wrong_parent_skips() {
let mut node = vec![0u8; 512];
let rec_off: u16 = 14;
node[510] = (rec_off >> 8) as u8;
node[511] = (rec_off & 0xFF) as u8;
let key_len: u8 = 7;
node[14] = key_len;
node[15] = 0; node[16..20].copy_from_slice(&99u32.to_be_bytes()); node[20] = 1; node[21] = b'A';
let mut entries = Vec::new();
let mut metas = Vec::new();
process_leaf_node(&node, 512, 1, 2, "/", &mut entries, &mut metas);
assert!(entries.is_empty());
}
#[test]
fn process_leaf_node_folder_record() {
let mut node = vec![0u8; 512];
let rec_off: u16 = 14;
node[510] = (rec_off >> 8) as u8;
node[511] = (rec_off & 0xFF) as u8;
node[14] = 7; node[15] = 0; node[16..20].copy_from_slice(&2u32.to_be_bytes()); node[20] = 1; node[21] = b'A'; let data_off = 22usize;
node[data_off] = 1; node[data_off + 6..data_off + 10].copy_from_slice(&42u32.to_be_bytes());
let mut entries = Vec::new();
let mut metas = Vec::new();
process_leaf_node(&node, 512, 1, 2, "/", &mut entries, &mut metas);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "A");
assert_eq!(entries[0].path, "/A");
assert!(entries[0].is_directory());
assert_eq!(entries[0].location, 42);
}
#[test]
fn process_leaf_node_file_record() {
let mut node = vec![0u8; 512];
let rec_off: u16 = 14;
node[510] = (rec_off >> 8) as u8;
node[511] = (rec_off & 0xFF) as u8;
node[14] = 9; node[15] = 0;
node[16..20].copy_from_slice(&2u32.to_be_bytes()); node[20] = 3; node[21..24].copy_from_slice(b"foo"); let data_off = 24usize;
node[data_off] = 2; node[data_off + 4..data_off + 8].copy_from_slice(b"TEXT");
node[data_off + 8..data_off + 12].copy_from_slice(b"ttxt");
node[data_off + 20..data_off + 24].copy_from_slice(&77u32.to_be_bytes());
node[data_off + 26..data_off + 30].copy_from_slice(&1024u32.to_be_bytes());
node[data_off + 36..data_off + 40].copy_from_slice(&256u32.to_be_bytes());
let mut entries = Vec::new();
let mut metas = Vec::new();
process_leaf_node(&node, 512, 1, 2, "/", &mut entries, &mut metas);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "foo");
assert!(entries[0].is_file());
assert_eq!(entries[0].size, 1024);
assert_eq!(entries[0].location, 77);
assert_eq!(entries[0].resource_fork_size, Some(256));
assert_eq!(entries[0].type_code.as_deref(), Some("TEXT"));
assert_eq!(entries[0].creator_code.as_deref(), Some("ttxt"));
}
#[test]
fn search_node_for_file_returns_both_forks() {
let mut node = vec![0u8; 512];
let rec_off: u16 = 14;
node[510] = (rec_off >> 8) as u8;
node[511] = (rec_off & 0xFF) as u8;
node[14] = 9; node[15] = 0;
node[16..20].copy_from_slice(&2u32.to_be_bytes());
node[20] = 3;
node[21..24].copy_from_slice(b"bar");
let data_off = 24usize;
node[data_off] = 2; node[data_off + 20..data_off + 24].copy_from_slice(&42u32.to_be_bytes()); node[data_off + 36..data_off + 40].copy_from_slice(&100u32.to_be_bytes()); node[data_off + 74..data_off + 76].copy_from_slice(&10u16.to_be_bytes());
node[data_off + 76..data_off + 78].copy_from_slice(&2u16.to_be_bytes());
node[data_off + 86..data_off + 88].copy_from_slice(&30u16.to_be_bytes());
node[data_off + 88..data_off + 90].copy_from_slice(&1u16.to_be_bytes());
let rec = search_node_for_file(&node, 512, 1, 42).expect("record found");
assert_eq!(rec.resource_size, 100);
assert_eq!(rec.data_extents[0].start_block, 10);
assert_eq!(rec.data_extents[0].block_count, 2);
assert_eq!(rec.resource_extents[0].start_block, 30);
assert_eq!(rec.resource_extents[0].block_count, 1);
}
}