use crate::error::{OpticaldiscsError, Result};
use crate::sector_reader::SectorReader;
pub(crate) const HFSPLUS_SIGNATURE: u16 = 0x482B;
pub(crate) const HFSX_SIGNATURE: u16 = 0x4858;
const HEADER_OFFSET: u64 = 1024;
const HFSPLUS_FOLDER_THREAD_RECORD: i16 = 3;
const HFSPLUS_ROOT_PARENT_ID: u32 = 1;
#[derive(Debug, Clone)]
pub struct HfsPlusVolumeHeader {
pub signature: u16,
pub version: u16,
pub block_size: u32,
pub total_blocks: u32,
pub free_blocks: u32,
pub file_count: u32,
pub folder_count: u32,
pub catalog_start_block: u32,
pub catalog_block_count: u32,
}
impl HfsPlusVolumeHeader {
pub fn read_from(reader: &mut dyn SectorReader, partition_offset: u64) -> Result<Self> {
let hdr = reader.read_bytes(partition_offset + HEADER_OFFSET, 512)?;
let signature = u16::from_be_bytes([hdr[0], hdr[1]]);
if signature != HFSPLUS_SIGNATURE && signature != HFSX_SIGNATURE {
return Err(OpticaldiscsError::Parse(format!(
"Invalid HFS+ volume header signature: 0x{signature:04X}"
)));
}
let version = u16::from_be_bytes([hdr[2], hdr[3]]);
let file_count = u32::from_be_bytes([hdr[32], hdr[33], hdr[34], hdr[35]]);
let folder_count = u32::from_be_bytes([hdr[36], hdr[37], hdr[38], hdr[39]]);
let block_size = u32::from_be_bytes([hdr[40], hdr[41], hdr[42], hdr[43]]);
let total_blocks = u32::from_be_bytes([hdr[44], hdr[45], hdr[46], hdr[47]]);
let free_blocks = u32::from_be_bytes([hdr[48], hdr[49], hdr[50], hdr[51]]);
let catalog_start_block = u32::from_be_bytes([hdr[288], hdr[289], hdr[290], hdr[291]]);
let catalog_block_count = u32::from_be_bytes([hdr[292], hdr[293], hdr[294], hdr[295]]);
Ok(Self {
signature,
version,
block_size,
total_blocks,
free_blocks,
file_count,
folder_count,
catalog_start_block,
catalog_block_count,
})
}
}
pub fn extract_volume_name_from_catalog(
reader: &mut dyn SectorReader,
partition_offset: u64,
) -> Result<Option<String>> {
let header = HfsPlusVolumeHeader::read_from(reader, partition_offset)?;
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)?;
let node_kind = btree_hdr[8] as i8;
if node_kind != 1 {
return Err(OpticaldiscsError::Parse(format!(
"Expected B-tree header node (kind 1), got kind {node_kind}"
)));
}
let first_leaf =
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]]) as u64;
let mut current = first_leaf;
let mut attempts = 0u32;
const MAX: u32 = 10_000;
while current != 0 && attempts < MAX {
attempts += 1;
let node_off = catalog_offset + current as u64 * node_size;
let node = reader.read_bytes(node_off, node_size as usize)?;
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;
}
let offsets_base = node_size as usize - 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],
]);
let name_len = u16::from_be_bytes([node[rec_off + 6], node[rec_off + 7]]) as usize;
if parent_id != HFSPLUS_ROOT_PARENT_ID || name_len != 0 {
continue;
}
let data_off = rec_off + 2 + key_len;
if data_off + 14 > node.len() {
continue;
}
let rec_type = i16::from_be_bytes([node[data_off], node[data_off + 1]]);
if rec_type != HFSPLUS_FOLDER_THREAD_RECORD {
continue;
}
let vol_name_len =
u16::from_be_bytes([node[data_off + 12], node[data_off + 13]]) as usize;
let name_start = data_off + 14;
let name_end = name_start + vol_name_len * 2;
if name_end > node.len() {
continue;
}
let utf16: Vec<u16> = node[name_start..name_end]
.chunks(2)
.filter_map(|ch| {
if ch.len() == 2 {
Some(u16::from_be_bytes([ch[0], ch[1]]))
} else {
None
}
})
.collect();
return Ok(String::from_utf16(&utf16).ok());
}
current = next;
}
Ok(None)
}
#[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_header_image(block_size: u32, catalog_start: u32, catalog_count: u32) -> Vec<u8> {
let mut img = vec![0u8; 3 * SECTOR_SIZE as usize];
let off = 1024usize;
img[off] = 0x48;
img[off + 1] = 0x2B; img[off + 2..off + 4].copy_from_slice(&4u16.to_be_bytes()); img[off + 40..off + 44].copy_from_slice(&block_size.to_be_bytes());
img[off + 44..off + 48].copy_from_slice(&1000u32.to_be_bytes()); img[off + 48..off + 52].copy_from_slice(&500u32.to_be_bytes()); img[off + 288..off + 292].copy_from_slice(&catalog_start.to_be_bytes());
img[off + 292..off + 296].copy_from_slice(&catalog_count.to_be_bytes());
img
}
#[test]
fn read_volume_header_fields() {
let img = make_header_image(4096, 10, 20);
let mut reader = CursorReader(Cursor::new(img));
let vh = HfsPlusVolumeHeader::read_from(&mut reader, 0).unwrap();
assert_eq!(vh.signature, HFSPLUS_SIGNATURE);
assert_eq!(vh.version, 4);
assert_eq!(vh.block_size, 4096);
assert_eq!(vh.total_blocks, 1000);
assert_eq!(vh.free_blocks, 500);
assert_eq!(vh.catalog_start_block, 10);
assert_eq!(vh.catalog_block_count, 20);
}
#[test]
fn hfsx_signature_accepted() {
let mut img = make_header_image(4096, 0, 0);
img[1024] = 0x48;
img[1025] = 0x58; let mut reader = CursorReader(Cursor::new(img));
let vh = HfsPlusVolumeHeader::read_from(&mut reader, 0).unwrap();
assert_eq!(vh.signature, HFSX_SIGNATURE);
}
#[test]
fn wrong_signature_is_parse_error() {
let mut img = make_header_image(4096, 0, 0);
img[1024] = 0xFF; let mut reader = CursorReader(Cursor::new(img));
assert!(matches!(
HfsPlusVolumeHeader::read_from(&mut reader, 0),
Err(OpticaldiscsError::Parse(_))
));
}
}