use super::entry::{EntryType, FileEntry};
use super::filesystem::{Filesystem, FilesystemError};
use crate::hfsplus::{extract_volume_name_from_catalog, HfsPlusVolumeHeader};
use crate::sector_reader::SectorReader;
const HFSPLUS_FOLDER_RECORD: i16 = 1;
const HFSPLUS_FILE_RECORD: i16 = 2;
const HFSPLUS_ROOT_FOLDER_ID: u32 = 2;
pub struct HfsPlusFilesystem {
reader: Box<dyn SectorReader>,
partition_offset: u64,
block_size: u32,
catalog_start_block: u32,
node_size: u16,
first_leaf_node: u32,
volume_name: String,
}
impl HfsPlusFilesystem {
pub fn new(
mut reader: Box<dyn SectorReader>,
partition_offset: u64,
) -> Result<Self, FilesystemError> {
let header = HfsPlusVolumeHeader::read_from(reader.as_mut(), partition_offset)
.map_err(hfsplus_err)?;
let catalog_offset =
partition_offset + header.catalog_start_block as u64 * header.block_size as u64;
let btree_hdr = reader
.read_bytes(catalog_offset, 256)
.map_err(hfsplus_err)?;
let node_kind = btree_hdr[8] as i8;
if node_kind != 1 {
return Err(FilesystemError::Parse(format!(
"Expected B-tree header node (kind 1), got {node_kind}"
)));
}
let first_leaf_node =
u32::from_be_bytes([btree_hdr[24], btree_hdr[25], btree_hdr[26], btree_hdr[27]]);
let node_size = u16::from_be_bytes([btree_hdr[32], btree_hdr[33]]);
let volume_name = extract_volume_name_from_catalog(reader.as_mut(), partition_offset)
.unwrap_or(None)
.unwrap_or_else(|| "HFS+ Volume".to_string());
Ok(Self {
reader,
partition_offset,
block_size: header.block_size,
catalog_start_block: header.catalog_start_block,
node_size,
first_leaf_node,
volume_name,
})
}
fn catalog_offset(&self) -> u64 {
self.partition_offset + self.catalog_start_block as u64 * self.block_size as u64
}
fn read_node(&mut self, node_num: u32) -> Result<Vec<u8>, FilesystemError> {
let offset = self.catalog_offset() + node_num as u64 * self.node_size as u64;
self.reader
.read_bytes(offset, self.node_size as usize)
.map_err(hfsplus_err)
}
fn list_by_cnid(
&mut self,
parent_cnid: u32,
parent_path: &str,
) -> Result<Vec<FileEntry>, FilesystemError> {
let mut entries: Vec<FileEntry> = Vec::new();
let mut metas: Vec<Option<HfsPlusFileMeta>> = 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() {
let meta = match meta {
Some(m) => m,
None => continue,
};
let entry = &mut entries[i];
let is_slnk = entry.type_code.as_deref() == Some(super::mac_alias::SLNK_TYPE)
&& entry.creator_code.as_deref() == Some(super::mac_alias::RHAP_CREATOR);
if is_slnk && meta.data_fork.logical_size > 0 && meta.data_fork.logical_size <= 4096 {
let len = meta.data_fork.logical_size as usize;
if let Ok(data) = self.read_fork_range(&meta.data_fork, 0, len) {
if let Ok(s) = std::str::from_utf8(&data) {
let trimmed = s.trim_end_matches('\0').trim();
if !trimmed.is_empty() {
entry.symlink_target = Some(trimmed.to_string());
}
}
}
}
if entry.symlink_target.is_none()
&& meta.finder_flags & super::mac_alias::IS_ALIAS_FLAG != 0
&& meta.resource_fork.logical_size > 0
{
let len = meta.resource_fork.logical_size as usize;
if let Ok(rsrc) = self.read_fork_range(&meta.resource_fork, 0, len) {
if let Some(target) = super::mac_alias::resolve_alias_target(&rsrc) {
entry.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<HfsPlusFileRecord, 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"
)))
}
fn read_fork_range(
&mut self,
fork: &HfsPlusForkData,
range_offset: u64,
range_length: usize,
) -> Result<Vec<u8>, FilesystemError> {
let end = (range_offset + range_length as u64).min(fork.logical_size);
if range_offset >= end {
return Ok(Vec::new());
}
let mut result = Vec::with_capacity((end - range_offset) as usize);
let mut logical_pos: u64 = 0;
for ext in &fork.extents {
if ext.block_count == 0 {
break;
}
let ext_size = ext.block_count as u64 * self.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 = self.partition_offset
+ ext.start_block as u64 * self.block_size as u64
+ offset_in_ext;
let chunk = self
.reader
.read_bytes(phys_off, read_len)
.map_err(hfsplus_err)?;
result.extend_from_slice(&chunk);
logical_pos = ext_end;
}
Ok(result)
}
}
impl Filesystem for HfsPlusFilesystem {
fn root(&mut self) -> Result<FileEntry, FilesystemError> {
Ok(FileEntry::root(HFSPLUS_ROOT_FOLDER_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_cnid(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)?;
if rec.data_fork.extents.is_empty() {
return Ok(Vec::new());
}
self.read_fork_range(&rec.data_fork, 0, rec.data_fork.logical_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 rec = self.find_file_record(entry.location as u32)?;
if rec.data_fork.extents.is_empty() {
return Ok(Vec::new());
}
let actual_len = length.min(rec.data_fork.logical_size.saturating_sub(offset) as usize);
if actual_len == 0 {
return Ok(Vec::new());
}
self.read_fork_range(&rec.data_fork, 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_fork.logical_size == 0 || rec.resource_fork.extents.is_empty() {
return Ok(None);
}
let bytes = self.read_fork_range(
&rec.resource_fork,
0,
rec.resource_fork.logical_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_fork.logical_size == 0 || rec.resource_fork.extents.is_empty() {
return Ok(None);
}
let actual_len = length.min(rec.resource_fork.logical_size.saturating_sub(offset) as usize);
if actual_len == 0 {
return Ok(Some(Vec::new()));
}
let bytes = self.read_fork_range(&rec.resource_fork, 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 HfsPlusExtent {
start_block: u32,
block_count: u32,
}
#[derive(Debug, Clone)]
struct HfsPlusForkData {
logical_size: u64,
extents: Vec<HfsPlusExtent>,
}
#[derive(Debug, Clone)]
struct HfsPlusFileRecord {
data_fork: HfsPlusForkData,
resource_fork: HfsPlusForkData,
}
#[derive(Debug, Clone)]
struct HfsPlusFileMeta {
finder_flags: u16,
data_fork: HfsPlusForkData,
resource_fork: HfsPlusForkData,
}
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<HfsPlusFileMeta>>,
) {
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 + 10 > node.len() {
continue;
}
let key_len = u16::from_be_bytes([node[rec_off], node[rec_off + 1]]) as usize;
if key_len < 6 {
continue;
}
let parent_id = u32::from_be_bytes([
node[rec_off + 2],
node[rec_off + 3],
node[rec_off + 4],
node[rec_off + 5],
]);
if parent_id != parent_cnid {
continue;
}
let name_len = u16::from_be_bytes([node[rec_off + 6], node[rec_off + 7]]) as usize;
if name_len == 0 {
continue;
}
let name_start = rec_off + 8;
let name_end = name_start + name_len * 2;
if name_end > node.len() {
continue;
}
let name = decode_utf16_be(&node[name_start..name_end]);
if name.is_empty() {
continue;
}
let data_off = rec_off + 2 + key_len;
if data_off + 4 > node.len() {
continue;
}
let rec_type = i16::from_be_bytes([node[data_off], node[data_off + 1]]);
let path = if parent_path == "/" {
format!("/{name}")
} else {
format!("{parent_path}/{name}")
};
match rec_type {
HFSPLUS_FOLDER_RECORD => {
if data_off + 12 > node.len() {
continue;
}
let cnid = u32::from_be_bytes([
node[data_off + 8],
node[data_off + 9],
node[data_off + 10],
node[data_off + 11],
]);
entries.push(FileEntry::new_directory(name, path, cnid as u64));
metas.push(None);
}
HFSPLUS_FILE_RECORD => {
if data_off + 248 > node.len() {
continue;
}
let cnid = u32::from_be_bytes([
node[data_off + 8],
node[data_off + 9],
node[data_off + 10],
node[data_off + 11],
]);
let type_code = [
node[data_off + 48],
node[data_off + 49],
node[data_off + 50],
node[data_off + 51],
];
let creator_code = [
node[data_off + 52],
node[data_off + 53],
node[data_off + 54],
node[data_off + 55],
];
let data_size = u64::from_be_bytes([
node[data_off + 88],
node[data_off + 89],
node[data_off + 90],
node[data_off + 91],
node[data_off + 92],
node[data_off + 93],
node[data_off + 94],
node[data_off + 95],
]);
let rsrc_size = u64::from_be_bytes([
node[data_off + 168],
node[data_off + 169],
node[data_off + 170],
node[data_off + 171],
node[data_off + 172],
node[data_off + 173],
node[data_off + 174],
node[data_off + 175],
]);
entries.push(FileEntry::new_hfs_file(
name,
path,
data_size,
cnid as u64,
rsrc_size,
type_code,
creator_code,
));
let finder_flags = u16::from_be_bytes([node[data_off + 56], node[data_off + 57]]);
let data_fork = parse_fork(node, data_off + 88);
let resource_fork = parse_fork(node, data_off + 168);
metas.push(Some(HfsPlusFileMeta {
finder_flags,
data_fork,
resource_fork,
}));
}
_ => {}
}
}
}
fn search_node_for_file(
node: &[u8],
node_size: usize,
num_rec: u16,
target_cnid: u32,
) -> Option<HfsPlusFileRecord> {
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 + 10 > node.len() {
continue;
}
let key_len = u16::from_be_bytes([node[rec_off], node[rec_off + 1]]) as usize;
if key_len < 6 {
continue;
}
let data_off = rec_off + 2 + key_len;
if data_off + 248 > node.len() {
continue;
}
let rec_type = i16::from_be_bytes([node[data_off], node[data_off + 1]]);
if rec_type != HFSPLUS_FILE_RECORD {
continue;
}
let cnid = u32::from_be_bytes([
node[data_off + 8],
node[data_off + 9],
node[data_off + 10],
node[data_off + 11],
]);
if cnid != target_cnid {
continue;
}
let data_fork = parse_fork(node, data_off + 88);
let resource_fork = parse_fork(node, data_off + 168);
return Some(HfsPlusFileRecord {
data_fork,
resource_fork,
});
}
None
}
fn parse_fork(node: &[u8], fork_off: usize) -> HfsPlusForkData {
let logical_size = u64::from_be_bytes([
node[fork_off],
node[fork_off + 1],
node[fork_off + 2],
node[fork_off + 3],
node[fork_off + 4],
node[fork_off + 5],
node[fork_off + 6],
node[fork_off + 7],
]);
let mut extents = Vec::new();
for j in 0..8usize {
let ext_off = fork_off + 16 + j * 8;
if ext_off + 8 > node.len() {
break;
}
let start_block = u32::from_be_bytes([
node[ext_off],
node[ext_off + 1],
node[ext_off + 2],
node[ext_off + 3],
]);
let block_count = u32::from_be_bytes([
node[ext_off + 4],
node[ext_off + 5],
node[ext_off + 6],
node[ext_off + 7],
]);
if block_count == 0 {
break;
}
extents.push(HfsPlusExtent {
start_block,
block_count,
});
}
HfsPlusForkData {
logical_size,
extents,
}
}
fn decode_utf16_be(bytes: &[u8]) -> String {
let utf16: Vec<u16> = bytes
.chunks(2)
.filter_map(|ch| {
if ch.len() == 2 {
Some(u16::from_be_bytes([ch[0], ch[1]]))
} else {
None
}
})
.collect();
String::from_utf16(&utf16).unwrap_or_default()
}
fn hfsplus_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 decode_utf16_be_ascii() {
let bytes = [0x00, 0x41, 0x00, 0x42];
assert_eq!(decode_utf16_be(&bytes), "AB");
}
#[test]
fn decode_utf16_be_empty() {
assert_eq!(decode_utf16_be(&[]), "");
}
#[test]
fn process_leaf_node_empty() {
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());
}
struct FileRecordSpec<'a> {
node_size: usize,
parent_id: u32,
name: &'a str,
cnid: u32,
data_size: u64,
rsrc_size: u64,
type_code: [u8; 4],
creator_code: [u8; 4],
}
fn build_file_record_node(spec: &FileRecordSpec) -> (Vec<u8>, usize) {
let mut node = vec![0u8; spec.node_size];
let rec_off: u16 = 14;
node[spec.node_size - 2] = (rec_off >> 8) as u8;
node[spec.node_size - 1] = (rec_off & 0xFF) as u8;
let name_chars: Vec<u16> = spec.name.encode_utf16().collect();
let name_len = name_chars.len();
let key_len: u16 = 4 + 2 + (name_len as u16) * 2;
node[14..16].copy_from_slice(&key_len.to_be_bytes());
node[16..20].copy_from_slice(&spec.parent_id.to_be_bytes());
node[20..22].copy_from_slice(&(name_len as u16).to_be_bytes());
for (i, &ch) in name_chars.iter().enumerate() {
let off = 22 + i * 2;
node[off..off + 2].copy_from_slice(&ch.to_be_bytes());
}
let data_off = 14 + 2 + key_len as usize;
node[data_off..data_off + 2].copy_from_slice(&2i16.to_be_bytes());
node[data_off + 8..data_off + 12].copy_from_slice(&spec.cnid.to_be_bytes());
node[data_off + 48..data_off + 52].copy_from_slice(&spec.type_code);
node[data_off + 52..data_off + 56].copy_from_slice(&spec.creator_code);
node[data_off + 88..data_off + 96].copy_from_slice(&spec.data_size.to_be_bytes());
node[data_off + 168..data_off + 176].copy_from_slice(&spec.rsrc_size.to_be_bytes());
(node, data_off)
}
#[test]
fn process_leaf_node_file_record_populates_hfs_metadata() {
let (node, _) = build_file_record_node(&FileRecordSpec {
node_size: 2048,
parent_id: 2,
name: "note",
cnid: 77,
data_size: 1024,
rsrc_size: 512,
type_code: *b"TEXT",
creator_code: *b"ttxt",
});
let mut entries = Vec::new();
let mut metas = Vec::new();
process_leaf_node(&node, 2048, 1, 2, "/", &mut entries, &mut metas);
assert_eq!(entries.len(), 1);
let e = &entries[0];
assert_eq!(e.name, "note");
assert!(e.is_file());
assert_eq!(e.size, 1024);
assert_eq!(e.location, 77);
assert_eq!(e.resource_fork_size, Some(512));
assert_eq!(e.type_code.as_deref(), Some("TEXT"));
assert_eq!(e.creator_code.as_deref(), Some("ttxt"));
}
#[test]
fn search_node_for_file_returns_both_forks() {
let (mut node, data_off) = build_file_record_node(&FileRecordSpec {
node_size: 2048,
parent_id: 2,
name: "bar",
cnid: 42,
data_size: 1024,
rsrc_size: 256,
type_code: *b"TEXT",
creator_code: *b"ttxt",
});
let dfe = data_off + 88 + 16;
node[dfe..dfe + 4].copy_from_slice(&10u32.to_be_bytes());
node[dfe + 4..dfe + 8].copy_from_slice(&2u32.to_be_bytes());
let rfe = data_off + 168 + 16;
node[rfe..rfe + 4].copy_from_slice(&30u32.to_be_bytes());
node[rfe + 4..rfe + 8].copy_from_slice(&1u32.to_be_bytes());
let rec = search_node_for_file(&node, 2048, 1, 42).expect("record found");
assert_eq!(rec.data_fork.logical_size, 1024);
assert_eq!(rec.data_fork.extents[0].start_block, 10);
assert_eq!(rec.data_fork.extents[0].block_count, 2);
assert_eq!(rec.resource_fork.logical_size, 256);
assert_eq!(rec.resource_fork.extents[0].start_block, 30);
assert_eq!(rec.resource_fork.extents[0].block_count, 1);
}
#[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;
let key_len: u16 = 8;
node[14..16].copy_from_slice(&key_len.to_be_bytes()); node[16..20].copy_from_slice(&2u32.to_be_bytes()); node[20..22].copy_from_slice(&1u16.to_be_bytes()); node[22..24].copy_from_slice(&[0x00, 0x41]); let data_off = 24usize;
node[data_off..data_off + 2].copy_from_slice(&1i16.to_be_bytes());
node[data_off + 8..data_off + 12].copy_from_slice(&55u32.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!(entries[0].is_directory());
assert_eq!(entries[0].location, 55);
}
}