#[cfg(feature = "rust173-polyfill")]
use disk_core::rust173::IntIsMultipleOf as _;
use std::collections::HashSet;
use byteorder::{BigEndian, ByteOrder};
use super::catalog::{collect_catalog_entries, CatalogEntry};
use super::{hfs_issue, HfsFsckCode};
use crate::hfs::{HfsExtDescriptor, HfsMasterDirectoryBlock, CATALOG_FILE};
use crate::hfs_common::{
bitmap_set_bit_be, bitmap_test_bit_be, btree_record_range, BTreeHeader, BTREE_LEAF_NODE,
};
use disk_core::fsck::{FsckIssue, RepairReport};
const MAX_BITMAP_MISMATCHES: usize = 20;
fn hfs_debug_issue(code: HfsFsckCode, message: impl Into<String>) -> FsckIssue {
let mut issue = hfs_issue(code, message);
issue.debug = true;
issue
}
pub(super) fn collect_overflow_extents(extents_data: &[u8]) -> Vec<(u32, u32, String)> {
let mut result = Vec::new();
if extents_data.len() < 512 {
return result;
}
let header = BTreeHeader::read(extents_data);
let node_size = header.node_size as usize;
if node_size == 0 || extents_data.len() < node_size {
return result;
}
let mut node_idx = header.first_leaf_node;
let mut visited = 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, _rec_end) = 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 fork_type = node[rec_start + 1]; let file_id = BigEndian::read_u32(&node[rec_start + 2..rec_start + 6]);
let mut data_off = rec_start + 1 + key_len;
#[allow(clippy::manual_is_multiple_of)]
if data_off % 2 != 0 {
data_off += 1;
}
if data_off + 12 > node_size {
continue;
}
let fork_label = if fork_type == 0xFF {
"resource fork"
} else {
"data fork"
};
for j in 0..3 {
let ext = HfsExtDescriptor::parse(&node[data_off + j * 4..data_off + j * 4 + 4]);
if ext.block_count > 0 {
result.push((
ext.start_block as u32,
ext.block_count as u32,
format!("file {} {} (overflow)", file_id, fork_label),
));
}
}
}
node_idx = flink;
}
result
}
fn collect_bad_block_extents(extents_data: &[u8]) -> Vec<(u32, u32)> {
const BAD_BLOCK_FILE_ID: u32 = 5;
let mut result = Vec::new();
if extents_data.len() < 512 {
return result;
}
let header = BTreeHeader::read(extents_data);
let node_size = header.node_size as usize;
if node_size == 0 || extents_data.len() < node_size {
return result;
}
let mut node_idx = header.first_leaf_node;
let mut visited = 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, _rec_end) = 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 file_id = BigEndian::read_u32(&node[rec_start + 2..rec_start + 6]);
if file_id != BAD_BLOCK_FILE_ID {
continue;
}
let mut data_off = rec_start + 1 + key_len;
#[allow(clippy::manual_is_multiple_of)]
if data_off % 2 != 0 {
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 {
result.push((ext.start_block as u32, ext.block_count as u32));
}
}
}
node_idx = flink;
}
result
}
pub(super) fn check_extents_and_bitmap(
mdb: &HfsMasterDirectoryBlock,
catalog_data: &[u8],
header: &BTreeHeader,
bitmap: &[u8],
extents_data: Option<&[u8]>,
errors: &mut Vec<FsckIssue>,
warnings: &mut Vec<FsckIssue>,
) {
let total_blocks = mdb.total_blocks as u32;
let mut computed_bitmap = vec![0u8; (total_blocks as usize).div_ceil(8)];
mark_extents(
&mdb.catalog_file_extents,
total_blocks,
&mut computed_bitmap,
errors,
"catalog file",
);
mark_extents(
&mdb.extents_file_extents,
total_blocks,
&mut computed_bitmap,
errors,
"extents overflow file",
);
let entries = collect_catalog_entries(catalog_data, header);
let mut extent_blocks: Vec<(u32, u32, String)> = Vec::new();
for entry in &entries {
if let CatalogEntry::File {
file_id,
data_extents,
data_size,
rsrc_extents,
rsrc_size,
..
} = entry
{
if *data_size > 0 {
for ext in data_extents {
if ext.block_count > 0 {
extent_blocks.push((
ext.start_block as u32,
ext.block_count as u32,
format!("file {} data fork", file_id),
));
}
}
}
if *rsrc_size > 0 {
for ext in rsrc_extents {
if ext.block_count > 0 {
extent_blocks.push((
ext.start_block as u32,
ext.block_count as u32,
format!("file {} resource fork", file_id),
));
}
}
}
}
}
if let Some(ext_data) = extents_data {
let overflow = collect_overflow_extents(ext_data);
extent_blocks.extend(overflow);
}
for (start, count, ref label) in &extent_blocks {
if *start + *count > total_blocks {
errors.push(hfs_issue(
HfsFsckCode::ExtentOutOfRange,
format!(
"{}: extent [{}, +{}) exceeds total_blocks {}",
label, start, count, total_blocks
),
));
continue;
}
for b in *start..*start + *count {
if bitmap_test_bit_be(&computed_bitmap, b) {
errors.push(hfs_issue(
HfsFsckCode::OverlappingExtents,
format!("{}: block {} already allocated by another extent", label, b),
));
break; }
bitmap_set_bit_be(&mut computed_bitmap, b);
}
}
let mut real_mismatch_count = 0usize;
let mut wasted_block_count = 0usize;
for block in 0..total_blocks {
let computed = bitmap_test_bit_be(&computed_bitmap, block);
let actual = bitmap_test_bit_be(bitmap, block);
if computed != actual {
if computed {
real_mismatch_count += 1;
if real_mismatch_count <= MAX_BITMAP_MISMATCHES {
warnings.push(hfs_issue(
HfsFsckCode::BitmapMismatch,
format!("block {}: allocated in catalog but free in bitmap", block),
));
}
} else {
wasted_block_count += 1;
}
}
}
if real_mismatch_count > MAX_BITMAP_MISMATCHES {
warnings.push(hfs_issue(
HfsFsckCode::BitmapMismatch,
format!(
"... and {} more bitmap mismatches (total {})",
real_mismatch_count - MAX_BITMAP_MISMATCHES,
real_mismatch_count
),
));
}
if wasted_block_count > 0 {
warnings.push(hfs_debug_issue(
HfsFsckCode::BitmapMismatch,
format!(
"{} block(s) marked allocated in bitmap but not referenced by any file \
(probably just small wasted space)",
wasted_block_count
),
));
}
if let Some(ext_data) = extents_data {
let bad_block_extents = collect_bad_block_extents(ext_data);
for (start, count) in &bad_block_extents {
for b in *start..(*start + *count).min(total_blocks) {
if !bitmap_test_bit_be(bitmap, b) {
warnings.push(hfs_issue(
HfsFsckCode::BadBlockExtentNotInBitmap,
format!(
"bad block file: block {} is marked free in bitmap (should be allocated)",
b
),
));
break; }
}
}
}
let mut actual_free = 0u32;
for block in 0..total_blocks {
if !bitmap_test_bit_be(bitmap, block) {
actual_free += 1;
}
}
if actual_free != mdb.free_blocks as u32 {
errors.push(hfs_issue(
HfsFsckCode::FreeBlockCountMismatch,
format!(
"MDB free_blocks = {} but bitmap has {} free blocks",
mdb.free_blocks, actual_free
),
));
}
}
fn mark_extents(
extents: &[HfsExtDescriptor; 3],
total_blocks: u32,
computed_bitmap: &mut [u8],
errors: &mut Vec<FsckIssue>,
label: &str,
) {
for ext in extents {
if ext.block_count == 0 {
continue;
}
let start = ext.start_block as u32;
let count = ext.block_count as u32;
if start + count > total_blocks {
errors.push(hfs_issue(
HfsFsckCode::ExtentOutOfRange,
format!(
"{}: extent [{}, +{}) exceeds total_blocks {}",
label, start, count, total_blocks
),
));
continue;
}
for b in start..start + count {
bitmap_set_bit_be(computed_bitmap, b);
}
}
}
pub(super) fn repair_extents(
mdb: &HfsMasterDirectoryBlock,
catalog_data: &mut [u8],
node_size: usize,
_extents_data: Option<&[u8]>,
report: &mut RepairReport,
) {
let total_blocks = mdb.total_blocks as u32;
let header = BTreeHeader::read(catalog_data);
let mut node_idx = header.first_leaf_node;
let mut visited = HashSet::new();
let mut used_blocks: HashSet<u32> = HashSet::new();
for ext in &mdb.catalog_file_extents {
if ext.block_count > 0 && (ext.start_block as u32 + ext.block_count as u32) <= total_blocks
{
for b in ext.start_block as u32..ext.start_block as u32 + ext.block_count as u32 {
used_blocks.insert(b);
}
}
}
for ext in &mdb.extents_file_extents {
if ext.block_count > 0 && (ext.start_block as u32 + ext.block_count as u32) <= total_blocks
{
for b in ext.start_block as u32..ext.start_block as u32 + ext.block_count as u32 {
used_blocks.insert(b);
}
}
}
while node_idx != 0 {
if !visited.insert(node_idx) {
break;
}
let off = node_idx as usize * node_size;
if off + node_size > catalog_data.len() {
break;
}
let num_records = BigEndian::read_u16(&catalog_data[off + 10..off + 12]) as usize;
for i in 0..num_records {
let (rec_start, _rec_end) =
btree_record_range(&catalog_data[off..off + node_size], node_size, i);
let abs_start = off + rec_start;
if abs_start + 7 > catalog_data.len() {
continue;
}
let key_len = catalog_data[abs_start] as usize;
let mut rec_data_offset = abs_start + 1 + key_len;
#[allow(clippy::manual_is_multiple_of)]
if rec_data_offset % 2 != 0 {
rec_data_offset += 1;
}
if rec_data_offset + 102 > catalog_data.len() {
continue;
}
let record_type = catalog_data[rec_data_offset] as i8;
if record_type != CATALOG_FILE {
continue;
}
let file_id =
BigEndian::read_u32(&catalog_data[rec_data_offset + 20..rec_data_offset + 24]);
for j in 0..3 {
let ext_off = rec_data_offset + 74 + j * 4;
let start = BigEndian::read_u16(&catalog_data[ext_off..ext_off + 2]) as u32;
let count = BigEndian::read_u16(&catalog_data[ext_off + 2..ext_off + 4]) as u32;
if count == 0 {
continue;
}
if start + count > total_blocks {
BigEndian::write_u16(&mut catalog_data[ext_off..ext_off + 2], 0);
BigEndian::write_u16(&mut catalog_data[ext_off + 2..ext_off + 4], 0);
report.fixes_applied.push(format!(
"Truncated out-of-range data extent for file CNID {} โ data loss possible",
file_id
));
} else {
let mut overlaps = false;
for b in start..start + count {
if !used_blocks.insert(b) {
overlaps = true;
break;
}
}
if overlaps {
for b in start..start + count {
used_blocks.remove(&b);
}
BigEndian::write_u16(&mut catalog_data[ext_off..ext_off + 2], 0);
BigEndian::write_u16(&mut catalog_data[ext_off + 2..ext_off + 4], 0);
report.fixes_applied.push(format!(
"Zeroed overlapping data extent for file CNID {} โ data loss possible",
file_id
));
}
}
}
for j in 0..3 {
let ext_off = rec_data_offset + 86 + j * 4;
let start = BigEndian::read_u16(&catalog_data[ext_off..ext_off + 2]) as u32;
let count = BigEndian::read_u16(&catalog_data[ext_off + 2..ext_off + 4]) as u32;
if count == 0 {
continue;
}
if start + count > total_blocks {
BigEndian::write_u16(&mut catalog_data[ext_off..ext_off + 2], 0);
BigEndian::write_u16(&mut catalog_data[ext_off + 2..ext_off + 4], 0);
report.fixes_applied.push(format!(
"Truncated out-of-range resource extent for file CNID {} โ data loss possible",
file_id
));
} else {
let mut overlaps = false;
for b in start..start + count {
if !used_blocks.insert(b) {
overlaps = true;
break;
}
}
if overlaps {
for b in start..start + count {
used_blocks.remove(&b);
}
BigEndian::write_u16(&mut catalog_data[ext_off..ext_off + 2], 0);
BigEndian::write_u16(&mut catalog_data[ext_off + 2..ext_off + 4], 0);
report.fixes_applied.push(format!(
"Zeroed overlapping resource extent for file CNID {} โ data loss possible",
file_id
));
}
}
}
}
node_idx = BigEndian::read_u32(&catalog_data[off..off + 4]);
}
}
pub(super) fn repair_mdb_counts_and_bitmap(
mdb: &mut HfsMasterDirectoryBlock,
catalog_data: &[u8],
bitmap: &mut Vec<u8>,
extents_data: Option<&[u8]>,
report: &mut RepairReport,
) {
let header = BTreeHeader::read(catalog_data);
let entries = collect_catalog_entries(catalog_data, &header);
let total_blocks = mdb.total_blocks as u32;
let mut file_count = 0u32;
let mut folder_count = 0u32;
for entry in &entries {
match entry {
CatalogEntry::Directory { .. } => folder_count += 1,
CatalogEntry::File { .. } => file_count += 1,
_ => {}
}
}
if mdb.file_count != file_count {
let old = mdb.file_count;
mdb.file_count = file_count;
report
.fixes_applied
.push(format!("Fixed MDB file_count: {} -> {}", old, file_count));
}
let non_root_folder_count = folder_count.saturating_sub(1);
if mdb.folder_count != non_root_folder_count {
let old = mdb.folder_count;
mdb.folder_count = non_root_folder_count;
report.fixes_applied.push(format!(
"Fixed MDB folder_count: {} -> {}",
old, non_root_folder_count
));
}
let mut computed_bitmap = vec![0u8; (total_blocks as usize).div_ceil(8)];
for ext in &mdb.catalog_file_extents {
if ext.block_count > 0 {
let start = ext.start_block as u32;
let count = ext.block_count as u32;
if start + count <= total_blocks {
for b in start..start + count {
bitmap_set_bit_be(&mut computed_bitmap, b);
}
}
}
}
for ext in &mdb.extents_file_extents {
if ext.block_count > 0 {
let start = ext.start_block as u32;
let count = ext.block_count as u32;
if start + count <= total_blocks {
for b in start..start + count {
bitmap_set_bit_be(&mut computed_bitmap, b);
}
}
}
}
for entry in &entries {
if let CatalogEntry::File {
data_extents,
data_size,
rsrc_extents,
rsrc_size,
..
} = entry
{
if *data_size > 0 {
for ext in data_extents {
if ext.block_count > 0 {
let start = ext.start_block as u32;
let count = ext.block_count as u32;
if start + count <= total_blocks {
for b in start..start + count {
bitmap_set_bit_be(&mut computed_bitmap, b);
}
}
}
}
}
if *rsrc_size > 0 {
for ext in rsrc_extents {
if ext.block_count > 0 {
let start = ext.start_block as u32;
let count = ext.block_count as u32;
if start + count <= total_blocks {
for b in start..start + count {
bitmap_set_bit_be(&mut computed_bitmap, b);
}
}
}
}
}
}
}
if let Some(ext_data) = extents_data {
let overflow = collect_overflow_extents(ext_data);
for (start, count, _) in &overflow {
if start + count <= total_blocks {
for b in *start..*start + *count {
bitmap_set_bit_be(&mut computed_bitmap, b);
}
}
}
}
let bitmap_changed = *bitmap != computed_bitmap;
if bitmap_changed {
*bitmap = computed_bitmap;
report
.fixes_applied
.push("Rebuilt volume allocation bitmap from extents".into());
}
let mut free_blocks = 0u32;
for block in 0..total_blocks {
if !bitmap_test_bit_be(bitmap, block) {
free_blocks += 1;
}
}
if mdb.free_blocks as u32 != free_blocks {
let old = mdb.free_blocks;
mdb.free_blocks = free_blocks as u16;
report
.fixes_applied
.push(format!("Fixed MDB free_blocks: {} -> {}", old, free_blocks));
}
}