use byteorder::{BigEndian, ByteOrder};
#[cfg(feature = "rust173-polyfill")]
use disk_core::rust173::IntIsMultipleOf as _;
use std::cmp::Ordering;
use std::io::{Read, Seek, SeekFrom, Write};
use crate::hfs_common::{
self, bitmap_clear_bit_be, bitmap_collect_clear_runs_be, bitmap_find_clear_run_be,
bitmap_set_bit_be, bitmap_test_bit_be, btree_free_node, btree_grow_blocks,
btree_map_chain_tail, btree_refresh_index_keys, btree_remove_record, BTreeHeader,
BTreeKeyFormat,
};
use disk_core::entry::{EntryType, FileEntry};
use disk_core::filesystem::CompactResult;
use disk_core::filesystem::{
CreateDirectoryOptions, CreateFileOptions, EditableFilesystem, Filesystem, FilesystemError,
};
const HFS_SIGNATURE: u16 = 0x4244;
const HFS_PLUS_EMBEDDED_SIGNATURE: u16 = 0x482B;
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{00F5}', '\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}',
];
pub fn utf8_to_mac_roman(s: &str) -> Result<Vec<u8>, FilesystemError> {
let mut result = Vec::with_capacity(s.len());
for ch in s.chars() {
if (ch as u32) < 0x80 {
result.push(ch as u8);
} else {
let mut found = false;
for (i, &table_char) in MAC_ROMAN_TABLE.iter().enumerate() {
if table_char == ch {
result.push((i as u8) + 0x80);
found = true;
break;
}
}
if !found {
return Err(FilesystemError::InvalidData(format!(
"character '{}' (U+{:04X}) cannot be encoded in Mac Roman",
ch, ch as u32
)));
}
}
}
Ok(result)
}
pub fn mac_roman_to_utf8(data: &[u8]) -> String {
data.iter()
.map(|&b| {
if b < 0x80 {
b as char
} else {
MAC_ROMAN_TABLE[(b - 0x80) as usize]
}
})
.collect()
}
pub fn decode_mac_filename(data: &[u8]) -> String {
if data.iter().any(|&b| b >= 0x80) {
if let Ok(s) = std::str::from_utf8(data) {
return s.to_string();
}
}
mac_roman_to_utf8(data)
}
pub fn format_ostype(code: &[u8; 4]) -> String {
code.iter()
.map(|&b| match b {
0x00..=0x1f => ' ',
b if b < 0x80 => b as char,
b => MAC_ROMAN_TABLE[(b - 0x80) as usize],
})
.collect::<String>()
.trim_end()
.to_string()
}
fn validate_hfs_create_name(name: &str) -> Result<Vec<u8>, FilesystemError> {
if name.is_empty() {
return Err(FilesystemError::InvalidData(
"filename is empty — pick a non-blank name".into(),
));
}
if name.contains(':') {
return Err(FilesystemError::InvalidData(
"filename contains ':', which classic Mac OS uses as a path separator — \
rename the file (try '-' or '_' instead)"
.into(),
));
}
let name_bytes = utf8_to_mac_roman(name).map_err(|_| {
FilesystemError::InvalidData(
"filename contains characters that can't be encoded in Mac Roman \
(HFS predates Unicode) — rename using plain ASCII or common accented letters"
.into(),
)
})?;
if name_bytes.is_empty() {
return Err(FilesystemError::InvalidData(
"filename is empty — pick a non-blank name".into(),
));
}
if name_bytes.len() > 31 {
return Err(FilesystemError::InvalidData(format!(
"filename is too long ({} chars); classic HFS allows up to 31 — shorten the name",
name_bytes.len()
)));
}
Ok(name_bytes)
}
fn hfs_fragmentation_stats(catalog_data: &[u8]) -> disk_core::filesystem::FragmentationStats {
use crate::hfs_common::walk_leaf_records;
let mut stats = disk_core::filesystem::FragmentationStats::default();
if catalog_data.len() < 34 {
return stats;
}
let node_size = BigEndian::read_u16(&catalog_data[32..34]) as usize;
let first_leaf = BigEndian::read_u32(&catalog_data[24..28]);
if node_size == 0 || first_leaf == 0 {
return stats;
}
walk_leaf_records::<(), _>(
catalog_data,
first_leaf,
node_size,
|_node_idx, _rec_idx, _abs_off, rec| {
if rec.is_empty() {
return None;
}
let key_len = rec[0] as usize;
let mut data_off = 1 + key_len;
if !data_off.is_multiple_of(2) {
data_off += 1;
}
if data_off + 102 > rec.len() {
return None;
}
if rec[data_off] as i8 != CATALOG_FILE {
return None;
}
let body = &rec[data_off..];
let phys_bytes = BigEndian::read_u32(&body[30..34]);
if phys_bytes == 0 {
return None;
}
let mut inline_extents: u32 = 0;
let mut inline_blocks: u32 = 0;
for j in 0..3 {
let count = BigEndian::read_u16(&body[74 + j * 4 + 2..74 + j * 4 + 4]);
if count > 0 {
inline_extents += 1;
inline_blocks = inline_blocks.saturating_add(count as u32);
}
}
stats.files_with_data += 1;
let overflows = inline_blocks == 0 && phys_bytes > 0;
if inline_extents > 1 || overflows {
stats.fragmented_files += 1;
}
None
},
);
stats
}
fn is_catalog_uninitialized(catalog_data: &[u8]) -> bool {
if catalog_data.len() < 34 {
return true;
}
BigEndian::read_u16(&catalog_data[32..34]) == 0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HfsTree {
Catalog,
Extents,
}
impl HfsTree {
fn label(self) -> &'static str {
match self {
HfsTree::Catalog => "catalog",
HfsTree::Extents => "extents-overflow",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct HfsExtDescriptor {
pub start_block: u16,
pub block_count: u16,
}
#[derive(Debug, Clone, Copy)]
pub struct HfsFileLocation {
pub offset_in_image: u64,
pub length: u64,
pub fragmented: bool,
}
#[derive(Debug, Clone)]
pub struct HfsVolumeSummary {
pub volume_name: String,
pub block_size: u32,
pub total_blocks: u16,
pub free_blocks: u16,
pub used_bytes: u64,
pub file_count: u32,
pub folder_count: u32,
pub catalog_file_size: u32,
pub extents_file_size: u32,
}
impl HfsExtDescriptor {
pub fn parse(data: &[u8]) -> Self {
HfsExtDescriptor {
start_block: BigEndian::read_u16(&data[0..2]),
block_count: BigEndian::read_u16(&data[2..4]),
}
}
fn serialize(&self, out: &mut [u8]) {
BigEndian::write_u16(&mut out[0..2], self.start_block);
BigEndian::write_u16(&mut out[2..4], self.block_count);
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct HfsMasterDirectoryBlock {
pub(crate) signature: u16,
pub(crate) create_date: u32,
pub(crate) modify_date: u32,
pub(crate) total_blocks: u16,
pub(crate) block_size: u32,
pub(crate) free_blocks: u16,
pub(crate) volume_name: String,
pub(crate) volume_name_raw: Vec<u8>,
pub(crate) volume_bitmap_block: u16,
pub(crate) first_alloc_block: u16,
pub(crate) next_catalog_id: u32,
pub(crate) file_count: u32,
pub(crate) folder_count: u32,
pub(crate) finder_info: [u32; 8],
pub(crate) catalog_file_size: u32,
pub(crate) catalog_file_extents: [HfsExtDescriptor; 3],
pub(crate) extents_file_size: u32,
pub(crate) extents_file_extents: [HfsExtDescriptor; 3],
pub(crate) embedded_signature: u16,
pub(crate) embedded_start_block: u16,
pub(crate) embedded_block_count: u16,
pub(crate) raw_sector: [u8; 512],
}
impl HfsMasterDirectoryBlock {
fn parse(data: &[u8]) -> Result<Self, FilesystemError> {
if data.len() < 162 {
return Err(FilesystemError::Parse("MDB too short".into()));
}
let sig = BigEndian::read_u16(&data[0..2]);
if sig != HFS_SIGNATURE {
return Err(FilesystemError::Parse(format!(
"bad MDB signature: 0x{sig:04X}"
)));
}
let name_len = data[36] as usize;
let name_bytes = &data[37..37 + name_len.min(27)];
let volume_name = mac_roman_to_utf8(name_bytes);
let volume_name_raw = name_bytes.to_vec();
let mut extents_extents = [HfsExtDescriptor {
start_block: 0,
block_count: 0,
}; 3];
for i in 0..3 {
extents_extents[i] = HfsExtDescriptor::parse(&data[134 + i * 4..138 + i * 4]);
}
let mut catalog_extents = [HfsExtDescriptor {
start_block: 0,
block_count: 0,
}; 3];
for i in 0..3 {
catalog_extents[i] = HfsExtDescriptor::parse(&data[150 + i * 4..154 + i * 4]);
}
let embedded_sig = BigEndian::read_u16(&data[124..126]);
let embedded_start = BigEndian::read_u16(&data[126..128]);
let embedded_count = BigEndian::read_u16(&data[128..130]);
let mut finder_info = [0u32; 8];
for i in 0..8 {
finder_info[i] = BigEndian::read_u32(&data[92 + i * 4..96 + i * 4]);
}
let mut raw_sector = [0u8; 512];
let copy_len = data.len().min(512);
raw_sector[..copy_len].copy_from_slice(&data[..copy_len]);
Ok(HfsMasterDirectoryBlock {
signature: sig,
create_date: BigEndian::read_u32(&data[2..6]),
modify_date: BigEndian::read_u32(&data[6..10]),
total_blocks: BigEndian::read_u16(&data[18..20]),
block_size: BigEndian::read_u32(&data[20..24]),
free_blocks: BigEndian::read_u16(&data[34..36]),
volume_name,
volume_name_raw,
volume_bitmap_block: BigEndian::read_u16(&data[14..16]),
first_alloc_block: BigEndian::read_u16(&data[28..30]),
next_catalog_id: BigEndian::read_u32(&data[30..34]),
file_count: BigEndian::read_u32(&data[84..88]),
folder_count: BigEndian::read_u32(&data[88..92]),
finder_info,
extents_file_size: BigEndian::read_u32(&data[130..134]),
extents_file_extents: extents_extents,
catalog_file_size: BigEndian::read_u32(&data[146..150]),
catalog_file_extents: catalog_extents,
embedded_signature: embedded_sig,
embedded_start_block: embedded_start,
embedded_block_count: embedded_count,
raw_sector,
})
}
fn serialize_to_sector(&self) -> [u8; 512] {
let mut out = self.raw_sector;
BigEndian::write_u32(&mut out[6..10], self.modify_date);
BigEndian::write_u16(&mut out[18..20], self.total_blocks);
BigEndian::write_u16(&mut out[34..36], self.free_blocks);
BigEndian::write_u32(&mut out[30..34], self.next_catalog_id);
BigEndian::write_u32(&mut out[84..88], self.file_count);
BigEndian::write_u32(&mut out[88..92], self.folder_count);
for i in 0..8 {
BigEndian::write_u32(&mut out[92 + i * 4..96 + i * 4], self.finder_info[i]);
}
BigEndian::write_u32(&mut out[130..134], self.extents_file_size);
for i in 0..3 {
self.extents_file_extents[i].serialize(&mut out[134 + i * 4..138 + i * 4]);
}
BigEndian::write_u32(&mut out[146..150], self.catalog_file_size);
for i in 0..3 {
self.catalog_file_extents[i].serialize(&mut out[150 + i * 4..154 + i * 4]);
}
out
}
fn has_embedded_hfs_plus(&self) -> bool {
self.embedded_signature == HFS_PLUS_EMBEDDED_SIGNATURE
}
}
pub(crate) const CATALOG_DIR: i8 = 1;
const CNID_CATALOG: u32 = 4;
pub(crate) const CATALOG_FILE: i8 = 2;
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum CatalogRecord {
Directory {
dir_id: u32,
name: String,
parent_id: u32,
dates: (u32, u32, u32),
},
File {
file_id: u32,
name: String,
parent_id: u32,
data_size: u32,
data_extents: [HfsExtDescriptor; 3],
rsrc_size: u32,
rsrc_extents: [HfsExtDescriptor; 3],
type_code: [u8; 4],
creator_code: [u8; 4],
finder_flags: u16,
dates: (u32, u32, u32),
},
}
pub struct HfsFilesystem<R: Read + Seek> {
reader: R,
partition_offset: u64,
partition_size: Option<u64>,
mdb: HfsMasterDirectoryBlock,
catalog_data: Vec<u8>,
extents_overflow_data: Option<Vec<u8>>,
bitmap: Option<Vec<u8>>,
pending_boot_blocks: Option<Box<[u8; 1024]>>,
pending_volume_dates: Option<(u32, u32, u32)>,
bulk_mode: bool,
btrees_initialized: bool,
}
impl<R: Read + Seek> HfsFilesystem<R> {
fn alt_mdb_offset(&self) -> Option<u64> {
if let Some(size) = self.partition_size.filter(|&s| s >= 2048) {
return Some(self.partition_offset + size - 1024);
}
if self.mdb.block_size == 0 || self.mdb.total_blocks == 0 {
return None;
}
let sectors_per_block = self.mdb.block_size as u64 / 512;
let alt_sector =
self.mdb.first_alloc_block as u64 + self.mdb.total_blocks as u64 * sectors_per_block;
Some(self.partition_offset + alt_sector * 512)
}
pub fn open(reader: R, partition_offset: u64) -> Result<Self, FilesystemError> {
Self::open_sized(reader, partition_offset, None)
}
pub fn open_for_edit(
reader: R,
partition_offset: u64,
partition_size: Option<u64>,
) -> Result<Self, FilesystemError> {
let fs = Self::open_sized(reader, partition_offset, partition_size)?;
if let Some(short) = fs.partition_shortfall() {
log::warn!(
"the HFS volume is {short} bytes smaller than its partition; Mac OS and Disk \
First Aid expect a classic HFS volume to fill its partition. Grow it with \
`rb-cli resize IMG@N --size <partition size>` (R-058)"
);
}
Ok(fs)
}
pub fn partition_shortfall(&self) -> Option<u64> {
let size = self.partition_size.filter(|&s| s >= 2048)?;
if self.mdb.block_size == 0 {
return None;
}
let volume_len = self.mdb.first_alloc_block as u64 * 512
+ self.mdb.total_blocks as u64 * self.mdb.block_size as u64
+ 1024;
if volume_len < size {
Some(size - volume_len)
} else {
None
}
}
pub fn open_sized(
mut reader: R,
partition_offset: u64,
partition_size: Option<u64>,
) -> Result<Self, FilesystemError> {
reader.seek(SeekFrom::Start(partition_offset + 1024))?;
let mut mdb_buf = [0u8; 512];
reader.read_exact(&mut mdb_buf)?;
let mdb = HfsMasterDirectoryBlock::parse(&mdb_buf)?;
if mdb.has_embedded_hfs_plus() {
return Err(FilesystemError::Unsupported(
"this HFS volume contains an embedded HFS+ volume; use HFS+ reader instead".into(),
));
}
let catalog_data = read_fork_data(
&mut reader,
partition_offset,
&mdb,
&mdb.catalog_file_extents,
mdb.catalog_file_size as u64,
)?;
let mut fs = HfsFilesystem {
reader,
partition_offset,
partition_size,
mdb,
catalog_data,
extents_overflow_data: None,
bitmap: None,
pending_boot_blocks: None,
pending_volume_dates: None,
bulk_mode: false,
btrees_initialized: false,
};
if (fs.catalog_data.len() as u64) < fs.mdb.catalog_file_size as u64 {
let inline = fs.mdb.catalog_file_extents;
let size = fs.mdb.catalog_file_size as u64;
fs.catalog_data = fs.read_fork_with_overflow(CNID_CATALOG, 0x00, &inline, size)?;
}
Ok(fs)
}
fn list_children(&self, parent_id: u32) -> Result<Vec<CatalogRecord>, FilesystemError> {
if self.catalog_data.len() < 512 {
return Ok(vec![]);
}
let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as u32;
if node_size == 0 || self.catalog_data.len() < node_size as usize {
return Ok(vec![]);
}
let header_node = &self.catalog_data[0..node_size as usize];
let first_leaf = BigEndian::read_u32(&header_node[24..28]);
let mut results = Vec::new();
let mut node_idx = first_leaf;
while node_idx != 0 {
let offset = node_idx as usize * node_size as usize;
if offset + node_size as usize > self.catalog_data.len() {
break;
}
let node = &self.catalog_data[offset..offset + node_size as usize];
let next_node = BigEndian::read_u32(&node[0..4]);
let _kind = node[8] as i8; let num_records = BigEndian::read_u16(&node[10..12]);
for i in 0..num_records as usize {
let offset_pos = node_size as usize - 2 * (i + 1);
if offset_pos + 2 > node.len() {
break;
}
let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
if rec_offset + 6 > node.len() {
continue;
}
let key_len = node[rec_offset] as usize;
if key_len < 6 || rec_offset + 1 + key_len > node.len() {
continue;
}
let key_data = &node[rec_offset + 1..rec_offset + 1 + key_len];
let rec_parent_id = BigEndian::read_u32(&key_data[1..5]);
let name_len = key_data[5] as usize;
let name = if name_len > 0 && 6 + name_len <= key_data.len() {
mac_roman_to_utf8(&key_data[6..6 + name_len])
} else {
String::new()
};
let mut rec_data_offset = rec_offset + 1 + key_len;
if !rec_data_offset.is_multiple_of(2) {
rec_data_offset += 1;
}
if rec_data_offset + 2 > node.len() {
continue;
}
let record_type = node[rec_data_offset] as i8;
if rec_parent_id != parent_id {
continue;
}
match record_type {
CATALOG_DIR => {
if rec_data_offset + 70 > node.len() {
continue;
}
let dir_id =
BigEndian::read_u32(&node[rec_data_offset + 6..rec_data_offset + 10]);
let dir = &node[rec_data_offset..];
let dates = (
BigEndian::read_u32(&dir[10..14]),
BigEndian::read_u32(&dir[14..18]),
BigEndian::read_u32(&dir[18..22]),
);
results.push(CatalogRecord::Directory {
dir_id,
name,
parent_id: rec_parent_id,
dates,
});
}
CATALOG_FILE => {
if rec_data_offset + 102 > node.len() {
continue;
}
let rec = &node[rec_data_offset..];
let type_code: [u8; 4] = rec[4..8].try_into().unwrap();
let creator_code: [u8; 4] = rec[8..12].try_into().unwrap();
let finder_flags = BigEndian::read_u16(&rec[12..14]);
let file_id = BigEndian::read_u32(&rec[20..24]);
let data_size = BigEndian::read_u32(&rec[26..30]);
let mut data_extents = [HfsExtDescriptor {
start_block: 0,
block_count: 0,
}; 3];
for j in 0..3 {
data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
}
let rsrc_size = BigEndian::read_u32(&rec[36..40]);
let mut rsrc_extents = [HfsExtDescriptor {
start_block: 0,
block_count: 0,
}; 3];
for j in 0..3 {
rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
}
let dates = (
BigEndian::read_u32(&rec[44..48]),
BigEndian::read_u32(&rec[48..52]),
BigEndian::read_u32(&rec[52..56]),
);
results.push(CatalogRecord::File {
file_id,
name,
parent_id: rec_parent_id,
data_size,
data_extents,
rsrc_size,
rsrc_extents,
type_code,
creator_code,
finder_flags,
dates,
});
}
_ => {}
}
}
node_idx = next_node;
}
Ok(results)
}
fn find_file_by_id(
&self,
file_id: u32,
) -> Option<(u32, [HfsExtDescriptor; 3], u32, [HfsExtDescriptor; 3])> {
if self.catalog_data.len() < 512 {
return None;
}
let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as u32;
if node_size == 0 || self.catalog_data.len() < node_size as usize {
return None;
}
let header_node = &self.catalog_data[0..node_size as usize];
let first_leaf = BigEndian::read_u32(&header_node[24..28]);
let mut node_idx = first_leaf;
while node_idx != 0 {
let offset = node_idx as usize * node_size as usize;
if offset + node_size as usize > self.catalog_data.len() {
break;
}
let node = &self.catalog_data[offset..offset + node_size as usize];
let next_node = BigEndian::read_u32(&node[0..4]);
let num_records = BigEndian::read_u16(&node[10..12]);
for i in 0..num_records as usize {
let offset_pos = node_size as usize - 2 * (i + 1);
if offset_pos + 2 > node.len() {
break;
}
let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
if rec_offset + 6 > node.len() {
continue;
}
let key_len = node[rec_offset] as usize;
if key_len < 6 || rec_offset + 1 + key_len > node.len() {
continue;
}
let mut rec_data_offset = rec_offset + 1 + key_len;
if !rec_data_offset.is_multiple_of(2) {
rec_data_offset += 1;
}
if rec_data_offset + 2 > node.len() {
continue;
}
let record_type = node[rec_data_offset] as i8;
if record_type != CATALOG_FILE {
continue;
}
if rec_data_offset + 102 > node.len() {
continue;
}
let rec = &node[rec_data_offset..];
let rec_file_id = BigEndian::read_u32(&rec[20..24]);
if rec_file_id != file_id {
continue;
}
let data_size = BigEndian::read_u32(&rec[26..30]);
let mut data_extents = [HfsExtDescriptor {
start_block: 0,
block_count: 0,
}; 3];
for j in 0..3 {
data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
}
let rsrc_size = BigEndian::read_u32(&rec[36..40]);
let mut rsrc_extents = [HfsExtDescriptor {
start_block: 0,
block_count: 0,
}; 3];
for j in 0..3 {
rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
}
return Some((data_size, data_extents, rsrc_size, rsrc_extents));
}
node_idx = next_node;
}
None
}
fn read_volume_bitmap(&mut self) -> Result<Vec<u8>, FilesystemError> {
let bitmap_offset = self.partition_offset + self.mdb.volume_bitmap_block as u64 * 512;
let bitmap_size = (self.mdb.total_blocks as u32).div_ceil(8) as usize;
self.reader.seek(SeekFrom::Start(bitmap_offset))?;
let mut bitmap = vec![0u8; bitmap_size];
self.reader.read_exact(&mut bitmap)?;
Ok(bitmap)
}
fn alloc_block_offset(&self, block: u32) -> u64 {
self.mdb.first_alloc_block as u64 * 512 + block as u64 * self.mdb.block_size as u64
}
fn ensure_bitmap(&mut self) -> Result<(), FilesystemError> {
if self.bitmap.is_none() {
let bm = self.read_volume_bitmap()?;
self.bitmap = Some(bm);
}
Ok(())
}
pub fn fsck(&mut self) -> Result<disk_core::fsck::FsckResult, FilesystemError> {
self.ensure_bitmap()?;
let extents_data = if self.mdb.extents_file_size > 0 {
read_fork_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
&self.mdb.extents_file_extents,
self.mdb.extents_file_size as u64,
)
.ok()
} else {
None
};
let alt_mdb_sector = if let Some(alt_offset) = self.alt_mdb_offset() {
let mut alt_buf = [0u8; 512];
if self
.reader
.seek(SeekFrom::Start(alt_offset))
.and_then(|_| self.reader.read_exact(&mut alt_buf))
.is_ok()
{
Some(alt_buf)
} else {
None
}
} else {
None
};
Ok(crate::hfs_fsck::check_hfs_integrity(
&self.mdb,
&self.catalog_data,
self.bitmap.as_ref().unwrap(),
extents_data.as_deref(),
alt_mdb_sector.as_ref(),
self.partition_size,
))
}
pub(crate) fn build_catalog_key(parent_id: u32, name: &[u8]) -> Vec<u8> {
let key_len = 1 + 4 + 1 + name.len(); let mut key = Vec::with_capacity(1 + key_len);
key.push(key_len as u8); key.push(0); let mut buf4 = [0u8; 4];
BigEndian::write_u32(&mut buf4, parent_id);
key.extend_from_slice(&buf4);
key.push(name.len() as u8);
key.extend_from_slice(name);
if key.len() % 2 != 0 {
key.push(0);
}
key
}
pub(crate) fn catalog_compare(a: &[u8], b: &[u8]) -> Ordering {
if a.len() < 7 || b.len() < 7 {
return a.len().cmp(&b.len());
}
let parent_a = BigEndian::read_u32(&a[2..6]);
let parent_b = BigEndian::read_u32(&b[2..6]);
let name_len_a = a[6] as usize;
let name_len_b = b[6] as usize;
let name_a = &a[7..7 + name_len_a.min(a.len() - 7)];
let name_b = &b[7..7 + name_len_b.min(b.len() - 7)];
hfs_common::compare_hfs_keys(parent_a, name_a, parent_b, name_b)
}
fn find_catalog_record_by_name(
&self,
parent_id: u32,
name: &[u8],
) -> Option<(u32, usize, usize)> {
let search_key = Self::build_catalog_key(parent_id, name);
let header = BTreeHeader::read(&self.catalog_data);
let node_size = header.node_size as usize;
if node_size == 0 || self.catalog_data.len() < node_size {
return None;
}
let (leaf_idx, _) = hfs_common::btree_find_insert_leaf(
&self.catalog_data,
&header,
&search_key,
&Self::catalog_compare,
);
let off = leaf_idx as usize * node_size;
if off + node_size > self.catalog_data.len() {
return None;
}
let node = &self.catalog_data[off..off + node_size];
let num_records = BigEndian::read_u16(&node[10..12]) as usize;
for rec_idx in 0..num_records {
let (start, end) = hfs_common::btree_record_range(node, node_size, rec_idx);
if start >= end || end > node_size {
continue;
}
let rec = &node[start..end];
if rec.len() < 7 {
continue;
}
let key_len = rec[0] as usize;
let key_end = 1 + key_len;
if key_end > rec.len() {
continue;
}
if Self::catalog_compare(&rec[..key_end], &search_key) == Ordering::Equal {
return Some((leaf_idx, rec_idx, off + start));
}
}
None
}
fn find_catalog_record_by_cnid(&self, cnid: u32) -> Option<(u32, usize, usize)> {
self.find_catalog_record_by_name(cnid, &[])
}
fn update_parent_valence(&mut self, parent_id: u32, delta: i32) -> Result<(), FilesystemError> {
let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
let first_leaf = BigEndian::read_u32(&self.catalog_data[24..28]);
let val_off = hfs_common::walk_leaf_records(
&self.catalog_data,
first_leaf,
node_size,
|_node_idx, _rec_idx, abs_off, rec| {
if rec.len() < 7 {
return None;
}
let key_len = rec[0] as usize;
let mut data_off = abs_off + 1 + key_len;
if !data_off.is_multiple_of(2) {
data_off += 1;
}
let abs_end = abs_off + rec.len();
if data_off + 10 > abs_end {
return None;
}
if self.catalog_data[data_off] as i8 != CATALOG_DIR {
return None;
}
let dir_id = BigEndian::read_u32(&self.catalog_data[data_off + 6..data_off + 10]);
if dir_id == parent_id {
Some(data_off + 4)
} else {
None
}
},
);
let val_off = val_off.ok_or_else(|| {
FilesystemError::NotFound(format!(
"parent directory CNID {} not found in catalog",
parent_id
))
})?;
let old_val = BigEndian::read_u16(&self.catalog_data[val_off..val_off + 2]);
let new_val = (old_val as i32 + delta).max(0) as u16;
BigEndian::write_u16(&mut self.catalog_data[val_off..val_off + 2], new_val);
Ok(())
}
fn insert_catalog_record(&mut self, key_record: &[u8]) -> Result<(), FilesystemError>
where
R: Write,
{
let mut grown = false;
loop {
match hfs_common::btree_insert_full(
&mut self.catalog_data,
key_record,
&hfs_common::BTreeKeyFormat::CLASSIC_CATALOG,
&Self::catalog_compare,
) {
Ok(()) => return Ok(()),
Err(FilesystemError::DiskFull(_)) if !grown => {
self.grow_btree(HfsTree::Catalog)?;
grown = true;
}
Err(e) => return Err(e),
}
}
}
fn grow_btree(&mut self, tree: HfsTree) -> Result<(), FilesystemError>
where
R: Write,
{
let block_size = self.mdb.block_size;
if tree == HfsTree::Extents {
self.ensure_extents_overflow()?;
}
let (node_size, total_nodes, existing_map) = {
let buf = self.tree_buf(tree)?;
let h = BTreeHeader::read(buf);
let segs = hfs_common::btree_bitmap_segments(buf, h.node_size as usize);
(
h.node_size as usize,
h.total_nodes,
(segs.len() as u32).saturating_sub(1),
)
};
if node_size < 256 || block_size == 0 {
return Err(FilesystemError::InvalidData(format!(
"{} B-tree has node size {node_size}",
tree.label()
)));
}
let clump_off = if tree == HfsTree::Catalog { 78 } else { 74 };
let clump = BigEndian::read_u32(&self.mdb.raw_sector[clump_off..clump_off + 4]) as usize;
let grow_nodes = (clump.div_ceil(node_size).max(1) as u32).max(8);
let header_cap = ((node_size - 256) * 8) as u32;
let per_map = (hfs_common::map_node_bitmap_bytes(node_size) * 8) as u32;
let need_map = total_nodes + grow_nodes > header_cap + existing_map * per_map;
let add_slots = grow_nodes + u32::from(need_map);
let grow_blocks = btree_grow_blocks(total_nodes, add_slots, node_size, block_size);
let mut add_slots = add_slots;
if grow_blocks > 0 {
let mut inline = match tree {
HfsTree::Catalog => self.mdb.catalog_file_extents,
HfsTree::Extents => self.mdb.extents_file_extents,
};
let mut all: Vec<HfsExtDescriptor> = inline
.iter()
.copied()
.filter(|e| e.block_count > 0)
.collect();
let inline_blocks: u32 = all.iter().map(|e| e.block_count as u32).sum();
if tree == HfsTree::Catalog && all.len() == 3 {
self.ensure_extents_overflow()?;
if let Some(x) = self.extents_overflow_data.as_ref() {
all.extend(collect_fork_overflow_extents(
x,
CNID_CATALOG,
0x00,
inline_blocks,
));
}
}
let tail = all
.last()
.map(|e| e.start_block as u32 + e.block_count as u32);
match tail {
Some(t) if self.allocate_blocks_at(t, grow_blocks)? => {
all.last_mut().unwrap().block_count += grow_blocks as u16;
}
_ => {
let want_nodes = (total_nodes + add_slots).max(grow_nodes + 8);
let more = btree_grow_blocks(
total_nodes,
want_nodes - total_nodes,
node_size,
block_size,
)
.max(grow_blocks);
add_slots = want_nodes.max(total_nodes + add_slots) - total_nodes;
if tree == HfsTree::Extents && all.len() >= 3 {
return Err(FilesystemError::DiskFull(
"extents-overflow file has no free extent slot to grow into".into(),
));
}
let (start, more) = match self.allocate_blocks(more) {
Ok(start) => (start, more),
Err(_) => {
add_slots = grow_nodes + u32::from(need_map);
(self.allocate_blocks(grow_blocks)?, grow_blocks)
}
};
all.push(HfsExtDescriptor {
start_block: start as u16,
block_count: more as u16,
});
}
}
inline = [HfsExtDescriptor {
start_block: 0,
block_count: 0,
}; 3];
for (i, e) in all.iter().take(3).enumerate() {
inline[i] = *e;
}
match tree {
HfsTree::Catalog => {
self.mdb.catalog_file_extents = inline;
if all.len() > 3 {
let data = self.extents_overflow_data.as_mut().expect("loaded above");
remove_fork_overflow_records(data, CNID_CATALOG, 0x00)?;
self.insert_fork_overflow(CNID_CATALOG, 0x00, &all)?;
self.write_extents_overflow_back()?;
}
}
HfsTree::Extents => self.mdb.extents_file_extents = inline,
}
}
let buf = self.tree_buf(tree)?;
buf.resize(buf.len() + add_slots as usize * node_size, 0);
if need_map {
let map_idx = total_nodes;
let tail = btree_map_chain_tail(buf, node_size);
hfs_common::init_map_node(buf, node_size, map_idx, tail, 0);
let linker = tail as usize * node_size;
BigEndian::write_u32(&mut buf[linker..linker + 4], map_idx);
hfs_common::btree_bitmap_set(buf, node_size, map_idx);
}
let mut h = BTreeHeader::read(buf);
h.total_nodes += add_slots;
h.free_nodes += add_slots - u32::from(need_map);
h.write(buf);
let added = add_slots * node_size as u32;
match tree {
HfsTree::Catalog => self.mdb.catalog_file_size += added,
HfsTree::Extents => self.mdb.extents_file_size += added,
}
Ok(())
}
fn tree_buf(&mut self, tree: HfsTree) -> Result<&mut Vec<u8>, FilesystemError> {
match tree {
HfsTree::Catalog => Ok(&mut self.catalog_data),
HfsTree::Extents => self.extents_overflow_data.as_mut().ok_or_else(|| {
FilesystemError::Unsupported("volume has no extents-overflow B-tree".into())
}),
}
}
fn allocate_blocks_at(&mut self, start: u32, count: u32) -> Result<bool, FilesystemError> {
self.ensure_bitmap()?;
let total = self.mdb.total_blocks as u32;
let bitmap = self.bitmap.as_mut().unwrap();
if start + count > total || (start..start + count).any(|b| bitmap_test_bit_be(bitmap, b)) {
return Ok(false);
}
for b in start..start + count {
bitmap_set_bit_be(bitmap, b);
}
self.mdb.free_blocks = self.mdb.free_blocks.saturating_sub(count as u16);
Ok(true)
}
fn remove_catalog_record(
&mut self,
node_idx: u32,
rec_idx: usize,
) -> Result<(), FilesystemError> {
let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
if node_size == 0 {
return Ok(());
}
let offset = node_idx as usize * node_size;
if offset + node_size > self.catalog_data.len() {
return Ok(());
}
let num_before = {
let node = &self.catalog_data[offset..offset + node_size];
BigEndian::read_u16(&node[10..12]) as usize
};
{
let node = &mut self.catalog_data[offset..offset + node_size];
btree_remove_record(node, node_size, rec_idx);
}
let node = &self.catalog_data[offset..offset + node_size];
let num_after = BigEndian::read_u16(&node[10..12]) as usize;
if num_after == 0 && num_before > 0 {
let prev = BigEndian::read_u32(&self.catalog_data[offset + 4..offset + 8]);
let next = BigEndian::read_u32(&self.catalog_data[offset..offset + 4]);
if prev != 0 {
let prev_off = prev as usize * node_size;
if prev_off + 4 <= self.catalog_data.len() {
BigEndian::write_u32(&mut self.catalog_data[prev_off..prev_off + 4], next);
}
}
if next != 0 {
let next_off = next as usize * node_size;
if next_off + 8 <= self.catalog_data.len() {
BigEndian::write_u32(&mut self.catalog_data[next_off + 4..next_off + 8], prev);
}
}
let mut h = BTreeHeader::read(&self.catalog_data);
if h.first_leaf_node == node_idx {
h.first_leaf_node = next;
}
if h.last_leaf_node == node_idx {
h.last_leaf_node = prev;
}
btree_free_node(&mut self.catalog_data, node_size, node_idx);
h.free_nodes += 1;
if h.root_node == node_idx {
h.root_node = 0;
h.depth = 0;
}
h.write(&mut self.catalog_data);
let header = BTreeHeader::read(&self.catalog_data);
if header.depth > 1 {
let mut dummy_report = disk_core::fsck::RepairReport {
fixes_applied: Vec::new(),
fixes_failed: Vec::new(),
unrepairable_count: 0,
};
crate::hfs_fsck::rebuild_index_nodes(
&mut self.catalog_data,
node_size,
&mut dummy_report,
);
}
} else if rec_idx == 0 && num_after > 0 {
btree_refresh_index_keys(
&mut self.catalog_data,
node_size,
node_idx,
&BTreeKeyFormat::CLASSIC_CATALOG,
&Self::catalog_compare,
)?;
}
let mut h = BTreeHeader::read(&self.catalog_data);
h.leaf_records = h.leaf_records.saturating_sub(1);
h.write(&mut self.catalog_data);
Ok(())
}
fn snapshot(&self) -> Option<(Vec<u8>, Option<Vec<u8>>, HfsMasterDirectoryBlock)> {
if self.bulk_mode {
return None;
}
Some((
self.catalog_data.clone(),
self.bitmap.clone(),
self.mdb.clone(),
))
}
fn restore_snapshot(
&mut self,
snap: Option<(Vec<u8>, Option<Vec<u8>>, HfsMasterDirectoryBlock)>,
) {
let Some((catalog_data, bitmap, mdb)) = snap else {
return;
};
self.catalog_data = catalog_data;
self.bitmap = bitmap;
self.mdb = mdb;
}
pub(crate) fn mdb(&self) -> &HfsMasterDirectoryBlock {
&self.mdb
}
pub fn volume_summary(&self) -> HfsVolumeSummary {
let used_blocks = self.mdb.total_blocks.saturating_sub(self.mdb.free_blocks) as u64;
HfsVolumeSummary {
volume_name: self.mdb.volume_name.clone(),
block_size: self.mdb.block_size,
total_blocks: self.mdb.total_blocks,
free_blocks: self.mdb.free_blocks,
used_bytes: used_blocks * self.mdb.block_size as u64,
file_count: self.mdb.file_count,
folder_count: self.mdb.folder_count,
catalog_file_size: self.mdb.catalog_file_size,
extents_file_size: self.mdb.extents_file_size,
}
}
pub(crate) fn catalog_data(&self) -> &[u8] {
&self.catalog_data
}
#[allow(dead_code)]
pub(crate) fn partition_offset(&self) -> u64 {
self.partition_offset
}
pub fn locate_file_by_cnid(&self, cnid: u32) -> Option<HfsFileLocation> {
let (data_size, data_extents, _rsrc_size, _rsrc_extents) = self.find_file_by_id(cnid)?;
let first = data_extents.first().copied()?;
if first.block_count == 0 {
return None;
}
let fragmented = data_extents.iter().skip(1).any(|e| e.block_count != 0);
let offset_in_image =
self.partition_offset + self.alloc_block_offset(first.start_block as u32);
Some(HfsFileLocation {
offset_in_image,
length: data_size as u64,
fragmented,
})
}
pub(crate) fn read_boot_blocks(&mut self) -> Result<[u8; 1024], FilesystemError> {
self.reader.seek(SeekFrom::Start(self.partition_offset))?;
let mut buf = [0u8; 1024];
self.reader.read_exact(&mut buf)?;
Ok(buf)
}
#[allow(dead_code)]
pub(crate) fn read_fork(
&mut self,
extents: &[HfsExtDescriptor; 3],
size: u64,
) -> Result<Vec<u8>, FilesystemError> {
read_fork_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
extents,
size,
)
}
fn ensure_extents_overflow(&mut self) -> Result<(), FilesystemError> {
if self.extents_overflow_data.is_some() {
return Ok(());
}
if self.mdb.extents_file_size == 0 {
return Ok(());
}
let data = read_fork_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
&self.mdb.extents_file_extents,
self.mdb.extents_file_size as u64,
)?;
self.extents_overflow_data = Some(data);
Ok(())
}
fn write_fork_with_overflow_to(
&mut self,
file_id: u32,
fork_type: u8,
inline: &[HfsExtDescriptor; 3],
size: u64,
writer: &mut dyn std::io::Write,
) -> Result<u64, FilesystemError> {
let mut written = write_fork_data_to(
&mut self.reader,
self.partition_offset,
&self.mdb,
inline,
writer,
size,
)?;
if written >= size {
return Ok(written);
}
self.ensure_extents_overflow()?;
let Some(ext_data) = self.extents_overflow_data.as_ref() else {
return Err(FilesystemError::InvalidData(format!(
"file {file_id} fork {fork_type:#x}: {size} bytes requested but only {written} \
bytes in inline extents and no extents-overflow B-tree present",
)));
};
let inline_blocks: u32 = inline.iter().map(|e| e.block_count as u32).sum();
let overflow = collect_fork_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
let block_size = self.mdb.block_size as u64;
let first_alloc_offset = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
let mut buf = vec![0u8; 64 * 1024];
for ext in overflow {
if written >= size {
break;
}
if ext.block_count == 0 {
continue;
}
let offset = first_alloc_offset + ext.start_block as u64 * block_size;
let extent_len = ext.block_count as u64 * block_size;
let to_emit = extent_len.min(size - written);
self.reader.seek(SeekFrom::Start(offset))?;
let mut left = to_emit;
while left > 0 {
let n = (buf.len() as u64).min(left) as usize;
self.reader.read_exact(&mut buf[..n])?;
writer.write_all(&buf[..n])?;
left -= n as u64;
}
written += to_emit;
}
if written < size {
return Err(FilesystemError::InvalidData(format!(
"file {file_id} fork {fork_type:#x}: extents (inline + overflow) cover \
{written} of {size} bytes",
)));
}
Ok(written)
}
fn read_fork_with_overflow(
&mut self,
file_id: u32,
fork_type: u8,
inline: &[HfsExtDescriptor; 3],
size: u64,
) -> Result<Vec<u8>, FilesystemError> {
let mut data = read_fork_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
inline,
size,
)?;
if (data.len() as u64) >= size {
return Ok(data);
}
self.ensure_extents_overflow()?;
let Some(ext_data) = self.extents_overflow_data.as_ref() else {
return Err(FilesystemError::InvalidData(format!(
"file {file_id} fork {fork_type:#x}: {size} bytes requested but only {} bytes \
in inline extents and no extents-overflow B-tree present",
data.len()
)));
};
let inline_blocks: u32 = inline.iter().map(|e| e.block_count as u32).sum();
let overflow = collect_fork_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
let block_size = self.mdb.block_size as u64;
let first_alloc_offset = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
for ext in overflow {
if data.len() as u64 >= size {
break;
}
if ext.block_count == 0 {
continue;
}
let offset = first_alloc_offset + ext.start_block as u64 * block_size;
let extent_len = ext.block_count as u64 * block_size;
let to_read = extent_len.min(size - data.len() as u64) as usize;
self.reader.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; to_read];
self.reader.read_exact(&mut buf)?;
data.extend_from_slice(&buf);
}
if (data.len() as u64) < size {
return Err(FilesystemError::InvalidData(format!(
"file {file_id} fork {fork_type:#x}: extents (inline + overflow) cover \
{} of {size} bytes",
data.len()
)));
}
Ok(data)
}
fn locate_record_data(&self, cnid: u32) -> Result<usize, FilesystemError> {
if let Some((_, _, t_offset)) = self.find_catalog_record_by_cnid(cnid) {
let key_len = self.catalog_data[t_offset] as usize;
let mut rec_data_start = t_offset + 1 + key_len;
if !rec_data_start.is_multiple_of(2) {
rec_data_start += 1;
}
let thread_parent =
BigEndian::read_u32(&self.catalog_data[rec_data_start + 10..rec_data_start + 14]);
let thread_name_len = self.catalog_data[rec_data_start + 14] as usize;
let thread_name = self.catalog_data
[rec_data_start + 15..rec_data_start + 15 + thread_name_len]
.to_vec();
if let Some((_, _, f_offset)) =
self.find_catalog_record_by_name(thread_parent, &thread_name)
{
let fkey_len = self.catalog_data[f_offset] as usize;
let mut frec_start = f_offset + 1 + fkey_len;
if !frec_start.is_multiple_of(2) {
frec_start += 1;
}
return Ok(frec_start);
}
}
if let Some((_, _, frec_start)) = self.find_file_record_offset_by_cnid(cnid) {
return Ok(frec_start);
}
Err(FilesystemError::NotFound(format!(
"catalog record for CNID {cnid} not found"
)))
}
fn find_file_record_offset_by_cnid(&self, cnid: u32) -> Option<(u32, usize, usize)> {
if self.catalog_data.len() < 512 {
return None;
}
let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
let first_leaf = BigEndian::read_u32(&self.catalog_data[24..28]);
hfs_common::walk_leaf_records(
&self.catalog_data,
first_leaf,
node_size,
|node_idx, rec_idx, abs_off, rec| {
if rec.len() < 7 {
return None;
}
let key_len = rec[0] as usize;
if key_len < 6 || 1 + key_len > rec.len() {
return None;
}
let mut data_rel = 1 + key_len;
if !data_rel.is_multiple_of(2) {
data_rel += 1;
}
if data_rel + 24 > rec.len() {
return None;
}
if rec[data_rel] as i8 != CATALOG_FILE {
return None;
}
let rec_cnid = BigEndian::read_u32(&rec[data_rel + 20..data_rel + 24]);
if rec_cnid == cnid {
Some((node_idx, rec_idx, abs_off + data_rel))
} else {
None
}
},
)
}
pub fn set_finder_info(
&mut self,
entry: &FileEntry,
finfo: [u8; 16],
fxinfo: [u8; 16],
) -> Result<(), FilesystemError> {
if entry.is_directory() {
return Err(FilesystemError::InvalidData(
"set_finder_info: entry is a directory".into(),
));
}
let snap = self.snapshot();
let result = (|| {
let rec = self.locate_record_data(entry.location as u32)?;
if self.catalog_data[rec] as i8 != CATALOG_FILE {
return Err(FilesystemError::InvalidData("not a file record".into()));
}
self.catalog_data[rec + 4..rec + 20].copy_from_slice(&finfo);
self.catalog_data[rec + 56..rec + 72].copy_from_slice(&fxinfo);
Ok(())
})();
if result.is_err() {
self.restore_snapshot(snap);
}
result
}
pub fn set_directory_finder_info(
&mut self,
entry: &FileEntry,
dinfo: [u8; 16],
dxinfo: [u8; 16],
) -> Result<(), FilesystemError> {
if !entry.is_directory() {
return Err(FilesystemError::InvalidData(
"set_directory_finder_info: entry is not a directory".into(),
));
}
let snap = self.snapshot();
let result = (|| {
let rec = self.locate_record_data(entry.location as u32)?;
if self.catalog_data[rec] as i8 != CATALOG_DIR {
return Err(FilesystemError::InvalidData(
"not a directory record".into(),
));
}
self.catalog_data[rec + 22..rec + 38].copy_from_slice(&dinfo);
self.catalog_data[rec + 38..rec + 54].copy_from_slice(&dxinfo);
Ok(())
})();
if result.is_err() {
self.restore_snapshot(snap);
}
result
}
pub fn set_dates(
&mut self,
entry: &FileEntry,
create: u32,
modify: u32,
backup: u32,
) -> Result<(), FilesystemError> {
let snap = self.snapshot();
let result = (|| {
let rec = self.locate_record_data(entry.location as u32)?;
let record_type = self.catalog_data[rec] as i8;
let (cr_off, md_off, bk_off) = match record_type {
CATALOG_FILE => (44, 48, 52),
CATALOG_DIR => (10, 14, 18),
other => {
return Err(FilesystemError::InvalidData(format!(
"unexpected catalog record type: {other}"
)))
}
};
BigEndian::write_u32(
&mut self.catalog_data[rec + cr_off..rec + cr_off + 4],
create,
);
BigEndian::write_u32(
&mut self.catalog_data[rec + md_off..rec + md_off + 4],
modify,
);
BigEndian::write_u32(
&mut self.catalog_data[rec + bk_off..rec + bk_off + 4],
backup,
);
Ok(())
})();
if result.is_err() {
self.restore_snapshot(snap);
}
result
}
pub fn set_file_locked(
&mut self,
entry: &FileEntry,
locked: bool,
) -> Result<(), FilesystemError> {
if entry.is_directory() {
return Err(FilesystemError::InvalidData(
"set_file_locked: entry is a directory".into(),
));
}
let snap = self.snapshot();
let result = (|| {
let rec = self.locate_record_data(entry.location as u32)?;
if self.catalog_data[rec] as i8 != CATALOG_FILE {
return Err(FilesystemError::InvalidData("not a file record".into()));
}
let flags = self.catalog_data[rec + 2];
self.catalog_data[rec + 2] = if locked { flags | 0x01 } else { flags & !0x01 };
Ok(())
})();
if result.is_err() {
self.restore_snapshot(snap);
}
result
}
pub fn set_volume_dates(&mut self, create: u32, modify: u32, backup: u32) {
self.pending_volume_dates = Some((create, modify, backup));
self.mdb.create_date = create;
self.mdb.modify_date = modify;
BigEndian::write_u32(&mut self.mdb.raw_sector[2..6], create);
BigEndian::write_u32(&mut self.mdb.raw_sector[6..10], modify);
BigEndian::write_u32(&mut self.mdb.raw_sector[64..68], backup);
}
pub fn set_boot_blocks(&mut self, blocks: &[u8; 1024]) {
self.pending_boot_blocks = Some(Box::new(*blocks));
}
pub fn set_volume_finder_info(&mut self, finder_info: &[u8; 32]) {
for i in 0..8 {
self.mdb.finder_info[i] = BigEndian::read_u32(&finder_info[i * 4..(i + 1) * 4]);
}
}
pub fn set_volume_name(&mut self, new_name: &str) -> Result<(), FilesystemError>
where
R: Write,
{
let new_name_raw = utf8_to_mac_roman(new_name)?;
if new_name_raw.is_empty() {
return Err(FilesystemError::InvalidData(
"HFS volume name must not be empty (Mac OS rejects unnamed volumes with -127)"
.into(),
));
}
if new_name_raw.len() > 27 {
return Err(FilesystemError::InvalidData(format!(
"HFS volume name '{new_name}' exceeds 27 Mac Roman bytes"
)));
}
let snap = self.snapshot();
let result = (|| -> Result<(), FilesystemError> {
let old_name_raw = self.mdb.volume_name_raw.clone();
if new_name_raw == old_name_raw {
return Ok(());
}
let (node_idx, rec_idx, key_abs) = self
.find_catalog_record_by_name(1, &old_name_raw)
.ok_or_else(|| {
FilesystemError::NotFound("root directory record not found in catalog".into())
})?;
let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
let node_off = node_idx as usize * node_size;
let node = &self.catalog_data[node_off..node_off + node_size];
let (rec_start, rec_end) = hfs_common::btree_record_range(node, node_size, rec_idx);
debug_assert_eq!(key_abs, node_off + rec_start);
let key_len = node[rec_start] as usize;
let mut data_rel = 1 + key_len;
if !data_rel.is_multiple_of(2) {
data_rel += 1;
}
let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
let new_key = Self::build_catalog_key(1, &new_name_raw);
let mut new_record = new_key;
if !new_record.len().is_multiple_of(2) {
new_record.push(0);
}
new_record.extend_from_slice(&data_bytes);
if !new_record.len().is_multiple_of(2) {
new_record.push(0);
}
self.remove_catalog_record(node_idx, rec_idx)?;
self.insert_catalog_record(&new_record)?;
if let Some((_n, _r, t_key_abs)) = self.find_catalog_record_by_cnid(2) {
let t_key_len = self.catalog_data[t_key_abs] as usize;
let mut t_data_off = t_key_abs + 1 + t_key_len;
if !t_data_off.is_multiple_of(2) {
t_data_off += 1;
}
if t_data_off + 15 + 27 <= self.catalog_data.len() {
self.catalog_data[t_data_off + 14] = new_name_raw.len() as u8;
for i in 0..27 {
self.catalog_data[t_data_off + 15 + i] = 0;
}
self.catalog_data[t_data_off + 15..t_data_off + 15 + new_name_raw.len()]
.copy_from_slice(&new_name_raw);
}
}
self.mdb.volume_name = mac_roman_to_utf8(&new_name_raw);
self.mdb.volume_name_raw = new_name_raw.clone();
self.mdb.raw_sector[36] = new_name_raw.len() as u8;
for i in 0..27 {
self.mdb.raw_sector[37 + i] = 0;
}
self.mdb.raw_sector[37..37 + new_name_raw.len()].copy_from_slice(&new_name_raw);
Ok(())
})();
if result.is_err() {
self.restore_snapshot(snap);
}
result
}
}
impl<R: Read + Write + Seek> HfsFilesystem<R> {
fn write_block(&mut self, block: u32, data: &[u8]) -> Result<(), FilesystemError> {
let offset = self.partition_offset + self.alloc_block_offset(block);
self.reader.seek(SeekFrom::Start(offset))?;
self.reader.write_all(data)?;
Ok(())
}
fn write_mdb(&mut self) -> Result<(), FilesystemError> {
BigEndian::write_u16(&mut self.mdb.raw_sector[10..12], 0x0100);
let (root_files, root_dirs) = match self.list_children(2) {
Ok(children) => {
let mut f = 0u16;
let mut d = 0u16;
for c in &children {
match c {
CatalogRecord::File { .. } => f = f.saturating_add(1),
CatalogRecord::Directory { .. } => d = d.saturating_add(1),
}
}
(f, d)
}
Err(_) => (0, 0),
};
BigEndian::write_u16(&mut self.mdb.raw_sector[12..14], root_files);
BigEndian::write_u16(&mut self.mdb.raw_sector[82..84], root_dirs);
let mdb_bytes = self.mdb.serialize_to_sector();
self.reader
.seek(SeekFrom::Start(self.partition_offset + 1024))?;
self.reader.write_all(&mdb_bytes)?;
if let Some(alt_offset) = self.alt_mdb_offset() {
self.reader.seek(SeekFrom::Start(alt_offset))?;
self.reader.write_all(&mdb_bytes)?;
}
Ok(())
}
fn write_catalog(&mut self) -> Result<(), FilesystemError> {
write_hfs_fork_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
&self.mdb.catalog_file_extents,
&self.catalog_data,
)?;
let inline_blocks: u32 = self
.mdb
.catalog_file_extents
.iter()
.map(|e| e.block_count as u32)
.sum();
let inline_bytes = inline_blocks as usize * self.mdb.block_size as usize;
if self.catalog_data.len() <= inline_bytes {
return Ok(());
}
self.ensure_extents_overflow()?;
let Some(ext_data) = self.extents_overflow_data.as_ref() else {
return Err(FilesystemError::InvalidData(
"catalog file runs past its inline extents and the volume has no extents-overflow tree".into(),
));
};
let overflow = collect_fork_overflow_extents(ext_data, CNID_CATALOG, 0x00, inline_blocks);
let first_alloc = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
let mut written = inline_bytes;
for ext in overflow {
if written >= self.catalog_data.len() {
break;
}
let off = first_alloc + ext.start_block as u64 * self.mdb.block_size as u64;
let len = (ext.block_count as usize * self.mdb.block_size as usize)
.min(self.catalog_data.len() - written);
self.reader.seek(SeekFrom::Start(off))?;
self.reader
.write_all(&self.catalog_data[written..written + len])?;
written += len;
}
if written < self.catalog_data.len() {
return Err(FilesystemError::InvalidData(format!(
"catalog file: extents cover {written} of {} bytes",
self.catalog_data.len()
)));
}
Ok(())
}
fn write_volume_bitmap(&mut self) -> Result<(), FilesystemError> {
if let Some(ref bm) = self.bitmap {
let bitmap_offset = self.partition_offset + self.mdb.volume_bitmap_block as u64 * 512;
self.reader.seek(SeekFrom::Start(bitmap_offset))?;
self.reader.write_all(bm)?;
}
Ok(())
}
fn allocate_blocks(&mut self, count: u32) -> Result<u32, FilesystemError> {
self.ensure_bitmap()?;
let bitmap = self.bitmap.as_mut().unwrap();
let start = bitmap_find_clear_run_be(bitmap, self.mdb.total_blocks as u32, count)
.ok_or_else(|| {
FilesystemError::DiskFull(format!("cannot find {} contiguous free blocks", count))
})?;
for i in 0..count {
bitmap_set_bit_be(bitmap, start + i);
}
self.mdb.free_blocks = self
.mdb
.free_blocks
.saturating_sub(count.min(u16::MAX as u32) as u16);
let next = (start + count) % self.mdb.total_blocks.max(1) as u32;
BigEndian::write_u16(&mut self.mdb.raw_sector[16..18], next as u16);
Ok(start)
}
fn allocate_extents(&mut self, count: u32) -> Result<Vec<HfsExtDescriptor>, FilesystemError> {
if count == 0 {
return Ok(Vec::new());
}
if count > u16::MAX as u32 {
return Err(FilesystemError::InvalidData(
"file too large for classic HFS (>65535 blocks)".into(),
));
}
if let Ok(start) = self.allocate_blocks(count) {
return Ok(vec![HfsExtDescriptor {
start_block: start as u16,
block_count: count as u16,
}]);
}
self.ensure_bitmap()?;
let total_blocks = self.mdb.total_blocks as u32;
let free = self.mdb.free_blocks as u32;
if free < count {
return Err(FilesystemError::DiskFull(format!(
"{free} blocks free, {count} requested"
)));
}
let bitmap = self.bitmap.as_mut().unwrap();
let mut extents = Vec::new();
let mut remaining = count;
for (start, len) in bitmap_collect_clear_runs_be(bitmap, total_blocks) {
if remaining == 0 {
break;
}
let take = len.min(remaining);
extents.push(HfsExtDescriptor {
start_block: start as u16,
block_count: take as u16,
});
remaining -= take;
}
if remaining > 0 {
return Err(FilesystemError::DiskFull(format!(
"needed {count} blocks, short by {remaining} after gathering every free run"
)));
}
for ext in &extents {
for i in 0..ext.block_count as u32 {
bitmap_set_bit_be(bitmap, ext.start_block as u32 + i);
}
}
self.mdb.free_blocks = self.mdb.free_blocks.saturating_sub(count as u16);
let last = extents.last().unwrap();
let next = (last.start_block as u32 + last.block_count as u32) % total_blocks.max(1);
BigEndian::write_u16(&mut self.mdb.raw_sector[16..18], next as u16);
Ok(extents)
}
fn free_blocks(&mut self, start: u32, count: u32) {
self.ensure_bitmap().ok();
if let Some(ref mut bitmap) = self.bitmap {
for i in 0..count {
bitmap_clear_bit_be(bitmap, start + i);
}
}
self.mdb.free_blocks += count as u16;
}
fn free_extent_blocks(&mut self, extents: &[HfsExtDescriptor; 3]) {
for ext in extents {
if ext.block_count == 0 {
break;
}
self.free_blocks(ext.start_block as u32, ext.block_count as u32);
}
}
fn free_fork_overflow(&mut self, file_id: u32, fork_type: u8) -> Result<bool, FilesystemError> {
self.ensure_extents_overflow()?;
let Some(data) = self.extents_overflow_data.as_ref() else {
return Ok(false);
};
let extents = collect_fork_overflow_extents(data, file_id, fork_type, 0);
for ext in &extents {
self.free_blocks(ext.start_block as u32, ext.block_count as u32);
}
let data = self.extents_overflow_data.as_mut().expect("loaded above");
Ok(remove_fork_overflow_records(data, file_id, fork_type)? > 0)
}
fn write_extents_overflow_back(&mut self) -> Result<(), FilesystemError> {
if let Some(data) = self.extents_overflow_data.as_ref() {
if self.mdb.extents_file_size > 0 {
write_extents_overflow_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
data,
)?;
}
}
Ok(())
}
fn write_data_to_blocks(
&mut self,
data: &mut dyn std::io::Read,
data_len: u64,
) -> Result<Vec<HfsExtDescriptor>, FilesystemError> {
if data_len == 0 {
return Ok(Vec::new());
}
let block_size = self.mdb.block_size as u64;
let blocks_needed = data_len.div_ceil(block_size) as u32;
let extents = self.allocate_extents(blocks_needed)?;
let mut buf = vec![0u8; block_size as usize];
let mut remaining = data_len;
for ext in &extents {
for i in 0..ext.block_count as u32 {
let to_read = remaining.min(block_size) as usize;
buf.fill(0);
data.read_exact(&mut buf[..to_read]).map_err(|e| {
FilesystemError::Io(std::io::Error::new(
e.kind(),
format!("reading file data: {e}"),
))
})?;
self.write_block(ext.start_block as u32 + i, &buf)?;
remaining -= to_read as u64;
}
}
Ok(extents)
}
fn extents_block_total(extents: &[HfsExtDescriptor]) -> u16 {
extents.iter().map(|e| e.block_count).sum()
}
fn set_inline_extents(rec: &mut [u8], base: usize, extents: &[HfsExtDescriptor]) {
rec[base..base + 12].fill(0);
for (i, ext) in extents.iter().take(3).enumerate() {
let off = base + i * 4;
BigEndian::write_u16(&mut rec[off..off + 2], ext.start_block);
BigEndian::write_u16(&mut rec[off + 2..off + 4], ext.block_count);
}
}
fn insert_fork_overflow(
&mut self,
file_id: u32,
fork_type: u8,
extents: &[HfsExtDescriptor],
) -> Result<bool, FilesystemError> {
use crate::hfs_common::compare_classic_extents_keys;
if extents.len() <= 3 {
return Ok(false);
}
self.ensure_extents_overflow()?;
if self.extents_overflow_data.is_none() {
return Err(FilesystemError::Unsupported(
"volume has no extents-overflow B-tree; a fragmented fork cannot be created".into(),
));
}
let mut fork_block: u32 = extents.iter().take(3).map(|e| e.block_count as u32).sum();
for chunk in extents[3..].chunks(3) {
let mut rec = vec![7u8, fork_type];
rec.extend_from_slice(&file_id.to_be_bytes());
rec.extend_from_slice(&(fork_block as u16).to_be_bytes());
for i in 0..3 {
let (start, count) = chunk
.get(i)
.map_or((0, 0), |e| (e.start_block, e.block_count));
rec.extend_from_slice(&start.to_be_bytes());
rec.extend_from_slice(&count.to_be_bytes());
}
let mut grown = false;
loop {
let data = self.extents_overflow_data.as_mut().expect("loaded above");
match hfs_common::btree_insert_full(
data,
&rec,
&BTreeKeyFormat::CLASSIC_EXTENTS,
&compare_classic_extents_keys,
) {
Ok(()) => break,
Err(FilesystemError::DiskFull(_)) if !grown => {
self.grow_btree(HfsTree::Extents)?;
grown = true;
}
Err(e) => return Err(e),
}
}
fork_block += chunk.iter().map(|e| e.block_count as u32).sum::<u32>();
}
Ok(true)
}
#[allow(clippy::too_many_arguments)]
fn build_file_record(
file_id: u32,
data_size: u32,
data_start: u16,
data_blocks: u16,
rsrc_size: u32,
rsrc_start: u16,
rsrc_blocks: u16,
type_code: &[u8; 4],
creator_code: &[u8; 4],
block_size: u32,
mtime_secs: Option<u64>,
finder_flags: u16,
) -> [u8; 102] {
let mut rec = [0u8; 102];
let now = match mtime_secs {
Some(s) => disk_core::times::unix_to_mac_local(s),
None => disk_core::times::mac_local_now(),
};
rec[0] = CATALOG_FILE as u8; rec[4..8].copy_from_slice(type_code);
rec[8..12].copy_from_slice(creator_code);
BigEndian::write_u16(&mut rec[12..14], finder_flags); BigEndian::write_u32(&mut rec[20..24], file_id);
BigEndian::write_u32(&mut rec[26..30], data_size);
BigEndian::write_u32(&mut rec[30..34], data_blocks as u32 * block_size);
BigEndian::write_u32(&mut rec[36..40], rsrc_size);
BigEndian::write_u32(&mut rec[40..44], rsrc_blocks as u32 * block_size);
BigEndian::write_u32(&mut rec[44..48], now);
BigEndian::write_u32(&mut rec[48..52], now);
if data_blocks > 0 {
BigEndian::write_u16(&mut rec[74..76], data_start);
BigEndian::write_u16(&mut rec[76..78], data_blocks);
}
if rsrc_blocks > 0 {
BigEndian::write_u16(&mut rec[86..88], rsrc_start);
BigEndian::write_u16(&mut rec[88..90], rsrc_blocks);
}
rec
}
fn build_dir_record_with_dates(dir_id: u32, mtime_secs: Option<u64>) -> [u8; 70] {
let mut rec = [0u8; 70];
let now = match mtime_secs {
Some(s) => disk_core::times::unix_to_mac_local(s),
None => disk_core::times::mac_local_now(),
};
rec[0] = CATALOG_DIR as u8; BigEndian::write_u32(&mut rec[6..10], dir_id);
BigEndian::write_u32(&mut rec[10..14], now);
BigEndian::write_u32(&mut rec[14..18], now);
rec
}
pub(crate) fn build_thread_record(
thread_type: i8,
parent_id: u32,
name: &[u8],
) -> (Vec<u8>, Vec<u8>) {
let mut rec = Vec::with_capacity(46);
rec.push(thread_type as u8); rec.push(0); rec.extend_from_slice(&[0u8; 8]); let mut buf4 = [0u8; 4];
BigEndian::write_u32(&mut buf4, parent_id);
rec.extend_from_slice(&buf4); let name_len = name.len().min(31);
let mut str31 = [0u8; 32];
str31[0] = name_len as u8;
str31[1..1 + name_len].copy_from_slice(&name[..name_len]);
rec.extend_from_slice(&str31);
(vec![], rec) }
fn do_sync_metadata(&mut self) -> Result<(), FilesystemError> {
if let Some((create, modify, backup)) = self.pending_volume_dates.take() {
self.mdb.create_date = create;
self.mdb.modify_date = modify;
BigEndian::write_u32(&mut self.mdb.raw_sector[2..6], create);
BigEndian::write_u32(&mut self.mdb.raw_sector[6..10], modify);
BigEndian::write_u32(&mut self.mdb.raw_sector[64..68], backup);
} else {
self.mdb.modify_date = disk_core::times::mac_local_now();
}
if let Some(blocks) = self.pending_boot_blocks.take() {
self.reader.seek(SeekFrom::Start(self.partition_offset))?;
self.reader.write_all(&blocks[..])?;
}
self.write_catalog()?;
self.write_volume_bitmap()?;
self.write_mdb()?;
self.reader.flush()?;
Ok(())
}
fn ensure_catalog_initialized(&mut self) -> Result<(), FilesystemError> {
if self.btrees_initialized {
return Ok(());
}
let catalog_blank = is_catalog_uninitialized(&self.catalog_data);
let extents_data = if self.mdb.extents_file_size > 0 {
read_fork_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
&self.mdb.extents_file_extents,
self.mdb.extents_file_size as u64,
)
.ok()
} else {
None
};
let extents_blank = extents_data
.as_deref()
.map(is_catalog_uninitialized)
.unwrap_or(false);
if !catalog_blank && !extents_blank {
self.btrees_initialized = true;
return Ok(());
}
self.initialize_empty_btrees(catalog_blank, extents_blank)?;
self.btrees_initialized = true;
Ok(())
}
fn initialize_empty_btrees(
&mut self,
init_catalog: bool,
init_extents: bool,
) -> Result<(), FilesystemError> {
if init_catalog {
let catalog_size = self.catalog_data.len();
if catalog_size < 1024 {
return Err(FilesystemError::InvalidData(format!(
"cannot initialize empty HFS catalog: region is only {catalog_size} bytes"
)));
}
let vol_name = self.mdb.volume_name_raw.clone();
let node_size = pick_btree_node_size(catalog_size as u64) as usize;
let buf = build_empty_hfs_catalog_with_node_size(
catalog_size,
node_size,
&vol_name,
hfs_common::hfs_now(),
)?;
self.catalog_data = buf;
self.write_catalog()?;
}
if init_extents {
let extents_size = self.mdb.extents_file_size as usize;
if extents_size >= 512 {
let buf = build_empty_hfs_extents_btree(extents_size)?;
write_hfs_fork_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
&self.mdb.extents_file_extents,
&buf,
)?;
}
}
let bitmap_size = (self.mdb.total_blocks as u32).div_ceil(8) as usize;
let mut bitmap = vec![0u8; bitmap_size];
let mut used_blocks = 0u32;
for ext in self.mdb.catalog_file_extents.iter() {
if ext.block_count == 0 {
break;
}
for i in 0..ext.block_count {
let bit = ext.start_block as u32 + i as u32;
if bit < self.mdb.total_blocks as u32 {
hfs_common::bitmap_set_bit_be(&mut bitmap, bit);
used_blocks += 1;
}
}
}
for ext in self.mdb.extents_file_extents.iter() {
if ext.block_count == 0 {
break;
}
for i in 0..ext.block_count {
let bit = ext.start_block as u32 + i as u32;
if bit < self.mdb.total_blocks as u32 {
hfs_common::bitmap_set_bit_be(&mut bitmap, bit);
used_blocks += 1;
}
}
}
self.bitmap = Some(bitmap);
self.mdb.free_blocks = (self.mdb.total_blocks as u32).saturating_sub(used_blocks) as u16;
self.mdb.file_count = 0;
self.mdb.folder_count = 0;
if self.mdb.next_catalog_id < 16 {
self.mdb.next_catalog_id = 16;
}
self.mdb.modify_date = disk_core::times::mac_local_now();
self.write_volume_bitmap()?;
self.write_mdb()?;
self.reader.flush()?;
Ok(())
}
}
fn pick_btree_node_size(_target_bytes: u64) -> u16 {
512
}
pub const HFS_MAX_BTREE_FILE_SIZE: u32 = 16 * 1024 * 1024;
fn hfs_map_nodes_required(total_nodes: u32, node_size: usize) -> u32 {
let header_cap = ((node_size - 256) * 8) as u32; if total_nodes <= header_cap {
return 0;
}
let per_map = (hfs_common::map_node_bitmap_bytes(node_size) * 8) as u32;
if per_map == 0 {
return 0;
}
let extra = total_nodes - header_cap;
extra.div_ceil(per_map)
}
fn build_empty_hfs_catalog_with_node_size(
catalog_size: usize,
node_size: usize,
volume_name_raw: &[u8],
now: u32,
) -> Result<Vec<u8>, FilesystemError> {
if volume_name_raw.len() > 27 {
return Err(FilesystemError::InvalidData(format!(
"HFS volume name length {} exceeds 27 Mac Roman bytes",
volume_name_raw.len()
)));
}
let total_nodes = (catalog_size / node_size) as u32;
if total_nodes < 2 {
return Err(FilesystemError::InvalidData(format!(
"cannot initialize empty HFS catalog: only {total_nodes} node(s) fit"
)));
}
let map_nodes = hfs_map_nodes_required(total_nodes, node_size);
let used_nodes = 2 + map_nodes;
if total_nodes < used_nodes {
return Err(FilesystemError::InvalidData(format!(
"cannot initialize empty HFS catalog: {total_nodes} nodes < {used_nodes} required \
(header + leaf + {map_nodes} map nodes)"
)));
}
let mut buf = vec![0u8; catalog_size];
if map_nodes > 0 {
BigEndian::write_u32(&mut buf[0..4], 2);
}
buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
buf[9] = 0;
BigEndian::write_u16(&mut buf[10..12], 3);
BigEndian::write_u16(&mut buf[14..16], 1); BigEndian::write_u32(&mut buf[16..20], 1); BigEndian::write_u32(&mut buf[20..24], 2); BigEndian::write_u32(&mut buf[24..28], 1); BigEndian::write_u32(&mut buf[28..32], 1); BigEndian::write_u16(&mut buf[32..34], node_size as u16); BigEndian::write_u16(&mut buf[34..36], 37); BigEndian::write_u32(&mut buf[36..40], total_nodes);
BigEndian::write_u32(&mut buf[40..44], total_nodes - used_nodes);
let bitmap_rec_offset = 248usize;
let header_bitmap_len = node_size - 256; for bit in 0..used_nodes {
let local = bit;
let byte_idx = (local / 8) as usize;
let bit_in_byte = 7 - (local % 8) as u8;
if byte_idx < header_bitmap_len {
buf[bitmap_rec_offset + byte_idx] |= 1u8 << bit_in_byte;
}
}
let free_offset = (node_size - 8) as u16;
BigEndian::write_u16(&mut buf[node_size - 2..node_size], 14);
BigEndian::write_u16(&mut buf[node_size - 4..node_size - 2], 120);
BigEndian::write_u16(
&mut buf[node_size - 6..node_size - 4],
bitmap_rec_offset as u16,
);
BigEndian::write_u16(&mut buf[node_size - 8..node_size - 6], free_offset);
let leaf = node_size;
buf[leaf + 8] = hfs_common::BTREE_LEAF_NODE as u8; buf[leaf + 9] = 1; BigEndian::write_u16(&mut buf[leaf + 10..leaf + 12], 2);
let name_len = volume_name_raw.len();
let r0_key = leaf + 14;
let key_data_len: u8 = (1 + 4 + 1 + name_len) as u8; buf[r0_key] = key_data_len;
buf[r0_key + 1] = 0;
BigEndian::write_u32(&mut buf[r0_key + 2..r0_key + 6], 1);
buf[r0_key + 6] = name_len as u8;
buf[r0_key + 7..r0_key + 7 + name_len].copy_from_slice(volume_name_raw);
let r0_key_total = 1 + key_data_len as usize;
let r0_pad = r0_key_total & 1;
let r0_data = r0_key + r0_key_total + r0_pad;
buf[r0_data] = CATALOG_DIR as u8;
BigEndian::write_u32(&mut buf[r0_data + 6..r0_data + 10], 2); BigEndian::write_u32(&mut buf[r0_data + 10..r0_data + 14], now); BigEndian::write_u32(&mut buf[r0_data + 14..r0_data + 18], now);
let r1_key = r0_data + 70;
buf[r1_key] = 6;
buf[r1_key + 1] = 0;
BigEndian::write_u32(&mut buf[r1_key + 2..r1_key + 6], 2);
buf[r1_key + 6] = 0;
buf[r1_key + 7] = 0;
let r1_data = r1_key + 8;
buf[r1_data] = CATALOG_DIR_THREAD as u8;
BigEndian::write_u32(&mut buf[r1_data + 10..r1_data + 14], 1); buf[r1_data + 14] = name_len as u8;
buf[r1_data + 15..r1_data + 15 + name_len].copy_from_slice(volume_name_raw);
let free_rel = (r1_data + 46 - leaf) as u16;
BigEndian::write_u16(&mut buf[leaf + node_size - 2..leaf + node_size], 14);
BigEndian::write_u16(
&mut buf[leaf + node_size - 4..leaf + node_size - 2],
(r1_key - leaf) as u16,
);
BigEndian::write_u16(
&mut buf[leaf + node_size - 6..leaf + node_size - 4],
free_rel,
);
for i in 0..map_nodes {
let node_idx = 2 + i;
let next_link = if i + 1 < map_nodes { node_idx + 1 } else { 0 };
let prev_link = if i == 0 { 0 } else { node_idx - 1 };
hfs_common::init_map_node(&mut buf, node_size, node_idx, prev_link, next_link);
}
Ok(buf)
}
fn build_empty_hfs_extents_btree(size: usize) -> Result<Vec<u8>, FilesystemError> {
build_empty_hfs_extents_btree_with_node_size(size, 512)
}
fn build_empty_hfs_extents_btree_with_node_size(
size: usize,
node_size: usize,
) -> Result<Vec<u8>, FilesystemError> {
let total_nodes = (size / node_size) as u32;
if total_nodes < 1 {
return Err(FilesystemError::InvalidData(format!(
"cannot initialize empty HFS extents B-tree: region is only {size} bytes"
)));
}
let map_nodes = hfs_map_nodes_required(total_nodes, node_size);
let used_nodes = 1 + map_nodes;
if total_nodes < used_nodes {
return Err(FilesystemError::InvalidData(format!(
"cannot initialize empty HFS extents B-tree: {total_nodes} nodes < {used_nodes} \
required (header + {map_nodes} map nodes)"
)));
}
let mut buf = vec![0u8; size];
if map_nodes > 0 {
BigEndian::write_u32(&mut buf[0..4], 1);
}
buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
buf[9] = 0;
BigEndian::write_u16(&mut buf[10..12], 3);
BigEndian::write_u16(&mut buf[14..16], 0); BigEndian::write_u32(&mut buf[16..20], 0); BigEndian::write_u32(&mut buf[20..24], 0); BigEndian::write_u32(&mut buf[24..28], 0); BigEndian::write_u32(&mut buf[28..32], 0); BigEndian::write_u16(&mut buf[32..34], node_size as u16); BigEndian::write_u16(&mut buf[34..36], 7); BigEndian::write_u32(&mut buf[36..40], total_nodes);
BigEndian::write_u32(&mut buf[40..44], total_nodes - used_nodes);
let bitmap_rec_offset = 248usize;
let header_bitmap_len = node_size - 256;
for bit in 0..used_nodes {
let byte_idx = (bit / 8) as usize;
let bit_in_byte = 7 - (bit % 8) as u8;
if byte_idx < header_bitmap_len {
buf[bitmap_rec_offset + byte_idx] |= 1u8 << bit_in_byte;
}
}
let free_offset = (node_size - 8) as u16;
BigEndian::write_u16(&mut buf[node_size - 2..node_size], 14);
BigEndian::write_u16(&mut buf[node_size - 4..node_size - 2], 120);
BigEndian::write_u16(
&mut buf[node_size - 6..node_size - 4],
bitmap_rec_offset as u16,
);
BigEndian::write_u16(&mut buf[node_size - 8..node_size - 6], free_offset);
for i in 0..map_nodes {
let node_idx = 1 + i;
let next_link = if i + 1 < map_nodes { node_idx + 1 } else { 0 };
let prev_link = if i == 0 { 0 } else { node_idx - 1 };
hfs_common::init_map_node(&mut buf, node_size, node_idx, prev_link, next_link);
}
Ok(buf)
}
pub fn create_blank_hfs(
target_size_bytes: u64,
block_size: u32,
volume_name: &str,
) -> Result<Vec<u8>, FilesystemError> {
create_blank_hfs_sized(target_size_bytes, block_size, volume_name, 0, 0)
}
pub fn default_btree_sizes(target_volume_bytes: u64, block_size: u32) -> (u32, u32) {
if block_size == 0 {
return (0, 0);
}
let clump = 4u64 * block_size as u64;
let floor = 24u64 * block_size as u64;
let raw = target_volume_bytes / 200; let catalog = raw.max(floor).div_ceil(clump) * clump;
let extents = (raw / 2).max(floor).div_ceil(clump) * clump;
(
catalog.min(u32::MAX as u64) as u32,
extents.min(u32::MAX as u64) as u32,
)
}
pub fn clone_target_btree_sizes(
target_size: u64,
block_size: u32,
source_catalog: u32,
source_extents: u32,
) -> (u32, u32) {
let (default_cat, default_ext) = default_btree_sizes(target_size, block_size);
let catalog = source_catalog
.saturating_mul(3)
.saturating_div(2)
.max(default_cat)
.min(HFS_MAX_BTREE_FILE_SIZE);
let extents = source_extents.max(default_ext).min(HFS_MAX_BTREE_FILE_SIZE);
(catalog, extents)
}
pub fn pick_block_size(volume_bytes: u64) -> u32 {
let mut bs: u32 = 512;
while volume_bytes / bs as u64 > 65535 {
bs = bs.saturating_mul(2);
if bs == 0 {
return 1 << 16;
}
}
bs
}
pub fn create_blank_hfs_sized(
target_size_bytes: u64,
block_size: u32,
volume_name: &str,
min_extents_bytes: u32,
min_catalog_bytes: u32,
) -> Result<Vec<u8>, FilesystemError> {
create_blank_hfs_sized_at(
target_size_bytes,
block_size,
volume_name,
min_extents_bytes,
min_catalog_bytes,
hfs_common::hfs_now(),
)
}
pub(crate) fn create_blank_hfs_sized_at(
target_size_bytes: u64,
block_size: u32,
volume_name: &str,
min_extents_bytes: u32,
min_catalog_bytes: u32,
now: u32,
) -> Result<Vec<u8>, FilesystemError> {
let (front, mdb, image_size) = build_blank_hfs_front(
target_size_bytes,
block_size,
volume_name,
min_extents_bytes,
min_catalog_bytes,
now,
)?;
let mut img = vec![0u8; image_size as usize];
img[..front.len()].copy_from_slice(&front);
let alt_off = image_size as usize - 1024;
img[alt_off..alt_off + 512].copy_from_slice(&mdb);
Ok(img)
}
pub fn write_blank_hfs_into<W: Write + Seek>(
target: &mut W,
at_offset: u64,
target_size_bytes: u64,
block_size: u32,
volume_name: &str,
) -> Result<u64, FilesystemError> {
write_blank_hfs_into_at(
target,
at_offset,
target_size_bytes,
block_size,
volume_name,
hfs_common::hfs_now(),
)
}
pub(crate) fn write_blank_hfs_into_at<W: Write + Seek>(
target: &mut W,
at_offset: u64,
target_size_bytes: u64,
block_size: u32,
volume_name: &str,
now: u32,
) -> Result<u64, FilesystemError> {
let (front, mdb, image_size) =
build_blank_hfs_front(target_size_bytes, block_size, volume_name, 0, 0, now)?;
target.seek(SeekFrom::Start(at_offset))?;
target.write_all(&front)?;
target.seek(SeekFrom::Start(at_offset + image_size - 1024))?;
target.write_all(&mdb)?;
target.write_all(&[0u8; 512])?;
Ok(image_size)
}
fn build_blank_hfs_front(
target_size_bytes: u64,
block_size: u32,
volume_name: &str,
min_extents_bytes: u32,
min_catalog_bytes: u32,
now: u32,
) -> Result<(Vec<u8>, [u8; 512], u64), FilesystemError> {
if block_size == 0 || !block_size.is_multiple_of(512) {
return Err(FilesystemError::InvalidData(format!(
"HFS block_size must be a non-zero multiple of 512 (got {block_size})"
)));
}
let name_raw = utf8_to_mac_roman(volume_name)?;
if name_raw.len() > 27 {
return Err(FilesystemError::InvalidData(format!(
"HFS volume name '{volume_name}' exceeds 27 Mac Roman bytes"
)));
}
let mut first_alloc_block: u16 = 3;
let total_blocks: u32;
loop {
let pre_alloc = first_alloc_block as u64 * 512;
if target_size_bytes <= pre_alloc + 512 {
return Err(FilesystemError::InvalidData(format!(
"HFS volume too small ({target_size_bytes} bytes) for block_size {block_size}"
)));
}
let usable = target_size_bytes - pre_alloc - 1024; let tb = usable / block_size as u64;
if tb == 0 {
return Err(FilesystemError::InvalidData(format!(
"HFS volume too small for block_size {block_size}"
)));
}
if tb > 65535 {
return Err(FilesystemError::InvalidData(format!(
"HFS would have {tb} allocation blocks at block_size {block_size}, exceeding u16 ceiling 65535"
)));
}
let tb = tb as u32;
let bitmap_bytes = (tb as u64).div_ceil(8);
let bitmap_sectors = bitmap_bytes.div_ceil(512) as u16;
let needed = 3u16 + bitmap_sectors;
if needed <= first_alloc_block {
total_blocks = tb;
break;
}
first_alloc_block = needed;
}
let (default_catalog, default_extents) = default_btree_sizes(target_size_bytes, block_size);
let blocks_for = |bytes: u32, default: u32| -> u16 {
let bytes = if bytes == 0 { default } else { bytes };
let n = (bytes as u64).div_ceil(block_size as u64).max(4);
n.min(u16::MAX as u64) as u16
};
let extents_blocks = blocks_for(min_extents_bytes, default_extents);
let catalog_blocks = blocks_for(min_catalog_bytes, default_catalog);
let extents_start: u16 = 0;
let catalog_start: u16 = extents_blocks;
let btree_blocks: u16 = extents_blocks + catalog_blocks;
if total_blocks < btree_blocks as u32 {
return Err(FilesystemError::InvalidData(format!(
"HFS volume too small: {total_blocks} allocation blocks is below the minimum of {btree_blocks} needed for B-trees"
)));
}
let extents_size = extents_blocks as u32 * block_size;
let catalog_size = catalog_blocks as u32 * block_size;
let extents_node_size = pick_btree_node_size(extents_size as u64) as usize;
let catalog_node_size = pick_btree_node_size(catalog_size as u64) as usize;
let image_size =
first_alloc_block as u64 * 512 + total_blocks as u64 * block_size as u64 + 1024;
let front_size = first_alloc_block as usize * 512 + btree_blocks as usize * block_size as usize;
let mut front = vec![0u8; front_size];
let bitmap_off = 3 * 512;
for b in 0..btree_blocks as u32 {
let byte_idx = (b / 8) as usize;
let bit_idx = 7 - (b % 8) as u8;
front[bitmap_off + byte_idx] |= 1 << bit_idx;
}
let extents_bytes =
build_empty_hfs_extents_btree_with_node_size(extents_size as usize, extents_node_size)?;
let extents_off = first_alloc_block as u64 * 512 + extents_start as u64 * block_size as u64;
front[extents_off as usize..extents_off as usize + extents_bytes.len()]
.copy_from_slice(&extents_bytes);
let catalog_bytes = build_empty_hfs_catalog_with_node_size(
catalog_size as usize,
catalog_node_size,
&name_raw,
now,
)?;
let catalog_off = first_alloc_block as u64 * 512 + catalog_start as u64 * block_size as u64;
front[catalog_off as usize..catalog_off as usize + catalog_bytes.len()]
.copy_from_slice(&catalog_bytes);
let mdb = build_blank_mdb(
now,
&name_raw,
total_blocks as u16,
block_size,
first_alloc_block,
catalog_start,
catalog_blocks,
catalog_size,
extents_start,
extents_blocks,
extents_size,
btree_blocks,
);
front[1024..1024 + 512].copy_from_slice(&mdb);
Ok((front, mdb, image_size))
}
#[allow(clippy::too_many_arguments)]
fn build_blank_mdb(
now: u32,
name_raw: &[u8],
total_blocks: u16,
block_size: u32,
first_alloc_block: u16,
catalog_start: u16,
catalog_blocks: u16,
catalog_size: u32,
extents_start: u16,
extents_blocks: u16,
extents_size: u32,
allocated_blocks: u16,
) -> [u8; 512] {
let mut mdb = [0u8; 512];
BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
BigEndian::write_u32(&mut mdb[2..6], now); BigEndian::write_u32(&mut mdb[6..10], now); BigEndian::write_u16(&mut mdb[10..12], 0x0100);
BigEndian::write_u16(&mut mdb[14..16], 3); BigEndian::write_u16(&mut mdb[18..20], total_blocks); BigEndian::write_u32(&mut mdb[20..24], block_size); BigEndian::write_u32(&mut mdb[24..28], 4 * block_size); BigEndian::write_u16(&mut mdb[28..30], first_alloc_block); BigEndian::write_u32(&mut mdb[30..34], 16); BigEndian::write_u16(&mut mdb[34..36], total_blocks - allocated_blocks); mdb[36] = name_raw.len() as u8;
mdb[37..37 + name_raw.len()].copy_from_slice(name_raw);
BigEndian::write_u32(&mut mdb[74..78], extents_size); BigEndian::write_u32(&mut mdb[78..82], catalog_size); BigEndian::write_u32(&mut mdb[130..134], extents_size); BigEndian::write_u16(&mut mdb[134..136], extents_start);
BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
BigEndian::write_u32(&mut mdb[146..150], catalog_size); BigEndian::write_u16(&mut mdb[150..152], catalog_start);
BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
mdb
}
impl<R: Read + Seek + Send> Filesystem for HfsFilesystem<R> {
fn root(&mut self) -> Result<FileEntry, FilesystemError> {
Ok(FileEntry {
name: "/".into(),
path: "/".into(),
entry_type: EntryType::Directory,
size: 0,
location: 2, modified: None,
modified_unix: None,
type_code: None,
creator_code: None,
symlink_target: None,
special_type: None,
mode: None,
uid: None,
gid: None,
resource_fork_size: None,
aux_type: None,
link_target_cnid: None,
amiga_protection: None,
amiga_comment: None,
amiga_date: None,
dos_attributes: None,
finder_flags: None,
prodos_file_type: None,
mac_dates: None,
owner_label: None,
native_attrs: None,
qnx2_attrs: None,
})
}
fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
let parent_id = entry.location as u32;
let children = self.list_children(parent_id)?;
let mut entries = Vec::new();
for child in children {
match child {
CatalogRecord::Directory {
dir_id,
name,
dates,
..
} => {
let path = if entry.path == "/" {
format!("/{name}")
} else {
format!("{}/{name}", entry.path)
};
let mut fe = FileEntry::new_directory(name, path, dir_id as u64);
fe.modified = hfs_common::format_mac_date(dates.1);
fe.modified_unix = disk_core::times::mac_local_to_unix(dates.1);
fe.mac_dates = Some(dates);
entries.push(fe);
}
CatalogRecord::File {
file_id,
name,
data_size,
rsrc_size,
rsrc_extents,
type_code,
creator_code,
finder_flags,
dates,
..
} => {
let path = if entry.path == "/" {
format!("/{name}")
} else {
format!("{}/{name}", entry.path)
};
let mut fe = FileEntry::new_file(name, path, data_size as u64, file_id as u64);
fe.type_code = Some(type_code);
fe.creator_code = Some(creator_code);
fe.finder_flags = Some(finder_flags);
fe.modified = hfs_common::format_mac_date(dates.1);
fe.modified_unix = disk_core::times::mac_local_to_unix(dates.1);
fe.mac_dates = Some(dates);
if rsrc_size > 0 {
fe.resource_fork_size = Some(rsrc_size as u64);
}
if finder_flags & crate::mac_alias::IS_ALIAS_FLAG != 0 && rsrc_size > 0 {
if let Ok(rsrc) = read_fork_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
&rsrc_extents,
rsrc_size as u64,
) {
if let Some(target) = crate::mac_alias::resolve_alias_target(&rsrc) {
fe.symlink_target = Some(target);
}
}
}
entries.push(fe);
}
}
}
entries.sort_by_key(|a| a.name.to_lowercase());
Ok(entries)
}
fn read_file(
&mut self,
entry: &FileEntry,
max_bytes: usize,
) -> Result<Vec<u8>, FilesystemError> {
let file_id = entry.location as u32;
let (data_size, extents, _rsrc_size, _rsrc_extents) =
self.find_file_by_id(file_id).ok_or_else(|| {
FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
})?;
let mut data = self.read_fork_with_overflow(file_id, 0x00, &extents, data_size as u64)?;
data.truncate(max_bytes);
Ok(data)
}
fn write_file_to(
&mut self,
entry: &FileEntry,
writer: &mut dyn std::io::Write,
) -> Result<u64, FilesystemError> {
if entry.is_directory() {
return Err(FilesystemError::NotADirectory(entry.path.clone()));
}
let file_id = entry.location as u32;
let (data_size, extents, _rsrc_size, _rsrc_extents) =
self.find_file_by_id(file_id).ok_or_else(|| {
FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
})?;
self.write_fork_with_overflow_to(file_id, 0x00, &extents, data_size as u64, writer)
}
fn volume_label(&self) -> Option<&str> {
if self.mdb.volume_name.is_empty() {
None
} else {
Some(&self.mdb.volume_name)
}
}
fn fs_type(&self) -> &str {
"HFS"
}
fn validate_name(&self, name: &str) -> Result<(), FilesystemError> {
validate_hfs_create_name(name).map(|_| ())
}
fn uses_colon_paths(&self) -> bool {
true
}
fn total_size(&self) -> u64 {
self.mdb.total_blocks as u64 * self.mdb.block_size as u64
}
fn used_size(&self) -> u64 {
(self.mdb.total_blocks - self.mdb.free_blocks) as u64 * self.mdb.block_size as u64
}
fn allocation_unit(&self) -> Option<u64> {
if self.mdb.block_size == 0 {
None
} else {
Some(self.mdb.block_size as u64)
}
}
fn last_data_byte(&mut self) -> Result<u64, FilesystemError> {
let bitmap = self.read_volume_bitmap()?;
let last_block = find_last_set_bit(&bitmap, self.mdb.total_blocks as u32);
match last_block {
Some(block) => {
let byte = self.alloc_block_offset(block + 1);
Ok(byte)
}
None => Ok(self.total_size()),
}
}
fn write_resource_fork_to(
&mut self,
entry: &FileEntry,
writer: &mut dyn std::io::Write,
) -> Result<u64, FilesystemError> {
let file_id = entry.location as u32;
let (_data_size, _data_ext, rsrc_size, rsrc_extents) =
self.find_file_by_id(file_id).ok_or_else(|| {
FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
})?;
if rsrc_size == 0 {
return Ok(0);
}
let data = self.read_fork_with_overflow(file_id, 0xFF, &rsrc_extents, rsrc_size as u64)?;
writer.write_all(&data)?;
Ok(data.len() as u64)
}
fn resource_fork_size(&mut self, entry: &FileEntry) -> u64 {
let file_id = entry.location as u32;
self.find_file_by_id(file_id)
.map(|(_ds, _de, rs, _re)| rs as u64)
.unwrap_or(0)
}
fn fragmentation_stats(
&mut self,
) -> Option<Result<disk_core::filesystem::FragmentationStats, FilesystemError>> {
Some(Ok(hfs_fragmentation_stats(&self.catalog_data)))
}
fn blessed_system_folder(&mut self) -> Option<(u64, String)> {
let cnid = self.mdb.finder_info[0];
if cnid == 0 {
return None;
}
if let Some((_node, _rec, offset)) = self.find_catalog_record_by_cnid(cnid) {
let key_len = self.catalog_data[offset] as usize;
let mut rec_data_start = offset + 1 + key_len;
if !rec_data_start.is_multiple_of(2) {
rec_data_start += 1;
}
if rec_data_start + 15 <= self.catalog_data.len() {
let name_len = self.catalog_data[rec_data_start + 14] as usize;
if rec_data_start + 15 + name_len <= self.catalog_data.len() {
let name = mac_roman_to_utf8(
&self.catalog_data[rec_data_start + 15..rec_data_start + 15 + name_len],
);
return Some((cnid as u64, name));
}
}
}
Some((cnid as u64, format!("CNID {}", cnid)))
}
fn fsck(&mut self) -> Option<Result<disk_core::fsck::FsckResult, FilesystemError>> {
Some(self.fsck())
}
}
const CATALOG_DIR_THREAD: i8 = 3;
impl<R: Read + Write + Seek + Send> EditableFilesystem for HfsFilesystem<R> {
fn as_filesystem(&self) -> &dyn disk_core::filesystem::Filesystem {
self
}
fn as_filesystem_mut(&mut self) -> &mut dyn disk_core::filesystem::Filesystem {
self
}
fn begin_bulk(&mut self) {
self.bulk_mode = true;
}
fn end_bulk(&mut self) {
self.bulk_mode = false;
}
fn create_file(
&mut self,
parent: &FileEntry,
name: &str,
data: &mut dyn std::io::Read,
data_len: u64,
options: &CreateFileOptions,
) -> Result<FileEntry, FilesystemError> {
self.ensure_catalog_initialized()?;
let snap = self.snapshot();
let extents_before = self.extents_overflow_data.clone();
let result = (|| {
let parent_id = parent.location as u32;
let name_bytes = validate_hfs_create_name(name)?;
if self
.find_catalog_record_by_name(parent_id, &name_bytes)
.is_some()
{
return Err(FilesystemError::AlreadyExists(name.into()));
}
let file_id = self.mdb.next_catalog_id;
self.mdb.next_catalog_id += 1;
let (type_code, creator_code) = hfs_common::resolve_create_type_creator(
name,
options.os_type,
options.os_creator,
options.type_code.as_deref(),
options.creator_code.as_deref(),
);
let data_extents = self.write_data_to_blocks(data, data_len)?;
let (rsrc_extents, rsrc_size) = if let Some(ref rsrc_src) = options.resource_fork {
match rsrc_src {
disk_core::filesystem::ResourceForkSource::Data(rsrc_data) => {
let mut cursor = std::io::Cursor::new(rsrc_data);
let ext = self.write_data_to_blocks(&mut cursor, rsrc_data.len() as u64)?;
(ext, rsrc_data.len() as u32)
}
disk_core::filesystem::ResourceForkSource::File(path) => {
let mut f = std::fs::File::open(path)?;
let len = f.metadata()?.len();
let ext = self.write_data_to_blocks(&mut f, len)?;
(ext, len as u32)
}
}
} else {
(Vec::new(), 0)
};
let mtime = options.unix_times.map(|t| t.mtime_or_now());
let mut file_rec = Self::build_file_record(
file_id,
data_len as u32,
data_extents.first().map_or(0, |e| e.start_block),
Self::extents_block_total(&data_extents),
rsrc_size,
rsrc_extents.first().map_or(0, |e| e.start_block),
Self::extents_block_total(&rsrc_extents),
&type_code,
&creator_code,
self.mdb.block_size,
mtime,
options.finder_flags.unwrap_or(0),
);
Self::set_inline_extents(&mut file_rec, 74, &data_extents);
Self::set_inline_extents(&mut file_rec, 86, &rsrc_extents);
let key = Self::build_catalog_key(parent_id, &name_bytes);
let mut key_record = key;
key_record.extend_from_slice(&file_rec);
self.insert_catalog_record(&key_record)?;
self.update_parent_valence(parent_id, 1)?;
self.mdb.file_count += 1;
let mut overflow_changed = self.insert_fork_overflow(file_id, 0x00, &data_extents)?;
overflow_changed |= self.insert_fork_overflow(file_id, 0xFF, &rsrc_extents)?;
if overflow_changed {
self.write_extents_overflow_back()?;
}
let path = if parent.path == "/" {
format!("/{name}")
} else {
format!("{}/{name}", parent.path)
};
let mut fe = FileEntry::new_file(name.to_string(), path, data_len, file_id as u64);
if type_code != [0; 4] {
fe.type_code = Some(type_code);
fe.creator_code = Some(creator_code);
}
fe.finder_flags = options.finder_flags;
if rsrc_size > 0 {
fe.resource_fork_size = Some(rsrc_size as u64);
}
let stamped = mtime.unwrap_or_else(disk_core::times::now);
fe.modified_unix = Some(stamped);
fe.mac_dates = Some((
disk_core::times::unix_to_mac_local(stamped),
disk_core::times::unix_to_mac_local(stamped),
0,
));
Ok(fe)
})();
if result.is_err() {
self.extents_overflow_data = extents_before;
self.restore_snapshot(snap);
}
result
}
fn create_directory(
&mut self,
parent: &FileEntry,
name: &str,
options: &CreateDirectoryOptions,
) -> Result<FileEntry, FilesystemError> {
self.ensure_catalog_initialized()?;
let snap = self.snapshot();
let result = (|| {
let parent_id = parent.location as u32;
let name_bytes = validate_hfs_create_name(name)?;
if self
.find_catalog_record_by_name(parent_id, &name_bytes)
.is_some()
{
return Err(FilesystemError::AlreadyExists(name.into()));
}
let folder_id = self.mdb.next_catalog_id;
self.mdb.next_catalog_id += 1;
let mtime = options.unix_times.map(|t| t.mtime_or_now());
let folder_rec = Self::build_dir_record_with_dates(folder_id, mtime);
let key = Self::build_catalog_key(parent_id, &name_bytes);
let mut key_record = key;
key_record.extend_from_slice(&folder_rec);
self.insert_catalog_record(&key_record)?;
let (_, thread_data) =
Self::build_thread_record(CATALOG_DIR_THREAD, parent_id, &name_bytes);
let thread_key = Self::build_catalog_key(folder_id, &[]);
let mut thread_record = thread_key;
thread_record.extend_from_slice(&thread_data);
self.insert_catalog_record(&thread_record)?;
self.update_parent_valence(parent_id, 1)?;
self.mdb.folder_count += 1;
let path = if parent.path == "/" {
format!("/{name}")
} else {
format!("{}/{name}", parent.path)
};
let mut fe = FileEntry::new_directory(name.to_string(), path, folder_id as u64);
let stamped = mtime.unwrap_or_else(disk_core::times::now);
fe.modified_unix = Some(stamped);
fe.mac_dates = Some((
disk_core::times::unix_to_mac_local(stamped),
disk_core::times::unix_to_mac_local(stamped),
0,
));
Ok(fe)
})();
if result.is_err() {
self.restore_snapshot(snap);
}
result
}
fn delete_entry(
&mut self,
parent: &FileEntry,
entry: &FileEntry,
) -> Result<(), FilesystemError> {
let snap = self.snapshot();
let extents_before = self.extents_overflow_data.clone();
let result = (|| {
let parent_id = parent.location as u32;
let cnid = entry.location as u32;
if entry.is_directory() {
let children = self.list_children(cnid)?;
if !children.is_empty() {
return Err(FilesystemError::InvalidData(
"cannot delete non-empty directory".into(),
));
}
}
let name_bytes = utf8_to_mac_roman(&entry.name)?;
let (node_idx, rec_idx, _offset) = self
.find_catalog_record_by_name(parent_id, &name_bytes)
.ok_or_else(|| {
FilesystemError::NotFound(format!(
"entry '{}' not found in catalog",
entry.name
))
})?;
let mut overflow_changed = false;
if !entry.is_directory() {
if let Some((_, data_extents, _, rsrc_extents)) = self.find_file_by_id(cnid) {
self.free_extent_blocks(&data_extents);
self.free_extent_blocks(&rsrc_extents);
overflow_changed |= self.free_fork_overflow(cnid, 0x00)?;
overflow_changed |= self.free_fork_overflow(cnid, 0xFF)?;
}
}
self.remove_catalog_record(node_idx, rec_idx)?;
if let Some((t_node, t_rec, _)) = self.find_catalog_record_by_cnid(cnid) {
self.remove_catalog_record(t_node, t_rec)?;
}
self.update_parent_valence(parent_id, -1)?;
if entry.is_directory() {
self.mdb.folder_count = self.mdb.folder_count.saturating_sub(1);
} else {
self.mdb.file_count = self.mdb.file_count.saturating_sub(1);
}
if overflow_changed {
self.write_extents_overflow_back()?;
}
Ok(())
})();
if result.is_err() {
self.extents_overflow_data = extents_before;
self.restore_snapshot(snap);
}
result
}
fn rename(
&mut self,
parent: &FileEntry,
entry: &FileEntry,
new_name: &str,
) -> Result<(), FilesystemError> {
if new_name == entry.name {
return Ok(());
}
let parent_id = parent.location as u32;
let cnid = entry.location as u32;
let new_name_raw = validate_hfs_create_name(new_name)?;
let old_name_raw = utf8_to_mac_roman(&entry.name)?;
let snap = self.snapshot();
let result = (|| -> Result<(), FilesystemError> {
let (node_idx, rec_idx, _key_abs) = self
.find_catalog_record_by_name(parent_id, &old_name_raw)
.ok_or_else(|| {
FilesystemError::NotFound(format!(
"entry '{}' not found in catalog",
entry.name
))
})?;
if let Some((n2, r2, _)) = self.find_catalog_record_by_name(parent_id, &new_name_raw) {
if (n2, r2) != (node_idx, rec_idx) {
return Err(FilesystemError::AlreadyExists(new_name.to_string()));
}
}
let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
let node_off = node_idx as usize * node_size;
let node = &self.catalog_data[node_off..node_off + node_size];
let (rec_start, rec_end) = hfs_common::btree_record_range(node, node_size, rec_idx);
let key_len = node[rec_start] as usize;
let mut data_rel = 1 + key_len;
if !data_rel.is_multiple_of(2) {
data_rel += 1;
}
let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
let mut new_record = Self::build_catalog_key(parent_id, &new_name_raw);
if !new_record.len().is_multiple_of(2) {
new_record.push(0);
}
new_record.extend_from_slice(&data_bytes);
if !new_record.len().is_multiple_of(2) {
new_record.push(0);
}
self.remove_catalog_record(node_idx, rec_idx)?;
self.insert_catalog_record(&new_record)?;
if let Some((_n, _r, t_key_abs)) = self.find_catalog_record_by_cnid(cnid) {
let t_key_len = self.catalog_data[t_key_abs] as usize;
let mut t_data_off = t_key_abs + 1 + t_key_len;
if !t_data_off.is_multiple_of(2) {
t_data_off += 1;
}
if t_data_off + 15 + 31 <= self.catalog_data.len() {
self.catalog_data[t_data_off + 14] = new_name_raw.len() as u8;
for i in 0..31 {
self.catalog_data[t_data_off + 15 + i] = 0;
}
self.catalog_data[t_data_off + 15..t_data_off + 15 + new_name_raw.len()]
.copy_from_slice(&new_name_raw);
}
}
Ok(())
})();
if result.is_err() {
self.restore_snapshot(snap);
}
result
}
fn set_type_creator(
&mut self,
entry: &FileEntry,
type_code: &str,
creator_code: &str,
) -> Result<(), FilesystemError> {
let snap = self.snapshot();
let result = (|| {
let frec_start = self.locate_record_data(entry.location as u32)?;
if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
return Err(FilesystemError::InvalidData(
"set_type_creator: not a file record".into(),
));
}
let tc = hfs_common::encode_fourcc(type_code);
let cc = hfs_common::encode_fourcc(creator_code);
self.catalog_data[frec_start + 4..frec_start + 8].copy_from_slice(&tc);
self.catalog_data[frec_start + 8..frec_start + 12].copy_from_slice(&cc);
Ok(())
})();
if result.is_err() {
self.restore_snapshot(snap);
}
result
}
fn write_resource_fork(
&mut self,
entry: &FileEntry,
data: &mut dyn std::io::Read,
len: u64,
) -> Result<(), FilesystemError> {
let snap = self.snapshot();
let extents_before = self.extents_overflow_data.clone();
let result = (|| {
let cnid = entry.location as u32;
let mut overflow_changed = false;
if let Some((_, _, _, rsrc_extents)) = self.find_file_by_id(cnid) {
self.free_extent_blocks(&rsrc_extents);
overflow_changed = self.free_fork_overflow(cnid, 0xFF)?;
}
let rsrc_extents = self.write_data_to_blocks(data, len)?;
let frec_start = self.locate_record_data(cnid)?;
if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
return Err(FilesystemError::InvalidData(
"write_resource_fork: not a file record".into(),
));
}
self.catalog_data[frec_start + 34..frec_start + 36].fill(0);
BigEndian::write_u32(
&mut self.catalog_data[frec_start + 36..frec_start + 40],
len as u32,
);
BigEndian::write_u32(
&mut self.catalog_data[frec_start + 40..frec_start + 44],
Self::extents_block_total(&rsrc_extents) as u32 * self.mdb.block_size,
);
Self::set_inline_extents(
&mut self.catalog_data[frec_start..frec_start + 102],
86,
&rsrc_extents,
);
overflow_changed |= self.insert_fork_overflow(cnid, 0xFF, &rsrc_extents)?;
if overflow_changed {
self.write_extents_overflow_back()?;
}
Ok(())
})();
if result.is_err() {
self.extents_overflow_data = extents_before;
self.restore_snapshot(snap);
}
result
}
fn repair(&mut self) -> Result<disk_core::fsck::RepairReport, FilesystemError> {
self.ensure_bitmap()?;
let mut extents_data = if self.mdb.extents_file_size > 0 {
read_fork_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
&self.mdb.extents_file_extents,
self.mdb.extents_file_size as u64,
)
.ok()
} else {
None
};
let report = crate::hfs_fsck::repair_hfs(
&mut self.mdb,
&mut self.catalog_data,
self.bitmap.as_mut().unwrap(),
extents_data.as_mut(),
);
if let Some(ref ext_data) = extents_data {
if self.mdb.extents_file_size > 0 {
write_extents_overflow_data(
&mut self.reader,
self.partition_offset,
&self.mdb,
ext_data,
)?;
}
}
self.do_sync_metadata()?;
Ok(report)
}
fn sync_metadata(&mut self) -> Result<(), FilesystemError> {
self.do_sync_metadata()
}
fn free_space(&mut self) -> Result<u64, FilesystemError> {
Ok(self.mdb.free_blocks as u64 * self.mdb.block_size as u64)
}
fn set_blessed_folder(&mut self, entry: &FileEntry) -> Result<(), FilesystemError> {
let snap = self.snapshot();
let result = (|| {
if !entry.is_directory() {
return Err(FilesystemError::InvalidData(
"can only bless a directory".into(),
));
}
self.mdb.finder_info[0] = entry.location as u32;
Ok(())
})();
if result.is_err() {
self.restore_snapshot(snap);
}
result
}
fn write_boot_blocks(&mut self, blocks: &[u8; 1024]) -> Result<(), FilesystemError> {
HfsFilesystem::set_boot_blocks(self, blocks);
Ok(())
}
fn set_volume_name(&mut self, new_name: &str) -> Result<(), FilesystemError> {
HfsFilesystem::set_volume_name(self, new_name)
}
fn set_finder_info(
&mut self,
entry: &FileEntry,
finfo: [u8; 16],
fxinfo: [u8; 16],
) -> Result<(), FilesystemError> {
HfsFilesystem::set_finder_info(self, entry, finfo, fxinfo)
}
fn set_dates(
&mut self,
entry: &FileEntry,
create: u32,
modify: u32,
backup: u32,
) -> Result<(), FilesystemError> {
HfsFilesystem::set_dates(self, entry, create, modify, backup)
}
}
fn collect_fork_overflow_extents(
extents_data: &[u8],
file_id: u32,
fork_type: u8,
min_start_block: u32,
) -> Vec<HfsExtDescriptor> {
use crate::hfs_common::{btree_record_range, BTreeHeader, BTREE_LEAF_NODE};
use std::collections::HashSet;
let mut out: Vec<(u16, HfsExtDescriptor)> = Vec::new();
if extents_data.len() < 512 {
return Vec::new();
}
let header = BTreeHeader::read(extents_data);
let node_size = header.node_size as usize;
if node_size == 0 || extents_data.len() < node_size {
return Vec::new();
}
let mut node_idx = header.first_leaf_node;
let mut visited: HashSet<u32> = HashSet::new();
while node_idx != 0 {
if !visited.insert(node_idx) {
break;
}
let off = node_idx as usize * node_size;
if off + node_size > extents_data.len() {
break;
}
let node = &extents_data[off..off + node_size];
let flink = BigEndian::read_u32(&node[0..4]);
let kind = node[8] as i8;
if kind != BTREE_LEAF_NODE {
break;
}
let num_records = BigEndian::read_u16(&node[10..12]) as usize;
for i in 0..num_records {
let (rec_start, _) = btree_record_range(node, node_size, i);
if rec_start + 8 > node_size {
continue;
}
let key_len = node[rec_start] as usize;
if key_len < 7 || rec_start + 1 + key_len > node_size {
continue;
}
let rec_fork_type = node[rec_start + 1];
let rec_file_id = BigEndian::read_u32(&node[rec_start + 2..rec_start + 6]);
let rec_start_block = BigEndian::read_u16(&node[rec_start + 6..rec_start + 8]);
if rec_fork_type != fork_type || rec_file_id != file_id {
continue;
}
if (rec_start_block as u32) < min_start_block {
continue;
}
let mut data_off = rec_start + 1 + key_len;
if !data_off.is_multiple_of(2) {
data_off += 1;
}
if data_off + 12 > node_size {
continue;
}
for j in 0..3 {
let ext = HfsExtDescriptor::parse(&node[data_off + j * 4..data_off + j * 4 + 4]);
if ext.block_count > 0 {
out.push((rec_start_block, ext));
}
}
}
node_idx = flink;
}
out.sort_by_key(|(sb, _)| *sb);
out.into_iter().map(|(_, e)| e).collect()
}
fn read_fork_data<R: Read + Seek>(
reader: &mut R,
partition_offset: u64,
mdb: &HfsMasterDirectoryBlock,
extents: &[HfsExtDescriptor; 3],
size: u64,
) -> Result<Vec<u8>, FilesystemError> {
let mut data = Vec::with_capacity(size as usize);
let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
for ext in extents {
if ext.block_count == 0 {
break;
}
let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
let len = ext.block_count as u64 * mdb.block_size as u64;
reader.seek(SeekFrom::Start(offset))?;
let read_len = len.min(size - data.len() as u64) as usize;
let mut buf = vec![0u8; read_len];
reader.read_exact(&mut buf)?;
data.extend_from_slice(&buf);
if data.len() as u64 >= size {
break;
}
}
data.truncate(size as usize);
Ok(data)
}
fn remove_fork_overflow_records(
extents_data: &mut [u8],
file_id: u32,
fork_type: u8,
) -> Result<u32, FilesystemError> {
use crate::hfs_common::{
btree_remove_record, btree_retire_empty_leaf, compare_classic_extents_keys,
walk_leaf_records, BTreeHeader,
};
let kf = BTreeKeyFormat::CLASSIC_EXTENTS;
if extents_data.len() < 512 {
return Ok(0);
}
let header = BTreeHeader::read(extents_data);
let node_size = header.node_size as usize;
if node_size == 0 || extents_data.len() < node_size {
return Ok(0);
}
let mut victims: Vec<(u32, usize)> = Vec::new();
walk_leaf_records::<(), _>(
extents_data,
header.first_leaf_node,
node_size,
|node_idx, rec_idx, _off, rec| {
if rec.len() >= 8
&& rec[0] >= 7
&& rec[1] == fork_type
&& BigEndian::read_u32(&rec[2..6]) == file_id
{
victims.push((node_idx, rec_idx));
}
None
},
);
if victims.is_empty() {
return Ok(0);
}
victims.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
for &(node_idx, rec_idx) in &victims {
let off = node_idx as usize * node_size;
btree_remove_record(&mut extents_data[off..off + node_size], node_size, rec_idx);
}
let mut h = BTreeHeader::read(extents_data);
h.leaf_records = h.leaf_records.saturating_sub(victims.len() as u32);
h.write(extents_data);
let mut nodes: Vec<u32> = victims.iter().map(|v| v.0).collect();
nodes.dedup();
for node_idx in nodes {
let off = node_idx as usize * node_size;
if BigEndian::read_u16(&extents_data[off + 10..off + 12]) == 0 {
btree_retire_empty_leaf(
extents_data,
node_size,
node_idx,
&kf,
&compare_classic_extents_keys,
)?;
} else if victims.iter().any(|&(n, r)| n == node_idx && r == 0) {
btree_refresh_index_keys(
extents_data,
node_size,
node_idx,
&kf,
&compare_classic_extents_keys,
)?;
}
}
Ok(victims.len() as u32)
}
fn write_extents_overflow_data<W: std::io::Write + Seek>(
writer: &mut W,
partition_offset: u64,
mdb: &HfsMasterDirectoryBlock,
data: &[u8],
) -> Result<(), FilesystemError> {
let alloc_start = partition_offset + mdb.first_alloc_block as u64 * 512;
let mut data_start = 0usize;
for extent in mdb.extents_file_extents.iter() {
if extent.block_count == 0 {
break;
}
let block_off = alloc_start + extent.start_block as u64 * mdb.block_size as u64;
let byte_len = extent.block_count as usize * mdb.block_size as usize;
let data_end = (data_start + byte_len).min(data.len());
if data_start < data_end {
writer.seek(std::io::SeekFrom::Start(block_off))?;
writer.write_all(&data[data_start..data_end])?;
}
data_start += byte_len;
}
Ok(())
}
fn write_fork_data_to<R: Read + Seek>(
reader: &mut R,
partition_offset: u64,
mdb: &HfsMasterDirectoryBlock,
extents: &[HfsExtDescriptor; 3],
writer: &mut dyn std::io::Write,
size: u64,
) -> Result<u64, FilesystemError> {
let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
let mut written: u64 = 0;
let mut buf = vec![0u8; 64 * 1024];
for ext in extents {
if ext.block_count == 0 || written >= size {
break;
}
let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
let extent_len = ext.block_count as u64 * mdb.block_size as u64;
let to_emit = extent_len.min(size - written);
reader.seek(SeekFrom::Start(offset))?;
let mut left = to_emit;
while left > 0 {
let n = (buf.len() as u64).min(left) as usize;
reader.read_exact(&mut buf[..n])?;
writer.write_all(&buf[..n])?;
left -= n as u64;
}
written += to_emit;
}
Ok(written)
}
fn write_hfs_fork_data<R: Write + Seek>(
writer: &mut R,
partition_offset: u64,
mdb: &HfsMasterDirectoryBlock,
extents: &[HfsExtDescriptor; 3],
data: &[u8],
) -> Result<(), FilesystemError> {
let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
let mut written = 0usize;
for ext in extents {
if ext.block_count == 0 || written >= data.len() {
break;
}
let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
let extent_len = ext.block_count as u64 * mdb.block_size as u64;
let to_write = extent_len.min((data.len() - written) as u64) as usize;
writer.seek(SeekFrom::Start(offset))?;
writer.write_all(&data[written..written + to_write])?;
written += to_write;
}
Ok(())
}
fn find_last_set_bit(bitmap: &[u8], max_bits: u32) -> Option<u32> {
let mut last = None;
for bit in 0..max_bits {
let byte_idx = bit as usize / 8;
let bit_idx = 7 - (bit % 8); if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
last = Some(bit);
}
}
last
}
pub struct CompactHfsReader<R: Read + Seek> {
reader: R,
partition_offset: u64,
mdb: HfsMasterDirectoryBlock,
bitmap: Vec<u8>,
phase: u8,
pre_alloc_pos: u64,
current_block: u32,
block_pos: u64,
#[allow(dead_code)]
original_size: u64,
#[allow(dead_code)]
allocated_blocks: u32,
}
impl<R: Read + Seek> CompactHfsReader<R> {
pub fn new(
mut reader: R,
partition_offset: u64,
) -> Result<(Self, CompactResult), FilesystemError> {
log::debug!(
"[HFS compact] seeking to MDB at offset {}",
partition_offset + 1024
);
reader.seek(SeekFrom::Start(partition_offset + 1024))?;
let mut mdb_buf = [0u8; 162];
reader.read_exact(&mut mdb_buf)?;
let mdb = HfsMasterDirectoryBlock::parse(&mdb_buf).map_err(|e| {
log::debug!("[HFS compact] MDB parse failed: {e}");
e
})?;
log::debug!(
"[HFS compact] MDB ok: block_size={}, total_blocks={}, first_alloc_block={}, volume_bitmap_block={}",
mdb.block_size, mdb.total_blocks, mdb.first_alloc_block, mdb.volume_bitmap_block,
);
if mdb.has_embedded_hfs_plus() {
log::debug!("[HFS compact] has embedded HFS+ — refusing to compact HFS wrapper");
return Err(FilesystemError::Unsupported(
"cannot compact HFS wrapper with embedded HFS+".into(),
));
}
let bitmap_offset = partition_offset + mdb.volume_bitmap_block as u64 * 512;
log::debug!("[HFS compact] reading bitmap at offset {bitmap_offset}");
let bitmap_size = (mdb.total_blocks as u32).div_ceil(8) as usize;
reader.seek(SeekFrom::Start(bitmap_offset))?;
let mut bitmap = vec![0u8; bitmap_size];
reader.read_exact(&mut bitmap)?;
log::debug!("[HFS compact] bitmap read: {bitmap_size} bytes");
let mut allocated = 0u32;
for bit in 0..mdb.total_blocks as u32 {
let byte_idx = bit as usize / 8;
let bit_idx = 7 - (bit % 8);
if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
allocated += 1;
}
}
log::debug!(
"[HFS compact] allocated={} / {} total blocks ({} free)",
allocated,
mdb.total_blocks,
mdb.total_blocks as u32 - allocated,
);
let pre_alloc_size = mdb.first_alloc_block as u64 * 512;
let original_size = pre_alloc_size + mdb.total_blocks as u64 * mdb.block_size as u64;
let compacted_size = original_size;
let data_size = pre_alloc_size + allocated as u64 * mdb.block_size as u64;
log::debug!(
"[HFS compact] pre_alloc_size={}, data_size={}, compacted_size={} original_size={} (layout-preserving; free blocks -> zeros)",
pre_alloc_size, data_size, compacted_size, original_size
);
let result = CompactResult {
original_size,
compacted_size,
data_size,
clusters_used: allocated,
};
Ok((
CompactHfsReader {
reader,
partition_offset,
mdb,
bitmap,
phase: 0,
pre_alloc_pos: 0,
current_block: 0,
block_pos: 0,
original_size,
allocated_blocks: allocated,
},
result,
))
}
fn is_block_allocated(&self, block: u32) -> bool {
let byte_idx = block as usize / 8;
let bit_idx = 7 - (block % 8);
byte_idx < self.bitmap.len() && (self.bitmap[byte_idx] >> bit_idx) & 1 == 1
}
}
impl<R: Read + Seek> Read for CompactHfsReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.phase > 1 {
return Ok(0);
}
if self.phase == 0 {
let pre_alloc_size = self.mdb.first_alloc_block as u64 * 512;
let remaining = pre_alloc_size - self.pre_alloc_pos;
if remaining == 0 {
self.phase = 1;
return self.read(buf);
}
let to_read = (remaining as usize).min(buf.len());
let offset = self.partition_offset + self.pre_alloc_pos;
self.reader
.seek(SeekFrom::Start(offset))
.map_err(disk_core::io_other)?;
let n = self.reader.read(&mut buf[..to_read])?;
self.pre_alloc_pos += n as u64;
return Ok(n);
}
let total_blocks = self.mdb.total_blocks as u32;
if self.current_block >= total_blocks {
self.phase = 2;
return Ok(0);
}
let block_size = self.mdb.block_size as u64;
let allocated = self.is_block_allocated(self.current_block);
let mut run_end = self.current_block + 1;
while run_end < total_blocks && self.is_block_allocated(run_end) == allocated {
run_end += 1;
}
let bytes_in_run = (run_end - self.current_block) as u64 * block_size - self.block_pos;
let to_read = (bytes_in_run as usize).min(buf.len());
let n = if allocated {
let offset = self.partition_offset
+ self.mdb.first_alloc_block as u64 * 512
+ self.current_block as u64 * block_size
+ self.block_pos;
self.reader
.seek(SeekFrom::Start(offset))
.map_err(disk_core::io_other)?;
self.reader.read(&mut buf[..to_read])?
} else {
buf[..to_read].fill(0);
to_read
};
let mut remaining_in_n = n as u64;
if self.block_pos > 0 {
let want = block_size - self.block_pos;
let take = remaining_in_n.min(want);
self.block_pos += take;
remaining_in_n -= take;
if self.block_pos >= block_size {
self.block_pos = 0;
self.current_block += 1;
}
}
let whole = (remaining_in_n / block_size) as u32;
self.current_block += whole;
remaining_in_n -= whole as u64 * block_size;
if remaining_in_n > 0 {
self.block_pos = remaining_in_n;
}
Ok(n)
}
}
pub fn fit_hfs_volume_to_partition(
device: &mut (impl Read + Write + Seek),
partition_offset: u64,
partition_len: u64,
log: &mut impl FnMut(&str),
) -> anyhow::Result<bool> {
device.seek(SeekFrom::Start(partition_offset + 1024))?;
let mut sector = [0u8; 512];
device.read_exact(&mut sector)?;
if BigEndian::read_u16(§or[0..2]) != HFS_SIGNATURE {
return Ok(false);
}
if BigEndian::read_u16(§or[0x7C..0x7E]) == 0x482B {
return Ok(false);
}
let total_blocks = BigEndian::read_u16(§or[0x12..0x14]) as u64;
let block_size = BigEndian::read_u32(§or[0x14..0x18]) as u64;
let first_alloc = BigEndian::read_u16(§or[0x1C..0x1E]) as u64;
let volume_len = first_alloc * 512 + total_blocks * block_size + 1024;
if block_size == 0 || volume_len >= partition_len {
return Ok(false);
}
let growable = hfs_max_growable_size(device, partition_offset).unwrap_or(0);
if growable < partition_len {
log(&format!(
"Warning: the HFS volume ({volume_len} bytes) does not fill its {partition_len}-byte \
partition and its volume bitmap cannot address more blocks; Disk First Aid will \
report an invalid allocation block start. Format the volume at the partition size"
));
return Ok(false);
}
resize_hfs_in_place(device, partition_offset, partition_len, log)?;
log(&format!(
"HFS volume grown from {volume_len} bytes to fill its {partition_len}-byte partition"
));
Ok(true)
}
pub fn hfs_max_growable_size(
device: &mut (impl Read + Seek),
partition_offset: u64,
) -> Option<u64> {
device.seek(SeekFrom::Start(partition_offset + 1024)).ok()?;
let mut sector = [0u8; 512];
device.read_exact(&mut sector).ok()?;
let sig = BigEndian::read_u16(§or[0..2]);
if sig != HFS_SIGNATURE {
return None;
}
let block_size = BigEndian::read_u32(§or[20..24]) as u64;
let vbm_start = BigEndian::read_u16(§or[14..16]) as u64;
let first_alloc = BigEndian::read_u16(§or[28..30]) as u64;
if block_size == 0 || first_alloc <= vbm_start {
return None;
}
let vbm_capacity = (first_alloc - vbm_start) * 512 * 8;
let max_blocks = vbm_capacity.min(u16::MAX as u64);
let overhead = first_alloc * 512 + 1024;
Some(max_blocks * block_size + overhead)
}
pub fn resize_hfs_in_place(
device: &mut (impl Read + Write + Seek),
partition_offset: u64,
new_size_bytes: u64,
log: &mut impl FnMut(&str),
) -> anyhow::Result<()> {
device.seek(SeekFrom::Start(partition_offset + 1024))?;
let mut sector = [0u8; 512];
device.read_exact(&mut sector)?;
let sig = BigEndian::read_u16(§or[0..2]);
if sig != HFS_SIGNATURE {
log("HFS resize: not an HFS volume, skipping");
return Ok(());
}
let block_size = BigEndian::read_u32(§or[20..24]);
let old_total = BigEndian::read_u16(§or[18..20]);
let free_blocks = BigEndian::read_u16(§or[34..36]);
let vbm_start = BigEndian::read_u16(§or[14..16]);
let first_alloc = BigEndian::read_u16(§or[28..30]);
let used_blocks = old_total - free_blocks;
let vbm_capacity_blocks = if first_alloc > vbm_start {
(first_alloc - vbm_start) as u64 * 512 * 8
} else {
0
};
let overhead = first_alloc as u64 * 512 + 1024;
if new_size_bytes <= overhead {
anyhow::bail!(
"HFS resize: new size {} bytes <= overhead {} bytes",
new_size_bytes,
overhead
);
}
let new_total_u64 = (new_size_bytes - overhead) / block_size as u64;
if new_total_u64 > u16::MAX as u64 {
let max_bytes = u16::MAX as u64 * block_size as u64 + overhead;
anyhow::bail!(
"HFS resize: requested size {} MB needs {} allocation blocks at {}-byte blocks, \
but HFS caps at 65535 blocks (max {} MB for this volume). \
Growing further would require reformatting with a larger allocation block size.",
new_size_bytes / 1024 / 1024,
new_total_u64,
block_size,
max_bytes / 1024 / 1024,
);
}
if new_total_u64 > vbm_capacity_blocks {
anyhow::bail!(
"HFS resize: requested {} allocation blocks exceeds the Volume Bitmap capacity \
({} blocks) for this volume. The VBM lives in fixed sectors [{}..{}) and cannot \
be grown without reformatting: the most this volume can grow to is {} bytes. \
Mac OS wants a classic HFS volume to fill its partition, so format one at the \
partition size instead (`new volume hfs --size`, then `new hd --fill`).",
new_total_u64,
vbm_capacity_blocks,
vbm_start,
first_alloc,
vbm_capacity_blocks.min(u16::MAX as u64) * block_size as u64 + overhead,
);
}
let new_total = new_total_u64 as u16;
let vbm_len = (first_alloc - vbm_start) as usize * 512;
let mut vbm = vec![0u8; vbm_len];
device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
device.read_exact(&mut vbm)?;
let highest_used = find_last_set_bit(&vbm, old_total as u32).map_or(0, |b| b + 1);
if (new_total as u32) < highest_used {
anyhow::bail!(
"HFS resize: new size {} blocks ends before the last allocated block ({}); \
{} of {} blocks are in use",
new_total,
highest_used,
used_blocks,
old_total
);
}
for b in old_total.min(new_total) as u32..old_total.max(new_total) as u32 {
bitmap_clear_bit_be(&mut vbm, b);
}
device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
device.write_all(&vbm)?;
let new_free = new_total - used_blocks;
log(&format!(
"HFS resize: {} -> {} blocks ({} free)",
old_total, new_total, new_free
));
BigEndian::write_u16(&mut sector[18..20], new_total);
BigEndian::write_u16(&mut sector[34..36], new_free);
device.seek(SeekFrom::Start(partition_offset + 1024))?;
device.write_all(§or)?;
if new_size_bytes > 1024 {
device.seek(SeekFrom::Start(partition_offset + new_size_bytes - 1024))?;
device.write_all(§or)?;
}
device.flush()?;
Ok(())
}
pub fn validate_hfs_integrity(
device: &mut (impl Read + Seek),
partition_offset: u64,
log: &mut impl FnMut(&str),
) -> anyhow::Result<()> {
device.seek(SeekFrom::Start(partition_offset + 1024))?;
let mut sector = [0u8; 512];
device.read_exact(&mut sector)?;
let sig = BigEndian::read_u16(§or[0..2]);
if sig != HFS_SIGNATURE {
log("HFS validate: not an HFS volume, skipping");
return Ok(());
}
let block_size = BigEndian::read_u32(§or[20..24]);
let total_blocks = BigEndian::read_u16(§or[18..20]);
if !block_size.is_power_of_two() || block_size < 512 {
anyhow::bail!("HFS validate: invalid block size {block_size}");
}
if total_blocks == 0 {
anyhow::bail!("HFS validate: zero total blocks");
}
log(&format!(
"HFS validate: OK ({total_blocks} blocks, {} block size)",
block_size
));
Ok(())
}
#[cfg(test)]
#[allow(clippy::identity_op)] mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn write_blank_hfs_into_matches_create_blank_hfs() {
let (size, bs) = (8 * 1024 * 1024u64, 4096u32);
let now = 0xB0A1_2345;
let mem = create_blank_hfs_sized_at(size, bs, "StreamEq", 0, 0, now).unwrap();
let mut sink = Cursor::new(vec![0u8; 0]);
let written = write_blank_hfs_into_at(&mut sink, 0, size, bs, "StreamEq", now).unwrap();
let mut streamed = sink.into_inner();
streamed.resize(written as usize, 0);
assert_eq!(written as usize, mem.len());
assert_eq!(streamed, mem);
}
#[test]
fn write_blank_hfs_into_honours_the_offset() {
let (size, bs) = (4 * 1024 * 1024u64, 2048u32);
let now = 0xB0A1_2345;
let mem = create_blank_hfs_sized_at(size, bs, "Offset", 0, 0, now).unwrap();
let offset = 64 * 512u64;
let mut sink = Cursor::new(vec![0u8; 0]);
let written = write_blank_hfs_into_at(&mut sink, offset, size, bs, "Offset", now).unwrap();
let mut streamed = sink.into_inner();
streamed.resize((offset + written) as usize, 0);
assert!(streamed[..offset as usize].iter().all(|&b| b == 0));
assert_eq!(&streamed[offset as usize..], &mem[..]);
}
#[test]
fn test_mac_roman_to_utf8_ascii() {
let data = b"Hello World";
assert_eq!(mac_roman_to_utf8(data), "Hello World");
}
#[test]
fn test_mac_roman_to_utf8_special() {
let data = [0x80, 0x81, 0x87];
assert_eq!(mac_roman_to_utf8(&data), "ÄÅá");
}
#[test]
fn test_decode_mac_filename_utf8() {
let mut data = b"HeapFixer".to_vec();
data.extend_from_slice(&[0xE2, 0x84, 0xA2]);
data.extend_from_slice(b" 1.01");
assert_eq!(decode_mac_filename(&data), "HeapFixer™ 1.01");
}
#[test]
fn test_decode_mac_filename_macroman() {
assert_eq!(decode_mac_filename(&[0xC4]), "ƒ");
assert_eq!(decode_mac_filename(b"Cache\xA5Flow"), "Cache•Flow");
assert_eq!(decode_mac_filename(b"Virex\xA8 3.51"), "Virex® 3.51");
}
#[test]
fn test_decode_mac_filename_ascii() {
assert_eq!(decode_mac_filename(b"ScriptEdit"), "ScriptEdit");
}
#[test]
fn test_format_ostype() {
assert_eq!(format_ostype(&[b'S', 0xA5, b'L', b' ']), "S•L");
assert_eq!(format_ostype(b"APPL"), "APPL");
assert_eq!(format_ostype(&[b'L', b's', 0x00, 0x00]), "Ls");
}
#[test]
fn test_mdb_parse() {
let mut data = [0u8; 162];
BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
BigEndian::write_u16(&mut data[18..20], 1000);
BigEndian::write_u32(&mut data[20..24], 4096);
BigEndian::write_u16(&mut data[34..36], 200);
data[36] = 7;
data[37..44].copy_from_slice(b"TestVol");
let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
assert_eq!(mdb.signature, HFS_SIGNATURE);
assert_eq!(mdb.total_blocks, 1000);
assert_eq!(mdb.block_size, 4096);
assert_eq!(mdb.free_blocks, 200);
assert_eq!(mdb.volume_name, "TestVol");
assert!(!mdb.has_embedded_hfs_plus());
}
#[test]
fn test_mdb_detect_embedded_hfs_plus() {
let mut data = [0u8; 162];
BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
BigEndian::write_u16(&mut data[18..20], 1000);
BigEndian::write_u32(&mut data[20..24], 4096);
BigEndian::write_u16(&mut data[124..126], HFS_PLUS_EMBEDDED_SIGNATURE);
BigEndian::write_u16(&mut data[126..128], 10); BigEndian::write_u16(&mut data[128..130], 500);
let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
assert!(mdb.has_embedded_hfs_plus());
}
#[test]
fn test_find_last_set_bit() {
let bitmap = [0b10100000, 0b00001000]; assert_eq!(find_last_set_bit(&bitmap, 16), Some(12));
let bitmap_empty = [0u8; 2];
assert_eq!(find_last_set_bit(&bitmap_empty, 16), None);
let bitmap_first = [0b10000000]; assert_eq!(find_last_set_bit(&bitmap_first, 8), Some(0));
}
#[test]
fn test_utf8_to_mac_roman_roundtrip() {
let text = "Hello World";
let mac = utf8_to_mac_roman(text).unwrap();
assert_eq!(mac, b"Hello World");
assert_eq!(mac_roman_to_utf8(&mac), text);
}
#[test]
fn test_utf8_to_mac_roman_special() {
let text = "Ä"; let mac = utf8_to_mac_roman(text).unwrap();
assert_eq!(mac, &[0x80]);
assert_eq!(mac_roman_to_utf8(&mac), text);
}
#[test]
fn test_utf8_to_mac_roman_unencodable() {
let text = "\u{4E2D}"; assert!(utf8_to_mac_roman(text).is_err());
}
#[test]
fn classic_hfs_stamps_are_local_time_and_round_trip_to_utc() {
use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
use disk_core::times::{local_utc_offset_secs, unix_to_mac_epoch, UnixTimes};
const T: u64 = 1_700_000_000; let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Zone").unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
let root = fs.root().unwrap();
let opts = CreateFileOptions {
unix_times: Some(UnixTimes::mtime_only(T)),
..Default::default()
};
fs.create_file(&root, "stamp", &mut &b"x"[..], 1, &opts)
.unwrap();
fs.sync_metadata().unwrap();
let entry = fs
.list_directory(&root)
.unwrap()
.into_iter()
.find(|e| e.name == "stamp")
.unwrap();
assert_eq!(entry.modified_unix, Some(T), "UTC must round-trip");
let raw = entry.mac_dates.expect("raw Mac dates").1;
let expected = (T as i64 + local_utc_offset_secs()) as u64;
assert_eq!(
raw,
unix_to_mac_epoch(expected),
"on disk it is the local wall clock"
);
}
#[test]
fn the_alternate_mdb_follows_the_partition_end_when_the_length_is_known() {
use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
const VOLUME: u64 = 8 * 1024 * 1024;
const PARTITION: u64 = VOLUME + 64 * 1024;
let mut img = create_blank_hfs(VOLUME, 4096, "Slack").unwrap();
img.resize(PARTITION as usize, 0);
{
let mut fs =
HfsFilesystem::open_sized(Cursor::new(&mut img), 0, Some(PARTITION)).unwrap();
let root = fs.root().unwrap();
fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
.unwrap();
fs.sync_metadata().unwrap();
}
let primary = &img[1024..1536];
let alt = &img[PARTITION as usize - 1024..PARTITION as usize - 512];
assert_eq!(
&alt[0..2],
b"BD",
"alternate MDB missing at the partition end"
);
assert_eq!(primary, alt, "the mirror must equal the primary MDB");
}
#[test]
fn create_file_writes_the_finder_flags_it_is_given() {
use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Flags").unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
let root = fs.root().unwrap();
let opts = CreateFileOptions {
os_type: Some(*b"APPL"),
os_creator: Some(*b"ttxt"),
finder_flags: Some(0x2000),
..Default::default()
};
fs.create_file(&root, "app", &mut &b"x"[..], 1, &opts)
.unwrap();
fs.sync_metadata().unwrap();
let entry = fs
.list_directory(&root)
.unwrap()
.into_iter()
.find(|e| e.name == "app")
.unwrap();
assert_eq!(entry.finder_flags, Some(0x2000));
}
#[test]
fn shrink_is_refused_when_data_sits_past_the_new_end() {
use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
const SIZE: u64 = 8 * 1024 * 1024;
let mut img = create_blank_hfs(SIZE, 4096, "Tail").unwrap();
{
let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
let root = fs.root().unwrap();
fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
.unwrap();
fs.sync_metadata().unwrap();
}
let total = BigEndian::read_u16(&img[1024 + 18..1024 + 20]) as usize;
let vbm_start = BigEndian::read_u16(&img[1024 + 14..1024 + 16]) as usize;
let tail = total - 1;
let byte = vbm_start * 512 + tail / 8;
img[byte] |= 0x80 >> (tail % 8);
let err = resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
.expect_err("a tail block in use must refuse the shrink");
assert!(format!("{err}").contains("last allocated block"), "{err}");
img[byte] &= !(0x80 >> (tail % 8));
resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
.expect("with the tail free the shrink goes through");
}
#[test]
fn test_resize_hfs_rejects_u16_overflow() {
let mut sector = [0u8; 512];
BigEndian::write_u16(&mut sector[0..2], HFS_SIGNATURE);
BigEndian::write_u16(&mut sector[14..16], 3); BigEndian::write_u16(&mut sector[18..20], 64110); BigEndian::write_u32(&mut sector[20..24], 8192); BigEndian::write_u16(&mut sector[28..30], 19); BigEndian::write_u16(&mut sector[34..36], 31423);
let mut img = vec![0u8; 2048];
img[1024..1024 + 512].copy_from_slice(§or);
let mut cursor = Cursor::new(img);
let res = resize_hfs_in_place(
&mut cursor,
0,
2047 * 1024 * 1024, &mut |_| {},
);
let err = res.expect_err("resize should have bailed on u16 overflow");
let msg = format!("{err}");
assert!(
msg.contains("65535") || msg.contains("u16") || msg.contains("Volume Bitmap"),
"unexpected error: {msg}"
);
}
#[test]
fn test_mdb_serialize_roundtrip() {
let mut data = [0u8; 512];
BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
BigEndian::write_u16(&mut data[18..20], 100);
BigEndian::write_u32(&mut data[20..24], 4096);
BigEndian::write_u16(&mut data[34..36], 50);
BigEndian::write_u32(&mut data[30..34], 10); BigEndian::write_u32(&mut data[92..96], 42); data[36] = 4;
data[37..41].copy_from_slice(b"Test");
let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
let serialized = mdb.serialize_to_sector();
assert_eq!(BigEndian::read_u16(&serialized[18..20]), 100);
assert_eq!(BigEndian::read_u32(&serialized[30..34]), 10);
assert_eq!(BigEndian::read_u32(&serialized[92..96]), 42);
}
fn make_editable_hfs_image() -> Vec<u8> {
let block_size = 4096u32;
let total_blocks = 128u16;
let first_alloc_block = 5u16; let alloc_start = first_alloc_block as usize * 512;
let image_size = alloc_start + total_blocks as usize * block_size as usize;
let mut img = vec![0u8; image_size];
let catalog_start = 0u16; let catalog_blocks = 8u16;
let catalog_size = catalog_blocks as u32 * block_size;
let bitmap_sector = 3u16;
let bitmap_off = bitmap_sector as usize * 512;
img[bitmap_off] = 0b11111111;
let node_size = 4096usize;
let catalog_offset = alloc_start;
let hdr_off = catalog_offset;
img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3);
let hr = hdr_off + 14;
BigEndian::write_u16(&mut img[hr..hr + 2], 1); BigEndian::write_u32(&mut img[hr + 2..hr + 6], 1); BigEndian::write_u32(&mut img[hr + 6..hr + 10], 2); BigEndian::write_u32(&mut img[hr + 10..hr + 14], 1); BigEndian::write_u32(&mut img[hr + 14..hr + 18], 1); BigEndian::write_u16(&mut img[hr + 18..hr + 20], node_size as u16); BigEndian::write_u16(&mut img[hr + 20..hr + 22], 37); BigEndian::write_u32(&mut img[hr + 22..hr + 26], 8); BigEndian::write_u32(&mut img[hr + 26..hr + 30], 6);
let ot = hdr_off + node_size;
BigEndian::write_u16(&mut img[ot - 2..ot], 14); BigEndian::write_u16(&mut img[ot - 4..ot - 2], 142); BigEndian::write_u16(&mut img[ot - 6..ot - 4], 270); BigEndian::write_u16(&mut img[ot - 8..ot - 6], 526);
img[hdr_off + 270] = 0b11000000;
let leaf_off = catalog_offset + node_size;
img[leaf_off + 8] = 0xFF; img[leaf_off + 9] = 1; BigEndian::write_u16(&mut img[leaf_off + 10..leaf_off + 12], 2);
let r0_off = leaf_off + 14;
img[r0_off] = 6; img[r0_off + 1] = 0; BigEndian::write_u32(&mut img[r0_off + 2..r0_off + 6], 1); img[r0_off + 6] = 0; img[r0_off + 7] = 0;
let r0_data = r0_off + 8;
img[r0_data] = CATALOG_DIR as u8; BigEndian::write_u32(&mut img[r0_data + 6..r0_data + 10], 2);
let r1_off = r0_data + 70;
img[r1_off] = 6; img[r1_off + 1] = 0; BigEndian::write_u32(&mut img[r1_off + 2..r1_off + 6], 2); img[r1_off + 6] = 0; img[r1_off + 7] = 0; let r1_data = r1_off + 8;
img[r1_data] = CATALOG_DIR_THREAD as u8; img[r1_data + 1] = 0; BigEndian::write_u32(&mut img[r1_data + 10..r1_data + 14], 1); img[r1_data + 14] = 0;
let lot = leaf_off + node_size;
BigEndian::write_u16(&mut img[lot - 2..lot], 14); let r1_rel = (r1_off - leaf_off) as u16;
BigEndian::write_u16(&mut img[lot - 4..lot - 2], r1_rel); let free_rel = (r1_data + 46 - leaf_off) as u16;
BigEndian::write_u16(&mut img[lot - 6..lot - 4], free_rel);
let mdb_off = 1024;
let mut mdb = [0u8; 512];
BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
BigEndian::write_u32(&mut mdb[2..6], hfs_common::hfs_now()); BigEndian::write_u32(&mut mdb[6..10], hfs_common::hfs_now()); BigEndian::write_u16(&mut mdb[14..16], bitmap_sector); BigEndian::write_u16(&mut mdb[18..20], total_blocks);
BigEndian::write_u32(&mut mdb[20..24], block_size);
BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
BigEndian::write_u32(&mut mdb[30..34], 16); BigEndian::write_u16(&mut mdb[34..36], total_blocks - catalog_blocks); mdb[36] = 7;
mdb[37..44].copy_from_slice(b"TestVol");
BigEndian::write_u32(&mut mdb[84..88], 0); BigEndian::write_u32(&mut mdb[88..92], 0); BigEndian::write_u32(&mut mdb[146..150], catalog_size);
BigEndian::write_u16(&mut mdb[150..152], catalog_start);
BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
img
}
fn make_uninitialized_hfs_image() -> Vec<u8> {
let block_size = 512u32;
let total_blocks = 256u16;
let first_alloc_block = 6u16;
let alloc_start = first_alloc_block as usize * 512;
let image_size = alloc_start + total_blocks as usize * block_size as usize;
let mut img = vec![0u8; image_size];
let extents_start = 0u16;
let extents_blocks = 4u16;
let catalog_start = 4u16;
let catalog_blocks = 4u16;
let mdb_off = 1024usize;
let mut mdb = [0u8; 512];
BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
BigEndian::write_u32(&mut mdb[2..6], hfs_common::hfs_now());
BigEndian::write_u32(&mut mdb[6..10], hfs_common::hfs_now());
BigEndian::write_u16(&mut mdb[14..16], 3); BigEndian::write_u16(&mut mdb[18..20], total_blocks);
BigEndian::write_u32(&mut mdb[20..24], block_size);
BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
BigEndian::write_u32(&mut mdb[30..34], 16);
BigEndian::write_u16(
&mut mdb[34..36],
total_blocks - extents_blocks - catalog_blocks,
);
mdb[36] = 5;
mdb[37..42].copy_from_slice(b"Blank");
BigEndian::write_u32(&mut mdb[130..134], extents_blocks as u32 * block_size);
BigEndian::write_u16(&mut mdb[134..136], extents_start);
BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
BigEndian::write_u32(&mut mdb[146..150], catalog_blocks as u32 * block_size);
BigEndian::write_u16(&mut mdb[150..152], catalog_start);
BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
img
}
#[test]
fn test_hfs_empty_volume_auto_initialize_and_create_file() {
let img = make_uninitialized_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
assert_eq!(fs.fs_type(), "HFS");
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
assert!(entries.is_empty());
let data = b"auto-init test";
let mut cursor = Cursor::new(data.as_slice());
let fe = fs
.create_file(
&root,
"hello.txt",
&mut cursor,
data.len() as u64,
&CreateFileOptions::default(),
)
.unwrap();
assert_eq!(fe.name, "hello.txt");
fs.sync_metadata().unwrap();
let entries = fs.list_directory(&root).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "hello.txt");
let read_back = fs.read_file(&fe, 1024).unwrap();
assert_eq!(&read_back, data);
let result = fs.fsck().unwrap();
let err_msgs: Vec<_> = result.errors.iter().map(|e| e.message.clone()).collect();
let warn_msgs: Vec<_> = result.warnings.iter().map(|w| w.message.clone()).collect();
assert!(
result.errors.is_empty(),
"unexpected fsck errors: {err_msgs:?}"
);
assert!(
result.warnings.is_empty(),
"unexpected fsck warnings: {warn_msgs:?}"
);
}
#[test]
fn test_hfs_editable_open_and_free_space() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
assert_eq!(fs.fs_type(), "HFS");
assert_eq!(fs.volume_label(), Some("TestVol"));
let free = fs.free_space().unwrap();
assert_eq!(free, 120 * 4096);
}
#[test]
fn test_hfs_editable_create_file_and_read() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let test_data = b"Hello, HFS World!";
let mut data_reader = Cursor::new(test_data.as_slice());
let options = CreateFileOptions::default();
let fe = fs
.create_file(
&root,
"test.txt",
&mut data_reader,
test_data.len() as u64,
&options,
)
.unwrap();
assert_eq!(fe.name, "test.txt");
assert_eq!(fe.size, test_data.len() as u64);
let entries = fs.list_directory(&root).unwrap();
assert!(entries.iter().any(|e| e.name == "test.txt"));
let read_back = fs.read_file(&fe, 1024).unwrap();
assert_eq!(&read_back, test_data);
}
#[test]
fn test_hfs_file_record_first_alloc_block_fields_are_zero() {
use crate::hfs_common::walk_leaf_records;
use disk_core::filesystem::ResourceForkSource;
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let data = b"data fork contents that span a block";
let options = CreateFileOptions {
resource_fork: Some(ResourceForkSource::Data(b"resource fork bytes".to_vec())),
..Default::default()
};
fs.create_file(
&root,
"forked.bin",
&mut Cursor::new(data.as_slice()),
data.len() as u64,
&options,
)
.unwrap();
fs.sync_metadata().unwrap();
let node_size = BigEndian::read_u16(&fs.catalog_data[32..34]) as usize;
let first_leaf = BigEndian::read_u32(&fs.catalog_data[24..28]);
let found = walk_leaf_records::<bool, _>(
&fs.catalog_data,
first_leaf,
node_size,
|_n, _i, _off, rec| {
let klen = rec[0] as usize;
let mut ds = 1 + klen;
if !ds.is_multiple_of(2) {
ds += 1;
}
let body = &rec[ds..];
if body.first() != Some(&2) {
return None; }
let fil_st_blk = BigEndian::read_u16(&body[24..26]);
let fil_rst_blk = BigEndian::read_u16(&body[34..36]);
let data_ext_start = BigEndian::read_u16(&body[74..76]);
let rsrc_ext_start = BigEndian::read_u16(&body[86..88]);
assert_eq!(fil_st_blk, 0, "filStBlk (offset 24) must be 0 on disk");
assert_eq!(fil_rst_blk, 0, "filRStBlk (offset 34) must be 0 on disk");
assert_ne!(data_ext_start, 0, "data fork extent should be allocated");
assert_ne!(rsrc_ext_start, 0, "rsrc fork extent should be allocated");
Some(true)
},
);
assert_eq!(found, Some(true), "file record not found in catalog");
}
#[test]
fn test_hfs_catalog_dates_round_trip() {
let img = make_editable_hfs_image();
let mut buf = img.clone();
{
let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
let root = fs.root().unwrap();
let data = b"x";
let fe = fs
.create_file(
&root,
"dated.txt",
&mut Cursor::new(data.as_slice()),
1,
&CreateFileOptions::default(),
)
.unwrap();
fs.sync_metadata().unwrap();
let listed = fs.list_directory(&root).unwrap();
let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
let (c, m, b) = f.mac_dates.expect("HFS file should carry mac_dates");
assert!(c != 0 && m != 0, "create/modify stamped by create_file");
assert_eq!(b, 0, "backup date defaults to 0");
let target = (0x10000000u32, 0x20000000u32, 0x30000000u32);
fs.set_dates(&fe, target.0, target.1, target.2).unwrap();
fs.sync_metadata().unwrap();
}
let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
let root = fs.root().unwrap();
let listed = fs.list_directory(&root).unwrap();
let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
assert_eq!(f.mac_dates, Some((0x10000000, 0x20000000, 0x30000000)));
assert_eq!(f.modified, hfs_common::format_mac_date(0x20000000));
}
#[test]
fn test_hfs_editable_create_directory() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let options = CreateDirectoryOptions::default();
let dir = fs.create_directory(&root, "NewDir", &options).unwrap();
assert_eq!(dir.name, "NewDir");
assert!(dir.is_directory());
let entries = fs.list_directory(&root).unwrap();
assert!(entries
.iter()
.any(|e| e.name == "NewDir" && e.is_directory()));
}
#[test]
fn test_hfs_editable_rename_file() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let data = b"keep my bytes across a rename";
let mut r = Cursor::new(data.as_slice());
let fe = fs
.create_file(
&root,
"Before",
&mut r,
data.len() as u64,
&CreateFileOptions::default(),
)
.unwrap();
let cnid = fe.location;
fs.rename(&root, &fe, "After Doc").unwrap();
let entries = fs.list_directory(&root).unwrap();
assert!(entries.iter().any(|e| e.name == "After Doc"), "{entries:?}");
assert!(!entries.iter().any(|e| e.name == "Before"), "{entries:?}");
let renamed = entries.iter().find(|e| e.name == "After Doc").unwrap();
assert_eq!(renamed.location, cnid);
assert_eq!(&fs.read_file(renamed, 1024).unwrap(), data);
fs.create_directory(&root, "OldDir", &CreateDirectoryOptions::default())
.unwrap();
let old_dir = fs
.list_directory(&root)
.unwrap()
.into_iter()
.find(|e| e.name == "OldDir")
.unwrap();
fs.rename(&root, &old_dir, "NewDir").unwrap();
let new_dir = fs
.list_directory(&root)
.unwrap()
.into_iter()
.find(|e| e.name == "NewDir")
.expect("renamed dir present");
assert!(fs.list_directory(&new_dir).is_ok());
let after = fs
.list_directory(&root)
.unwrap()
.into_iter()
.find(|e| e.name == "After Doc")
.unwrap();
assert!(matches!(
fs.rename(&root, &after, "NewDir"),
Err(FilesystemError::AlreadyExists(_))
));
fs.sync_metadata().unwrap();
}
#[test]
fn test_hfs_editable_delete_file() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let test_data = b"delete me";
let mut data_reader = Cursor::new(test_data.as_slice());
let options = CreateFileOptions::default();
let fe = fs
.create_file(
&root,
"gone.txt",
&mut data_reader,
test_data.len() as u64,
&options,
)
.unwrap();
let free_before = fs.free_space().unwrap();
fs.delete_entry(&root, &fe).unwrap();
let entries = fs.list_directory(&root).unwrap();
assert!(!entries.iter().any(|e| e.name == "gone.txt"));
let free_after = fs.free_space().unwrap();
assert!(free_after > free_before);
}
#[test]
fn test_hfs_editable_duplicate_name_rejected() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let test_data = b"first";
let mut r1 = Cursor::new(test_data.as_slice());
let options = CreateFileOptions::default();
fs.create_file(&root, "dup.txt", &mut r1, 5, &options)
.unwrap();
let mut r2 = Cursor::new(test_data.as_slice());
let result = fs.create_file(&root, "dup.txt", &mut r2, 5, &options);
assert!(matches!(result, Err(FilesystemError::AlreadyExists(_))));
}
#[test]
fn test_hfs_editable_name_too_long() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let long_name = "a".repeat(32); let test_data = b"x";
let mut r = Cursor::new(test_data.as_slice());
let options = CreateFileOptions::default();
let result = fs.create_file(&root, &long_name, &mut r, 1, &options);
assert!(matches!(result, Err(FilesystemError::InvalidData(_))));
}
#[test]
fn test_hfs_editable_blessed_folder() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
assert!(fs.blessed_system_folder().is_none());
let root = fs.root().unwrap();
let options = CreateDirectoryOptions::default();
let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
fs.set_blessed_folder(&sys_dir).unwrap();
let blessed = fs.blessed_system_folder();
assert!(blessed.is_some());
let (cnid, name) = blessed.unwrap();
assert_eq!(cnid, sys_dir.location);
assert_eq!(name, "System");
}
#[test]
fn test_hfs_editable_type_creator_auto_detect() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let test_data = b"text content";
let mut data_reader = Cursor::new(test_data.as_slice());
let options = CreateFileOptions::default();
let fe = fs
.create_file(
&root,
"readme.txt",
&mut data_reader,
test_data.len() as u64,
&options,
)
.unwrap();
assert!(fe.type_code.is_some());
assert_eq!(fe.type_code, Some(*b"TEXT"));
}
fn assert_fsck_clean<R: Read + Seek>(fs: &mut HfsFilesystem<R>) {
let result = fs.fsck().unwrap();
if !result.is_clean() {
let msgs: Vec<_> = result
.errors
.iter()
.map(|e| format!("[{}] {}", e.code, e.message))
.collect();
panic!("fsck found {} error(s):\n{}", msgs.len(), msgs.join("\n"));
}
}
#[test]
fn test_fsck_clean_fresh_image() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let result = fs.fsck().unwrap();
let err_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
assert!(
result.is_clean(),
"fresh image should be clean: {:?}",
err_msgs
);
assert_eq!(result.stats.directories_checked, 1); assert_eq!(result.stats.files_checked, 0);
}
#[test]
fn test_fsck_clean_after_create_file() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let test_data = b"Hello, HFS World!";
let mut data_reader = Cursor::new(test_data.as_slice());
let options = CreateFileOptions::default();
fs.create_file(
&root,
"test.txt",
&mut data_reader,
test_data.len() as u64,
&options,
)
.unwrap();
assert_fsck_clean(&mut fs);
}
#[test]
fn test_fsck_clean_after_create_directory() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let options = CreateDirectoryOptions::default();
fs.create_directory(&root, "NewDir", &options).unwrap();
assert_fsck_clean(&mut fs);
}
#[test]
fn test_fsck_clean_after_delete_file() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let test_data = b"delete me";
let mut data_reader = Cursor::new(test_data.as_slice());
let options = CreateFileOptions::default();
let fe = fs
.create_file(
&root,
"gone.txt",
&mut data_reader,
test_data.len() as u64,
&options,
)
.unwrap();
assert_fsck_clean(&mut fs);
fs.delete_entry(&root, &fe).unwrap();
assert_fsck_clean(&mut fs);
}
#[test]
fn test_hfs_random_insert_packs_densely_and_fsck_clean() {
use crate::hfs_common::BTreeHeader;
type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
let block_size = 32 * 1024u32;
let img = create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Pack", 0, 16 * 1024 * 1024)
.unwrap();
let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
let root_id = fs.root().unwrap().location as u32;
let n = 16000u32;
let mut seen = std::collections::HashSet::new();
let order: Vec<u32> = (0..n)
.map(|i| i.wrapping_mul(2654435761) % n)
.filter(|x| seen.insert(*x))
.collect();
let count = order.len() as u32;
fs.begin_bulk();
for &i in &order {
let name = format!("f{:05}", i);
let mut kr = Fs::build_catalog_key(root_id, name.as_bytes());
kr.extend_from_slice(&Fs::build_file_record(
16 + i,
0,
0,
0,
0,
0,
0,
&[0u8; 4],
&[0u8; 4],
block_size,
None,
0,
));
fs.insert_catalog_record(&kr).unwrap();
}
fs.end_bulk();
fs.mdb.file_count = count;
fs.mdb.next_catalog_id = 16 + n;
fs.update_parent_valence(root_id, count as i32).unwrap();
let h = BTreeHeader::read(fs.catalog_data());
let used = h.total_nodes - h.free_nodes;
let rec_per_node = count as f64 / used as f64;
assert!(
rec_per_node > 2.8,
"random-order packing too sparse: {count} records in {used} nodes ({rec_per_node:.2}/node)"
);
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"fsck errors after random import: {:?}",
result
.errors
.iter()
.take(8)
.map(|e| &e.message)
.collect::<Vec<_>>()
);
}
#[test]
fn test_hfs_bulk_mode_import_fsck_clean() {
let block_size = 32 * 1024u32;
let img = create_blank_hfs_sized(
64 * 1024 * 1024,
block_size,
"BulkImport",
0,
8 * 1024 * 1024,
)
.expect("create blank volume");
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let root_id = root.location as u32;
let options = CreateFileOptions::default();
fs.begin_bulk();
for i in 0..9000 {
let name = format!("f{:05}.txt", i);
let mut empty = Cursor::new(Vec::new());
fs.create_file(&root, &name, &mut empty, 0, &options)
.unwrap_or_else(|e| panic!("bulk create_file #{i}: {e}"));
}
fs.end_bulk();
assert_eq!(fs.mdb.file_count, 9000);
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"bulk-mode import left fsck errors: {:?}",
result
.errors
.iter()
.take(8)
.map(|e| &e.message)
.collect::<Vec<_>>()
);
assert!(fs
.find_catalog_record_by_name(root_id, b"f04500.txt")
.is_some());
assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
let mut empty = Cursor::new(Vec::new());
assert!(fs
.create_file(&root, "f00000.txt", &mut empty, 0, &options)
.is_err());
assert_fsck_clean(&mut fs);
}
#[test]
fn test_hfs_catalog_scales_past_header_bitmap_fsck_clean() {
use crate::hfs_common::BTreeHeader;
type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
let block_size = 32 * 1024u32;
let img = create_blank_hfs_sized(
64 * 1024 * 1024,
block_size,
"Scale20k",
0,
10 * 1024 * 1024,
)
.expect("create blank volume");
let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let root_id = root.location as u32;
let n: u32 = 22_000;
for i in 0..n {
let name = format!("f{:05}.txt", i);
let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
let file_rec = Fs::build_file_record(
16 + i,
0,
0,
0,
0,
0,
0,
&[0u8; 4],
&[0u8; 4],
block_size,
None,
0,
);
key_record.extend_from_slice(&file_rec);
fs.insert_catalog_record(&key_record)
.unwrap_or_else(|e| panic!("insert #{i} ({name}): {e}"));
}
fs.mdb.file_count = n;
fs.mdb.next_catalog_id = 16 + n;
fs.update_parent_valence(root_id, n as i32).unwrap();
let header = BTreeHeader::read(fs.catalog_data());
assert!(
header.depth >= 3,
"expected a multi-level index, depth = {}",
header.depth
);
let used = header.total_nodes - header.free_nodes;
assert!(
used > 2048,
"expected > 2048 catalog nodes in use, got {used}"
);
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"fsck found {} errors after {n}-record import: {:?}",
result.errors.len(),
result
.errors
.iter()
.take(8)
.map(|e| &e.message)
.collect::<Vec<_>>()
);
for i in [0u32, 1, 7386, 12345, n - 1] {
let name = format!("f{:05}.txt", i);
assert!(
fs.find_catalog_record_by_name(root_id, name.as_bytes())
.is_some(),
"record {name} not findable after bulk insert"
);
}
}
#[test]
fn test_hfs_incremental_shuffled_multidir_packs_dense_fsck_clean() {
use crate::hfs_common::BTreeHeader;
type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
let block_size = 32 * 1024u32;
let img = create_blank_hfs_sized(
128 * 1024 * 1024,
block_size,
"PutShuffle",
0,
6 * 1024 * 1024,
)
.expect("create blank volume");
let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
const DIRS: u32 = 200;
let root = fs.root().unwrap();
let mut dir_ids = Vec::with_capacity(DIRS as usize);
for d in 0..DIRS {
let name = format!("dir{d:03}");
let de = fs
.create_directory(&root, &name, &Default::default())
.unwrap();
dir_ids.push(de.location as u32);
}
let base_cnid = fs.mdb.next_catalog_id;
let n: u32 = 20_000;
let mut per_dir = vec![0i32; DIRS as usize];
for i in 0..n {
let hash = i.wrapping_mul(2_654_435_761);
let d = (hash % DIRS) as usize;
let name = format!("f{hash:08x}");
let mut kr = Fs::build_catalog_key(dir_ids[d], name.as_bytes());
kr.extend_from_slice(&Fs::build_file_record(
base_cnid + i,
0,
0,
0,
0,
0,
0,
&[0u8; 4],
&[0u8; 4],
block_size,
None,
0,
));
fs.insert_catalog_record(&kr)
.unwrap_or_else(|e| panic!("insert #{i} into dir{d}: {e}"));
per_dir[d] += 1;
}
fs.mdb.file_count += n;
fs.mdb.next_catalog_id = base_cnid + n;
for (d, &cnt) in per_dir.iter().enumerate() {
fs.update_parent_valence(dir_ids[d], cnt).unwrap();
}
let h = BTreeHeader::read(fs.catalog_data());
let used = h.total_nodes - h.free_nodes;
let rec_per_node = h.leaf_records as f64 / used as f64;
assert!(
rec_per_node > 2.6,
"shuffled multi-dir packing too sparse: {} records in {used} nodes ({rec_per_node:.2}/node)",
h.leaf_records
);
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"fsck found {} errors after 20k shuffled multi-dir put: {:?}",
result.errors.len(),
result
.errors
.iter()
.take(8)
.map(|e| &e.message)
.collect::<Vec<_>>()
);
for i in [0u32, 7, 9_999, n - 1] {
let hash = i.wrapping_mul(2_654_435_761);
let parent = dir_ids[(hash % DIRS) as usize];
let name = format!("f{hash:08x}");
assert!(
fs.find_catalog_record_by_name(parent, name.as_bytes())
.is_some(),
"record {name} not findable after shuffled multi-dir insert"
);
}
}
#[test]
fn test_rebuild_index_nodes_past_header_bitmap_fsck_clean() {
use crate::hfs_common::BTreeHeader;
type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
let block_size = 32 * 1024u32;
let img =
create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Rebuild", 0, 10 * 1024 * 1024)
.expect("create blank volume");
let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
let root_id = fs.root().unwrap().location as u32;
let n: u32 = 12_000;
for i in 0..n {
let name = format!("f{:05}.txt", i);
let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
let file_rec = Fs::build_file_record(
16 + i,
0,
0,
0,
0,
0,
0,
&[0u8; 4],
&[0u8; 4],
block_size,
None,
0,
);
key_record.extend_from_slice(&file_rec);
fs.insert_catalog_record(&key_record).unwrap();
}
fs.mdb.file_count = n;
fs.mdb.next_catalog_id = 16 + n;
fs.update_parent_valence(root_id, n as i32).unwrap();
let node_size = BTreeHeader::read(fs.catalog_data()).node_size as usize;
let used_before = {
let h = BTreeHeader::read(fs.catalog_data());
h.total_nodes - h.free_nodes
};
assert!(
used_before > 2048,
"test needs > 2048 nodes, got {used_before}"
);
let mut report = disk_core::fsck::RepairReport {
fixes_applied: Vec::new(),
fixes_failed: Vec::new(),
unrepairable_count: 0,
};
crate::hfs_fsck::rebuild_index_nodes(&mut fs.catalog_data, node_size, &mut report);
assert!(
report.fixes_failed.is_empty(),
"rebuild reported failures: {:?}",
report.fixes_failed
);
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"fsck after rebuild: {:?}",
result
.errors
.iter()
.take(8)
.map(|e| &e.message)
.collect::<Vec<_>>()
);
}
#[test]
fn test_hfs_create_file_multilevel_index_fsck_clean() {
use crate::hfs_common::BTreeHeader;
let block_size = 32 * 1024u32;
let img =
create_blank_hfs_sized(32 * 1024 * 1024, block_size, "MultiIdx", 0, 4 * 1024 * 1024)
.expect("create blank volume");
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let options = CreateFileOptions::default();
for i in 0..2000 {
let name = format!("f{:05}.txt", i);
let mut empty = Cursor::new(Vec::new());
fs.create_file(&root, &name, &mut empty, 0, &options)
.unwrap_or_else(|e| panic!("create_file #{i}: {e}"));
}
let header = BTreeHeader::read(fs.catalog_data());
assert!(
header.depth >= 2,
"expected an index level, depth = {}",
header.depth
);
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"fsck errors: {:?}",
result
.errors
.iter()
.take(8)
.map(|e| &e.message)
.collect::<Vec<_>>()
);
assert_eq!(fs.mdb.file_count, 2000);
}
#[test]
fn deleting_a_leafs_first_record_refreshes_the_separators() {
use crate::hfs_common::{
btree_record_range, btree_stale_index_keys, BTreeHeader, BTreeKeyFormat,
};
let block_size = 32 * 1024u32;
let img = create_blank_hfs_sized(32 * 1024 * 1024, block_size, "IKey", 0, 4 * 1024 * 1024)
.expect("create blank volume");
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let options = CreateFileOptions::default();
for i in 0..2000 {
let mut empty = Cursor::new(Vec::new());
fs.create_file(&root, &format!("f{i:05}.txt"), &mut empty, 0, &options)
.unwrap();
}
let node_size = BigEndian::read_u16(&fs.catalog_data()[32..34]) as usize;
let header = BTreeHeader::read(fs.catalog_data());
assert!(
header.depth >= 2,
"need an index level, depth {}",
header.depth
);
let first = header.first_leaf_node as usize;
let second =
BigEndian::read_u32(&fs.catalog_data()[first * node_size..first * node_size + 4])
as usize;
let name = {
let node = &fs.catalog_data()[second * node_size..(second + 1) * node_size];
let (s, e) = btree_record_range(node, node_size, 0);
let rec = &node[s..e];
let name_len = rec[6] as usize;
String::from_utf8_lossy(&rec[7..7 + name_len]).into_owned()
};
assert!(
name.starts_with('f'),
"expected a file record first, got {name:?}"
);
let victim = fs
.list_directory(&root)
.unwrap()
.into_iter()
.find(|e| e.name == name)
.expect("victim listed");
fs.delete_entry(&root, &victim).unwrap();
let stale = btree_stale_index_keys(
fs.catalog_data(),
node_size,
&BTreeKeyFormat::CLASSIC_CATALOG,
&HfsFilesystem::<Cursor<Vec<u8>>>::catalog_compare,
);
assert!(stale.is_empty(), "separators still stale: {stale:?}");
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"{:?}",
result
.errors
.iter()
.map(|e| &e.message)
.take(4)
.collect::<Vec<_>>()
);
}
#[test]
fn test_hfs_control_char_filename_roundtrip() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let options = CreateFileOptions::default();
let name = "IPNetRouter\u{7f}\u{7f}_154_68k";
let mut data = Cursor::new(b"x".to_vec());
fs.create_file(&root, name, &mut data, 1, &options)
.expect("create_file with raw 0x7F bytes in the name");
let entries = fs.list_directory(&root).unwrap();
assert!(
entries.iter().any(|e| e.name == name),
"0x7F-laden name not preserved; got {:?}",
entries.iter().map(|e| &e.name).collect::<Vec<_>>()
);
assert_fsck_clean(&mut fs);
}
#[test]
fn test_hfs_null_byte_name_is_warning_not_error() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let options = CreateFileOptions::default();
let mut data = Cursor::new(b"x".to_vec());
fs.create_file(&root, "\u{0}Sorts First", &mut data, 1, &options)
.expect("create_file with a leading null byte in the name");
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"a null-byte name must not be a fsck error: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
assert!(
result
.warnings
.iter()
.any(|w| w.code == "UnusualCatalogName"),
"expected an UnusualCatalogName warning for the null-byte name"
);
}
#[test]
fn test_fsck_stress_many_files() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let options = CreateFileOptions::default();
for i in 0..35 {
let name = format!("file_{:03}.txt", i);
let data = format!("content of file {}", i);
let mut reader = Cursor::new(data.as_bytes().to_vec());
fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
.unwrap();
assert_fsck_clean(&mut fs);
}
let result = fs.fsck().unwrap();
assert!(result.is_clean());
assert_eq!(result.stats.files_checked, 35);
assert_eq!(result.stats.directories_checked, 1); }
#[test]
fn test_fsck_clean_after_blessed_folder() {
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let options = CreateDirectoryOptions::default();
let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
fs.set_blessed_folder(&sys_dir).unwrap();
assert_fsck_clean(&mut fs);
}
#[test]
fn unerased_free_nodes_warn_once_and_repair_zeroes_them() {
use crate::hfs_common::{btree_bitmap_test, BTreeHeader};
let mut fs = HfsFilesystem::open(Cursor::new(make_editable_hfs_image()), 0).unwrap();
let node_size = BTreeHeader::read(&fs.catalog_data).node_size as usize;
let free: Vec<usize> = (1..fs.catalog_data.len() / node_size)
.filter(|&i| !btree_bitmap_test(&fs.catalog_data, node_size, i as u32))
.take(3)
.collect();
assert_eq!(free.len(), 3, "blank catalog has free nodes");
for &i in &free {
fs.catalog_data[i * node_size + 20] = 0xA5;
}
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"not an error: {:?}",
result.errors
);
let warns: Vec<_> = result
.warnings
.iter()
.filter(|w| w.code == "FreeNodeNotErased")
.collect();
assert_eq!(warns.len(), 1, "one summary per tree, not one per node");
assert!(
warns[0].message.contains("3 unused node(s)"),
"{}",
warns[0].message
);
assert!(result.repairable);
let report = fs.repair().unwrap();
assert!(
report
.fixes_applied
.iter()
.any(|f| f == "Erased 3 unused catalog B-tree node(s)"),
"{:?}",
report.fixes_applied
);
let after = fs.fsck().unwrap();
assert!(after.warnings.iter().all(|w| w.code != "FreeNodeNotErased"));
assert!(after.errors.is_empty(), "{:?}", after.errors);
}
#[test]
fn test_repair_index_records_are_even_aligned() {
use crate::hfs_common::{btree_record_range, BTreeHeader};
let img = make_editable_hfs_image();
let cursor = Cursor::new(img);
let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
let root = fs.root().unwrap();
let options = CreateFileOptions::default();
for i in 0..40 {
let name = if i % 2 == 0 {
format!("f{:03}.txt", i) } else {
format!("fi{:03}.txt", i) };
let data = format!("data{}", i);
let mut reader = Cursor::new(data.as_bytes().to_vec());
fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
.unwrap();
}
let report = fs.repair().unwrap();
assert!(
report.fixes_failed.is_empty(),
"repair failures: {:?}",
report.fixes_failed
);
let cat = &fs.catalog_data;
let header = BTreeHeader::read(cat);
let node_size = header.node_size as usize;
let max_nodes = cat.len() / node_size;
let mut index_recs_checked = 0u32;
for n in 1..max_nodes {
let off = n * node_size;
if off + node_size > cat.len() {
break;
}
let kind = cat[off + 8] as i8;
if kind != 0 {
continue;
}
let num_recs = BigEndian::read_u16(&cat[off + 10..off + 12]) as usize;
for r in 0..num_recs {
let (rec_start, rec_end) =
btree_record_range(&cat[off..off + node_size], node_size, r);
if rec_start >= rec_end || rec_end > node_size {
continue;
}
let key_len = cat[off + rec_start] as usize;
let rec_len = rec_end - rec_start;
assert_eq!(
key_len, 0x25,
"Index node {} rec {}: key_len={} but expected 0x25",
n, r, key_len
);
assert_eq!(
rec_len, 42,
"Index node {} rec {}: rec_len={} but expected 42",
n, r, rec_len
);
index_recs_checked += 1;
}
}
assert!(
index_recs_checked > 0,
"no index records found — test needs more files"
);
let result = fs.fsck().unwrap();
let error_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
assert!(
result.errors.is_empty(),
"post-repair check found errors: {:?}",
error_msgs
);
}
#[test]
fn test_create_blank_hfs_roundtrip() {
let cases: &[(u64, u32, &str)] = &[
(1 * 1024 * 1024, 512, "1MB @ 512"),
(8 * 1024 * 1024, 4096, "8MB @ 4K"),
(64 * 1024 * 1024, 16384, "64MB @ 16K"),
(256 * 1024 * 1024, 32768, "256MB @ 32K"),
];
for (total_size, block_size, label) in cases {
let img = create_blank_hfs(*total_size, *block_size, "Fresh")
.unwrap_or_else(|e| panic!("{label}: create_blank_hfs failed: {e}"));
let atrb = BigEndian::read_u16(&img[1024 + 10..1024 + 12]);
assert_eq!(
atrb & 0x0100,
0x0100,
"{label}: drAtrb missing kHFSVolumeUnmountedMask (got 0x{atrb:04x})"
);
let alt_off = img.len() - 1024;
let alt_atrb = BigEndian::read_u16(&img[alt_off + 10..alt_off + 12]);
assert_eq!(
alt_atrb, atrb,
"{label}: alt MDB drAtrb (0x{alt_atrb:04x}) does not mirror primary (0x{atrb:04x})"
);
assert!(
(img.len() as u64) <= *total_size,
"{label}: image {} exceeds requested {}",
img.len(),
total_size
);
let mut fs = HfsFilesystem::open(Cursor::new(img.clone()), 0)
.unwrap_or_else(|e| panic!("{label}: open failed: {e}"));
assert_eq!(fs.mdb().block_size, *block_size, "{label}: block_size");
assert_eq!(fs.mdb().volume_name, "Fresh", "{label}: volume_name");
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
assert!(
entries.is_empty(),
"{label}: root not empty on fresh volume"
);
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"{label}: fsck errors: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
let mut backing = img;
{
let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
let root = fs.root().unwrap();
let payload = b"contents".to_vec();
let mut src = payload.as_slice();
fs.create_file(
&root,
"hello",
&mut src,
payload.len() as u64,
&CreateFileOptions::default(),
)
.unwrap_or_else(|e| panic!("{label}: create_file failed: {e}"));
fs.sync_metadata()
.unwrap_or_else(|e| panic!("{label}: sync_metadata failed: {e}"));
}
let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
assert_eq!(entries.len(), 1, "{label}: expected 1 child after create");
assert_eq!(entries[0].name, "hello", "{label}: child name");
let data = fs.read_file(&entries[0], 64).unwrap();
assert_eq!(data, b"contents", "{label}: file content round-trip");
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"{label}: post-create fsck errors: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
}
}
#[test]
#[cfg(target_pointer_width = "64")]
fn test_create_blank_hfs_near_2gb() {
let total = 2u64 * 1024 * 1024 * 1024 - 1;
let img = create_blank_hfs(total, 32768, "Near2GB")
.expect("create_blank_hfs near 2GB should succeed");
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
assert_eq!(fs.mdb().block_size, 32768);
assert!(fs.mdb().total_blocks >= 65000);
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"fsck errors: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
}
#[test]
fn catalog_grows_when_its_nodes_run_out() {
let img = create_blank_hfs_sized(16 * 1024 * 1024, 4096, "Grow", 0, 16384).unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let size0 = fs.mdb.catalog_file_size;
assert_eq!(size0, 16384);
let root = fs.root().unwrap();
for i in 0..1000 {
let name = format!("f{i:04}.txt");
fs.create_file(
&root,
&name,
&mut &b"x"[..],
1,
&CreateFileOptions::default(),
)
.unwrap_or_else(|e| panic!("creating {name}: {e}"));
}
assert!(fs.mdb.catalog_file_size > size0, "the catalog never grew");
let inline: u32 = fs
.mdb
.catalog_file_extents
.iter()
.map(|e| e.block_count as u32)
.sum();
let spilled: u32 = collect_fork_overflow_extents(
fs.extents_overflow_data
.as_ref()
.expect("overflow tree loaded"),
CNID_CATALOG,
0x00,
inline,
)
.iter()
.map(|e| e.block_count as u32)
.sum();
assert!(spilled > 0, "no CNID 4 overflow records");
assert_eq!(
(inline + spilled) * 4096,
fs.mdb.catalog_file_size,
"extents cover the file"
);
fs.sync_metadata().unwrap();
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
let img = fs.reader.get_ref().clone();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let kids = fs.list_directory(&root).unwrap();
assert_eq!(kids.len(), 1000);
for e in &kids {
fs.delete_entry(&root, e).unwrap();
}
fs.create_file(
&root,
"last.txt",
&mut &b"y"[..],
1,
&CreateFileOptions::default(),
)
.unwrap();
fs.sync_metadata().unwrap();
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
}
#[test]
fn extents_tree_grows_when_its_nodes_run_out() {
let img = create_blank_hfs_sized(4 * 1024 * 1024, 512, "Frag", 2048, 0).unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let xt0 = fs.mdb.extents_file_size;
assert_eq!(xt0, 2048);
let root = fs.root().unwrap();
let block = vec![0x5Au8; 512];
for i in 0..400 {
fs.create_file(
&root,
&format!("h{i:03}"),
&mut Cursor::new(&block),
512,
&CreateFileOptions::default(),
)
.unwrap();
}
for e in fs.list_directory(&root).unwrap() {
if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
fs.delete_entry(&root, &e).unwrap();
}
}
let tail = (fs.mdb.free_blocks - 264) as usize * 512;
fs.create_file(
&root,
"tail",
&mut Cursor::new(vec![0u8; tail]),
tail as u64,
&CreateFileOptions::default(),
)
.unwrap();
let data: Vec<u8> = (0..200 * 512).map(|i| (i / 512) as u8).collect();
let entry = fs
.create_file(
&root,
"spread",
&mut Cursor::new(&data),
data.len() as u64,
&CreateFileOptions::default(),
)
.unwrap();
assert!(
fs.mdb.extents_file_size > xt0,
"the extents tree never grew"
);
assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
fs.sync_metadata().unwrap();
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
let img = fs.reader.get_ref().clone();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let entry = fs
.list_directory(&root)
.unwrap()
.into_iter()
.find(|e| e.name == "spread")
.unwrap();
assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
fs.delete_entry(&root, &entry).unwrap();
fs.sync_metadata().unwrap();
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
}
#[test]
fn catalog_growth_adds_a_map_node_past_the_header_bitmap() {
let img = create_blank_hfs_sized(24 * 1024 * 1024, 512, "Map", 0, 16384).unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
for i in 0..9000 {
let name = format!("f{i:04}");
fs.create_file(
&root,
&name,
&mut &b"x"[..],
1,
&CreateFileOptions::default(),
)
.unwrap_or_else(|e| panic!("creating {name}: {e}"));
}
let h = BTreeHeader::read(&fs.catalog_data);
assert!(h.total_nodes > 2048, "{} nodes", h.total_nodes);
assert!(
hfs_common::btree_bitmap_segments(&fs.catalog_data, 512).len() > 1,
"no map node"
);
fs.sync_metadata().unwrap();
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
let img = fs.reader.get_ref().clone();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
}
#[test]
fn thousand_file_churn_keeps_the_catalog_clean() {
let img = create_blank_hfs(64 * 1024 * 1024, 4096, "Churn").unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let clean = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, stage: &str| {
fs.sync_metadata().unwrap();
let r = fs.fsck().unwrap();
assert!(r.errors.is_empty(), "{stage}: {:?}", r.errors);
};
let put = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, name: &str, data: &[u8]| {
fs.create_file(
&root,
name,
&mut Cursor::new(data),
data.len() as u64,
&CreateFileOptions::default(),
)
.unwrap_or_else(|e| panic!("creating {name}: {e}"))
};
let free0 = fs.mdb.free_blocks;
for i in 0..1000 {
put(
&mut fs,
&format!("f{i:04}.txt"),
format!("file {i:04}").as_bytes(),
);
}
clean(&mut fs, "after 1000 creates");
let extra = put(&mut fs, "extra.bin", &[7u8; 5000]);
clean(&mut fs, "after one more");
fs.delete_entry(&root, &extra).unwrap();
clean(&mut fs, "after deleting it");
for e in fs.list_directory(&root).unwrap() {
fs.delete_entry(&root, &e).unwrap();
}
assert_eq!(fs.mdb.free_blocks, free0, "every block came back");
clean(&mut fs, "after deleting the 1000");
put(&mut fs, "last.bin", &[9u8; 100]);
clean(&mut fs, "after the last create");
let names: Vec<String> = fs
.list_directory(&root)
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert_eq!(names, ["last.bin"]);
}
#[test]
fn fsck_reports_a_volume_that_does_not_fill_its_partition() {
let img = create_blank_hfs(4 * 1024 * 1024, 2048, "Short").unwrap();
let exact = img.len() as u64;
let mut fs = HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact)).unwrap();
assert!(fs.partition_shortfall().is_none());
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
let mut fs =
HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact + 512)).unwrap();
assert_eq!(fs.partition_shortfall(), Some(512));
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
let mut fs =
HfsFilesystem::open_sized(Cursor::new(img), 0, Some(exact + 1024 * 1024)).unwrap();
assert_eq!(fs.partition_shortfall(), Some(1024 * 1024));
let report = fs.fsck().unwrap();
let codes: Vec<&str> = report.errors.iter().map(|e| e.code.as_str()).collect();
assert_eq!(codes, ["AllocationAreaEnd"], "{:?}", report.errors);
assert!(report.errors[0].message.contains("resize --size"));
}
#[test]
fn create_file_spreads_a_fork_over_free_runs() {
let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Runs").unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let block = vec![0xA5u8; 4096];
for i in 0..40 {
fs.create_file(
&root,
&format!("f{i:02}"),
&mut Cursor::new(block.as_slice()),
4096,
&CreateFileOptions::default(),
)
.unwrap();
}
for e in fs.list_directory(&root).unwrap() {
if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
fs.delete_entry(&root, &e).unwrap();
}
}
let tail = (fs.mdb.free_blocks - 20) as usize;
let tail_data = vec![0x5Au8; tail * 4096];
fs.create_file(
&root,
"tail",
&mut Cursor::new(tail_data.as_slice()),
tail_data.len() as u64,
&CreateFileOptions::default(),
)
.unwrap();
assert_eq!(fs.mdb.free_blocks, 20);
let free_before = fs.mdb.free_blocks;
let data: Vec<u8> = (0..7 * 4096).map(|i| (i / 4096) as u8 + 1).collect();
let rsrc: Vec<u8> = (0..5 * 4096).map(|i| 0x80 | (i / 4096) as u8).collect();
let opts = CreateFileOptions {
resource_fork: Some(disk_core::filesystem::ResourceForkSource::Data(
rsrc.clone(),
)),
..Default::default()
};
let entry = fs
.create_file(
&root,
"frag",
&mut Cursor::new(data.as_slice()),
data.len() as u64,
&opts,
)
.unwrap();
let cnid = entry.location as u32;
assert_eq!(fs.mdb.free_blocks, free_before - 12);
let (dsize, dext, rsize, rext) = fs.find_file_by_id(cnid).unwrap();
assert_eq!((dsize as usize, rsize as usize), (data.len(), rsrc.len()));
assert!(dext.iter().all(|e| e.block_count == 1) && rext.iter().all(|e| e.block_count == 1));
let tree = fs
.extents_overflow_data
.as_ref()
.expect("overflow tree loaded");
assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 4);
assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 2);
assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
let mut got = Vec::new();
fs.write_resource_fork_to(&entry, &mut got).unwrap();
assert_eq!(got, rsrc);
let rsrc2: Vec<u8> = (0..4 * 4096).map(|i| 0xC0 | (i / 4096) as u8).collect();
fs.write_resource_fork(
&entry,
&mut Cursor::new(rsrc2.as_slice()),
rsrc2.len() as u64,
)
.unwrap();
assert_eq!(fs.mdb.free_blocks, free_before - 11);
let tree = fs.extents_overflow_data.as_ref().unwrap();
assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 1);
let mut got = Vec::new();
fs.write_resource_fork_to(&entry, &mut got).unwrap();
assert_eq!(got, rsrc2);
fs.sync_metadata().unwrap();
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
let img = fs.reader.get_ref().clone();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let entry = fs
.list_directory(&root)
.unwrap()
.into_iter()
.find(|e| e.name == "frag")
.unwrap();
assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
fs.delete_entry(&root, &entry).unwrap();
assert_eq!(fs.mdb.free_blocks, free_before);
let tree = fs.extents_overflow_data.as_ref().unwrap();
assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
fs.sync_metadata().unwrap();
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
}
#[test]
fn delete_frees_overflow_extents_and_their_records() {
use crate::hfs_common::{
bitmap_test_bit_be, btree_alloc_node, BTreeHeader, BTREE_LEAF_NODE,
};
let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Overflow").unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
assert!(fs.mdb.extents_file_size > 0);
let root = fs.root().unwrap();
let free_at_start = fs.mdb.free_blocks;
let data = b"inline extent contents";
fs.create_file(
&root,
"frag.bin",
&mut Cursor::new(data.as_slice()),
data.len() as u64,
&CreateFileOptions::default(),
)
.unwrap();
let entry = fs
.list_directory(&root)
.unwrap()
.into_iter()
.find(|e| e.name == "frag.bin")
.unwrap();
let cnid = entry.location as u32;
let start = fs.allocate_blocks(2).unwrap();
fs.ensure_extents_overflow().unwrap();
{
let tree = fs.extents_overflow_data.as_mut().expect("extents tree");
let mut h = BTreeHeader::read(tree);
let node_size = h.node_size as usize;
let leaf = btree_alloc_node(tree, node_size, h.total_nodes).unwrap();
h.root_node = leaf;
h.first_leaf_node = leaf;
h.last_leaf_node = leaf;
h.depth = 1;
h.leaf_records = 1;
h.free_nodes -= 1;
h.write(tree);
let off = leaf as usize * node_size;
let node = &mut tree[off..off + node_size];
node[8] = BTREE_LEAF_NODE as u8;
node[9] = 1;
BigEndian::write_u16(&mut node[10..12], 1);
node[14] = 7;
node[15] = 0x00;
BigEndian::write_u32(&mut node[16..20], cnid);
BigEndian::write_u16(&mut node[20..22], 1);
BigEndian::write_u16(&mut node[22..24], start as u16);
BigEndian::write_u16(&mut node[24..26], 2);
BigEndian::write_u16(&mut node[node_size - 2..], 14);
BigEndian::write_u16(&mut node[node_size - 4..node_size - 2], 34);
}
let tree = fs.extents_overflow_data.as_ref().unwrap();
assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 1);
fs.delete_entry(&root, &entry).unwrap();
let tree = fs.extents_overflow_data.as_ref().unwrap();
assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
let h = BTreeHeader::read(tree);
assert_eq!((h.leaf_records, h.root_node, h.depth), (0, 0, 0));
assert!(
crate::hfs_common::btree_lonely_empty_leaves(tree, h.node_size as usize).is_empty()
);
let bitmap = fs.bitmap.as_ref().unwrap();
assert!(!bitmap_test_bit_be(bitmap, start) && !bitmap_test_bit_be(bitmap, start + 1));
assert_eq!(fs.mdb.free_blocks, free_at_start, "every block came back");
fs.sync_metadata().unwrap();
let report = fs.fsck().unwrap();
assert!(report.errors.is_empty(), "{:?}", report.errors);
}
#[test]
fn test_collect_fork_overflow_extents() {
let node_size = 512usize;
let total_nodes = 3u32;
let mut buf = vec![0u8; node_size * total_nodes as usize];
buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
BigEndian::write_u16(&mut buf[10..12], 3); BigEndian::write_u16(&mut buf[14..16], 1); BigEndian::write_u32(&mut buf[16..20], 1); BigEndian::write_u32(&mut buf[20..24], 1); BigEndian::write_u32(&mut buf[24..28], 1); BigEndian::write_u32(&mut buf[28..32], 1); BigEndian::write_u16(&mut buf[32..34], node_size as u16);
BigEndian::write_u16(&mut buf[34..36], 7);
BigEndian::write_u32(&mut buf[36..40], total_nodes);
BigEndian::write_u32(&mut buf[40..44], total_nodes - 2);
buf[248] = 0b11000000;
let leaf_off = node_size;
let leaf = &mut buf[leaf_off..leaf_off + node_size];
leaf[8] = hfs_common::BTREE_LEAF_NODE as u8;
BigEndian::write_u16(&mut leaf[10..12], 1);
let rec_off = 14usize;
leaf[rec_off] = 7;
leaf[rec_off + 1] = 0x00;
BigEndian::write_u32(&mut leaf[rec_off + 2..rec_off + 6], 0x100);
BigEndian::write_u16(&mut leaf[rec_off + 6..rec_off + 8], 3);
let data_off = rec_off + 8;
BigEndian::write_u16(&mut leaf[data_off..data_off + 2], 50); BigEndian::write_u16(&mut leaf[data_off + 2..data_off + 4], 2); BigEndian::write_u16(&mut leaf[data_off + 4..data_off + 6], 60);
BigEndian::write_u16(&mut leaf[data_off + 6..data_off + 8], 1);
BigEndian::write_u16(&mut leaf[data_off + 8..data_off + 10], 0); BigEndian::write_u16(&mut leaf[data_off + 10..data_off + 12], 0);
let nr = 1usize;
BigEndian::write_u16(&mut leaf[node_size - 2..], 14);
BigEndian::write_u16(&mut leaf[node_size - 4..node_size - 2], 34);
let _ = nr;
let got = collect_fork_overflow_extents(&buf, 0x100, 0x00, 3);
assert_eq!(got.len(), 2, "expected 2 non-empty extents, got {got:?}");
assert_eq!((got[0].start_block, got[0].block_count), (50, 2));
assert_eq!((got[1].start_block, got[1].block_count), (60, 1));
let none = collect_fork_overflow_extents(&buf, 0x100, 0x00, 4);
assert!(
none.is_empty(),
"expected empty when filtered, got {none:?}"
);
let rsrc = collect_fork_overflow_extents(&buf, 0x100, 0xFF, 0);
assert!(rsrc.is_empty());
let other = collect_fork_overflow_extents(&buf, 0x101, 0x00, 0);
assert!(other.is_empty());
}
#[test]
fn test_create_blank_hfs_sized_custom_btree_sizes() {
let block_size = 32 * 1024u32;
let img =
create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Sized", 64 * 1024, 256 * 1024)
.expect("create_blank_hfs_sized");
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
assert_eq!(fs.mdb().extents_file_size, 4 * block_size);
assert_eq!(fs.mdb().catalog_file_size, 8 * block_size);
let result = fs.fsck().expect("fsck");
assert!(
result.errors.is_empty(),
"unexpected fsck errors: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
}
#[test]
fn test_pick_btree_node_size_thresholds() {
assert_eq!(pick_btree_node_size(512 * 2048), 512);
assert_eq!(pick_btree_node_size(512 * 2049), 512);
assert_eq!(pick_btree_node_size(1024 * 6144), 512);
assert_eq!(pick_btree_node_size(u32::MAX as u64), 512);
}
#[test]
fn test_create_blank_hfs_sized_large_catalog() {
let block_size = 32 * 1024u32;
let img = create_blank_hfs_sized(
128 * 1024 * 1024,
block_size,
"Big",
0,
HFS_MAX_BTREE_FILE_SIZE,
)
.expect("create_blank_hfs_sized big catalog");
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open big");
let cat = fs.catalog_data();
let node_size = BigEndian::read_u16(&cat[32..34]);
assert_eq!(
node_size, 512,
"node_size must stay at 512 for Mac OS compat"
);
let result = fs.fsck().expect("fsck big");
assert!(
result.errors.is_empty(),
"unexpected fsck errors: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
}
#[test]
fn test_create_blank_hfs_with_map_nodes() {
let img =
create_blank_hfs_sized(64 * 1024 * 1024, 32 * 1024, "MapNodes", 0, 2 * 1024 * 1024)
.expect("create_blank_hfs_sized 2 MiB cat");
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
let cat = fs.catalog_data().to_vec();
let header_flink = BigEndian::read_u32(&cat[0..4]);
assert_eq!(header_flink, 2, "header.fLink should point to map node 2");
let map = &cat[1024..1536];
assert_eq!(map[8] as i8, 2, "node 2 should be a map node");
assert_eq!(BigEndian::read_u16(&map[10..12]), 1);
assert_eq!(
BigEndian::read_u32(&map[0..4]),
0,
"single map node, fLink=0"
);
let header = hfs_common::BTreeHeader::read(&cat);
assert_eq!(header.total_nodes, 4096);
assert_eq!(header.free_nodes, 4093);
let segs = hfs_common::btree_bitmap_segments(&cat, 512);
assert_eq!(segs.len(), 2);
assert_eq!(segs[0].base_bit, 0);
assert_eq!(segs[1].base_bit, 2048);
assert!(
(segs[0].len + segs[1].len) * 8 >= 4096,
"bitmap chain must address all 4096 nodes"
);
let result = fs.fsck().expect("fsck");
assert!(
result.errors.is_empty(),
"unexpected fsck errors: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
}
#[test]
fn test_create_blank_hfs_rejects_bad_block_size() {
assert!(create_blank_hfs(1 << 20, 0, "X").is_err());
assert!(create_blank_hfs(1 << 20, 500, "X").is_err());
assert!(create_blank_hfs(1 << 20, 513, "X").is_err());
}
fn make_fs_with_file() -> (HfsFilesystem<Cursor<Vec<u8>>>, FileEntry) {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let data = b"hello";
let mut r = Cursor::new(data.as_slice());
let fe = fs
.create_file(
&root,
"thing.bin",
&mut r,
data.len() as u64,
&CreateFileOptions::default(),
)
.unwrap();
(fs, fe)
}
#[test]
fn test_hfs_set_finder_info_round_trip() {
let (mut fs, fe) = make_fs_with_file();
let mut finfo = [0u8; 16];
finfo[0..4].copy_from_slice(b"APPL");
finfo[4..8].copy_from_slice(b"MACS");
BigEndian::write_u16(&mut finfo[8..10], 0x4000); BigEndian::write_i16(&mut finfo[10..12], 100); BigEndian::write_i16(&mut finfo[12..14], 200); BigEndian::write_u16(&mut finfo[14..16], 0x1234); let mut fxinfo = [0u8; 16];
BigEndian::write_u16(&mut fxinfo[0..2], 0xABCD); fxinfo[8] = 0x55; BigEndian::write_u32(&mut fxinfo[12..16], 0xDEADBEEF);
fs.set_finder_info(&fe, finfo, fxinfo).unwrap();
let rec = fs.locate_record_data(fe.location as u32).unwrap();
assert_eq!(&fs.catalog_data[rec + 4..rec + 20], &finfo);
assert_eq!(&fs.catalog_data[rec + 56..rec + 72], &fxinfo);
let root = fs.root().unwrap();
let listed = fs.list_directory(&root).unwrap();
let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
assert_eq!(entry.finder_flags, Some(0x4000));
}
#[test]
fn test_hfs_read_surfaces_has_bundle_flag() {
let (mut fs, fe) = make_fs_with_file();
let mut finfo = [0u8; 16];
finfo[0..4].copy_from_slice(b"APPL");
finfo[4..8].copy_from_slice(b"Po.P");
BigEndian::write_u16(&mut finfo[8..10], 0x2000); fs.set_finder_info(&fe, finfo, [0u8; 16]).unwrap();
let root = fs.root().unwrap();
let listed = fs.list_directory(&root).unwrap();
let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
assert_eq!(entry.finder_flags, Some(0x2000));
}
#[test]
fn test_hfs_set_finder_info_rejects_directory() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let dir = fs
.create_directory(&root, "Dir", &CreateDirectoryOptions::default())
.unwrap();
let err = fs.set_finder_info(&dir, [0u8; 16], [0u8; 16]).unwrap_err();
assert!(matches!(err, FilesystemError::InvalidData(_)));
}
#[test]
fn test_hfs_set_directory_finder_info_round_trip() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let dir = fs
.create_directory(&root, "Apps", &CreateDirectoryOptions::default())
.unwrap();
let mut dinfo = [0u8; 16];
BigEndian::write_i16(&mut dinfo[0..2], 10); BigEndian::write_i16(&mut dinfo[2..4], 20); BigEndian::write_i16(&mut dinfo[4..6], 300); BigEndian::write_i16(&mut dinfo[6..8], 400); BigEndian::write_u16(&mut dinfo[8..10], 0x0100); let mut dxinfo = [0u8; 16];
BigEndian::write_i16(&mut dxinfo[0..2], -5); BigEndian::write_i16(&mut dxinfo[2..4], -7); BigEndian::write_u32(&mut dxinfo[4..8], 0xCAFEBABE);
fs.set_directory_finder_info(&dir, dinfo, dxinfo).unwrap();
let rec = fs.locate_record_data(dir.location as u32).unwrap();
assert_eq!(&fs.catalog_data[rec + 22..rec + 38], &dinfo);
assert_eq!(&fs.catalog_data[rec + 38..rec + 54], &dxinfo);
}
#[test]
fn test_hfs_set_directory_finder_info_rejects_file() {
let (mut fs, fe) = make_fs_with_file();
let err = fs
.set_directory_finder_info(&fe, [0u8; 16], [0u8; 16])
.unwrap_err();
assert!(matches!(err, FilesystemError::InvalidData(_)));
}
#[test]
fn test_hfs_set_dates_file() {
let (mut fs, fe) = make_fs_with_file();
let create = 0x1000_0001u32;
let modify = 0x2000_0002u32;
let backup = 0x3000_0003u32;
fs.set_dates(&fe, create, modify, backup).unwrap();
let rec = fs.locate_record_data(fe.location as u32).unwrap();
assert_eq!(
BigEndian::read_u32(&fs.catalog_data[rec + 44..rec + 48]),
create
);
assert_eq!(
BigEndian::read_u32(&fs.catalog_data[rec + 48..rec + 52]),
modify
);
assert_eq!(
BigEndian::read_u32(&fs.catalog_data[rec + 52..rec + 56]),
backup
);
}
#[test]
fn test_hfs_set_dates_directory() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let dir = fs
.create_directory(&root, "D", &CreateDirectoryOptions::default())
.unwrap();
let create = 0xAAAAu32;
let modify = 0xBBBBu32;
let backup = 0xCCCCu32;
fs.set_dates(&dir, create, modify, backup).unwrap();
let rec = fs.locate_record_data(dir.location as u32).unwrap();
assert_eq!(
BigEndian::read_u32(&fs.catalog_data[rec + 10..rec + 14]),
create
);
assert_eq!(
BigEndian::read_u32(&fs.catalog_data[rec + 14..rec + 18]),
modify
);
assert_eq!(
BigEndian::read_u32(&fs.catalog_data[rec + 18..rec + 22]),
backup
);
}
#[test]
fn test_hfs_set_file_locked_toggle() {
let (mut fs, fe) = make_fs_with_file();
let rec = fs.locate_record_data(fe.location as u32).unwrap();
let initial = fs.catalog_data[rec + 2];
assert_eq!(initial & 0x01, 0);
fs.set_file_locked(&fe, true).unwrap();
let rec = fs.locate_record_data(fe.location as u32).unwrap();
assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0x01);
fs.set_file_locked(&fe, false).unwrap();
let rec = fs.locate_record_data(fe.location as u32).unwrap();
assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0);
}
#[test]
fn test_hfs_set_file_locked_rejects_directory() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let root = fs.root().unwrap();
let dir = fs
.create_directory(&root, "Dir", &CreateDirectoryOptions::default())
.unwrap();
let err = fs.set_file_locked(&dir, true).unwrap_err();
assert!(matches!(err, FilesystemError::InvalidData(_)));
}
#[test]
fn test_hfs_set_volume_dates_persists_through_sync() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let create = 0x4242_4242u32;
let modify = 0x5151_5151u32;
let backup = 0x6060_6060u32;
fs.set_volume_dates(create, modify, backup);
fs.sync_metadata().unwrap();
assert_eq!(fs.mdb.create_date, create);
assert_eq!(fs.mdb.modify_date, modify);
assert_eq!(BigEndian::read_u32(&fs.mdb.raw_sector[64..68]), backup);
let bytes = fs.reader.get_ref().clone();
let mut fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
assert_eq!(fs2.mdb.create_date, create);
assert_eq!(fs2.mdb.modify_date, modify);
assert_eq!(BigEndian::read_u32(&fs2.mdb.raw_sector[64..68]), backup);
fs2.sync_metadata().unwrap();
assert_ne!(fs2.mdb.modify_date, modify);
}
#[test]
fn test_hfs_set_boot_blocks_written_on_sync() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let mut bb = [0u8; 1024];
bb[0] = 0x4C; bb[1] = 0x4B;
for (i, slot) in bb.iter_mut().enumerate().skip(2).take(200) {
*slot = (i & 0xFF) as u8;
}
fs.set_boot_blocks(&bb);
fs.sync_metadata().unwrap();
let read_back = fs.read_boot_blocks().unwrap();
assert_eq!(&read_back[..], &bb[..]);
}
#[test]
fn test_hfs_set_volume_finder_info_round_trip() {
let img = make_editable_hfs_image();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let mut fi = [0u8; 32];
BigEndian::write_u32(&mut fi[0..4], 100); BigEndian::write_u32(&mut fi[12..16], 200); BigEndian::write_u32(&mut fi[20..24], 300); fs.set_volume_finder_info(&fi);
assert_eq!(fs.mdb.finder_info[0], 100);
assert_eq!(fs.mdb.finder_info[3], 200);
assert_eq!(fs.mdb.finder_info[5], 300);
fs.sync_metadata().unwrap();
let bytes = fs.reader.get_ref().clone();
let fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
assert_eq!(fs2.mdb.finder_info[0], 100);
assert_eq!(fs2.mdb.finder_info[3], 200);
assert_eq!(fs2.mdb.finder_info[5], 300);
}
#[test]
fn test_default_btree_sizes_scale_and_floor() {
let (cat, ext) = default_btree_sizes(1 * 1024 * 1024, 512);
let floor = 24 * 512;
assert_eq!(cat, floor, "small-volume catalog should hit floor");
assert_eq!(ext, floor, "small-volume extents should hit floor");
let (cat, ext) = default_btree_sizes(20 * 1024 * 1024, 512);
assert!(cat >= 100 * 1024, "20MB catalog too small: {cat}");
assert_eq!(cat % 2048, 0, "catalog not clump-aligned");
assert_eq!(ext % 2048, 0, "extents not clump-aligned");
assert!(ext <= cat, "extents should not exceed catalog");
assert!(ext * 2 >= cat - 2048, "extents should be ~half catalog");
let (cat_big, _) = default_btree_sizes(200 * 1024 * 1024, 4096);
assert!(cat_big > cat, "larger volume should get a larger catalog");
let (c, e) = default_btree_sizes(20 * 1024 * 1024, 4096);
let img = create_blank_hfs_sized(20 * 1024 * 1024, 4096, "Ref20", e, c).unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"fsck errors on default-sized volume: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
}
#[test]
fn test_clone_target_btree_sizes_floors_at_blank_default() {
let target = 2046 * 1024 * 1024;
let (default_cat, _) = default_btree_sizes(target, 32768);
let (cat, _ext) = clone_target_btree_sizes(target, 32768, 2 * 1024 * 1024, 1024 * 1024);
assert_eq!(
cat, default_cat,
"large target must floor catalog at the blank default, not 1.5x source"
);
assert!(
cat > 2 * 1024 * 1024 * 3 / 2,
"floor must exceed 1.5x source"
);
let small_target = 40 * 1024 * 1024;
let (cat2, _) = clone_target_btree_sizes(small_target, 4096, 4 * 1024 * 1024, 256 * 1024);
assert_eq!(
cat2,
4 * 1024 * 1024 * 3 / 2,
"source-derived value should win"
);
let (cat3, ext3) = clone_target_btree_sizes(target, 32768, u32::MAX, u32::MAX);
assert!(cat3 <= HFS_MAX_BTREE_FILE_SIZE);
assert!(ext3 <= HFS_MAX_BTREE_FILE_SIZE);
}
#[test]
fn test_hfs_set_volume_name_roundtrip() {
let img = create_blank_hfs(8 * 1024 * 1024, 4096, "OldName").unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
fs.set_volume_name("NewName").unwrap();
assert_eq!(fs.mdb.volume_name, "NewName");
fs.sync_metadata().unwrap();
let bytes = fs.reader.get_ref().clone();
let mut fs2 = HfsFilesystem::open(Cursor::new(bytes.clone()), 0).unwrap();
assert_eq!(fs2.mdb.volume_name, "NewName");
let new_raw = b"NewName".to_vec();
assert!(
fs2.find_catalog_record_by_name(1, &new_raw).is_some(),
"root dir record not re-keyed to new name"
);
assert!(
fs2.find_catalog_record_by_name(1, b"OldName").is_none(),
"old root dir key still present"
);
let (_n, _r, key_abs) = fs2.find_catalog_record_by_cnid(2).unwrap();
let key_len = fs2.catalog_data[key_abs] as usize;
let mut data_off = key_abs + 1 + key_len;
if !data_off.is_multiple_of(2) {
data_off += 1;
}
let thd_len = fs2.catalog_data[data_off + 14] as usize;
assert_eq!(thd_len, new_raw.len(), "thdCName length not updated");
assert_eq!(
&fs2.catalog_data[data_off + 15..data_off + 15 + thd_len],
new_raw.as_slice(),
"thdCName bytes not updated"
);
let mdb_off = 1024 + 36;
assert_eq!(bytes[mdb_off] as usize, new_raw.len());
assert_eq!(
&bytes[mdb_off + 1..mdb_off + 1 + new_raw.len()],
new_raw.as_slice()
);
let result = fs2.fsck().unwrap();
assert!(
result.errors.is_empty(),
"fsck errors after rename: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
}
#[test]
fn test_drallocptr_advances_with_allocations() {
let img = create_blank_hfs(4 * 1024 * 1024, 512, "AllocPtr").unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
assert_eq!(
BigEndian::read_u16(&fs.mdb.raw_sector[16..18]),
0,
"fresh volume drAllocPtr should be 0"
);
let root = fs.root().unwrap();
let payload = vec![0xABu8; 8 * 1024]; let mut src = payload.as_slice();
fs.create_file(
&root,
"ptr",
&mut src,
payload.len() as u64,
&CreateFileOptions::default(),
)
.unwrap();
let after = BigEndian::read_u16(&fs.mdb.raw_sector[16..18]);
assert!(after > 0, "drAllocPtr should advance after allocation");
}
#[test]
fn test_hfs_set_volume_name_rejects_invalid() {
let img = create_blank_hfs(1 * 1024 * 1024, 512, "X").unwrap();
let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
assert!(
fs.set_volume_name("").is_err(),
"empty name must be rejected"
);
let too_long = "A".repeat(28);
assert!(
fs.set_volume_name(&too_long).is_err(),
">27 byte name must be rejected"
);
assert_eq!(fs.mdb.volume_name, "X");
}
#[test]
fn test_hfs_finder_info_setters_survive_fsck() {
let (mut fs, fe) = make_fs_with_file();
fs.set_finder_info(&fe, [0x11; 16], [0x22; 16]).unwrap();
fs.set_dates(&fe, 1, 2, 3).unwrap();
fs.set_file_locked(&fe, true).unwrap();
fs.set_volume_dates(10, 20, 30);
fs.set_volume_finder_info(&[0x33; 32]);
fs.sync_metadata().unwrap();
let result = fs.fsck().unwrap();
assert!(
result.errors.is_empty(),
"fsck errors: {:?}",
result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
);
}
}