1use byteorder::{BigEndian, ByteOrder};
2#[cfg(feature = "rust173-polyfill")]
3use disk_core::rust173::IntIsMultipleOf as _;
4use std::cmp::Ordering;
5use std::io::{Read, Seek, SeekFrom, Write};
6
7use crate::hfs_common::{
8 self, bitmap_clear_bit_be, bitmap_collect_clear_runs_be, bitmap_find_clear_run_be,
9 bitmap_set_bit_be, bitmap_test_bit_be, btree_free_node, btree_grow_blocks,
10 btree_map_chain_tail, btree_refresh_index_keys, btree_remove_record, BTreeHeader,
11 BTreeKeyFormat,
12};
13use disk_core::entry::{EntryType, FileEntry};
14use disk_core::filesystem::CompactResult;
15use disk_core::filesystem::{
16 CreateDirectoryOptions, CreateFileOptions, EditableFilesystem, Filesystem, FilesystemError,
17};
18
19const HFS_SIGNATURE: u16 = 0x4244;
20const HFS_PLUS_EMBEDDED_SIGNATURE: u16 = 0x482B;
21
22static MAC_ROMAN_TABLE: [char; 128] = [
24 '\u{00C4}', '\u{00C5}', '\u{00C7}', '\u{00C9}', '\u{00D1}', '\u{00D6}', '\u{00DC}', '\u{00E1}',
25 '\u{00E0}', '\u{00E2}', '\u{00E4}', '\u{00E3}', '\u{00E5}', '\u{00E7}', '\u{00E9}', '\u{00E8}',
26 '\u{00EA}', '\u{00EB}', '\u{00ED}', '\u{00EC}', '\u{00EE}', '\u{00EF}', '\u{00F1}', '\u{00F3}',
27 '\u{00F2}', '\u{00F4}', '\u{00F6}', '\u{00F5}', '\u{00FA}', '\u{00F9}', '\u{00FB}', '\u{00FC}',
28 '\u{2020}', '\u{00B0}', '\u{00A2}', '\u{00A3}', '\u{00A7}', '\u{2022}', '\u{00B6}', '\u{00DF}',
29 '\u{00AE}', '\u{00A9}', '\u{2122}', '\u{00B4}', '\u{00A8}', '\u{2260}', '\u{00C6}', '\u{00D8}',
30 '\u{221E}', '\u{00B1}', '\u{2264}', '\u{2265}', '\u{00A5}', '\u{00B5}', '\u{2202}', '\u{2211}',
31 '\u{220F}', '\u{03C0}', '\u{222B}', '\u{00AA}', '\u{00BA}', '\u{03A9}', '\u{00E6}', '\u{00F8}',
32 '\u{00BF}', '\u{00A1}', '\u{00AC}', '\u{221A}', '\u{0192}', '\u{2248}', '\u{2206}', '\u{00AB}',
33 '\u{00BB}', '\u{2026}', '\u{00A0}', '\u{00C0}', '\u{00C3}', '\u{00D5}', '\u{0152}', '\u{0153}',
34 '\u{2013}', '\u{2014}', '\u{201C}', '\u{201D}', '\u{2018}', '\u{2019}', '\u{00F7}', '\u{25CA}',
35 '\u{00FF}', '\u{0178}', '\u{2044}', '\u{20AC}', '\u{2039}', '\u{203A}', '\u{FB01}', '\u{FB02}',
36 '\u{2021}', '\u{00B7}', '\u{201A}', '\u{201E}', '\u{2030}', '\u{00C2}', '\u{00CA}', '\u{00C1}',
37 '\u{00CB}', '\u{00C8}', '\u{00CD}', '\u{00CE}', '\u{00CF}', '\u{00CC}', '\u{00D3}', '\u{00D4}',
38 '\u{F8FF}', '\u{00D2}', '\u{00DA}', '\u{00DB}', '\u{00D9}', '\u{0131}', '\u{02C6}', '\u{02DC}',
39 '\u{00AF}', '\u{02D8}', '\u{02D9}', '\u{02DA}', '\u{00B8}', '\u{02DD}', '\u{02DB}', '\u{02C7}',
40];
41
42pub fn utf8_to_mac_roman(s: &str) -> Result<Vec<u8>, FilesystemError> {
45 let mut result = Vec::with_capacity(s.len());
46 for ch in s.chars() {
47 if (ch as u32) < 0x80 {
48 result.push(ch as u8);
49 } else {
50 let mut found = false;
52 for (i, &table_char) in MAC_ROMAN_TABLE.iter().enumerate() {
53 if table_char == ch {
54 result.push((i as u8) + 0x80);
55 found = true;
56 break;
57 }
58 }
59 if !found {
60 return Err(FilesystemError::InvalidData(format!(
61 "character '{}' (U+{:04X}) cannot be encoded in Mac Roman",
62 ch, ch as u32
63 )));
64 }
65 }
66 }
67 Ok(result)
68}
69
70pub fn mac_roman_to_utf8(data: &[u8]) -> String {
72 data.iter()
73 .map(|&b| {
74 if b < 0x80 {
75 b as char
76 } else {
77 MAC_ROMAN_TABLE[(b - 0x80) as usize]
78 }
79 })
80 .collect()
81}
82
83pub fn decode_mac_filename(data: &[u8]) -> String {
97 if data.iter().any(|&b| b >= 0x80) {
98 if let Ok(s) = std::str::from_utf8(data) {
99 return s.to_string();
100 }
101 }
102 mac_roman_to_utf8(data)
103}
104
105pub fn format_ostype(code: &[u8; 4]) -> String {
112 code.iter()
113 .map(|&b| match b {
114 0x00..=0x1f => ' ',
115 b if b < 0x80 => b as char,
116 b => MAC_ROMAN_TABLE[(b - 0x80) as usize],
117 })
118 .collect::<String>()
119 .trim_end()
120 .to_string()
121}
122
123fn validate_hfs_create_name(name: &str) -> Result<Vec<u8>, FilesystemError> {
127 if name.is_empty() {
128 return Err(FilesystemError::InvalidData(
129 "filename is empty — pick a non-blank name".into(),
130 ));
131 }
132 if name.contains(':') {
133 return Err(FilesystemError::InvalidData(
134 "filename contains ':', which classic Mac OS uses as a path separator — \
135 rename the file (try '-' or '_' instead)"
136 .into(),
137 ));
138 }
139 let name_bytes = utf8_to_mac_roman(name).map_err(|_| {
140 FilesystemError::InvalidData(
141 "filename contains characters that can't be encoded in Mac Roman \
142 (HFS predates Unicode) — rename using plain ASCII or common accented letters"
143 .into(),
144 )
145 })?;
146 if name_bytes.is_empty() {
147 return Err(FilesystemError::InvalidData(
148 "filename is empty — pick a non-blank name".into(),
149 ));
150 }
151 if name_bytes.len() > 31 {
152 return Err(FilesystemError::InvalidData(format!(
153 "filename is too long ({} chars); classic HFS allows up to 31 — shorten the name",
154 name_bytes.len()
155 )));
156 }
157 Ok(name_bytes)
158}
159
160fn hfs_fragmentation_stats(catalog_data: &[u8]) -> disk_core::filesystem::FragmentationStats {
169 use crate::hfs_common::walk_leaf_records;
170
171 let mut stats = disk_core::filesystem::FragmentationStats::default();
172 if catalog_data.len() < 34 {
173 return stats;
174 }
175 let node_size = BigEndian::read_u16(&catalog_data[32..34]) as usize;
176 let first_leaf = BigEndian::read_u32(&catalog_data[24..28]);
177 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || first_leaf == 0 {
178 return stats;
179 }
180 walk_leaf_records::<(), _>(
181 catalog_data,
182 first_leaf,
183 node_size,
184 |_node_idx, _rec_idx, _abs_off, rec| {
185 if rec.is_empty() {
186 return None;
187 }
188 let key_len = rec[0] as usize;
189 let mut data_off = 1 + key_len;
190 if !data_off.is_multiple_of(2) {
191 data_off += 1;
192 }
193 if data_off + 102 > rec.len() {
194 return None;
195 }
196 if rec[data_off] as i8 != CATALOG_FILE {
197 return None;
198 }
199 let body = &rec[data_off..];
200 let phys_bytes = BigEndian::read_u32(&body[30..34]);
201 if phys_bytes == 0 {
202 return None;
203 }
204 let mut inline_extents: u32 = 0;
205 let mut inline_blocks: u32 = 0;
206 for j in 0..3 {
207 let count = BigEndian::read_u16(&body[74 + j * 4 + 2..74 + j * 4 + 4]);
208 if count > 0 {
209 inline_extents += 1;
210 inline_blocks = inline_blocks.saturating_add(count as u32);
211 }
212 }
213 stats.files_with_data += 1;
214 let overflows = inline_blocks == 0 && phys_bytes > 0;
221 if inline_extents > 1 || overflows {
222 stats.fragmented_files += 1;
223 }
224 None
225 },
226 );
227 stats
228}
229
230fn is_catalog_uninitialized(catalog_data: &[u8]) -> bool {
231 if catalog_data.len() < 34 {
232 return true;
233 }
234 BigEndian::read_u16(&catalog_data[32..34]) == 0
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240enum HfsTree {
241 Catalog,
242 Extents,
243}
244
245impl HfsTree {
246 fn label(self) -> &'static str {
247 match self {
248 HfsTree::Catalog => "catalog",
249 HfsTree::Extents => "extents-overflow",
250 }
251 }
252}
253
254#[derive(Debug, Clone, Copy)]
256pub struct HfsExtDescriptor {
257 pub start_block: u16,
258 pub block_count: u16,
259}
260
261#[derive(Debug, Clone, Copy)]
266pub struct HfsFileLocation {
267 pub offset_in_image: u64,
268 pub length: u64,
269 pub fragmented: bool,
270}
271
272#[derive(Debug, Clone)]
277pub struct HfsVolumeSummary {
278 pub volume_name: String,
279 pub block_size: u32,
280 pub total_blocks: u16,
281 pub free_blocks: u16,
282 pub used_bytes: u64,
283 pub file_count: u32,
284 pub folder_count: u32,
285 pub catalog_file_size: u32,
286 pub extents_file_size: u32,
287}
288
289impl HfsExtDescriptor {
290 pub fn parse(data: &[u8]) -> Self {
291 HfsExtDescriptor {
292 start_block: BigEndian::read_u16(&data[0..2]),
293 block_count: BigEndian::read_u16(&data[2..4]),
294 }
295 }
296
297 fn serialize(&self, out: &mut [u8]) {
298 BigEndian::write_u16(&mut out[0..2], self.start_block);
299 BigEndian::write_u16(&mut out[2..4], self.block_count);
300 }
301}
302
303#[derive(Debug, Clone)]
305#[allow(dead_code)]
306pub(crate) struct HfsMasterDirectoryBlock {
307 pub(crate) signature: u16,
308 pub(crate) create_date: u32,
309 pub(crate) modify_date: u32,
310 pub(crate) total_blocks: u16,
311 pub(crate) block_size: u32,
312 pub(crate) free_blocks: u16,
313 pub(crate) volume_name: String,
314 pub(crate) volume_name_raw: Vec<u8>,
315 pub(crate) volume_bitmap_block: u16,
316 pub(crate) first_alloc_block: u16,
318 pub(crate) next_catalog_id: u32,
319 pub(crate) file_count: u32,
320 pub(crate) folder_count: u32,
321 pub(crate) finder_info: [u32; 8],
322 pub(crate) catalog_file_size: u32,
323 pub(crate) catalog_file_extents: [HfsExtDescriptor; 3],
324 pub(crate) extents_file_size: u32,
325 pub(crate) extents_file_extents: [HfsExtDescriptor; 3],
326 pub(crate) embedded_signature: u16,
327 pub(crate) embedded_start_block: u16,
328 pub(crate) embedded_block_count: u16,
329 pub(crate) raw_sector: [u8; 512],
331}
332
333impl HfsMasterDirectoryBlock {
334 fn parse(data: &[u8]) -> Result<Self, FilesystemError> {
335 if data.len() < 162 {
336 return Err(FilesystemError::Parse("MDB too short".into()));
337 }
338 let sig = BigEndian::read_u16(&data[0..2]);
339 if sig != HFS_SIGNATURE {
340 return Err(FilesystemError::Parse(format!(
341 "bad MDB signature: 0x{sig:04X}"
342 )));
343 }
344
345 let name_len = data[36] as usize;
347 let name_bytes = &data[37..37 + name_len.min(27)];
348 let volume_name = mac_roman_to_utf8(name_bytes);
349 let volume_name_raw = name_bytes.to_vec();
350
351 let mut extents_extents = [HfsExtDescriptor {
352 start_block: 0,
353 block_count: 0,
354 }; 3];
355 for i in 0..3 {
356 extents_extents[i] = HfsExtDescriptor::parse(&data[134 + i * 4..138 + i * 4]);
357 }
358
359 let mut catalog_extents = [HfsExtDescriptor {
360 start_block: 0,
361 block_count: 0,
362 }; 3];
363 for i in 0..3 {
364 catalog_extents[i] = HfsExtDescriptor::parse(&data[150 + i * 4..154 + i * 4]);
365 }
366
367 let embedded_sig = BigEndian::read_u16(&data[124..126]);
369 let embedded_start = BigEndian::read_u16(&data[126..128]);
370 let embedded_count = BigEndian::read_u16(&data[128..130]);
371
372 let mut finder_info = [0u32; 8];
374 for i in 0..8 {
375 finder_info[i] = BigEndian::read_u32(&data[92 + i * 4..96 + i * 4]);
376 }
377
378 let mut raw_sector = [0u8; 512];
380 let copy_len = data.len().min(512);
381 raw_sector[..copy_len].copy_from_slice(&data[..copy_len]);
382
383 Ok(HfsMasterDirectoryBlock {
384 signature: sig,
385 create_date: BigEndian::read_u32(&data[2..6]),
386 modify_date: BigEndian::read_u32(&data[6..10]),
387 total_blocks: BigEndian::read_u16(&data[18..20]),
388 block_size: BigEndian::read_u32(&data[20..24]),
389 free_blocks: BigEndian::read_u16(&data[34..36]),
390 volume_name,
391 volume_name_raw,
392 volume_bitmap_block: BigEndian::read_u16(&data[14..16]),
393 first_alloc_block: BigEndian::read_u16(&data[28..30]),
394 next_catalog_id: BigEndian::read_u32(&data[30..34]),
395 file_count: BigEndian::read_u32(&data[84..88]),
396 folder_count: BigEndian::read_u32(&data[88..92]),
397 finder_info,
398 extents_file_size: BigEndian::read_u32(&data[130..134]),
399 extents_file_extents: extents_extents,
400 catalog_file_size: BigEndian::read_u32(&data[146..150]),
401 catalog_file_extents: catalog_extents,
402 embedded_signature: embedded_sig,
403 embedded_start_block: embedded_start,
404 embedded_block_count: embedded_count,
405 raw_sector,
406 })
407 }
408
409 fn serialize_to_sector(&self) -> [u8; 512] {
412 let mut out = self.raw_sector;
413 BigEndian::write_u32(&mut out[6..10], self.modify_date);
414 BigEndian::write_u16(&mut out[18..20], self.total_blocks);
415 BigEndian::write_u16(&mut out[34..36], self.free_blocks);
416 BigEndian::write_u32(&mut out[30..34], self.next_catalog_id);
417 BigEndian::write_u32(&mut out[84..88], self.file_count);
418 BigEndian::write_u32(&mut out[88..92], self.folder_count);
419 for i in 0..8 {
420 BigEndian::write_u32(&mut out[92 + i * 4..96 + i * 4], self.finder_info[i]);
421 }
422 BigEndian::write_u32(&mut out[130..134], self.extents_file_size);
423 for i in 0..3 {
424 self.extents_file_extents[i].serialize(&mut out[134 + i * 4..138 + i * 4]);
425 }
426 BigEndian::write_u32(&mut out[146..150], self.catalog_file_size);
427 for i in 0..3 {
428 self.catalog_file_extents[i].serialize(&mut out[150 + i * 4..154 + i * 4]);
429 }
430 out
431 }
432
433 fn has_embedded_hfs_plus(&self) -> bool {
435 self.embedded_signature == HFS_PLUS_EMBEDDED_SIGNATURE
436 }
437}
438
439pub(crate) const CATALOG_DIR: i8 = 1;
441const CNID_CATALOG: u32 = 4;
443pub(crate) const CATALOG_FILE: i8 = 2;
444
445#[derive(Debug, Clone)]
449#[allow(dead_code)]
450enum CatalogRecord {
451 Directory {
452 dir_id: u32,
453 name: String,
454 parent_id: u32,
455 dates: (u32, u32, u32),
457 },
458 File {
459 file_id: u32,
460 name: String,
461 parent_id: u32,
462 data_size: u32,
463 data_extents: [HfsExtDescriptor; 3],
464 rsrc_size: u32,
465 rsrc_extents: [HfsExtDescriptor; 3],
466 type_code: [u8; 4],
469 creator_code: [u8; 4],
470 finder_flags: u16,
472 dates: (u32, u32, u32),
474 },
475}
476
477pub struct HfsFilesystem<R: Read + Seek> {
479 reader: R,
480 partition_offset: u64,
481 partition_size: Option<u64>,
484 mdb: HfsMasterDirectoryBlock,
485 catalog_data: Vec<u8>,
487 extents_overflow_data: Option<Vec<u8>>,
490 bitmap: Option<Vec<u8>>,
492 pending_boot_blocks: Option<Box<[u8; 1024]>>,
494 pending_volume_dates: Option<(u32, u32, u32)>,
497 bulk_mode: bool,
503 btrees_initialized: bool,
508}
509
510impl<R: Read + Seek> HfsFilesystem<R> {
511 fn alt_mdb_offset(&self) -> Option<u64> {
514 if let Some(size) = self.partition_size.filter(|&s| s >= 2048) {
515 return Some(self.partition_offset + size - 1024);
516 }
517 if self.mdb.block_size == 0 || self.mdb.total_blocks == 0 {
518 return None;
519 }
520 let sectors_per_block = self.mdb.block_size as u64 / 512;
521 let alt_sector =
522 self.mdb.first_alloc_block as u64 + self.mdb.total_blocks as u64 * sectors_per_block;
523 Some(self.partition_offset + alt_sector * 512)
524 }
525
526 pub fn open(reader: R, partition_offset: u64) -> Result<Self, FilesystemError> {
527 Self::open_sized(reader, partition_offset, None)
528 }
529
530 pub fn open_for_edit(
533 reader: R,
534 partition_offset: u64,
535 partition_size: Option<u64>,
536 ) -> Result<Self, FilesystemError> {
537 let fs = Self::open_sized(reader, partition_offset, partition_size)?;
538 if let Some(short) = fs.partition_shortfall() {
539 log::warn!(
540 "the HFS volume is {short} bytes smaller than its partition; Mac OS and Disk \
541 First Aid expect a classic HFS volume to fill its partition. Grow it with \
542 `rb-cli resize IMG@N --size <partition size>` (R-058)"
543 );
544 }
545 Ok(fs)
546 }
547
548 pub fn partition_shortfall(&self) -> Option<u64> {
551 let size = self.partition_size.filter(|&s| s >= 2048)?;
552 if self.mdb.block_size == 0 {
553 return None;
554 }
555 let volume_len = self.mdb.first_alloc_block as u64 * 512
556 + self.mdb.total_blocks as u64 * self.mdb.block_size as u64
557 + 1024;
558 if volume_len < size {
559 Some(size - volume_len)
560 } else {
561 None
562 }
563 }
564
565 pub fn open_sized(
568 mut reader: R,
569 partition_offset: u64,
570 partition_size: Option<u64>,
571 ) -> Result<Self, FilesystemError> {
572 reader.seek(SeekFrom::Start(partition_offset + 1024))?;
574 let mut mdb_buf = [0u8; 512];
575 reader.read_exact(&mut mdb_buf)?;
576 let mdb = HfsMasterDirectoryBlock::parse(&mdb_buf)?;
577
578 if mdb.has_embedded_hfs_plus() {
579 return Err(FilesystemError::Unsupported(
580 "this HFS volume contains an embedded HFS+ volume; use HFS+ reader instead".into(),
581 ));
582 }
583
584 let catalog_data = read_fork_data(
586 &mut reader,
587 partition_offset,
588 &mdb,
589 &mdb.catalog_file_extents,
590 mdb.catalog_file_size as u64,
591 )?;
592
593 let mut fs = HfsFilesystem {
594 reader,
595 partition_offset,
596 partition_size,
597 mdb,
598 catalog_data,
599 extents_overflow_data: None,
600 bitmap: None,
601 pending_boot_blocks: None,
602 pending_volume_dates: None,
603 bulk_mode: false,
604 btrees_initialized: false,
605 };
606 if (fs.catalog_data.len() as u64) < fs.mdb.catalog_file_size as u64 {
609 let inline = fs.mdb.catalog_file_extents;
610 let size = fs.mdb.catalog_file_size as u64;
611 fs.catalog_data = fs.read_fork_with_overflow(CNID_CATALOG, 0x00, &inline, size)?;
612 }
613 Ok(fs)
614 }
615
616 fn list_children(&self, parent_id: u32) -> Result<Vec<CatalogRecord>, FilesystemError> {
618 if self.catalog_data.len() < 512 {
619 return Ok(vec![]);
620 }
621
622 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as u32;
626 if (node_size as usize) < crate::hfs_common::BTREE_MIN_NODE_SIZE
627 || self.catalog_data.len() < node_size as usize
628 {
629 return Ok(vec![]);
630 }
631 let header_node = &self.catalog_data[0..node_size as usize];
632 let first_leaf = BigEndian::read_u32(&header_node[24..28]);
633
634 let mut results = Vec::new();
635 let mut node_idx = first_leaf;
636
637 while node_idx != 0 {
638 let offset = node_idx as usize * node_size as usize;
639 if offset + node_size as usize > self.catalog_data.len() {
640 break;
641 }
642 let node = &self.catalog_data[offset..offset + node_size as usize];
643
644 let next_node = BigEndian::read_u32(&node[0..4]);
646 let _kind = node[8] as i8; let num_records = BigEndian::read_u16(&node[10..12]);
648
649 for i in 0..num_records as usize {
650 let offset_pos = node_size as usize - 2 * (i + 1);
652 if offset_pos + 2 > node.len() {
653 break;
654 }
655 let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
656 if rec_offset + 6 > node.len() {
657 continue;
658 }
659
660 let key_len = node[rec_offset] as usize;
662 if key_len < 6 || rec_offset + 1 + key_len > node.len() {
663 continue;
664 }
665 let key_data = &node[rec_offset + 1..rec_offset + 1 + key_len];
666 let rec_parent_id = BigEndian::read_u32(&key_data[1..5]);
668 let name_len = key_data[5] as usize;
669 let name = if name_len > 0 && 6 + name_len <= key_data.len() {
670 mac_roman_to_utf8(&key_data[6..6 + name_len])
671 } else {
672 String::new()
673 };
674
675 let mut rec_data_offset = rec_offset + 1 + key_len;
677 if !rec_data_offset.is_multiple_of(2) {
678 rec_data_offset += 1;
679 }
680 if rec_data_offset + 2 > node.len() {
681 continue;
682 }
683
684 let record_type = node[rec_data_offset] as i8;
685
686 if rec_parent_id != parent_id {
687 continue;
688 }
689
690 match record_type {
691 CATALOG_DIR => {
692 if rec_data_offset + 70 > node.len() {
693 continue;
694 }
695 let dir_id =
696 BigEndian::read_u32(&node[rec_data_offset + 6..rec_data_offset + 10]);
697 let dir = &node[rec_data_offset..];
699 let dates = (
700 BigEndian::read_u32(&dir[10..14]),
701 BigEndian::read_u32(&dir[14..18]),
702 BigEndian::read_u32(&dir[18..22]),
703 );
704 results.push(CatalogRecord::Directory {
705 dir_id,
706 name,
707 parent_id: rec_parent_id,
708 dates,
709 });
710 }
711 CATALOG_FILE => {
712 if rec_data_offset + 102 > node.len() {
713 continue;
714 }
715 let rec = &node[rec_data_offset..];
716 let type_code: [u8; 4] = rec[4..8].try_into().unwrap();
718 let creator_code: [u8; 4] = rec[8..12].try_into().unwrap();
719 let finder_flags = BigEndian::read_u16(&rec[12..14]);
720 let file_id = BigEndian::read_u32(&rec[20..24]);
722 let data_size = BigEndian::read_u32(&rec[26..30]);
724 let mut data_extents = [HfsExtDescriptor {
725 start_block: 0,
726 block_count: 0,
727 }; 3];
728 for j in 0..3 {
729 data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
730 }
731 let rsrc_size = BigEndian::read_u32(&rec[36..40]);
733 let mut rsrc_extents = [HfsExtDescriptor {
734 start_block: 0,
735 block_count: 0,
736 }; 3];
737 for j in 0..3 {
738 rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
739 }
740 let dates = (
742 BigEndian::read_u32(&rec[44..48]),
743 BigEndian::read_u32(&rec[48..52]),
744 BigEndian::read_u32(&rec[52..56]),
745 );
746 results.push(CatalogRecord::File {
747 file_id,
748 name,
749 parent_id: rec_parent_id,
750 data_size,
751 data_extents,
752 rsrc_size,
753 rsrc_extents,
754 type_code,
755 creator_code,
756 finder_flags,
757 dates,
758 });
759 }
760 _ => {}
761 }
762 }
763
764 node_idx = next_node;
765 }
766
767 Ok(results)
768 }
769
770 fn find_file_by_id(
773 &self,
774 file_id: u32,
775 ) -> Option<(u32, [HfsExtDescriptor; 3], u32, [HfsExtDescriptor; 3])> {
776 if self.catalog_data.len() < 512 {
777 return None;
778 }
779 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as u32;
780 if (node_size as usize) < crate::hfs_common::BTREE_MIN_NODE_SIZE
781 || self.catalog_data.len() < node_size as usize
782 {
783 return None;
784 }
785 let header_node = &self.catalog_data[0..node_size as usize];
786 let first_leaf = BigEndian::read_u32(&header_node[24..28]);
787
788 let mut node_idx = first_leaf;
789 while node_idx != 0 {
790 let offset = node_idx as usize * node_size as usize;
791 if offset + node_size as usize > self.catalog_data.len() {
792 break;
793 }
794 let node = &self.catalog_data[offset..offset + node_size as usize];
795 let next_node = BigEndian::read_u32(&node[0..4]);
796 let num_records = BigEndian::read_u16(&node[10..12]);
797
798 for i in 0..num_records as usize {
799 let offset_pos = node_size as usize - 2 * (i + 1);
800 if offset_pos + 2 > node.len() {
801 break;
802 }
803 let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
804 if rec_offset + 6 > node.len() {
805 continue;
806 }
807 let key_len = node[rec_offset] as usize;
808 if key_len < 6 || rec_offset + 1 + key_len > node.len() {
809 continue;
810 }
811 let mut rec_data_offset = rec_offset + 1 + key_len;
812 if !rec_data_offset.is_multiple_of(2) {
813 rec_data_offset += 1;
814 }
815 if rec_data_offset + 2 > node.len() {
816 continue;
817 }
818 let record_type = node[rec_data_offset] as i8;
819 if record_type != CATALOG_FILE {
820 continue;
821 }
822 if rec_data_offset + 102 > node.len() {
823 continue;
824 }
825 let rec = &node[rec_data_offset..];
826 let rec_file_id = BigEndian::read_u32(&rec[20..24]);
828 if rec_file_id != file_id {
829 continue;
830 }
831 let data_size = BigEndian::read_u32(&rec[26..30]);
833 let mut data_extents = [HfsExtDescriptor {
834 start_block: 0,
835 block_count: 0,
836 }; 3];
837 for j in 0..3 {
838 data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
839 }
840 let rsrc_size = BigEndian::read_u32(&rec[36..40]);
842 let mut rsrc_extents = [HfsExtDescriptor {
843 start_block: 0,
844 block_count: 0,
845 }; 3];
846 for j in 0..3 {
847 rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
848 }
849 return Some((data_size, data_extents, rsrc_size, rsrc_extents));
850 }
851 node_idx = next_node;
852 }
853 None
854 }
855
856 fn read_volume_bitmap(&mut self) -> Result<Vec<u8>, FilesystemError> {
858 let bitmap_offset = self.partition_offset + self.mdb.volume_bitmap_block as u64 * 512;
859 let bitmap_size = (self.mdb.total_blocks as u32).div_ceil(8) as usize;
860 self.reader.seek(SeekFrom::Start(bitmap_offset))?;
861 let mut bitmap = vec![0u8; bitmap_size];
862 self.reader.read_exact(&mut bitmap)?;
863 Ok(bitmap)
864 }
865
866 fn alloc_block_offset(&self, block: u32) -> u64 {
868 self.mdb.first_alloc_block as u64 * 512 + block as u64 * self.mdb.block_size as u64
869 }
870
871 fn ensure_bitmap(&mut self) -> Result<(), FilesystemError> {
873 if self.bitmap.is_none() {
874 let bm = self.read_volume_bitmap()?;
875 self.bitmap = Some(bm);
876 }
877 Ok(())
878 }
879
880 pub fn fsck(&mut self) -> Result<disk_core::fsck::FsckResult, FilesystemError> {
882 self.ensure_bitmap()?;
883 let extents_data = if self.mdb.extents_file_size > 0 {
885 read_fork_data(
886 &mut self.reader,
887 self.partition_offset,
888 &self.mdb,
889 &self.mdb.extents_file_extents,
890 self.mdb.extents_file_size as u64,
891 )
892 .ok()
893 } else {
894 None
895 };
896 let alt_mdb_sector = if let Some(alt_offset) = self.alt_mdb_offset() {
899 let mut alt_buf = [0u8; 512];
900 if self
901 .reader
902 .seek(SeekFrom::Start(alt_offset))
903 .and_then(|_| self.reader.read_exact(&mut alt_buf))
904 .is_ok()
905 {
906 Some(alt_buf)
907 } else {
908 None
909 }
910 } else {
911 None
912 };
913 Ok(crate::hfs_fsck::check_hfs_integrity(
914 &self.mdb,
915 &self.catalog_data,
916 self.bitmap.as_ref().unwrap(),
917 extents_data.as_deref(),
918 alt_mdb_sector.as_ref(),
919 self.partition_size,
920 ))
921 }
922
923 pub(crate) fn build_catalog_key(parent_id: u32, name: &[u8]) -> Vec<u8> {
925 let key_len = 1 + 4 + 1 + name.len(); let mut key = Vec::with_capacity(1 + key_len);
927 key.push(key_len as u8); key.push(0); let mut buf4 = [0u8; 4];
930 BigEndian::write_u32(&mut buf4, parent_id);
931 key.extend_from_slice(&buf4);
932 key.push(name.len() as u8);
933 key.extend_from_slice(name);
934 if key.len() % 2 != 0 {
936 key.push(0);
937 }
938 key
939 }
940
941 pub(crate) fn catalog_compare(a: &[u8], b: &[u8]) -> Ordering {
943 if a.len() < 7 || b.len() < 7 {
945 return a.len().cmp(&b.len());
946 }
947 let parent_a = BigEndian::read_u32(&a[2..6]);
948 let parent_b = BigEndian::read_u32(&b[2..6]);
949 let name_len_a = a[6] as usize;
950 let name_len_b = b[6] as usize;
951 let name_a = &a[7..7 + name_len_a.min(a.len() - 7)];
952 let name_b = &b[7..7 + name_len_b.min(b.len() - 7)];
953 hfs_common::compare_hfs_keys(parent_a, name_a, parent_b, name_b)
954 }
955
956 fn find_catalog_record_by_name(
969 &self,
970 parent_id: u32,
971 name: &[u8],
972 ) -> Option<(u32, usize, usize)> {
973 let search_key = Self::build_catalog_key(parent_id, name);
974 let header = BTreeHeader::read(&self.catalog_data);
975 let node_size = header.node_size as usize;
976 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || self.catalog_data.len() < node_size
977 {
978 return None;
979 }
980
981 let (leaf_idx, _) = hfs_common::btree_find_insert_leaf(
982 &self.catalog_data,
983 &header,
984 &search_key,
985 &Self::catalog_compare,
986 );
987
988 let off = leaf_idx as usize * node_size;
989 if off + node_size > self.catalog_data.len() {
990 return None;
991 }
992 let node = &self.catalog_data[off..off + node_size];
993 let num_records = BigEndian::read_u16(&node[10..12]) as usize;
994 for rec_idx in 0..num_records {
995 let (start, end) = hfs_common::btree_record_range(node, node_size, rec_idx);
996 if start >= end || end > node_size {
997 continue;
998 }
999 let rec = &node[start..end];
1000 if rec.len() < 7 {
1001 continue;
1002 }
1003 let key_len = rec[0] as usize;
1004 let key_end = 1 + key_len;
1005 if key_end > rec.len() {
1006 continue;
1007 }
1008 if Self::catalog_compare(&rec[..key_end], &search_key) == Ordering::Equal {
1009 return Some((leaf_idx, rec_idx, off + start));
1010 }
1011 }
1012 None
1013 }
1014
1015 fn find_catalog_record_by_cnid(&self, cnid: u32) -> Option<(u32, usize, usize)> {
1017 self.find_catalog_record_by_name(cnid, &[])
1018 }
1019
1020 fn update_parent_valence(&mut self, parent_id: u32, delta: i32) -> Result<(), FilesystemError> {
1022 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1023 let first_leaf = BigEndian::read_u32(&self.catalog_data[24..28]);
1024
1025 let val_off = hfs_common::walk_leaf_records(
1028 &self.catalog_data,
1029 first_leaf,
1030 node_size,
1031 |_node_idx, _rec_idx, abs_off, rec| {
1032 if rec.len() < 7 {
1033 return None;
1034 }
1035 let key_len = rec[0] as usize;
1036 let mut data_off = abs_off + 1 + key_len;
1037 if !data_off.is_multiple_of(2) {
1038 data_off += 1;
1039 }
1040 let abs_end = abs_off + rec.len();
1041 if data_off + 10 > abs_end {
1042 return None;
1043 }
1044 if self.catalog_data[data_off] as i8 != CATALOG_DIR {
1045 return None;
1046 }
1047 let dir_id = BigEndian::read_u32(&self.catalog_data[data_off + 6..data_off + 10]);
1049 if dir_id == parent_id {
1050 Some(data_off + 4)
1051 } else {
1052 None
1053 }
1054 },
1055 );
1056 let val_off = val_off.ok_or_else(|| {
1057 FilesystemError::NotFound(format!(
1058 "parent directory CNID {} not found in catalog",
1059 parent_id
1060 ))
1061 })?;
1062 let old_val = BigEndian::read_u16(&self.catalog_data[val_off..val_off + 2]);
1063 let new_val = (old_val as i32 + delta).max(0) as u16;
1064 BigEndian::write_u16(&mut self.catalog_data[val_off..val_off + 2], new_val);
1065 Ok(())
1066 }
1067
1068 fn insert_catalog_record(&mut self, key_record: &[u8]) -> Result<(), FilesystemError>
1086 where
1087 R: Write,
1088 {
1089 let mut grown = false;
1092 loop {
1093 match hfs_common::btree_insert_full(
1094 &mut self.catalog_data,
1095 key_record,
1096 &hfs_common::BTreeKeyFormat::CLASSIC_CATALOG,
1097 &Self::catalog_compare,
1098 ) {
1099 Ok(()) => return Ok(()),
1100 Err(FilesystemError::DiskFull(_)) if !grown => {
1101 self.grow_btree(HfsTree::Catalog)?;
1102 grown = true;
1103 }
1104 Err(e) => return Err(e),
1105 }
1106 }
1107 }
1108
1109 fn grow_btree(&mut self, tree: HfsTree) -> Result<(), FilesystemError>
1114 where
1115 R: Write,
1116 {
1117 let block_size = self.mdb.block_size;
1118 if tree == HfsTree::Extents {
1119 self.ensure_extents_overflow()?;
1120 }
1121 let (node_size, total_nodes, existing_map) = {
1122 let buf = self.tree_buf(tree)?;
1123 let h = BTreeHeader::read(buf);
1124 let segs = hfs_common::btree_bitmap_segments(buf, h.node_size as usize);
1125 (
1126 h.node_size as usize,
1127 h.total_nodes,
1128 (segs.len() as u32).saturating_sub(1),
1129 )
1130 };
1131 if node_size < 256 || block_size == 0 {
1132 return Err(FilesystemError::InvalidData(format!(
1133 "{} B-tree has node size {node_size}",
1134 tree.label()
1135 )));
1136 }
1137 let clump_off = if tree == HfsTree::Catalog { 78 } else { 74 };
1139 let clump = BigEndian::read_u32(&self.mdb.raw_sector[clump_off..clump_off + 4]) as usize;
1140 let grow_nodes = (clump.div_ceil(node_size).max(1) as u32).max(8);
1141 let header_cap = ((node_size - 256) * 8) as u32;
1142 let per_map = (hfs_common::map_node_bitmap_bytes(node_size) * 8) as u32;
1143 let need_map = total_nodes + grow_nodes > header_cap + existing_map * per_map;
1144 let add_slots = grow_nodes + u32::from(need_map);
1145 let grow_blocks = btree_grow_blocks(total_nodes, add_slots, node_size, block_size);
1146 let mut add_slots = add_slots;
1147 if grow_blocks > 0 {
1148 let mut inline = match tree {
1149 HfsTree::Catalog => self.mdb.catalog_file_extents,
1150 HfsTree::Extents => self.mdb.extents_file_extents,
1151 };
1152 let mut all: Vec<HfsExtDescriptor> = inline
1154 .iter()
1155 .copied()
1156 .filter(|e| e.block_count > 0)
1157 .collect();
1158 let inline_blocks: u32 = all.iter().map(|e| e.block_count as u32).sum();
1159 if tree == HfsTree::Catalog && all.len() == 3 {
1160 self.ensure_extents_overflow()?;
1161 if let Some(x) = self.extents_overflow_data.as_ref() {
1162 all.extend(collect_fork_overflow_extents(
1163 x,
1164 CNID_CATALOG,
1165 0x00,
1166 inline_blocks,
1167 ));
1168 }
1169 }
1170 let tail = all
1171 .last()
1172 .map(|e| e.start_block as u32 + e.block_count as u32);
1173 match tail {
1174 Some(t) if self.allocate_blocks_at(t, grow_blocks)? => {
1175 all.last_mut().unwrap().block_count += grow_blocks as u16;
1176 }
1177 _ => {
1178 let want_nodes = (total_nodes + add_slots).max(grow_nodes + 8);
1181 let more = btree_grow_blocks(
1182 total_nodes,
1183 want_nodes - total_nodes,
1184 node_size,
1185 block_size,
1186 )
1187 .max(grow_blocks);
1188 add_slots = want_nodes.max(total_nodes + add_slots) - total_nodes;
1189 if tree == HfsTree::Extents && all.len() >= 3 {
1190 return Err(FilesystemError::DiskFull(
1191 "extents-overflow file has no free extent slot to grow into".into(),
1192 ));
1193 }
1194 let (start, more) = match self.allocate_blocks(more) {
1196 Ok(start) => (start, more),
1197 Err(_) => {
1198 add_slots = grow_nodes + u32::from(need_map);
1199 (self.allocate_blocks(grow_blocks)?, grow_blocks)
1200 }
1201 };
1202 all.push(HfsExtDescriptor {
1203 start_block: start as u16,
1204 block_count: more as u16,
1205 });
1206 }
1207 }
1208 inline = [HfsExtDescriptor {
1209 start_block: 0,
1210 block_count: 0,
1211 }; 3];
1212 for (i, e) in all.iter().take(3).enumerate() {
1213 inline[i] = *e;
1214 }
1215 match tree {
1216 HfsTree::Catalog => {
1217 self.mdb.catalog_file_extents = inline;
1218 if all.len() > 3 {
1219 let data = self.extents_overflow_data.as_mut().expect("loaded above");
1221 remove_fork_overflow_records(data, CNID_CATALOG, 0x00)?;
1222 self.insert_fork_overflow(CNID_CATALOG, 0x00, &all)?;
1223 self.write_extents_overflow_back()?;
1224 }
1225 }
1226 HfsTree::Extents => self.mdb.extents_file_extents = inline,
1227 }
1228 }
1229 let buf = self.tree_buf(tree)?;
1230 buf.resize(buf.len() + add_slots as usize * node_size, 0);
1231 if need_map {
1232 let map_idx = total_nodes;
1235 let tail = btree_map_chain_tail(buf, node_size);
1236 hfs_common::init_map_node(buf, node_size, map_idx, tail, 0);
1237 let linker = tail as usize * node_size;
1238 BigEndian::write_u32(&mut buf[linker..linker + 4], map_idx);
1239 hfs_common::btree_bitmap_set(buf, node_size, map_idx);
1240 }
1241 let mut h = BTreeHeader::read(buf);
1242 h.total_nodes += add_slots;
1243 h.free_nodes += add_slots - u32::from(need_map);
1244 h.write(buf);
1245 let added = add_slots * node_size as u32;
1246 match tree {
1247 HfsTree::Catalog => self.mdb.catalog_file_size += added,
1248 HfsTree::Extents => self.mdb.extents_file_size += added,
1249 }
1250 Ok(())
1251 }
1252
1253 fn tree_buf(&mut self, tree: HfsTree) -> Result<&mut Vec<u8>, FilesystemError> {
1255 match tree {
1256 HfsTree::Catalog => Ok(&mut self.catalog_data),
1257 HfsTree::Extents => self.extents_overflow_data.as_mut().ok_or_else(|| {
1258 FilesystemError::Unsupported("volume has no extents-overflow B-tree".into())
1259 }),
1260 }
1261 }
1262
1263 fn allocate_blocks_at(&mut self, start: u32, count: u32) -> Result<bool, FilesystemError> {
1265 self.ensure_bitmap()?;
1266 let total = self.mdb.total_blocks as u32;
1267 let bitmap = self.bitmap.as_mut().unwrap();
1268 if start + count > total || (start..start + count).any(|b| bitmap_test_bit_be(bitmap, b)) {
1269 return Ok(false);
1270 }
1271 for b in start..start + count {
1272 bitmap_set_bit_be(bitmap, b);
1273 }
1274 self.mdb.free_blocks = self.mdb.free_blocks.saturating_sub(count as u16);
1275 Ok(true)
1276 }
1277
1278 fn remove_catalog_record(
1280 &mut self,
1281 node_idx: u32,
1282 rec_idx: usize,
1283 ) -> Result<(), FilesystemError> {
1284 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1285 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE {
1286 return Ok(());
1287 }
1288 let offset = node_idx as usize * node_size;
1289 if offset + node_size > self.catalog_data.len() {
1290 return Ok(());
1291 }
1292
1293 let num_before = {
1294 let node = &self.catalog_data[offset..offset + node_size];
1295 BigEndian::read_u16(&node[10..12]) as usize
1296 };
1297
1298 {
1299 let node = &mut self.catalog_data[offset..offset + node_size];
1300 btree_remove_record(node, node_size, rec_idx);
1301 }
1302
1303 let node = &self.catalog_data[offset..offset + node_size];
1305 let num_after = BigEndian::read_u16(&node[10..12]) as usize;
1306 if num_after == 0 && num_before > 0 {
1307 let prev = BigEndian::read_u32(&self.catalog_data[offset + 4..offset + 8]);
1308 let next = BigEndian::read_u32(&self.catalog_data[offset..offset + 4]);
1309
1310 if prev != 0 {
1311 let prev_off = prev as usize * node_size;
1312 if prev_off + 4 <= self.catalog_data.len() {
1313 BigEndian::write_u32(&mut self.catalog_data[prev_off..prev_off + 4], next);
1314 }
1315 }
1316 if next != 0 {
1317 let next_off = next as usize * node_size;
1318 if next_off + 8 <= self.catalog_data.len() {
1319 BigEndian::write_u32(&mut self.catalog_data[next_off + 4..next_off + 8], prev);
1320 }
1321 }
1322
1323 let mut h = BTreeHeader::read(&self.catalog_data);
1324 if h.first_leaf_node == node_idx {
1325 h.first_leaf_node = next;
1326 }
1327 if h.last_leaf_node == node_idx {
1328 h.last_leaf_node = prev;
1329 }
1330 btree_free_node(&mut self.catalog_data, node_size, node_idx);
1331 h.free_nodes += 1;
1332 if h.root_node == node_idx {
1334 h.root_node = 0;
1335 h.depth = 0;
1336 }
1337 h.write(&mut self.catalog_data);
1338
1339 let header = BTreeHeader::read(&self.catalog_data);
1341 if header.depth > 1 {
1342 let mut dummy_report = disk_core::fsck::RepairReport {
1343 fixes_applied: Vec::new(),
1344 fixes_failed: Vec::new(),
1345 unrepairable_count: 0,
1346 };
1347 crate::hfs_fsck::rebuild_index_nodes(
1348 &mut self.catalog_data,
1349 node_size,
1350 &mut dummy_report,
1351 );
1352 }
1353 } else if rec_idx == 0 && num_after > 0 {
1354 btree_refresh_index_keys(
1357 &mut self.catalog_data,
1358 node_size,
1359 node_idx,
1360 &BTreeKeyFormat::CLASSIC_CATALOG,
1361 &Self::catalog_compare,
1362 )?;
1363 }
1364
1365 let mut h = BTreeHeader::read(&self.catalog_data);
1367 h.leaf_records = h.leaf_records.saturating_sub(1);
1368 h.write(&mut self.catalog_data);
1369 Ok(())
1370 }
1371
1372 fn snapshot(&self) -> Option<(Vec<u8>, Option<Vec<u8>>, HfsMasterDirectoryBlock)> {
1381 if self.bulk_mode {
1382 return None;
1383 }
1384 Some((
1385 self.catalog_data.clone(),
1386 self.bitmap.clone(),
1387 self.mdb.clone(),
1388 ))
1389 }
1390
1391 fn restore_snapshot(
1394 &mut self,
1395 snap: Option<(Vec<u8>, Option<Vec<u8>>, HfsMasterDirectoryBlock)>,
1396 ) {
1397 let Some((catalog_data, bitmap, mdb)) = snap else {
1398 return;
1399 };
1400 self.catalog_data = catalog_data;
1401 self.bitmap = bitmap;
1402 self.mdb = mdb;
1403 }
1404
1405 pub(crate) fn mdb(&self) -> &HfsMasterDirectoryBlock {
1406 &self.mdb
1407 }
1408
1409 pub fn volume_summary(&self) -> HfsVolumeSummary {
1413 let used_blocks = self.mdb.total_blocks.saturating_sub(self.mdb.free_blocks) as u64;
1414 HfsVolumeSummary {
1415 volume_name: self.mdb.volume_name.clone(),
1416 block_size: self.mdb.block_size,
1417 total_blocks: self.mdb.total_blocks,
1418 free_blocks: self.mdb.free_blocks,
1419 used_bytes: used_blocks * self.mdb.block_size as u64,
1420 file_count: self.mdb.file_count,
1421 folder_count: self.mdb.folder_count,
1422 catalog_file_size: self.mdb.catalog_file_size,
1423 extents_file_size: self.mdb.extents_file_size,
1424 }
1425 }
1426
1427 pub(crate) fn catalog_data(&self) -> &[u8] {
1428 &self.catalog_data
1429 }
1430
1431 #[allow(dead_code)]
1432 pub(crate) fn partition_offset(&self) -> u64 {
1433 self.partition_offset
1434 }
1435
1436 pub fn locate_file_by_cnid(&self, cnid: u32) -> Option<HfsFileLocation> {
1448 let (data_size, data_extents, _rsrc_size, _rsrc_extents) = self.find_file_by_id(cnid)?;
1449 let first = data_extents.first().copied()?;
1450 if first.block_count == 0 {
1451 return None;
1452 }
1453 let fragmented = data_extents.iter().skip(1).any(|e| e.block_count != 0);
1454 let offset_in_image =
1458 self.partition_offset + self.alloc_block_offset(first.start_block as u32);
1459 Some(HfsFileLocation {
1460 offset_in_image,
1461 length: data_size as u64,
1462 fragmented,
1463 })
1464 }
1465
1466 pub(crate) fn read_boot_blocks(&mut self) -> Result<[u8; 1024], FilesystemError> {
1468 self.reader.seek(SeekFrom::Start(self.partition_offset))?;
1469 let mut buf = [0u8; 1024];
1470 self.reader.read_exact(&mut buf)?;
1471 Ok(buf)
1472 }
1473
1474 #[allow(dead_code)]
1477 pub(crate) fn read_fork(
1478 &mut self,
1479 extents: &[HfsExtDescriptor; 3],
1480 size: u64,
1481 ) -> Result<Vec<u8>, FilesystemError> {
1482 read_fork_data(
1483 &mut self.reader,
1484 self.partition_offset,
1485 &self.mdb,
1486 extents,
1487 size,
1488 )
1489 }
1490
1491 fn ensure_extents_overflow(&mut self) -> Result<(), FilesystemError> {
1494 if self.extents_overflow_data.is_some() {
1495 return Ok(());
1496 }
1497 if self.mdb.extents_file_size == 0 {
1498 return Ok(());
1499 }
1500 let data = read_fork_data(
1501 &mut self.reader,
1502 self.partition_offset,
1503 &self.mdb,
1504 &self.mdb.extents_file_extents,
1505 self.mdb.extents_file_size as u64,
1506 )?;
1507 self.extents_overflow_data = Some(data);
1508 Ok(())
1509 }
1510
1511 fn write_fork_with_overflow_to(
1518 &mut self,
1519 file_id: u32,
1520 fork_type: u8,
1521 inline: &[HfsExtDescriptor; 3],
1522 size: u64,
1523 writer: &mut dyn std::io::Write,
1524 ) -> Result<u64, FilesystemError> {
1525 let mut written = write_fork_data_to(
1526 &mut self.reader,
1527 self.partition_offset,
1528 &self.mdb,
1529 inline,
1530 writer,
1531 size,
1532 )?;
1533 if written >= size {
1534 return Ok(written);
1535 }
1536 self.ensure_extents_overflow()?;
1537 let Some(ext_data) = self.extents_overflow_data.as_ref() else {
1538 return Err(FilesystemError::InvalidData(format!(
1539 "file {file_id} fork {fork_type:#x}: {size} bytes requested but only {written} \
1540 bytes in inline extents and no extents-overflow B-tree present",
1541 )));
1542 };
1543 let inline_blocks: u32 = inline.iter().map(|e| e.block_count as u32).sum();
1544 let overflow = collect_fork_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
1545 let block_size = self.mdb.block_size as u64;
1546 let first_alloc_offset = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
1547 let mut buf = vec![0u8; 64 * 1024];
1548 for ext in overflow {
1549 if written >= size {
1550 break;
1551 }
1552 if ext.block_count == 0 {
1553 continue;
1554 }
1555 let offset = first_alloc_offset + ext.start_block as u64 * block_size;
1556 let extent_len = ext.block_count as u64 * block_size;
1557 let to_emit = extent_len.min(size - written);
1558 self.reader.seek(SeekFrom::Start(offset))?;
1559 let mut left = to_emit;
1560 while left > 0 {
1561 let n = (buf.len() as u64).min(left) as usize;
1562 self.reader.read_exact(&mut buf[..n])?;
1563 writer.write_all(&buf[..n])?;
1564 left -= n as u64;
1565 }
1566 written += to_emit;
1567 }
1568 if written < size {
1569 return Err(FilesystemError::InvalidData(format!(
1570 "file {file_id} fork {fork_type:#x}: extents (inline + overflow) cover \
1571 {written} of {size} bytes",
1572 )));
1573 }
1574 Ok(written)
1575 }
1576
1577 fn read_fork_with_overflow(
1578 &mut self,
1579 file_id: u32,
1580 fork_type: u8,
1581 inline: &[HfsExtDescriptor; 3],
1582 size: u64,
1583 ) -> Result<Vec<u8>, FilesystemError> {
1584 let mut data = read_fork_data(
1585 &mut self.reader,
1586 self.partition_offset,
1587 &self.mdb,
1588 inline,
1589 size,
1590 )?;
1591 if (data.len() as u64) >= size {
1592 return Ok(data);
1593 }
1594
1595 self.ensure_extents_overflow()?;
1596 let Some(ext_data) = self.extents_overflow_data.as_ref() else {
1597 return Err(FilesystemError::InvalidData(format!(
1598 "file {file_id} fork {fork_type:#x}: {size} bytes requested but only {} bytes \
1599 in inline extents and no extents-overflow B-tree present",
1600 data.len()
1601 )));
1602 };
1603
1604 let inline_blocks: u32 = inline.iter().map(|e| e.block_count as u32).sum();
1605 let overflow = collect_fork_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
1606
1607 let block_size = self.mdb.block_size as u64;
1608 let first_alloc_offset = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
1609
1610 for ext in overflow {
1611 if data.len() as u64 >= size {
1612 break;
1613 }
1614 if ext.block_count == 0 {
1615 continue;
1616 }
1617 let offset = first_alloc_offset + ext.start_block as u64 * block_size;
1618 let extent_len = ext.block_count as u64 * block_size;
1619 let to_read = extent_len.min(size - data.len() as u64) as usize;
1620 self.reader.seek(SeekFrom::Start(offset))?;
1621 let mut buf = vec![0u8; to_read];
1622 self.reader.read_exact(&mut buf)?;
1623 data.extend_from_slice(&buf);
1624 }
1625
1626 if (data.len() as u64) < size {
1627 return Err(FilesystemError::InvalidData(format!(
1628 "file {file_id} fork {fork_type:#x}: extents (inline + overflow) cover \
1629 {} of {size} bytes",
1630 data.len()
1631 )));
1632 }
1633 Ok(data)
1634 }
1635
1636 fn locate_record_data(&self, cnid: u32) -> Result<usize, FilesystemError> {
1648 if let Some((_, _, t_offset)) = self.find_catalog_record_by_cnid(cnid) {
1649 let key_len = self.catalog_data[t_offset] as usize;
1650 let mut rec_data_start = t_offset + 1 + key_len;
1651 if !rec_data_start.is_multiple_of(2) {
1652 rec_data_start += 1;
1653 }
1654 let thread_parent =
1655 BigEndian::read_u32(&self.catalog_data[rec_data_start + 10..rec_data_start + 14]);
1656 let thread_name_len = self.catalog_data[rec_data_start + 14] as usize;
1657 let thread_name = self.catalog_data
1658 [rec_data_start + 15..rec_data_start + 15 + thread_name_len]
1659 .to_vec();
1660
1661 if let Some((_, _, f_offset)) =
1662 self.find_catalog_record_by_name(thread_parent, &thread_name)
1663 {
1664 let fkey_len = self.catalog_data[f_offset] as usize;
1665 let mut frec_start = f_offset + 1 + fkey_len;
1666 if !frec_start.is_multiple_of(2) {
1667 frec_start += 1;
1668 }
1669 return Ok(frec_start);
1670 }
1671 }
1672
1673 if let Some((_, _, frec_start)) = self.find_file_record_offset_by_cnid(cnid) {
1674 return Ok(frec_start);
1675 }
1676
1677 Err(FilesystemError::NotFound(format!(
1678 "catalog record for CNID {cnid} not found"
1679 )))
1680 }
1681
1682 fn find_file_record_offset_by_cnid(&self, cnid: u32) -> Option<(u32, usize, usize)> {
1691 if self.catalog_data.len() < 512 {
1692 return None;
1693 }
1694 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1695 let first_leaf = BigEndian::read_u32(&self.catalog_data[24..28]);
1696
1697 hfs_common::walk_leaf_records(
1698 &self.catalog_data,
1699 first_leaf,
1700 node_size,
1701 |node_idx, rec_idx, abs_off, rec| {
1702 if rec.len() < 7 {
1703 return None;
1704 }
1705 let key_len = rec[0] as usize;
1706 if key_len < 6 || 1 + key_len > rec.len() {
1707 return None;
1708 }
1709 let mut data_rel = 1 + key_len;
1710 if !data_rel.is_multiple_of(2) {
1711 data_rel += 1;
1712 }
1713 if data_rel + 24 > rec.len() {
1714 return None;
1715 }
1716 if rec[data_rel] as i8 != CATALOG_FILE {
1717 return None;
1718 }
1719 let rec_cnid = BigEndian::read_u32(&rec[data_rel + 20..data_rel + 24]);
1721 if rec_cnid == cnid {
1722 Some((node_idx, rec_idx, abs_off + data_rel))
1723 } else {
1724 None
1725 }
1726 },
1727 )
1728 }
1729
1730 pub fn set_finder_info(
1733 &mut self,
1734 entry: &FileEntry,
1735 finfo: [u8; 16],
1736 fxinfo: [u8; 16],
1737 ) -> Result<(), FilesystemError> {
1738 if entry.is_directory() {
1739 return Err(FilesystemError::InvalidData(
1740 "set_finder_info: entry is a directory".into(),
1741 ));
1742 }
1743 let snap = self.snapshot();
1744 let result = (|| {
1745 let rec = self.locate_record_data(entry.location as u32)?;
1746 if self.catalog_data[rec] as i8 != CATALOG_FILE {
1747 return Err(FilesystemError::InvalidData("not a file record".into()));
1748 }
1749 self.catalog_data[rec + 4..rec + 20].copy_from_slice(&finfo);
1750 self.catalog_data[rec + 56..rec + 72].copy_from_slice(&fxinfo);
1751 Ok(())
1752 })();
1753 if result.is_err() {
1754 self.restore_snapshot(snap);
1755 }
1756 result
1757 }
1758
1759 pub fn set_directory_finder_info(
1762 &mut self,
1763 entry: &FileEntry,
1764 dinfo: [u8; 16],
1765 dxinfo: [u8; 16],
1766 ) -> Result<(), FilesystemError> {
1767 if !entry.is_directory() {
1768 return Err(FilesystemError::InvalidData(
1769 "set_directory_finder_info: entry is not a directory".into(),
1770 ));
1771 }
1772 let snap = self.snapshot();
1773 let result = (|| {
1774 let rec = self.locate_record_data(entry.location as u32)?;
1775 if self.catalog_data[rec] as i8 != CATALOG_DIR {
1776 return Err(FilesystemError::InvalidData(
1777 "not a directory record".into(),
1778 ));
1779 }
1780 self.catalog_data[rec + 22..rec + 38].copy_from_slice(&dinfo);
1781 self.catalog_data[rec + 38..rec + 54].copy_from_slice(&dxinfo);
1782 Ok(())
1783 })();
1784 if result.is_err() {
1785 self.restore_snapshot(snap);
1786 }
1787 result
1788 }
1789
1790 pub fn set_dates(
1794 &mut self,
1795 entry: &FileEntry,
1796 create: u32,
1797 modify: u32,
1798 backup: u32,
1799 ) -> Result<(), FilesystemError> {
1800 let snap = self.snapshot();
1801 let result = (|| {
1802 let rec = self.locate_record_data(entry.location as u32)?;
1803 let record_type = self.catalog_data[rec] as i8;
1804 let (cr_off, md_off, bk_off) = match record_type {
1805 CATALOG_FILE => (44, 48, 52),
1806 CATALOG_DIR => (10, 14, 18),
1807 other => {
1808 return Err(FilesystemError::InvalidData(format!(
1809 "unexpected catalog record type: {other}"
1810 )))
1811 }
1812 };
1813 BigEndian::write_u32(
1814 &mut self.catalog_data[rec + cr_off..rec + cr_off + 4],
1815 create,
1816 );
1817 BigEndian::write_u32(
1818 &mut self.catalog_data[rec + md_off..rec + md_off + 4],
1819 modify,
1820 );
1821 BigEndian::write_u32(
1822 &mut self.catalog_data[rec + bk_off..rec + bk_off + 4],
1823 backup,
1824 );
1825 Ok(())
1826 })();
1827 if result.is_err() {
1828 self.restore_snapshot(snap);
1829 }
1830 result
1831 }
1832
1833 pub fn set_file_locked(
1836 &mut self,
1837 entry: &FileEntry,
1838 locked: bool,
1839 ) -> Result<(), FilesystemError> {
1840 if entry.is_directory() {
1841 return Err(FilesystemError::InvalidData(
1842 "set_file_locked: entry is a directory".into(),
1843 ));
1844 }
1845 let snap = self.snapshot();
1846 let result = (|| {
1847 let rec = self.locate_record_data(entry.location as u32)?;
1848 if self.catalog_data[rec] as i8 != CATALOG_FILE {
1849 return Err(FilesystemError::InvalidData("not a file record".into()));
1850 }
1851 let flags = self.catalog_data[rec + 2];
1852 self.catalog_data[rec + 2] = if locked { flags | 0x01 } else { flags & !0x01 };
1853 Ok(())
1854 })();
1855 if result.is_err() {
1856 self.restore_snapshot(snap);
1857 }
1858 result
1859 }
1860
1861 pub fn set_volume_dates(&mut self, create: u32, modify: u32, backup: u32) {
1865 self.pending_volume_dates = Some((create, modify, backup));
1866 self.mdb.create_date = create;
1867 self.mdb.modify_date = modify;
1868 BigEndian::write_u32(&mut self.mdb.raw_sector[2..6], create);
1869 BigEndian::write_u32(&mut self.mdb.raw_sector[6..10], modify);
1870 BigEndian::write_u32(&mut self.mdb.raw_sector[64..68], backup);
1871 }
1872
1873 pub fn set_boot_blocks(&mut self, blocks: &[u8; 1024]) {
1876 self.pending_boot_blocks = Some(Box::new(*blocks));
1877 }
1878
1879 pub fn set_volume_finder_info(&mut self, finder_info: &[u8; 32]) {
1883 for i in 0..8 {
1884 self.mdb.finder_info[i] = BigEndian::read_u32(&finder_info[i * 4..(i + 1) * 4]);
1885 }
1886 }
1887
1888 pub fn set_volume_name(&mut self, new_name: &str) -> Result<(), FilesystemError>
1894 where
1895 R: Write,
1896 {
1897 let new_name_raw = utf8_to_mac_roman(new_name)?;
1898 if new_name_raw.is_empty() {
1899 return Err(FilesystemError::InvalidData(
1900 "HFS volume name must not be empty (Mac OS rejects unnamed volumes with -127)"
1901 .into(),
1902 ));
1903 }
1904 if new_name_raw.len() > 27 {
1905 return Err(FilesystemError::InvalidData(format!(
1906 "HFS volume name '{new_name}' exceeds 27 Mac Roman bytes"
1907 )));
1908 }
1909
1910 let snap = self.snapshot();
1911 let result = (|| -> Result<(), FilesystemError> {
1912 let old_name_raw = self.mdb.volume_name_raw.clone();
1913 if new_name_raw == old_name_raw {
1914 return Ok(());
1915 }
1916
1917 let (node_idx, rec_idx, key_abs) = self
1919 .find_catalog_record_by_name(1, &old_name_raw)
1920 .ok_or_else(|| {
1921 FilesystemError::NotFound("root directory record not found in catalog".into())
1922 })?;
1923
1924 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1927 let node_off = node_idx as usize * node_size;
1928 let node = &self.catalog_data[node_off..node_off + node_size];
1929 let (rec_start, rec_end) = hfs_common::btree_record_range(node, node_size, rec_idx);
1930 debug_assert_eq!(key_abs, node_off + rec_start);
1932 let key_len = node[rec_start] as usize;
1933 let mut data_rel = 1 + key_len;
1934 if !data_rel.is_multiple_of(2) {
1935 data_rel += 1;
1936 }
1937 let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
1938
1939 let new_key = Self::build_catalog_key(1, &new_name_raw);
1941 let mut new_record = new_key;
1942 if !new_record.len().is_multiple_of(2) {
1943 new_record.push(0);
1944 }
1945 new_record.extend_from_slice(&data_bytes);
1946 if !new_record.len().is_multiple_of(2) {
1947 new_record.push(0);
1948 }
1949
1950 self.remove_catalog_record(node_idx, rec_idx)?;
1951 self.insert_catalog_record(&new_record)?;
1952
1953 if let Some((_n, _r, t_key_abs)) = self.find_catalog_record_by_cnid(2) {
1957 let t_key_len = self.catalog_data[t_key_abs] as usize;
1958 let mut t_data_off = t_key_abs + 1 + t_key_len;
1959 if !t_data_off.is_multiple_of(2) {
1960 t_data_off += 1;
1961 }
1962 if t_data_off + 15 + 27 <= self.catalog_data.len() {
1963 self.catalog_data[t_data_off + 14] = new_name_raw.len() as u8;
1964 for i in 0..27 {
1965 self.catalog_data[t_data_off + 15 + i] = 0;
1966 }
1967 self.catalog_data[t_data_off + 15..t_data_off + 15 + new_name_raw.len()]
1968 .copy_from_slice(&new_name_raw);
1969 }
1970 }
1971
1972 self.mdb.volume_name = mac_roman_to_utf8(&new_name_raw);
1977 self.mdb.volume_name_raw = new_name_raw.clone();
1978 self.mdb.raw_sector[36] = new_name_raw.len() as u8;
1979 for i in 0..27 {
1980 self.mdb.raw_sector[37 + i] = 0;
1981 }
1982 self.mdb.raw_sector[37..37 + new_name_raw.len()].copy_from_slice(&new_name_raw);
1983
1984 Ok(())
1985 })();
1986 if result.is_err() {
1987 self.restore_snapshot(snap);
1988 }
1989 result
1990 }
1991}
1992
1993impl<R: Read + Write + Seek> HfsFilesystem<R> {
1995 fn write_block(&mut self, block: u32, data: &[u8]) -> Result<(), FilesystemError> {
1997 let offset = self.partition_offset + self.alloc_block_offset(block);
1998 self.reader.seek(SeekFrom::Start(offset))?;
1999 self.reader.write_all(data)?;
2000 Ok(())
2001 }
2002
2003 fn write_mdb(&mut self) -> Result<(), FilesystemError> {
2009 BigEndian::write_u16(&mut self.mdb.raw_sector[10..12], 0x0100);
2017
2018 let (root_files, root_dirs) = match self.list_children(2) {
2024 Ok(children) => {
2025 let mut f = 0u16;
2026 let mut d = 0u16;
2027 for c in &children {
2028 match c {
2029 CatalogRecord::File { .. } => f = f.saturating_add(1),
2030 CatalogRecord::Directory { .. } => d = d.saturating_add(1),
2031 }
2032 }
2033 (f, d)
2034 }
2035 Err(_) => (0, 0),
2036 };
2037 BigEndian::write_u16(&mut self.mdb.raw_sector[12..14], root_files);
2038 BigEndian::write_u16(&mut self.mdb.raw_sector[82..84], root_dirs);
2039
2040 let mdb_bytes = self.mdb.serialize_to_sector();
2041 self.reader
2042 .seek(SeekFrom::Start(self.partition_offset + 1024))?;
2043 self.reader.write_all(&mdb_bytes)?;
2044 if let Some(alt_offset) = self.alt_mdb_offset() {
2048 self.reader.seek(SeekFrom::Start(alt_offset))?;
2049 self.reader.write_all(&mdb_bytes)?;
2050 }
2051 Ok(())
2052 }
2053
2054 fn write_catalog(&mut self) -> Result<(), FilesystemError> {
2056 write_hfs_fork_data(
2057 &mut self.reader,
2058 self.partition_offset,
2059 &self.mdb,
2060 &self.mdb.catalog_file_extents,
2061 &self.catalog_data,
2062 )?;
2063 let inline_blocks: u32 = self
2066 .mdb
2067 .catalog_file_extents
2068 .iter()
2069 .map(|e| e.block_count as u32)
2070 .sum();
2071 let inline_bytes = inline_blocks as usize * self.mdb.block_size as usize;
2072 if self.catalog_data.len() <= inline_bytes {
2073 return Ok(());
2074 }
2075 self.ensure_extents_overflow()?;
2076 let Some(ext_data) = self.extents_overflow_data.as_ref() else {
2077 return Err(FilesystemError::InvalidData(
2078 "catalog file runs past its inline extents and the volume has no extents-overflow tree".into(),
2079 ));
2080 };
2081 let overflow = collect_fork_overflow_extents(ext_data, CNID_CATALOG, 0x00, inline_blocks);
2082 let first_alloc = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
2083 let mut written = inline_bytes;
2084 for ext in overflow {
2085 if written >= self.catalog_data.len() {
2086 break;
2087 }
2088 let off = first_alloc + ext.start_block as u64 * self.mdb.block_size as u64;
2089 let len = (ext.block_count as usize * self.mdb.block_size as usize)
2090 .min(self.catalog_data.len() - written);
2091 self.reader.seek(SeekFrom::Start(off))?;
2092 self.reader
2093 .write_all(&self.catalog_data[written..written + len])?;
2094 written += len;
2095 }
2096 if written < self.catalog_data.len() {
2097 return Err(FilesystemError::InvalidData(format!(
2098 "catalog file: extents cover {written} of {} bytes",
2099 self.catalog_data.len()
2100 )));
2101 }
2102 Ok(())
2103 }
2104
2105 fn write_volume_bitmap(&mut self) -> Result<(), FilesystemError> {
2107 if let Some(ref bm) = self.bitmap {
2108 let bitmap_offset = self.partition_offset + self.mdb.volume_bitmap_block as u64 * 512;
2109 self.reader.seek(SeekFrom::Start(bitmap_offset))?;
2110 self.reader.write_all(bm)?;
2111 }
2112 Ok(())
2113 }
2114
2115 fn allocate_blocks(&mut self, count: u32) -> Result<u32, FilesystemError> {
2118 self.ensure_bitmap()?;
2119 let bitmap = self.bitmap.as_mut().unwrap();
2120 let start = bitmap_find_clear_run_be(bitmap, self.mdb.total_blocks as u32, count)
2121 .ok_or_else(|| {
2122 FilesystemError::DiskFull(format!("cannot find {} contiguous free blocks", count))
2123 })?;
2124 for i in 0..count {
2125 bitmap_set_bit_be(bitmap, start + i);
2126 }
2127 self.mdb.free_blocks = self
2129 .mdb
2130 .free_blocks
2131 .saturating_sub(count.min(u16::MAX as u32) as u16);
2132 let next = (start + count) % self.mdb.total_blocks.max(1) as u32;
2138 BigEndian::write_u16(&mut self.mdb.raw_sector[16..18], next as u16);
2139 Ok(start)
2140 }
2141
2142 fn allocate_extents(&mut self, count: u32) -> Result<Vec<HfsExtDescriptor>, FilesystemError> {
2145 if count == 0 {
2146 return Ok(Vec::new());
2147 }
2148 if count > u16::MAX as u32 {
2149 return Err(FilesystemError::InvalidData(
2150 "file too large for classic HFS (>65535 blocks)".into(),
2151 ));
2152 }
2153 if let Ok(start) = self.allocate_blocks(count) {
2154 return Ok(vec![HfsExtDescriptor {
2155 start_block: start as u16,
2156 block_count: count as u16,
2157 }]);
2158 }
2159 self.ensure_bitmap()?;
2160 let total_blocks = self.mdb.total_blocks as u32;
2161 let free = self.mdb.free_blocks as u32;
2162 if free < count {
2163 return Err(FilesystemError::DiskFull(format!(
2164 "{free} blocks free, {count} requested"
2165 )));
2166 }
2167 let bitmap = self.bitmap.as_mut().unwrap();
2168 let mut extents = Vec::new();
2169 let mut remaining = count;
2170 for (start, len) in bitmap_collect_clear_runs_be(bitmap, total_blocks) {
2171 if remaining == 0 {
2172 break;
2173 }
2174 let take = len.min(remaining);
2175 extents.push(HfsExtDescriptor {
2176 start_block: start as u16,
2177 block_count: take as u16,
2178 });
2179 remaining -= take;
2180 }
2181 if remaining > 0 {
2182 return Err(FilesystemError::DiskFull(format!(
2183 "needed {count} blocks, short by {remaining} after gathering every free run"
2184 )));
2185 }
2186 for ext in &extents {
2187 for i in 0..ext.block_count as u32 {
2188 bitmap_set_bit_be(bitmap, ext.start_block as u32 + i);
2189 }
2190 }
2191 self.mdb.free_blocks = self.mdb.free_blocks.saturating_sub(count as u16);
2192 let last = extents.last().unwrap();
2193 let next = (last.start_block as u32 + last.block_count as u32) % total_blocks.max(1);
2194 BigEndian::write_u16(&mut self.mdb.raw_sector[16..18], next as u16);
2195 Ok(extents)
2196 }
2197
2198 fn free_blocks(&mut self, start: u32, count: u32) {
2200 self.ensure_bitmap().ok();
2201 if let Some(ref mut bitmap) = self.bitmap {
2202 for i in 0..count {
2203 bitmap_clear_bit_be(bitmap, start + i);
2204 }
2205 }
2206 self.mdb.free_blocks += count as u16;
2207 }
2208
2209 fn free_extent_blocks(&mut self, extents: &[HfsExtDescriptor; 3]) {
2211 for ext in extents {
2212 if ext.block_count == 0 {
2213 break;
2214 }
2215 self.free_blocks(ext.start_block as u32, ext.block_count as u32);
2216 }
2217 }
2218
2219 fn free_fork_overflow(&mut self, file_id: u32, fork_type: u8) -> Result<bool, FilesystemError> {
2221 self.ensure_extents_overflow()?;
2222 let Some(data) = self.extents_overflow_data.as_ref() else {
2223 return Ok(false);
2224 };
2225 let extents = collect_fork_overflow_extents(data, file_id, fork_type, 0);
2226 for ext in &extents {
2227 self.free_blocks(ext.start_block as u32, ext.block_count as u32);
2228 }
2229 let data = self.extents_overflow_data.as_mut().expect("loaded above");
2230 Ok(remove_fork_overflow_records(data, file_id, fork_type)? > 0)
2231 }
2232
2233 fn write_extents_overflow_back(&mut self) -> Result<(), FilesystemError> {
2235 if let Some(data) = self.extents_overflow_data.as_ref() {
2236 if self.mdb.extents_file_size > 0 {
2237 write_extents_overflow_data(
2238 &mut self.reader,
2239 self.partition_offset,
2240 &self.mdb,
2241 data,
2242 )?;
2243 }
2244 }
2245 Ok(())
2246 }
2247
2248 fn write_data_to_blocks(
2251 &mut self,
2252 data: &mut dyn std::io::Read,
2253 data_len: u64,
2254 ) -> Result<Vec<HfsExtDescriptor>, FilesystemError> {
2255 if data_len == 0 {
2256 return Ok(Vec::new());
2257 }
2258 let block_size = self.mdb.block_size as u64;
2259 let blocks_needed = data_len.div_ceil(block_size) as u32;
2260 let extents = self.allocate_extents(blocks_needed)?;
2261
2262 let mut buf = vec![0u8; block_size as usize];
2263 let mut remaining = data_len;
2264 for ext in &extents {
2265 for i in 0..ext.block_count as u32 {
2266 let to_read = remaining.min(block_size) as usize;
2267 buf.fill(0);
2268 data.read_exact(&mut buf[..to_read]).map_err(|e| {
2269 FilesystemError::Io(std::io::Error::new(
2270 e.kind(),
2271 format!("reading file data: {e}"),
2272 ))
2273 })?;
2274 self.write_block(ext.start_block as u32 + i, &buf)?;
2275 remaining -= to_read as u64;
2276 }
2277 }
2278
2279 Ok(extents)
2280 }
2281
2282 fn extents_block_total(extents: &[HfsExtDescriptor]) -> u16 {
2284 extents.iter().map(|e| e.block_count).sum()
2285 }
2286
2287 fn set_inline_extents(rec: &mut [u8], base: usize, extents: &[HfsExtDescriptor]) {
2289 rec[base..base + 12].fill(0);
2290 for (i, ext) in extents.iter().take(3).enumerate() {
2291 let off = base + i * 4;
2292 BigEndian::write_u16(&mut rec[off..off + 2], ext.start_block);
2293 BigEndian::write_u16(&mut rec[off + 2..off + 4], ext.block_count);
2294 }
2295 }
2296
2297 fn insert_fork_overflow(
2300 &mut self,
2301 file_id: u32,
2302 fork_type: u8,
2303 extents: &[HfsExtDescriptor],
2304 ) -> Result<bool, FilesystemError> {
2305 use crate::hfs_common::compare_classic_extents_keys;
2306 if extents.len() <= 3 {
2307 return Ok(false);
2308 }
2309 self.ensure_extents_overflow()?;
2310 if self.extents_overflow_data.is_none() {
2311 return Err(FilesystemError::Unsupported(
2312 "volume has no extents-overflow B-tree; a fragmented fork cannot be created".into(),
2313 ));
2314 }
2315 let mut fork_block: u32 = extents.iter().take(3).map(|e| e.block_count as u32).sum();
2316 for chunk in extents[3..].chunks(3) {
2317 let mut rec = vec![7u8, fork_type];
2319 rec.extend_from_slice(&file_id.to_be_bytes());
2320 rec.extend_from_slice(&(fork_block as u16).to_be_bytes());
2321 for i in 0..3 {
2322 let (start, count) = chunk
2323 .get(i)
2324 .map_or((0, 0), |e| (e.start_block, e.block_count));
2325 rec.extend_from_slice(&start.to_be_bytes());
2326 rec.extend_from_slice(&count.to_be_bytes());
2327 }
2328 let mut grown = false;
2329 loop {
2330 let data = self.extents_overflow_data.as_mut().expect("loaded above");
2331 match hfs_common::btree_insert_full(
2332 data,
2333 &rec,
2334 &BTreeKeyFormat::CLASSIC_EXTENTS,
2335 &compare_classic_extents_keys,
2336 ) {
2337 Ok(()) => break,
2338 Err(FilesystemError::DiskFull(_)) if !grown => {
2339 self.grow_btree(HfsTree::Extents)?;
2340 grown = true;
2341 }
2342 Err(e) => return Err(e),
2343 }
2344 }
2345 fork_block += chunk.iter().map(|e| e.block_count as u32).sum::<u32>();
2346 }
2347 Ok(true)
2348 }
2349
2350 #[allow(clippy::too_many_arguments)]
2356 fn build_file_record(
2357 file_id: u32,
2358 data_size: u32,
2359 data_start: u16,
2360 data_blocks: u16,
2361 rsrc_size: u32,
2362 rsrc_start: u16,
2363 rsrc_blocks: u16,
2364 type_code: &[u8; 4],
2365 creator_code: &[u8; 4],
2366 block_size: u32,
2367 mtime_secs: Option<u64>,
2368 finder_flags: u16,
2369 ) -> [u8; 102] {
2370 let mut rec = [0u8; 102];
2371 let now = match mtime_secs {
2373 Some(s) => disk_core::times::unix_to_mac_local(s),
2374 None => disk_core::times::mac_local_now(),
2375 };
2376 rec[0] = CATALOG_FILE as u8; rec[4..8].copy_from_slice(type_code);
2380 rec[8..12].copy_from_slice(creator_code);
2381 BigEndian::write_u16(&mut rec[12..14], finder_flags); BigEndian::write_u32(&mut rec[20..24], file_id);
2384 BigEndian::write_u32(&mut rec[26..30], data_size);
2392 BigEndian::write_u32(&mut rec[30..34], data_blocks as u32 * block_size);
2394 BigEndian::write_u32(&mut rec[36..40], rsrc_size);
2397 BigEndian::write_u32(&mut rec[40..44], rsrc_blocks as u32 * block_size);
2399 BigEndian::write_u32(&mut rec[44..48], now);
2401 BigEndian::write_u32(&mut rec[48..52], now);
2403 if data_blocks > 0 {
2405 BigEndian::write_u16(&mut rec[74..76], data_start);
2406 BigEndian::write_u16(&mut rec[76..78], data_blocks);
2407 }
2408 if rsrc_blocks > 0 {
2410 BigEndian::write_u16(&mut rec[86..88], rsrc_start);
2411 BigEndian::write_u16(&mut rec[88..90], rsrc_blocks);
2412 }
2413 rec
2414 }
2415
2416 fn build_dir_record_with_dates(dir_id: u32, mtime_secs: Option<u64>) -> [u8; 70] {
2421 let mut rec = [0u8; 70];
2422 let now = match mtime_secs {
2424 Some(s) => disk_core::times::unix_to_mac_local(s),
2425 None => disk_core::times::mac_local_now(),
2426 };
2427 rec[0] = CATALOG_DIR as u8; BigEndian::write_u32(&mut rec[6..10], dir_id);
2432 BigEndian::write_u32(&mut rec[10..14], now);
2434 BigEndian::write_u32(&mut rec[14..18], now);
2436 rec
2437 }
2438
2439 pub(crate) fn build_thread_record(
2442 thread_type: i8,
2443 parent_id: u32,
2444 name: &[u8],
2445 ) -> (Vec<u8>, Vec<u8>) {
2446 let mut rec = Vec::with_capacity(46);
2450 rec.push(thread_type as u8); rec.push(0); rec.extend_from_slice(&[0u8; 8]); let mut buf4 = [0u8; 4];
2454 BigEndian::write_u32(&mut buf4, parent_id);
2455 rec.extend_from_slice(&buf4); let name_len = name.len().min(31);
2458 let mut str31 = [0u8; 32];
2459 str31[0] = name_len as u8;
2460 str31[1..1 + name_len].copy_from_slice(&name[..name_len]);
2461 rec.extend_from_slice(&str31);
2462 (vec![], rec) }
2465
2466 fn do_sync_metadata(&mut self) -> Result<(), FilesystemError> {
2468 if let Some((create, modify, backup)) = self.pending_volume_dates.take() {
2469 self.mdb.create_date = create;
2470 self.mdb.modify_date = modify;
2471 BigEndian::write_u32(&mut self.mdb.raw_sector[2..6], create);
2472 BigEndian::write_u32(&mut self.mdb.raw_sector[6..10], modify);
2473 BigEndian::write_u32(&mut self.mdb.raw_sector[64..68], backup);
2474 } else {
2475 self.mdb.modify_date = disk_core::times::mac_local_now();
2476 }
2477 if let Some(blocks) = self.pending_boot_blocks.take() {
2478 self.reader.seek(SeekFrom::Start(self.partition_offset))?;
2479 self.reader.write_all(&blocks[..])?;
2480 }
2481 self.write_catalog()?;
2482 self.write_volume_bitmap()?;
2483 self.write_mdb()?;
2484 self.reader.flush()?;
2485 Ok(())
2486 }
2487
2488 fn ensure_catalog_initialized(&mut self) -> Result<(), FilesystemError> {
2494 if self.btrees_initialized {
2498 return Ok(());
2499 }
2500 let catalog_blank = is_catalog_uninitialized(&self.catalog_data);
2501 let extents_data = if self.mdb.extents_file_size > 0 {
2503 read_fork_data(
2504 &mut self.reader,
2505 self.partition_offset,
2506 &self.mdb,
2507 &self.mdb.extents_file_extents,
2508 self.mdb.extents_file_size as u64,
2509 )
2510 .ok()
2511 } else {
2512 None
2513 };
2514 let extents_blank = extents_data
2515 .as_deref()
2516 .map(is_catalog_uninitialized)
2517 .unwrap_or(false);
2518 if !catalog_blank && !extents_blank {
2519 self.btrees_initialized = true;
2520 return Ok(());
2521 }
2522 self.initialize_empty_btrees(catalog_blank, extents_blank)?;
2523 self.btrees_initialized = true;
2524 Ok(())
2525 }
2526
2527 fn initialize_empty_btrees(
2528 &mut self,
2529 init_catalog: bool,
2530 init_extents: bool,
2531 ) -> Result<(), FilesystemError> {
2532 if init_catalog {
2533 let catalog_size = self.catalog_data.len();
2534 if catalog_size < 1024 {
2535 return Err(FilesystemError::InvalidData(format!(
2536 "cannot initialize empty HFS catalog: region is only {catalog_size} bytes"
2537 )));
2538 }
2539 let vol_name = self.mdb.volume_name_raw.clone();
2543 let node_size = pick_btree_node_size(catalog_size as u64) as usize;
2544 let buf = build_empty_hfs_catalog_with_node_size(
2545 catalog_size,
2546 node_size,
2547 &vol_name,
2548 hfs_common::hfs_now(),
2549 )?;
2550 self.catalog_data = buf;
2551 self.write_catalog()?;
2552 }
2553
2554 if init_extents {
2555 let extents_size = self.mdb.extents_file_size as usize;
2556 if extents_size >= 512 {
2557 let buf = build_empty_hfs_extents_btree(extents_size)?;
2558 write_hfs_fork_data(
2559 &mut self.reader,
2560 self.partition_offset,
2561 &self.mdb,
2562 &self.mdb.extents_file_extents,
2563 &buf,
2564 )?;
2565 }
2566 }
2567
2568 let bitmap_size = (self.mdb.total_blocks as u32).div_ceil(8) as usize;
2572 let mut bitmap = vec![0u8; bitmap_size];
2573 let mut used_blocks = 0u32;
2574 for ext in self.mdb.catalog_file_extents.iter() {
2575 if ext.block_count == 0 {
2576 break;
2577 }
2578 for i in 0..ext.block_count {
2579 let bit = ext.start_block as u32 + i as u32;
2580 if bit < self.mdb.total_blocks as u32 {
2581 hfs_common::bitmap_set_bit_be(&mut bitmap, bit);
2582 used_blocks += 1;
2583 }
2584 }
2585 }
2586 for ext in self.mdb.extents_file_extents.iter() {
2587 if ext.block_count == 0 {
2588 break;
2589 }
2590 for i in 0..ext.block_count {
2591 let bit = ext.start_block as u32 + i as u32;
2592 if bit < self.mdb.total_blocks as u32 {
2593 hfs_common::bitmap_set_bit_be(&mut bitmap, bit);
2594 used_blocks += 1;
2595 }
2596 }
2597 }
2598 self.bitmap = Some(bitmap);
2599 self.mdb.free_blocks = (self.mdb.total_blocks as u32).saturating_sub(used_blocks) as u16;
2600
2601 self.mdb.file_count = 0;
2603 self.mdb.folder_count = 0;
2604 if self.mdb.next_catalog_id < 16 {
2605 self.mdb.next_catalog_id = 16;
2606 }
2607 self.mdb.modify_date = disk_core::times::mac_local_now();
2608
2609 self.write_volume_bitmap()?;
2610 self.write_mdb()?;
2611 self.reader.flush()?;
2612 Ok(())
2613 }
2614}
2615
2616fn pick_btree_node_size(_target_bytes: u64) -> u16 {
2631 512
2632}
2633
2634pub const HFS_MAX_BTREE_FILE_SIZE: u32 = 16 * 1024 * 1024;
2640
2641fn hfs_map_nodes_required(total_nodes: u32, node_size: usize) -> u32 {
2644 let header_cap = ((node_size - 256) * 8) as u32; if total_nodes <= header_cap {
2646 return 0;
2647 }
2648 let per_map = (hfs_common::map_node_bitmap_bytes(node_size) * 8) as u32;
2649 if per_map == 0 {
2650 return 0;
2651 }
2652 let extra = total_nodes - header_cap;
2653 extra.div_ceil(per_map)
2654}
2655
2656fn build_empty_hfs_catalog_with_node_size(
2657 catalog_size: usize,
2658 node_size: usize,
2659 volume_name_raw: &[u8],
2660 now: u32,
2661) -> Result<Vec<u8>, FilesystemError> {
2662 if volume_name_raw.len() > 27 {
2663 return Err(FilesystemError::InvalidData(format!(
2664 "HFS volume name length {} exceeds 27 Mac Roman bytes",
2665 volume_name_raw.len()
2666 )));
2667 }
2668 let total_nodes = (catalog_size / node_size) as u32;
2669 if total_nodes < 2 {
2670 return Err(FilesystemError::InvalidData(format!(
2671 "cannot initialize empty HFS catalog: only {total_nodes} node(s) fit"
2672 )));
2673 }
2674 let map_nodes = hfs_map_nodes_required(total_nodes, node_size);
2677 let used_nodes = 2 + map_nodes;
2678 if total_nodes < used_nodes {
2679 return Err(FilesystemError::InvalidData(format!(
2680 "cannot initialize empty HFS catalog: {total_nodes} nodes < {used_nodes} required \
2681 (header + leaf + {map_nodes} map nodes)"
2682 )));
2683 }
2684 let mut buf = vec![0u8; catalog_size];
2685
2686 if map_nodes > 0 {
2689 BigEndian::write_u32(&mut buf[0..4], 2);
2690 }
2691 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
2692 buf[9] = 0;
2693 BigEndian::write_u16(&mut buf[10..12], 3);
2694
2695 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);
2704 BigEndian::write_u32(&mut buf[40..44], total_nodes - used_nodes); let bitmap_rec_offset = 248usize;
2713 let header_bitmap_len = node_size - 256; for bit in 0..used_nodes {
2715 let local = bit;
2717 let byte_idx = (local / 8) as usize;
2718 let bit_in_byte = 7 - (local % 8) as u8;
2719 if byte_idx < header_bitmap_len {
2720 buf[bitmap_rec_offset + byte_idx] |= 1u8 << bit_in_byte;
2721 }
2722 }
2723
2724 let free_offset = (node_size - 8) as u16;
2726 BigEndian::write_u16(&mut buf[node_size - 2..node_size], 14);
2727 BigEndian::write_u16(&mut buf[node_size - 4..node_size - 2], 120);
2728 BigEndian::write_u16(
2729 &mut buf[node_size - 6..node_size - 4],
2730 bitmap_rec_offset as u16,
2731 );
2732 BigEndian::write_u16(&mut buf[node_size - 8..node_size - 6], free_offset);
2733
2734 let leaf = node_size;
2736 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();
2744 let r0_key = leaf + 14;
2745 let key_data_len: u8 = (1 + 4 + 1 + name_len) as u8; buf[r0_key] = key_data_len;
2747 buf[r0_key + 1] = 0;
2748 BigEndian::write_u32(&mut buf[r0_key + 2..r0_key + 6], 1);
2749 buf[r0_key + 6] = name_len as u8;
2750 buf[r0_key + 7..r0_key + 7 + name_len].copy_from_slice(volume_name_raw);
2751 let r0_key_total = 1 + key_data_len as usize;
2754 let r0_pad = r0_key_total & 1;
2755 let r0_data = r0_key + r0_key_total + r0_pad;
2756 buf[r0_data] = CATALOG_DIR as u8;
2757 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;
2767 buf[r1_key] = 6;
2768 buf[r1_key + 1] = 0;
2769 BigEndian::write_u32(&mut buf[r1_key + 2..r1_key + 6], 2);
2770 buf[r1_key + 6] = 0;
2771 buf[r1_key + 7] = 0;
2772 let r1_data = r1_key + 8;
2773 buf[r1_data] = CATALOG_DIR_THREAD as u8;
2774 BigEndian::write_u32(&mut buf[r1_data + 10..r1_data + 14], 1); buf[r1_data + 14] = name_len as u8;
2776 buf[r1_data + 15..r1_data + 15 + name_len].copy_from_slice(volume_name_raw);
2777 let free_rel = (r1_data + 46 - leaf) as u16;
2780
2781 BigEndian::write_u16(&mut buf[leaf + node_size - 2..leaf + node_size], 14);
2783 BigEndian::write_u16(
2784 &mut buf[leaf + node_size - 4..leaf + node_size - 2],
2785 (r1_key - leaf) as u16,
2786 );
2787 BigEndian::write_u16(
2788 &mut buf[leaf + node_size - 6..leaf + node_size - 4],
2789 free_rel,
2790 );
2791
2792 for i in 0..map_nodes {
2794 let node_idx = 2 + i;
2795 let next_link = if i + 1 < map_nodes { node_idx + 1 } else { 0 };
2796 let prev_link = if i == 0 { 0 } else { node_idx - 1 };
2797 hfs_common::init_map_node(&mut buf, node_size, node_idx, prev_link, next_link);
2798 }
2799
2800 Ok(buf)
2801}
2802
2803fn build_empty_hfs_extents_btree(size: usize) -> Result<Vec<u8>, FilesystemError> {
2807 build_empty_hfs_extents_btree_with_node_size(size, 512)
2808}
2809
2810fn build_empty_hfs_extents_btree_with_node_size(
2811 size: usize,
2812 node_size: usize,
2813) -> Result<Vec<u8>, FilesystemError> {
2814 let total_nodes = (size / node_size) as u32;
2815 if total_nodes < 1 {
2816 return Err(FilesystemError::InvalidData(format!(
2817 "cannot initialize empty HFS extents B-tree: region is only {size} bytes"
2818 )));
2819 }
2820 let map_nodes = hfs_map_nodes_required(total_nodes, node_size);
2821 let used_nodes = 1 + map_nodes;
2822 if total_nodes < used_nodes {
2823 return Err(FilesystemError::InvalidData(format!(
2824 "cannot initialize empty HFS extents B-tree: {total_nodes} nodes < {used_nodes} \
2825 required (header + {map_nodes} map nodes)"
2826 )));
2827 }
2828 let mut buf = vec![0u8; size];
2829
2830 if map_nodes > 0 {
2832 BigEndian::write_u32(&mut buf[0..4], 1);
2833 }
2834 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
2835 buf[9] = 0;
2836 BigEndian::write_u16(&mut buf[10..12], 3);
2837
2838 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);
2847 BigEndian::write_u32(&mut buf[40..44], total_nodes - used_nodes);
2848
2849 let bitmap_rec_offset = 248usize;
2853 let header_bitmap_len = node_size - 256;
2854 for bit in 0..used_nodes {
2855 let byte_idx = (bit / 8) as usize;
2856 let bit_in_byte = 7 - (bit % 8) as u8;
2857 if byte_idx < header_bitmap_len {
2858 buf[bitmap_rec_offset + byte_idx] |= 1u8 << bit_in_byte;
2859 }
2860 }
2861
2862 let free_offset = (node_size - 8) as u16;
2863 BigEndian::write_u16(&mut buf[node_size - 2..node_size], 14);
2864 BigEndian::write_u16(&mut buf[node_size - 4..node_size - 2], 120);
2865 BigEndian::write_u16(
2866 &mut buf[node_size - 6..node_size - 4],
2867 bitmap_rec_offset as u16,
2868 );
2869 BigEndian::write_u16(&mut buf[node_size - 8..node_size - 6], free_offset);
2870
2871 for i in 0..map_nodes {
2873 let node_idx = 1 + i;
2874 let next_link = if i + 1 < map_nodes { node_idx + 1 } else { 0 };
2875 let prev_link = if i == 0 { 0 } else { node_idx - 1 };
2876 hfs_common::init_map_node(&mut buf, node_size, node_idx, prev_link, next_link);
2877 }
2878
2879 Ok(buf)
2880}
2881
2882pub fn create_blank_hfs(
2903 target_size_bytes: u64,
2904 block_size: u32,
2905 volume_name: &str,
2906) -> Result<Vec<u8>, FilesystemError> {
2907 create_blank_hfs_sized(target_size_bytes, block_size, volume_name, 0, 0)
2908}
2909
2910pub fn default_btree_sizes(target_volume_bytes: u64, block_size: u32) -> (u32, u32) {
2921 if block_size == 0 {
2922 return (0, 0);
2923 }
2924 let clump = 4u64 * block_size as u64;
2925 let floor = 24u64 * block_size as u64;
2926 let raw = target_volume_bytes / 200; let catalog = raw.max(floor).div_ceil(clump) * clump;
2928 let extents = (raw / 2).max(floor).div_ceil(clump) * clump;
2929 (
2930 catalog.min(u32::MAX as u64) as u32,
2931 extents.min(u32::MAX as u64) as u32,
2932 )
2933}
2934
2935pub fn clone_target_btree_sizes(
2952 target_size: u64,
2953 block_size: u32,
2954 source_catalog: u32,
2955 source_extents: u32,
2956) -> (u32, u32) {
2957 let (default_cat, default_ext) = default_btree_sizes(target_size, block_size);
2958 let catalog = source_catalog
2959 .saturating_mul(3)
2960 .saturating_div(2)
2961 .max(default_cat)
2962 .min(HFS_MAX_BTREE_FILE_SIZE);
2963 let extents = source_extents.max(default_ext).min(HFS_MAX_BTREE_FILE_SIZE);
2964 (catalog, extents)
2965}
2966
2967pub fn pick_block_size(volume_bytes: u64) -> u32 {
2970 let mut bs: u32 = 512;
2971 while volume_bytes / bs as u64 > 65535 {
2972 bs = bs.saturating_mul(2);
2973 if bs == 0 {
2974 return 1 << 16;
2975 }
2976 }
2977 bs
2978}
2979
2980pub fn create_blank_hfs_sized(
2987 target_size_bytes: u64,
2988 block_size: u32,
2989 volume_name: &str,
2990 min_extents_bytes: u32,
2991 min_catalog_bytes: u32,
2992) -> Result<Vec<u8>, FilesystemError> {
2993 create_blank_hfs_sized_at(
2994 target_size_bytes,
2995 block_size,
2996 volume_name,
2997 min_extents_bytes,
2998 min_catalog_bytes,
2999 hfs_common::hfs_now(),
3000 )
3001}
3002
3003pub(crate) fn create_blank_hfs_sized_at(
3005 target_size_bytes: u64,
3006 block_size: u32,
3007 volume_name: &str,
3008 min_extents_bytes: u32,
3009 min_catalog_bytes: u32,
3010 now: u32,
3011) -> Result<Vec<u8>, FilesystemError> {
3012 let (front, mdb, image_size) = build_blank_hfs_front(
3013 target_size_bytes,
3014 block_size,
3015 volume_name,
3016 min_extents_bytes,
3017 min_catalog_bytes,
3018 now,
3019 )?;
3020 let mut img = vec![0u8; image_size as usize];
3021 img[..front.len()].copy_from_slice(&front);
3022 let alt_off = image_size as usize - 1024;
3023 img[alt_off..alt_off + 512].copy_from_slice(&mdb);
3024 Ok(img)
3025}
3026
3027pub fn write_blank_hfs_into<W: Write + Seek>(
3036 target: &mut W,
3037 at_offset: u64,
3038 target_size_bytes: u64,
3039 block_size: u32,
3040 volume_name: &str,
3041) -> Result<u64, FilesystemError> {
3042 write_blank_hfs_into_at(
3043 target,
3044 at_offset,
3045 target_size_bytes,
3046 block_size,
3047 volume_name,
3048 hfs_common::hfs_now(),
3049 )
3050}
3051
3052pub(crate) fn write_blank_hfs_into_at<W: Write + Seek>(
3054 target: &mut W,
3055 at_offset: u64,
3056 target_size_bytes: u64,
3057 block_size: u32,
3058 volume_name: &str,
3059 now: u32,
3060) -> Result<u64, FilesystemError> {
3061 let (front, mdb, image_size) =
3062 build_blank_hfs_front(target_size_bytes, block_size, volume_name, 0, 0, now)?;
3063 target.seek(SeekFrom::Start(at_offset))?;
3064 target.write_all(&front)?;
3065 target.seek(SeekFrom::Start(at_offset + image_size - 1024))?;
3066 target.write_all(&mdb)?;
3067 target.write_all(&[0u8; 512])?;
3068 Ok(image_size)
3069}
3070
3071fn build_blank_hfs_front(
3076 target_size_bytes: u64,
3077 block_size: u32,
3078 volume_name: &str,
3079 min_extents_bytes: u32,
3080 min_catalog_bytes: u32,
3081 now: u32,
3082) -> Result<(Vec<u8>, [u8; 512], u64), FilesystemError> {
3083 if block_size == 0 || !block_size.is_multiple_of(512) {
3084 return Err(FilesystemError::InvalidData(format!(
3085 "HFS block_size must be a non-zero multiple of 512 (got {block_size})"
3086 )));
3087 }
3088
3089 let name_raw = utf8_to_mac_roman(volume_name)?;
3090 if name_raw.len() > 27 {
3091 return Err(FilesystemError::InvalidData(format!(
3092 "HFS volume name '{volume_name}' exceeds 27 Mac Roman bytes"
3093 )));
3094 }
3095
3096 let mut first_alloc_block: u16 = 3;
3100 let total_blocks: u32;
3101 loop {
3102 let pre_alloc = first_alloc_block as u64 * 512;
3103 if target_size_bytes <= pre_alloc + 512 {
3104 return Err(FilesystemError::InvalidData(format!(
3105 "HFS volume too small ({target_size_bytes} bytes) for block_size {block_size}"
3106 )));
3107 }
3108 let usable = target_size_bytes - pre_alloc - 1024; let tb = usable / block_size as u64;
3110 if tb == 0 {
3111 return Err(FilesystemError::InvalidData(format!(
3112 "HFS volume too small for block_size {block_size}"
3113 )));
3114 }
3115 if tb > 65535 {
3116 return Err(FilesystemError::InvalidData(format!(
3117 "HFS would have {tb} allocation blocks at block_size {block_size}, exceeding u16 ceiling 65535"
3118 )));
3119 }
3120 let tb = tb as u32;
3121 let bitmap_bytes = (tb as u64).div_ceil(8);
3122 let bitmap_sectors = bitmap_bytes.div_ceil(512) as u16;
3123 let needed = 3u16 + bitmap_sectors;
3124 if needed <= first_alloc_block {
3125 total_blocks = tb;
3126 break;
3127 }
3128 first_alloc_block = needed;
3129 }
3130
3131 let (default_catalog, default_extents) = default_btree_sizes(target_size_bytes, block_size);
3135 let blocks_for = |bytes: u32, default: u32| -> u16 {
3136 let bytes = if bytes == 0 { default } else { bytes };
3137 let n = (bytes as u64).div_ceil(block_size as u64).max(4);
3138 n.min(u16::MAX as u64) as u16
3139 };
3140 let extents_blocks = blocks_for(min_extents_bytes, default_extents);
3141 let catalog_blocks = blocks_for(min_catalog_bytes, default_catalog);
3142 let extents_start: u16 = 0;
3143 let catalog_start: u16 = extents_blocks;
3144 let btree_blocks: u16 = extents_blocks + catalog_blocks;
3145
3146 if total_blocks < btree_blocks as u32 {
3147 return Err(FilesystemError::InvalidData(format!(
3148 "HFS volume too small: {total_blocks} allocation blocks is below the minimum of {btree_blocks} needed for B-trees"
3149 )));
3150 }
3151
3152 let extents_size = extents_blocks as u32 * block_size;
3153 let catalog_size = catalog_blocks as u32 * block_size;
3154
3155 let extents_node_size = pick_btree_node_size(extents_size as u64) as usize;
3159 let catalog_node_size = pick_btree_node_size(catalog_size as u64) as usize;
3160
3161 let image_size =
3164 first_alloc_block as u64 * 512 + total_blocks as u64 * block_size as u64 + 1024;
3165 let front_size = first_alloc_block as usize * 512 + btree_blocks as usize * block_size as usize;
3169 let mut front = vec![0u8; front_size];
3170
3171 let bitmap_off = 3 * 512;
3174 for b in 0..btree_blocks as u32 {
3175 let byte_idx = (b / 8) as usize;
3176 let bit_idx = 7 - (b % 8) as u8;
3177 front[bitmap_off + byte_idx] |= 1 << bit_idx;
3178 }
3179
3180 let extents_bytes =
3182 build_empty_hfs_extents_btree_with_node_size(extents_size as usize, extents_node_size)?;
3183 let extents_off = first_alloc_block as u64 * 512 + extents_start as u64 * block_size as u64;
3184 front[extents_off as usize..extents_off as usize + extents_bytes.len()]
3185 .copy_from_slice(&extents_bytes);
3186
3187 let catalog_bytes = build_empty_hfs_catalog_with_node_size(
3192 catalog_size as usize,
3193 catalog_node_size,
3194 &name_raw,
3195 now,
3196 )?;
3197 let catalog_off = first_alloc_block as u64 * 512 + catalog_start as u64 * block_size as u64;
3198 front[catalog_off as usize..catalog_off as usize + catalog_bytes.len()]
3199 .copy_from_slice(&catalog_bytes);
3200
3201 let mdb = build_blank_mdb(
3203 now,
3204 &name_raw,
3205 total_blocks as u16,
3206 block_size,
3207 first_alloc_block,
3208 catalog_start,
3209 catalog_blocks,
3210 catalog_size,
3211 extents_start,
3212 extents_blocks,
3213 extents_size,
3214 btree_blocks,
3215 );
3216 front[1024..1024 + 512].copy_from_slice(&mdb);
3217
3218 Ok((front, mdb, image_size))
3222}
3223
3224#[allow(clippy::too_many_arguments)]
3225fn build_blank_mdb(
3226 now: u32,
3227 name_raw: &[u8],
3228 total_blocks: u16,
3229 block_size: u32,
3230 first_alloc_block: u16,
3231 catalog_start: u16,
3232 catalog_blocks: u16,
3233 catalog_size: u32,
3234 extents_start: u16,
3235 extents_blocks: u16,
3236 extents_size: u32,
3237 allocated_blocks: u16,
3238) -> [u8; 512] {
3239 let mut mdb = [0u8; 512];
3240 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
3241 BigEndian::write_u32(&mut mdb[2..6], now); BigEndian::write_u32(&mut mdb[6..10], now); BigEndian::write_u16(&mut mdb[10..12], 0x0100);
3247 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;
3255 mdb[37..37 + name_raw.len()].copy_from_slice(name_raw);
3256 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);
3263 BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
3264 BigEndian::write_u32(&mut mdb[146..150], catalog_size); BigEndian::write_u16(&mut mdb[150..152], catalog_start);
3266 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
3267 mdb
3268}
3269
3270impl<R: Read + Seek + Send> Filesystem for HfsFilesystem<R> {
3271 fn root(&mut self) -> Result<FileEntry, FilesystemError> {
3272 Ok(FileEntry {
3273 name: "/".into(),
3274 path: "/".into(),
3275 entry_type: EntryType::Directory,
3276 size: 0,
3277 location: 2, modified: None,
3279 modified_unix: None,
3280 type_code: None,
3281 creator_code: None,
3282 symlink_target: None,
3283 special_type: None,
3284 mode: None,
3285 uid: None,
3286 gid: None,
3287 resource_fork_size: None,
3288 aux_type: None,
3289 link_target_cnid: None,
3290 amiga_protection: None,
3291 amiga_comment: None,
3292 amiga_date: None,
3293 dos_attributes: None,
3294 finder_flags: None,
3295 prodos_file_type: None,
3296 mac_dates: None,
3297 owner_label: None,
3298 native_attrs: None,
3299 qnx2_attrs: None,
3300 })
3301 }
3302
3303 fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
3304 let parent_id = entry.location as u32;
3305 let children = self.list_children(parent_id)?;
3306
3307 let mut entries = Vec::new();
3308 for child in children {
3309 match child {
3310 CatalogRecord::Directory {
3311 dir_id,
3312 name,
3313 dates,
3314 ..
3315 } => {
3316 let path = if entry.path == "/" {
3317 format!("/{name}")
3318 } else {
3319 format!("{}/{name}", entry.path)
3320 };
3321 let mut fe = FileEntry::new_directory(name, path, dir_id as u64);
3322 fe.modified = hfs_common::format_mac_date(dates.1);
3323 fe.modified_unix = disk_core::times::mac_local_to_unix(dates.1);
3324 fe.mac_dates = Some(dates);
3325 entries.push(fe);
3326 }
3327 CatalogRecord::File {
3328 file_id,
3329 name,
3330 data_size,
3331 rsrc_size,
3332 rsrc_extents,
3333 type_code,
3334 creator_code,
3335 finder_flags,
3336 dates,
3337 ..
3338 } => {
3339 let path = if entry.path == "/" {
3340 format!("/{name}")
3341 } else {
3342 format!("{}/{name}", entry.path)
3343 };
3344 let mut fe = FileEntry::new_file(name, path, data_size as u64, file_id as u64);
3345 fe.type_code = Some(type_code);
3346 fe.creator_code = Some(creator_code);
3347 fe.finder_flags = Some(finder_flags);
3348 fe.modified = hfs_common::format_mac_date(dates.1);
3349 fe.modified_unix = disk_core::times::mac_local_to_unix(dates.1);
3350 fe.mac_dates = Some(dates);
3351 if rsrc_size > 0 {
3352 fe.resource_fork_size = Some(rsrc_size as u64);
3353 }
3354 if finder_flags & crate::mac_alias::IS_ALIAS_FLAG != 0 && rsrc_size > 0 {
3358 if let Ok(rsrc) = read_fork_data(
3359 &mut self.reader,
3360 self.partition_offset,
3361 &self.mdb,
3362 &rsrc_extents,
3363 rsrc_size as u64,
3364 ) {
3365 if let Some(target) = crate::mac_alias::resolve_alias_target(&rsrc) {
3366 fe.symlink_target = Some(target);
3367 }
3368 }
3369 }
3370 entries.push(fe);
3371 }
3372 }
3373 }
3374
3375 entries.sort_by_key(|a| a.name.to_lowercase());
3376 Ok(entries)
3377 }
3378
3379 fn read_file(
3380 &mut self,
3381 entry: &FileEntry,
3382 max_bytes: usize,
3383 ) -> Result<Vec<u8>, FilesystemError> {
3384 let file_id = entry.location as u32;
3385 let (data_size, extents, _rsrc_size, _rsrc_extents) =
3386 self.find_file_by_id(file_id).ok_or_else(|| {
3387 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3388 })?;
3389
3390 let mut data = self.read_fork_with_overflow(file_id, 0x00, &extents, data_size as u64)?;
3391 data.truncate(max_bytes);
3392 Ok(data)
3393 }
3394
3395 fn write_file_to(
3396 &mut self,
3397 entry: &FileEntry,
3398 writer: &mut dyn std::io::Write,
3399 ) -> Result<u64, FilesystemError> {
3400 if entry.is_directory() {
3401 return Err(FilesystemError::NotADirectory(entry.path.clone()));
3402 }
3403 let file_id = entry.location as u32;
3404 let (data_size, extents, _rsrc_size, _rsrc_extents) =
3405 self.find_file_by_id(file_id).ok_or_else(|| {
3406 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3407 })?;
3408 self.write_fork_with_overflow_to(file_id, 0x00, &extents, data_size as u64, writer)
3409 }
3410
3411 fn volume_label(&self) -> Option<&str> {
3412 if self.mdb.volume_name.is_empty() {
3413 None
3414 } else {
3415 Some(&self.mdb.volume_name)
3416 }
3417 }
3418
3419 fn fs_type(&self) -> &str {
3420 "HFS"
3421 }
3422
3423 fn validate_name(&self, name: &str) -> Result<(), FilesystemError> {
3424 validate_hfs_create_name(name).map(|_| ())
3425 }
3426
3427 fn uses_colon_paths(&self) -> bool {
3428 true
3429 }
3430
3431 fn total_size(&self) -> u64 {
3432 self.mdb.total_blocks as u64 * self.mdb.block_size as u64
3433 }
3434
3435 fn used_size(&self) -> u64 {
3436 (self.mdb.total_blocks - self.mdb.free_blocks) as u64 * self.mdb.block_size as u64
3437 }
3438
3439 fn allocation_unit(&self) -> Option<u64> {
3445 if self.mdb.block_size == 0 {
3446 None
3447 } else {
3448 Some(self.mdb.block_size as u64)
3449 }
3450 }
3451
3452 fn last_data_byte(&mut self) -> Result<u64, FilesystemError> {
3453 let bitmap = self.read_volume_bitmap()?;
3454 let last_block = find_last_set_bit(&bitmap, self.mdb.total_blocks as u32);
3455 match last_block {
3456 Some(block) => {
3457 let byte = self.alloc_block_offset(block + 1);
3458 Ok(byte)
3459 }
3460 None => Ok(self.total_size()),
3461 }
3462 }
3463
3464 fn write_resource_fork_to(
3465 &mut self,
3466 entry: &FileEntry,
3467 writer: &mut dyn std::io::Write,
3468 ) -> Result<u64, FilesystemError> {
3469 let file_id = entry.location as u32;
3470 let (_data_size, _data_ext, rsrc_size, rsrc_extents) =
3471 self.find_file_by_id(file_id).ok_or_else(|| {
3472 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3473 })?;
3474 if rsrc_size == 0 {
3475 return Ok(0);
3476 }
3477 let data = self.read_fork_with_overflow(file_id, 0xFF, &rsrc_extents, rsrc_size as u64)?;
3478 writer.write_all(&data)?;
3479 Ok(data.len() as u64)
3480 }
3481
3482 fn resource_fork_size(&mut self, entry: &FileEntry) -> u64 {
3483 let file_id = entry.location as u32;
3484 self.find_file_by_id(file_id)
3485 .map(|(_ds, _de, rs, _re)| rs as u64)
3486 .unwrap_or(0)
3487 }
3488
3489 fn fragmentation_stats(
3490 &mut self,
3491 ) -> Option<Result<disk_core::filesystem::FragmentationStats, FilesystemError>> {
3492 Some(Ok(hfs_fragmentation_stats(&self.catalog_data)))
3493 }
3494
3495 fn blessed_system_folder(&mut self) -> Option<(u64, String)> {
3496 let cnid = self.mdb.finder_info[0];
3497 if cnid == 0 {
3498 return None;
3499 }
3500 if let Some((_node, _rec, offset)) = self.find_catalog_record_by_cnid(cnid) {
3502 let key_len = self.catalog_data[offset] as usize;
3503 let mut rec_data_start = offset + 1 + key_len;
3504 if !rec_data_start.is_multiple_of(2) {
3505 rec_data_start += 1;
3506 }
3507 if rec_data_start + 15 <= self.catalog_data.len() {
3509 let name_len = self.catalog_data[rec_data_start + 14] as usize;
3510 if rec_data_start + 15 + name_len <= self.catalog_data.len() {
3511 let name = mac_roman_to_utf8(
3512 &self.catalog_data[rec_data_start + 15..rec_data_start + 15 + name_len],
3513 );
3514 return Some((cnid as u64, name));
3515 }
3516 }
3517 }
3518 Some((cnid as u64, format!("CNID {}", cnid)))
3519 }
3520
3521 fn fsck(&mut self) -> Option<Result<disk_core::fsck::FsckResult, FilesystemError>> {
3522 Some(self.fsck())
3523 }
3524}
3525
3526const CATALOG_DIR_THREAD: i8 = 3;
3529
3530impl<R: Read + Write + Seek + Send> EditableFilesystem for HfsFilesystem<R> {
3531 fn as_filesystem(&self) -> &dyn disk_core::filesystem::Filesystem {
3532 self
3533 }
3534 fn as_filesystem_mut(&mut self) -> &mut dyn disk_core::filesystem::Filesystem {
3535 self
3536 }
3537 fn begin_bulk(&mut self) {
3538 self.bulk_mode = true;
3539 }
3540
3541 fn end_bulk(&mut self) {
3542 self.bulk_mode = false;
3543 }
3544
3545 fn create_file(
3546 &mut self,
3547 parent: &FileEntry,
3548 name: &str,
3549 data: &mut dyn std::io::Read,
3550 data_len: u64,
3551 options: &CreateFileOptions,
3552 ) -> Result<FileEntry, FilesystemError> {
3553 self.ensure_catalog_initialized()?;
3554 let snap = self.snapshot();
3555 let extents_before = self.extents_overflow_data.clone();
3556 let result = (|| {
3557 let parent_id = parent.location as u32;
3558
3559 let name_bytes = validate_hfs_create_name(name)?;
3560
3561 if self
3563 .find_catalog_record_by_name(parent_id, &name_bytes)
3564 .is_some()
3565 {
3566 return Err(FilesystemError::AlreadyExists(name.into()));
3567 }
3568
3569 let file_id = self.mdb.next_catalog_id;
3571 self.mdb.next_catalog_id += 1;
3572
3573 let (type_code, creator_code) = hfs_common::resolve_create_type_creator(
3577 name,
3578 options.os_type,
3579 options.os_creator,
3580 options.type_code.as_deref(),
3581 options.creator_code.as_deref(),
3582 );
3583
3584 let data_extents = self.write_data_to_blocks(data, data_len)?;
3586
3587 let (rsrc_extents, rsrc_size) = if let Some(ref rsrc_src) = options.resource_fork {
3589 match rsrc_src {
3590 disk_core::filesystem::ResourceForkSource::Data(rsrc_data) => {
3591 let mut cursor = std::io::Cursor::new(rsrc_data);
3592 let ext = self.write_data_to_blocks(&mut cursor, rsrc_data.len() as u64)?;
3593 (ext, rsrc_data.len() as u32)
3594 }
3595 disk_core::filesystem::ResourceForkSource::File(path) => {
3596 let mut f = std::fs::File::open(path)?;
3597 let len = f.metadata()?.len();
3598 let ext = self.write_data_to_blocks(&mut f, len)?;
3599 (ext, len as u32)
3600 }
3601 }
3602 } else {
3603 (Vec::new(), 0)
3604 };
3605
3606 let mtime = options.unix_times.map(|t| t.mtime_or_now());
3611 let mut file_rec = Self::build_file_record(
3612 file_id,
3613 data_len as u32,
3614 data_extents.first().map_or(0, |e| e.start_block),
3615 Self::extents_block_total(&data_extents),
3616 rsrc_size,
3617 rsrc_extents.first().map_or(0, |e| e.start_block),
3618 Self::extents_block_total(&rsrc_extents),
3619 &type_code,
3620 &creator_code,
3621 self.mdb.block_size,
3622 mtime,
3623 options.finder_flags.unwrap_or(0),
3624 );
3625 Self::set_inline_extents(&mut file_rec, 74, &data_extents);
3626 Self::set_inline_extents(&mut file_rec, 86, &rsrc_extents);
3627
3628 let key = Self::build_catalog_key(parent_id, &name_bytes);
3630 let mut key_record = key;
3631 key_record.extend_from_slice(&file_rec);
3632
3633 self.insert_catalog_record(&key_record)?;
3634
3635 self.update_parent_valence(parent_id, 1)?;
3645
3646 self.mdb.file_count += 1;
3648
3649 let mut overflow_changed = self.insert_fork_overflow(file_id, 0x00, &data_extents)?;
3650 overflow_changed |= self.insert_fork_overflow(file_id, 0xFF, &rsrc_extents)?;
3651 if overflow_changed {
3652 self.write_extents_overflow_back()?;
3653 }
3654
3655 let path = if parent.path == "/" {
3656 format!("/{name}")
3657 } else {
3658 format!("{}/{name}", parent.path)
3659 };
3660 let mut fe = FileEntry::new_file(name.to_string(), path, data_len, file_id as u64);
3661 if type_code != [0; 4] {
3662 fe.type_code = Some(type_code);
3663 fe.creator_code = Some(creator_code);
3664 }
3665 fe.finder_flags = options.finder_flags;
3666 if rsrc_size > 0 {
3667 fe.resource_fork_size = Some(rsrc_size as u64);
3668 }
3669 let stamped = mtime.unwrap_or_else(disk_core::times::now);
3670 fe.modified_unix = Some(stamped);
3671 fe.mac_dates = Some((
3672 disk_core::times::unix_to_mac_local(stamped),
3673 disk_core::times::unix_to_mac_local(stamped),
3674 0,
3675 ));
3676 Ok(fe)
3677 })();
3678 if result.is_err() {
3679 self.extents_overflow_data = extents_before;
3680 self.restore_snapshot(snap);
3681 }
3682 result
3683 }
3684
3685 fn create_directory(
3686 &mut self,
3687 parent: &FileEntry,
3688 name: &str,
3689 options: &CreateDirectoryOptions,
3690 ) -> Result<FileEntry, FilesystemError> {
3691 self.ensure_catalog_initialized()?;
3692 let snap = self.snapshot();
3693 let result = (|| {
3694 let parent_id = parent.location as u32;
3695
3696 let name_bytes = validate_hfs_create_name(name)?;
3697
3698 if self
3700 .find_catalog_record_by_name(parent_id, &name_bytes)
3701 .is_some()
3702 {
3703 return Err(FilesystemError::AlreadyExists(name.into()));
3704 }
3705
3706 let folder_id = self.mdb.next_catalog_id;
3708 self.mdb.next_catalog_id += 1;
3709
3710 let mtime = options.unix_times.map(|t| t.mtime_or_now());
3713 let folder_rec = Self::build_dir_record_with_dates(folder_id, mtime);
3714
3715 let key = Self::build_catalog_key(parent_id, &name_bytes);
3717 let mut key_record = key;
3718 key_record.extend_from_slice(&folder_rec);
3719
3720 self.insert_catalog_record(&key_record)?;
3721
3722 let (_, thread_data) =
3724 Self::build_thread_record(CATALOG_DIR_THREAD, parent_id, &name_bytes);
3725 let thread_key = Self::build_catalog_key(folder_id, &[]);
3726 let mut thread_record = thread_key;
3727 thread_record.extend_from_slice(&thread_data);
3728 self.insert_catalog_record(&thread_record)?;
3729
3730 self.update_parent_valence(parent_id, 1)?;
3732
3733 self.mdb.folder_count += 1;
3735
3736 let path = if parent.path == "/" {
3737 format!("/{name}")
3738 } else {
3739 format!("{}/{name}", parent.path)
3740 };
3741 let mut fe = FileEntry::new_directory(name.to_string(), path, folder_id as u64);
3742 let stamped = mtime.unwrap_or_else(disk_core::times::now);
3743 fe.modified_unix = Some(stamped);
3744 fe.mac_dates = Some((
3745 disk_core::times::unix_to_mac_local(stamped),
3746 disk_core::times::unix_to_mac_local(stamped),
3747 0,
3748 ));
3749 Ok(fe)
3750 })();
3751 if result.is_err() {
3752 self.restore_snapshot(snap);
3753 }
3754 result
3755 }
3756
3757 fn delete_entry(
3758 &mut self,
3759 parent: &FileEntry,
3760 entry: &FileEntry,
3761 ) -> Result<(), FilesystemError> {
3762 let snap = self.snapshot();
3763 let extents_before = self.extents_overflow_data.clone();
3764 let result = (|| {
3765 let parent_id = parent.location as u32;
3766 let cnid = entry.location as u32;
3767
3768 if entry.is_directory() {
3770 let children = self.list_children(cnid)?;
3771 if !children.is_empty() {
3772 return Err(FilesystemError::InvalidData(
3773 "cannot delete non-empty directory".into(),
3774 ));
3775 }
3776 }
3777
3778 let name_bytes = utf8_to_mac_roman(&entry.name)?;
3780 let (node_idx, rec_idx, _offset) = self
3781 .find_catalog_record_by_name(parent_id, &name_bytes)
3782 .ok_or_else(|| {
3783 FilesystemError::NotFound(format!(
3784 "entry '{}' not found in catalog",
3785 entry.name
3786 ))
3787 })?;
3788
3789 let mut overflow_changed = false;
3791 if !entry.is_directory() {
3792 if let Some((_, data_extents, _, rsrc_extents)) = self.find_file_by_id(cnid) {
3793 self.free_extent_blocks(&data_extents);
3794 self.free_extent_blocks(&rsrc_extents);
3795 overflow_changed |= self.free_fork_overflow(cnid, 0x00)?;
3797 overflow_changed |= self.free_fork_overflow(cnid, 0xFF)?;
3798 }
3799 }
3800
3801 self.remove_catalog_record(node_idx, rec_idx)?;
3802
3803 if let Some((t_node, t_rec, _)) = self.find_catalog_record_by_cnid(cnid) {
3805 self.remove_catalog_record(t_node, t_rec)?;
3806 }
3807
3808 self.update_parent_valence(parent_id, -1)?;
3810
3811 if entry.is_directory() {
3813 self.mdb.folder_count = self.mdb.folder_count.saturating_sub(1);
3814 } else {
3815 self.mdb.file_count = self.mdb.file_count.saturating_sub(1);
3816 }
3817
3818 if overflow_changed {
3819 self.write_extents_overflow_back()?;
3820 }
3821
3822 Ok(())
3823 })();
3824 if result.is_err() {
3825 self.extents_overflow_data = extents_before;
3826 self.restore_snapshot(snap);
3827 }
3828 result
3829 }
3830
3831 fn rename(
3832 &mut self,
3833 parent: &FileEntry,
3834 entry: &FileEntry,
3835 new_name: &str,
3836 ) -> Result<(), FilesystemError> {
3837 if new_name == entry.name {
3838 return Ok(());
3839 }
3840 let parent_id = parent.location as u32;
3841 let cnid = entry.location as u32;
3842 let new_name_raw = validate_hfs_create_name(new_name)?;
3843 let old_name_raw = utf8_to_mac_roman(&entry.name)?;
3844
3845 let snap = self.snapshot();
3846 let result = (|| -> Result<(), FilesystemError> {
3847 let (node_idx, rec_idx, _key_abs) = self
3849 .find_catalog_record_by_name(parent_id, &old_name_raw)
3850 .ok_or_else(|| {
3851 FilesystemError::NotFound(format!(
3852 "entry '{}' not found in catalog",
3853 entry.name
3854 ))
3855 })?;
3856
3857 if let Some((n2, r2, _)) = self.find_catalog_record_by_name(parent_id, &new_name_raw) {
3860 if (n2, r2) != (node_idx, rec_idx) {
3861 return Err(FilesystemError::AlreadyExists(new_name.to_string()));
3862 }
3863 }
3864
3865 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
3868 let node_off = node_idx as usize * node_size;
3869 let node = &self.catalog_data[node_off..node_off + node_size];
3870 let (rec_start, rec_end) = hfs_common::btree_record_range(node, node_size, rec_idx);
3871 let key_len = node[rec_start] as usize;
3872 let mut data_rel = 1 + key_len;
3873 if !data_rel.is_multiple_of(2) {
3874 data_rel += 1;
3875 }
3876 let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
3877
3878 let mut new_record = Self::build_catalog_key(parent_id, &new_name_raw);
3879 if !new_record.len().is_multiple_of(2) {
3880 new_record.push(0);
3881 }
3882 new_record.extend_from_slice(&data_bytes);
3883 if !new_record.len().is_multiple_of(2) {
3884 new_record.push(0);
3885 }
3886
3887 self.remove_catalog_record(node_idx, rec_idx)?;
3888 self.insert_catalog_record(&new_record)?;
3889
3890 if let Some((_n, _r, t_key_abs)) = self.find_catalog_record_by_cnid(cnid) {
3894 let t_key_len = self.catalog_data[t_key_abs] as usize;
3895 let mut t_data_off = t_key_abs + 1 + t_key_len;
3896 if !t_data_off.is_multiple_of(2) {
3897 t_data_off += 1;
3898 }
3899 if t_data_off + 15 + 31 <= self.catalog_data.len() {
3900 self.catalog_data[t_data_off + 14] = new_name_raw.len() as u8;
3901 for i in 0..31 {
3902 self.catalog_data[t_data_off + 15 + i] = 0;
3903 }
3904 self.catalog_data[t_data_off + 15..t_data_off + 15 + new_name_raw.len()]
3905 .copy_from_slice(&new_name_raw);
3906 }
3907 }
3908 Ok(())
3910 })();
3911 if result.is_err() {
3912 self.restore_snapshot(snap);
3913 }
3914 result
3915 }
3916
3917 fn set_type_creator(
3918 &mut self,
3919 entry: &FileEntry,
3920 type_code: &str,
3921 creator_code: &str,
3922 ) -> Result<(), FilesystemError> {
3923 let snap = self.snapshot();
3924 let result = (|| {
3925 let frec_start = self.locate_record_data(entry.location as u32)?;
3926 if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
3927 return Err(FilesystemError::InvalidData(
3928 "set_type_creator: not a file record".into(),
3929 ));
3930 }
3931
3932 let tc = hfs_common::encode_fourcc(type_code);
3934 let cc = hfs_common::encode_fourcc(creator_code);
3935 self.catalog_data[frec_start + 4..frec_start + 8].copy_from_slice(&tc);
3936 self.catalog_data[frec_start + 8..frec_start + 12].copy_from_slice(&cc);
3937
3938 Ok(())
3939 })();
3940 if result.is_err() {
3941 self.restore_snapshot(snap);
3942 }
3943 result
3944 }
3945
3946 fn write_resource_fork(
3947 &mut self,
3948 entry: &FileEntry,
3949 data: &mut dyn std::io::Read,
3950 len: u64,
3951 ) -> Result<(), FilesystemError> {
3952 let snap = self.snapshot();
3953 let extents_before = self.extents_overflow_data.clone();
3954 let result = (|| {
3955 let cnid = entry.location as u32;
3956
3957 let mut overflow_changed = false;
3959 if let Some((_, _, _, rsrc_extents)) = self.find_file_by_id(cnid) {
3960 self.free_extent_blocks(&rsrc_extents);
3961 overflow_changed = self.free_fork_overflow(cnid, 0xFF)?;
3962 }
3963
3964 let rsrc_extents = self.write_data_to_blocks(data, len)?;
3966
3967 let frec_start = self.locate_record_data(cnid)?;
3968 if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
3969 return Err(FilesystemError::InvalidData(
3970 "write_resource_fork: not a file record".into(),
3971 ));
3972 }
3973
3974 self.catalog_data[frec_start + 34..frec_start + 36].fill(0);
3980 BigEndian::write_u32(
3982 &mut self.catalog_data[frec_start + 36..frec_start + 40],
3983 len as u32,
3984 );
3985 BigEndian::write_u32(
3987 &mut self.catalog_data[frec_start + 40..frec_start + 44],
3988 Self::extents_block_total(&rsrc_extents) as u32 * self.mdb.block_size,
3989 );
3990 Self::set_inline_extents(
3992 &mut self.catalog_data[frec_start..frec_start + 102],
3993 86,
3994 &rsrc_extents,
3995 );
3996 overflow_changed |= self.insert_fork_overflow(cnid, 0xFF, &rsrc_extents)?;
3997
3998 if overflow_changed {
3999 self.write_extents_overflow_back()?;
4000 }
4001
4002 Ok(())
4003 })();
4004 if result.is_err() {
4005 self.extents_overflow_data = extents_before;
4006 self.restore_snapshot(snap);
4007 }
4008 result
4009 }
4010
4011 fn repair(&mut self) -> Result<disk_core::fsck::RepairReport, FilesystemError> {
4012 self.ensure_bitmap()?;
4013 let mut extents_data = if self.mdb.extents_file_size > 0 {
4014 read_fork_data(
4015 &mut self.reader,
4016 self.partition_offset,
4017 &self.mdb,
4018 &self.mdb.extents_file_extents,
4019 self.mdb.extents_file_size as u64,
4020 )
4021 .ok()
4022 } else {
4023 None
4024 };
4025
4026 let report = crate::hfs_fsck::repair_hfs(
4027 &mut self.mdb,
4028 &mut self.catalog_data,
4029 self.bitmap.as_mut().unwrap(),
4030 extents_data.as_mut(),
4031 );
4032
4033 if let Some(ref ext_data) = extents_data {
4035 if self.mdb.extents_file_size > 0 {
4036 write_extents_overflow_data(
4037 &mut self.reader,
4038 self.partition_offset,
4039 &self.mdb,
4040 ext_data,
4041 )?;
4042 }
4043 }
4044
4045 self.do_sync_metadata()?;
4046 Ok(report)
4047 }
4048
4049 fn sync_metadata(&mut self) -> Result<(), FilesystemError> {
4050 self.do_sync_metadata()
4051 }
4052
4053 fn free_space(&mut self) -> Result<u64, FilesystemError> {
4054 Ok(self.mdb.free_blocks as u64 * self.mdb.block_size as u64)
4055 }
4056
4057 fn set_blessed_folder(&mut self, entry: &FileEntry) -> Result<(), FilesystemError> {
4058 let snap = self.snapshot();
4059 let result = (|| {
4060 if !entry.is_directory() {
4061 return Err(FilesystemError::InvalidData(
4062 "can only bless a directory".into(),
4063 ));
4064 }
4065 self.mdb.finder_info[0] = entry.location as u32;
4066 Ok(())
4067 })();
4068 if result.is_err() {
4069 self.restore_snapshot(snap);
4070 }
4071 result
4072 }
4073
4074 fn write_boot_blocks(&mut self, blocks: &[u8; 1024]) -> Result<(), FilesystemError> {
4075 HfsFilesystem::set_boot_blocks(self, blocks);
4076 Ok(())
4077 }
4078
4079 fn set_volume_name(&mut self, new_name: &str) -> Result<(), FilesystemError> {
4080 HfsFilesystem::set_volume_name(self, new_name)
4081 }
4082
4083 fn set_finder_info(
4084 &mut self,
4085 entry: &FileEntry,
4086 finfo: [u8; 16],
4087 fxinfo: [u8; 16],
4088 ) -> Result<(), FilesystemError> {
4089 HfsFilesystem::set_finder_info(self, entry, finfo, fxinfo)
4090 }
4091
4092 fn set_dates(
4093 &mut self,
4094 entry: &FileEntry,
4095 create: u32,
4096 modify: u32,
4097 backup: u32,
4098 ) -> Result<(), FilesystemError> {
4099 HfsFilesystem::set_dates(self, entry, create, modify, backup)
4100 }
4101}
4102
4103fn collect_fork_overflow_extents(
4109 extents_data: &[u8],
4110 file_id: u32,
4111 fork_type: u8,
4112 min_start_block: u32,
4113) -> Vec<HfsExtDescriptor> {
4114 use crate::hfs_common::{btree_record_range, BTreeHeader, BTREE_LEAF_NODE};
4115 use std::collections::HashSet;
4116
4117 let mut out: Vec<(u16, HfsExtDescriptor)> = Vec::new();
4118 if extents_data.len() < 512 {
4119 return Vec::new();
4120 }
4121 let header = BTreeHeader::read(extents_data);
4122 let node_size = header.node_size as usize;
4123 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || extents_data.len() < node_size {
4124 return Vec::new();
4125 }
4126
4127 let mut node_idx = header.first_leaf_node;
4128 let mut visited: HashSet<u32> = HashSet::new();
4129 while node_idx != 0 {
4130 if !visited.insert(node_idx) {
4131 break;
4132 }
4133 let off = node_idx as usize * node_size;
4134 if off + node_size > extents_data.len() {
4135 break;
4136 }
4137 let node = &extents_data[off..off + node_size];
4138 let flink = BigEndian::read_u32(&node[0..4]);
4139 let kind = node[8] as i8;
4140 if kind != BTREE_LEAF_NODE {
4141 break;
4142 }
4143 let num_records = BigEndian::read_u16(&node[10..12]) as usize;
4144 for i in 0..num_records {
4145 let (rec_start, _) = btree_record_range(node, node_size, i);
4146 if rec_start + 8 > node_size {
4147 continue;
4148 }
4149 let key_len = node[rec_start] as usize;
4150 if key_len < 7 || rec_start + 1 + key_len > node_size {
4151 continue;
4152 }
4153 let rec_fork_type = node[rec_start + 1];
4154 let rec_file_id = BigEndian::read_u32(&node[rec_start + 2..rec_start + 6]);
4155 let rec_start_block = BigEndian::read_u16(&node[rec_start + 6..rec_start + 8]);
4156 if rec_fork_type != fork_type || rec_file_id != file_id {
4157 continue;
4158 }
4159 if (rec_start_block as u32) < min_start_block {
4160 continue;
4161 }
4162 let mut data_off = rec_start + 1 + key_len;
4163 if !data_off.is_multiple_of(2) {
4164 data_off += 1;
4165 }
4166 if data_off + 12 > node_size {
4167 continue;
4168 }
4169 for j in 0..3 {
4170 let ext = HfsExtDescriptor::parse(&node[data_off + j * 4..data_off + j * 4 + 4]);
4171 if ext.block_count > 0 {
4172 out.push((rec_start_block, ext));
4173 }
4174 }
4175 }
4176 node_idx = flink;
4177 }
4178
4179 out.sort_by_key(|(sb, _)| *sb);
4180 out.into_iter().map(|(_, e)| e).collect()
4181}
4182
4183fn read_fork_data<R: Read + Seek>(
4184 reader: &mut R,
4185 partition_offset: u64,
4186 mdb: &HfsMasterDirectoryBlock,
4187 extents: &[HfsExtDescriptor; 3],
4188 size: u64,
4189) -> Result<Vec<u8>, FilesystemError> {
4190 let mut data = Vec::with_capacity(size as usize);
4191 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4192
4193 for ext in extents {
4194 if ext.block_count == 0 {
4195 break;
4196 }
4197 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4198 let len = ext.block_count as u64 * mdb.block_size as u64;
4199 reader.seek(SeekFrom::Start(offset))?;
4200 let read_len = len.min(size - data.len() as u64) as usize;
4201 let mut buf = vec![0u8; read_len];
4202 reader.read_exact(&mut buf)?;
4203 data.extend_from_slice(&buf);
4204 if data.len() as u64 >= size {
4205 break;
4206 }
4207 }
4208
4209 data.truncate(size as usize);
4210 Ok(data)
4211}
4212
4213fn remove_fork_overflow_records(
4216 extents_data: &mut [u8],
4217 file_id: u32,
4218 fork_type: u8,
4219) -> Result<u32, FilesystemError> {
4220 use crate::hfs_common::{
4221 btree_remove_record, btree_retire_empty_leaf, compare_classic_extents_keys,
4222 walk_leaf_records, BTreeHeader,
4223 };
4224 let kf = BTreeKeyFormat::CLASSIC_EXTENTS;
4225 if extents_data.len() < 512 {
4226 return Ok(0);
4227 }
4228 let header = BTreeHeader::read(extents_data);
4229 let node_size = header.node_size as usize;
4230 if node_size < crate::hfs_common::BTREE_MIN_NODE_SIZE || extents_data.len() < node_size {
4231 return Ok(0);
4232 }
4233 let mut victims: Vec<(u32, usize)> = Vec::new();
4234 walk_leaf_records::<(), _>(
4235 extents_data,
4236 header.first_leaf_node,
4237 node_size,
4238 |node_idx, rec_idx, _off, rec| {
4239 if rec.len() >= 8
4241 && rec[0] >= 7
4242 && rec[1] == fork_type
4243 && BigEndian::read_u32(&rec[2..6]) == file_id
4244 {
4245 victims.push((node_idx, rec_idx));
4246 }
4247 None
4248 },
4249 );
4250 if victims.is_empty() {
4251 return Ok(0);
4252 }
4253 victims.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
4255 for &(node_idx, rec_idx) in &victims {
4256 let off = node_idx as usize * node_size;
4257 btree_remove_record(&mut extents_data[off..off + node_size], node_size, rec_idx);
4258 }
4259 let mut h = BTreeHeader::read(extents_data);
4260 h.leaf_records = h.leaf_records.saturating_sub(victims.len() as u32);
4261 h.write(extents_data);
4262 let mut nodes: Vec<u32> = victims.iter().map(|v| v.0).collect();
4263 nodes.dedup();
4264 for node_idx in nodes {
4265 let off = node_idx as usize * node_size;
4266 if BigEndian::read_u16(&extents_data[off + 10..off + 12]) == 0 {
4267 btree_retire_empty_leaf(
4269 extents_data,
4270 node_size,
4271 node_idx,
4272 &kf,
4273 &compare_classic_extents_keys,
4274 )?;
4275 } else if victims.iter().any(|&(n, r)| n == node_idx && r == 0) {
4276 btree_refresh_index_keys(
4278 extents_data,
4279 node_size,
4280 node_idx,
4281 &kf,
4282 &compare_classic_extents_keys,
4283 )?;
4284 }
4285 }
4286 Ok(victims.len() as u32)
4287}
4288
4289fn write_extents_overflow_data<W: std::io::Write + Seek>(
4291 writer: &mut W,
4292 partition_offset: u64,
4293 mdb: &HfsMasterDirectoryBlock,
4294 data: &[u8],
4295) -> Result<(), FilesystemError> {
4296 let alloc_start = partition_offset + mdb.first_alloc_block as u64 * 512;
4297 let mut data_start = 0usize;
4298 for extent in mdb.extents_file_extents.iter() {
4299 if extent.block_count == 0 {
4300 break;
4301 }
4302 let block_off = alloc_start + extent.start_block as u64 * mdb.block_size as u64;
4303 let byte_len = extent.block_count as usize * mdb.block_size as usize;
4304 let data_end = (data_start + byte_len).min(data.len());
4305 if data_start < data_end {
4306 writer.seek(std::io::SeekFrom::Start(block_off))?;
4307 writer.write_all(&data[data_start..data_end])?;
4308 }
4309 data_start += byte_len;
4310 }
4311 Ok(())
4312}
4313
4314fn write_fork_data_to<R: Read + Seek>(
4316 reader: &mut R,
4317 partition_offset: u64,
4318 mdb: &HfsMasterDirectoryBlock,
4319 extents: &[HfsExtDescriptor; 3],
4320 writer: &mut dyn std::io::Write,
4321 size: u64,
4322) -> Result<u64, FilesystemError> {
4323 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4324 let mut written: u64 = 0;
4325 let mut buf = vec![0u8; 64 * 1024];
4326
4327 for ext in extents {
4328 if ext.block_count == 0 || written >= size {
4329 break;
4330 }
4331 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4332 let extent_len = ext.block_count as u64 * mdb.block_size as u64;
4333 let to_emit = extent_len.min(size - written);
4334 reader.seek(SeekFrom::Start(offset))?;
4335 let mut left = to_emit;
4336 while left > 0 {
4337 let n = (buf.len() as u64).min(left) as usize;
4338 reader.read_exact(&mut buf[..n])?;
4339 writer.write_all(&buf[..n])?;
4340 left -= n as u64;
4341 }
4342 written += to_emit;
4343 }
4344 Ok(written)
4345}
4346
4347fn write_hfs_fork_data<R: Write + Seek>(
4349 writer: &mut R,
4350 partition_offset: u64,
4351 mdb: &HfsMasterDirectoryBlock,
4352 extents: &[HfsExtDescriptor; 3],
4353 data: &[u8],
4354) -> Result<(), FilesystemError> {
4355 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4356 let mut written = 0usize;
4357 for ext in extents {
4358 if ext.block_count == 0 || written >= data.len() {
4359 break;
4360 }
4361 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4362 let extent_len = ext.block_count as u64 * mdb.block_size as u64;
4363 let to_write = extent_len.min((data.len() - written) as u64) as usize;
4364 writer.seek(SeekFrom::Start(offset))?;
4365 writer.write_all(&data[written..written + to_write])?;
4366 written += to_write;
4367 }
4368 Ok(())
4369}
4370
4371fn find_last_set_bit(bitmap: &[u8], max_bits: u32) -> Option<u32> {
4373 let mut last = None;
4374 for bit in 0..max_bits {
4375 let byte_idx = bit as usize / 8;
4376 let bit_idx = 7 - (bit % 8); if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
4378 last = Some(bit);
4379 }
4380 }
4381 last
4382}
4383
4384pub struct CompactHfsReader<R: Read + Seek> {
4392 reader: R,
4393 partition_offset: u64,
4394 mdb: HfsMasterDirectoryBlock,
4395 bitmap: Vec<u8>,
4396 phase: u8,
4398 pre_alloc_pos: u64,
4400 current_block: u32,
4402 block_pos: u64,
4404 #[allow(dead_code)]
4405 original_size: u64,
4406 #[allow(dead_code)]
4407 allocated_blocks: u32,
4408}
4409
4410impl<R: Read + Seek> CompactHfsReader<R> {
4411 pub fn new(
4412 mut reader: R,
4413 partition_offset: u64,
4414 ) -> Result<(Self, CompactResult), FilesystemError> {
4415 log::debug!(
4417 "[HFS compact] seeking to MDB at offset {}",
4418 partition_offset + 1024
4419 );
4420 reader.seek(SeekFrom::Start(partition_offset + 1024))?;
4421 let mut mdb_buf = [0u8; 162];
4422 reader.read_exact(&mut mdb_buf)?;
4423 let mdb = HfsMasterDirectoryBlock::parse(&mdb_buf).map_err(|e| {
4424 log::debug!("[HFS compact] MDB parse failed: {e}");
4425 e
4426 })?;
4427 log::debug!(
4428 "[HFS compact] MDB ok: block_size={}, total_blocks={}, first_alloc_block={}, volume_bitmap_block={}",
4429 mdb.block_size, mdb.total_blocks, mdb.first_alloc_block, mdb.volume_bitmap_block,
4430 );
4431
4432 if mdb.has_embedded_hfs_plus() {
4433 log::debug!("[HFS compact] has embedded HFS+ — refusing to compact HFS wrapper");
4434 return Err(FilesystemError::Unsupported(
4435 "cannot compact HFS wrapper with embedded HFS+".into(),
4436 ));
4437 }
4438
4439 let bitmap_offset = partition_offset + mdb.volume_bitmap_block as u64 * 512;
4441 log::debug!("[HFS compact] reading bitmap at offset {bitmap_offset}");
4442 let bitmap_size = (mdb.total_blocks as u32).div_ceil(8) as usize;
4443 reader.seek(SeekFrom::Start(bitmap_offset))?;
4444 let mut bitmap = vec![0u8; bitmap_size];
4445 reader.read_exact(&mut bitmap)?;
4446 log::debug!("[HFS compact] bitmap read: {bitmap_size} bytes");
4447
4448 let mut allocated = 0u32;
4450 for bit in 0..mdb.total_blocks as u32 {
4451 let byte_idx = bit as usize / 8;
4452 let bit_idx = 7 - (bit % 8);
4453 if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
4454 allocated += 1;
4455 }
4456 }
4457 log::debug!(
4458 "[HFS compact] allocated={} / {} total blocks ({} free)",
4459 allocated,
4460 mdb.total_blocks,
4461 mdb.total_blocks as u32 - allocated,
4462 );
4463
4464 let pre_alloc_size = mdb.first_alloc_block as u64 * 512;
4466 let original_size = pre_alloc_size + mdb.total_blocks as u64 * mdb.block_size as u64;
4467 let compacted_size = original_size;
4470 let data_size = pre_alloc_size + allocated as u64 * mdb.block_size as u64;
4472 log::debug!(
4473 "[HFS compact] pre_alloc_size={}, data_size={}, compacted_size={} original_size={} (layout-preserving; free blocks -> zeros)",
4474 pre_alloc_size, data_size, compacted_size, original_size
4475 );
4476
4477 let result = CompactResult {
4478 original_size,
4479 compacted_size,
4480 data_size,
4481 clusters_used: allocated,
4482 };
4483
4484 Ok((
4485 CompactHfsReader {
4486 reader,
4487 partition_offset,
4488 mdb,
4489 bitmap,
4490 phase: 0,
4491 pre_alloc_pos: 0,
4492 current_block: 0,
4493 block_pos: 0,
4494 original_size,
4495 allocated_blocks: allocated,
4496 },
4497 result,
4498 ))
4499 }
4500
4501 fn is_block_allocated(&self, block: u32) -> bool {
4502 let byte_idx = block as usize / 8;
4503 let bit_idx = 7 - (block % 8);
4504 byte_idx < self.bitmap.len() && (self.bitmap[byte_idx] >> bit_idx) & 1 == 1
4505 }
4506}
4507
4508impl<R: Read + Seek> Read for CompactHfsReader<R> {
4509 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
4510 if self.phase > 1 {
4511 return Ok(0);
4512 }
4513
4514 if self.phase == 0 {
4515 let pre_alloc_size = self.mdb.first_alloc_block as u64 * 512;
4518 let remaining = pre_alloc_size - self.pre_alloc_pos;
4519 if remaining == 0 {
4520 self.phase = 1;
4521 return self.read(buf);
4522 }
4523 let to_read = (remaining as usize).min(buf.len());
4524 let offset = self.partition_offset + self.pre_alloc_pos;
4525 self.reader
4526 .seek(SeekFrom::Start(offset))
4527 .map_err(disk_core::io_other)?;
4528 let n = self.reader.read(&mut buf[..to_read])?;
4529 self.pre_alloc_pos += n as u64;
4530 return Ok(n);
4531 }
4532
4533 let total_blocks = self.mdb.total_blocks as u32;
4536 if self.current_block >= total_blocks {
4537 self.phase = 2;
4538 return Ok(0);
4539 }
4540
4541 let block_size = self.mdb.block_size as u64;
4542
4543 let allocated = self.is_block_allocated(self.current_block);
4548 let mut run_end = self.current_block + 1;
4549 while run_end < total_blocks && self.is_block_allocated(run_end) == allocated {
4550 run_end += 1;
4551 }
4552 let bytes_in_run = (run_end - self.current_block) as u64 * block_size - self.block_pos;
4553 let to_read = (bytes_in_run as usize).min(buf.len());
4554
4555 let n = if allocated {
4556 let offset = self.partition_offset
4557 + self.mdb.first_alloc_block as u64 * 512
4558 + self.current_block as u64 * block_size
4559 + self.block_pos;
4560 self.reader
4561 .seek(SeekFrom::Start(offset))
4562 .map_err(disk_core::io_other)?;
4563 self.reader.read(&mut buf[..to_read])?
4564 } else {
4565 buf[..to_read].fill(0);
4567 to_read
4568 };
4569
4570 let mut remaining_in_n = n as u64;
4572 if self.block_pos > 0 {
4573 let want = block_size - self.block_pos;
4574 let take = remaining_in_n.min(want);
4575 self.block_pos += take;
4576 remaining_in_n -= take;
4577 if self.block_pos >= block_size {
4578 self.block_pos = 0;
4579 self.current_block += 1;
4580 }
4581 }
4582 let whole = (remaining_in_n / block_size) as u32;
4583 self.current_block += whole;
4584 remaining_in_n -= whole as u64 * block_size;
4585 if remaining_in_n > 0 {
4586 self.block_pos = remaining_in_n;
4587 }
4588
4589 Ok(n)
4590 }
4591}
4592
4593pub fn fit_hfs_volume_to_partition(
4608 device: &mut (impl Read + Write + Seek),
4609 partition_offset: u64,
4610 partition_len: u64,
4611 log: &mut impl FnMut(&str),
4612) -> anyhow::Result<bool> {
4613 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4614 let mut sector = [0u8; 512];
4615 device.read_exact(&mut sector)?;
4616 if BigEndian::read_u16(§or[0..2]) != HFS_SIGNATURE {
4617 return Ok(false);
4618 }
4619 if BigEndian::read_u16(§or[0x7C..0x7E]) == 0x482B {
4621 return Ok(false);
4622 }
4623 let total_blocks = BigEndian::read_u16(§or[0x12..0x14]) as u64;
4624 let block_size = BigEndian::read_u32(§or[0x14..0x18]) as u64;
4625 let first_alloc = BigEndian::read_u16(§or[0x1C..0x1E]) as u64;
4626 let volume_len = first_alloc * 512 + total_blocks * block_size + 1024;
4627 if block_size == 0 || volume_len >= partition_len {
4628 return Ok(false);
4629 }
4630 let growable = hfs_max_growable_size(device, partition_offset).unwrap_or(0);
4631 if growable < partition_len {
4632 log(&format!(
4633 "Warning: the HFS volume ({volume_len} bytes) does not fill its {partition_len}-byte \
4634 partition and its volume bitmap cannot address more blocks; Disk First Aid will \
4635 report an invalid allocation block start. Format the volume at the partition size"
4636 ));
4637 return Ok(false);
4638 }
4639 resize_hfs_in_place(device, partition_offset, partition_len, log)?;
4640 log(&format!(
4641 "HFS volume grown from {volume_len} bytes to fill its {partition_len}-byte partition"
4642 ));
4643 Ok(true)
4644}
4645
4646pub fn hfs_max_growable_size(
4647 device: &mut (impl Read + Seek),
4648 partition_offset: u64,
4649) -> Option<u64> {
4650 device.seek(SeekFrom::Start(partition_offset + 1024)).ok()?;
4651 let mut sector = [0u8; 512];
4652 device.read_exact(&mut sector).ok()?;
4653
4654 let sig = BigEndian::read_u16(§or[0..2]);
4655 if sig != HFS_SIGNATURE {
4656 return None;
4657 }
4658
4659 let block_size = BigEndian::read_u32(§or[20..24]) as u64;
4660 let vbm_start = BigEndian::read_u16(§or[14..16]) as u64;
4661 let first_alloc = BigEndian::read_u16(§or[28..30]) as u64;
4662
4663 if block_size == 0 || first_alloc <= vbm_start {
4664 return None;
4665 }
4666
4667 let vbm_capacity = (first_alloc - vbm_start) * 512 * 8;
4668 let max_blocks = vbm_capacity.min(u16::MAX as u64);
4669 let overhead = first_alloc * 512 + 1024;
4671
4672 Some(max_blocks * block_size + overhead)
4673}
4674
4675pub fn resize_hfs_in_place(
4676 device: &mut (impl Read + Write + Seek),
4677 partition_offset: u64,
4678 new_size_bytes: u64,
4679 log: &mut impl FnMut(&str),
4680) -> anyhow::Result<()> {
4681 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4685 let mut sector = [0u8; 512];
4686 device.read_exact(&mut sector)?;
4687
4688 let sig = BigEndian::read_u16(§or[0..2]);
4689 if sig != HFS_SIGNATURE {
4690 log("HFS resize: not an HFS volume, skipping");
4691 return Ok(());
4692 }
4693
4694 let block_size = BigEndian::read_u32(§or[20..24]);
4695 let old_total = BigEndian::read_u16(§or[18..20]);
4696 let free_blocks = BigEndian::read_u16(§or[34..36]);
4697 let vbm_start = BigEndian::read_u16(§or[14..16]);
4698 let first_alloc = BigEndian::read_u16(§or[28..30]);
4699 let used_blocks = old_total - free_blocks;
4700
4701 let vbm_capacity_blocks = if first_alloc > vbm_start {
4705 (first_alloc - vbm_start) as u64 * 512 * 8
4706 } else {
4707 0
4708 };
4709
4710 let overhead = first_alloc as u64 * 512 + 1024;
4713 if new_size_bytes <= overhead {
4714 anyhow::bail!(
4715 "HFS resize: new size {} bytes <= overhead {} bytes",
4716 new_size_bytes,
4717 overhead
4718 );
4719 }
4720 let new_total_u64 = (new_size_bytes - overhead) / block_size as u64;
4721
4722 if new_total_u64 > u16::MAX as u64 {
4725 let max_bytes = u16::MAX as u64 * block_size as u64 + overhead;
4726 anyhow::bail!(
4727 "HFS resize: requested size {} MB needs {} allocation blocks at {}-byte blocks, \
4728 but HFS caps at 65535 blocks (max {} MB for this volume). \
4729 Growing further would require reformatting with a larger allocation block size.",
4730 new_size_bytes / 1024 / 1024,
4731 new_total_u64,
4732 block_size,
4733 max_bytes / 1024 / 1024,
4734 );
4735 }
4736 if new_total_u64 > vbm_capacity_blocks {
4737 anyhow::bail!(
4738 "HFS resize: requested {} allocation blocks exceeds the Volume Bitmap capacity \
4739 ({} blocks) for this volume. The VBM lives in fixed sectors [{}..{}) and cannot \
4740 be grown without reformatting: the most this volume can grow to is {} bytes. \
4741 Mac OS wants a classic HFS volume to fill its partition, so format one at the \
4742 partition size instead (`new volume hfs --size`, then `new hd --fill`).",
4743 new_total_u64,
4744 vbm_capacity_blocks,
4745 vbm_start,
4746 first_alloc,
4747 vbm_capacity_blocks.min(u16::MAX as u64) * block_size as u64 + overhead,
4748 );
4749 }
4750 let new_total = new_total_u64 as u16;
4751
4752 let vbm_len = (first_alloc - vbm_start) as usize * 512;
4755 let mut vbm = vec![0u8; vbm_len];
4756 device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
4757 device.read_exact(&mut vbm)?;
4758 let highest_used = find_last_set_bit(&vbm, old_total as u32).map_or(0, |b| b + 1);
4759 if (new_total as u32) < highest_used {
4760 anyhow::bail!(
4761 "HFS resize: new size {} blocks ends before the last allocated block ({}); \
4762 {} of {} blocks are in use",
4763 new_total,
4764 highest_used,
4765 used_blocks,
4766 old_total
4767 );
4768 }
4769 for b in old_total.min(new_total) as u32..old_total.max(new_total) as u32 {
4772 bitmap_clear_bit_be(&mut vbm, b);
4773 }
4774 device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
4775 device.write_all(&vbm)?;
4776
4777 let new_free = new_total - used_blocks;
4778
4779 log(&format!(
4780 "HFS resize: {} -> {} blocks ({} free)",
4781 old_total, new_total, new_free
4782 ));
4783
4784 BigEndian::write_u16(&mut sector[18..20], new_total);
4786 BigEndian::write_u16(&mut sector[34..36], new_free);
4787
4788 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4790 device.write_all(§or)?;
4791
4792 if new_size_bytes > 1024 {
4794 device.seek(SeekFrom::Start(partition_offset + new_size_bytes - 1024))?;
4795 device.write_all(§or)?;
4796 }
4797
4798 device.flush()?;
4799 Ok(())
4800}
4801
4802pub fn validate_hfs_integrity(
4804 device: &mut (impl Read + Seek),
4805 partition_offset: u64,
4806 log: &mut impl FnMut(&str),
4807) -> anyhow::Result<()> {
4808 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4810 let mut sector = [0u8; 512];
4811 device.read_exact(&mut sector)?;
4812
4813 let sig = BigEndian::read_u16(§or[0..2]);
4814 if sig != HFS_SIGNATURE {
4815 log("HFS validate: not an HFS volume, skipping");
4816 return Ok(());
4817 }
4818
4819 let block_size = BigEndian::read_u32(§or[20..24]);
4820 let total_blocks = BigEndian::read_u16(§or[18..20]);
4821
4822 if !block_size.is_power_of_two() || block_size < 512 {
4824 anyhow::bail!("HFS validate: invalid block size {block_size}");
4825 }
4826 if total_blocks == 0 {
4827 anyhow::bail!("HFS validate: zero total blocks");
4828 }
4829
4830 log(&format!(
4831 "HFS validate: OK ({total_blocks} blocks, {} block size)",
4832 block_size
4833 ));
4834 Ok(())
4835}
4836
4837#[cfg(test)]
4838#[allow(clippy::identity_op)] mod tests {
4840 use super::*;
4841 use std::io::Cursor;
4842
4843 #[test]
4847 fn write_blank_hfs_into_matches_create_blank_hfs() {
4848 let (size, bs) = (8 * 1024 * 1024u64, 4096u32);
4849 let now = 0xB0A1_2345;
4850 let mem = create_blank_hfs_sized_at(size, bs, "StreamEq", 0, 0, now).unwrap();
4851
4852 let mut sink = Cursor::new(vec![0u8; 0]);
4853 let written = write_blank_hfs_into_at(&mut sink, 0, size, bs, "StreamEq", now).unwrap();
4854 let mut streamed = sink.into_inner();
4855 streamed.resize(written as usize, 0);
4856
4857 assert_eq!(written as usize, mem.len());
4858 assert_eq!(streamed, mem);
4859 }
4860
4861 #[test]
4864 fn write_blank_hfs_into_honours_the_offset() {
4865 let (size, bs) = (4 * 1024 * 1024u64, 2048u32);
4866 let now = 0xB0A1_2345;
4867 let mem = create_blank_hfs_sized_at(size, bs, "Offset", 0, 0, now).unwrap();
4868
4869 let offset = 64 * 512u64;
4870 let mut sink = Cursor::new(vec![0u8; 0]);
4871 let written = write_blank_hfs_into_at(&mut sink, offset, size, bs, "Offset", now).unwrap();
4872 let mut streamed = sink.into_inner();
4873 streamed.resize((offset + written) as usize, 0);
4874
4875 assert!(streamed[..offset as usize].iter().all(|&b| b == 0));
4876 assert_eq!(&streamed[offset as usize..], &mem[..]);
4877 }
4878
4879 #[test]
4880 fn test_mac_roman_to_utf8_ascii() {
4881 let data = b"Hello World";
4882 assert_eq!(mac_roman_to_utf8(data), "Hello World");
4883 }
4884
4885 #[test]
4886 fn test_mac_roman_to_utf8_special() {
4887 let data = [0x80, 0x81, 0x87];
4889 assert_eq!(mac_roman_to_utf8(&data), "ÄÅá");
4890 }
4891
4892 #[test]
4893 fn test_decode_mac_filename_utf8() {
4894 let mut data = b"HeapFixer".to_vec();
4897 data.extend_from_slice(&[0xE2, 0x84, 0xA2]);
4898 data.extend_from_slice(b" 1.01");
4899 assert_eq!(decode_mac_filename(&data), "HeapFixer™ 1.01");
4900 }
4901
4902 #[test]
4903 fn test_decode_mac_filename_macroman() {
4904 assert_eq!(decode_mac_filename(&[0xC4]), "ƒ");
4907 assert_eq!(decode_mac_filename(b"Cache\xA5Flow"), "Cache•Flow");
4908 assert_eq!(decode_mac_filename(b"Virex\xA8 3.51"), "Virex® 3.51");
4909 }
4910
4911 #[test]
4912 fn test_decode_mac_filename_ascii() {
4913 assert_eq!(decode_mac_filename(b"ScriptEdit"), "ScriptEdit");
4914 }
4915
4916 #[test]
4917 fn test_format_ostype() {
4918 assert_eq!(format_ostype(&[b'S', 0xA5, b'L', b' ']), "S•L");
4920 assert_eq!(format_ostype(b"APPL"), "APPL");
4921 assert_eq!(format_ostype(&[b'L', b's', 0x00, 0x00]), "Ls");
4923 }
4924
4925 #[test]
4926 fn test_mdb_parse() {
4927 let mut data = [0u8; 162];
4928 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
4930 BigEndian::write_u16(&mut data[18..20], 1000);
4932 BigEndian::write_u32(&mut data[20..24], 4096);
4934 BigEndian::write_u16(&mut data[34..36], 200);
4936 data[36] = 7;
4938 data[37..44].copy_from_slice(b"TestVol");
4939
4940 let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
4941 assert_eq!(mdb.signature, HFS_SIGNATURE);
4942 assert_eq!(mdb.total_blocks, 1000);
4943 assert_eq!(mdb.block_size, 4096);
4944 assert_eq!(mdb.free_blocks, 200);
4945 assert_eq!(mdb.volume_name, "TestVol");
4946 assert!(!mdb.has_embedded_hfs_plus());
4947 }
4948
4949 #[test]
4950 fn test_mdb_detect_embedded_hfs_plus() {
4951 let mut data = [0u8; 162];
4952 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
4953 BigEndian::write_u16(&mut data[18..20], 1000);
4954 BigEndian::write_u32(&mut data[20..24], 4096);
4955 BigEndian::write_u16(&mut data[124..126], HFS_PLUS_EMBEDDED_SIGNATURE);
4957 BigEndian::write_u16(&mut data[126..128], 10); BigEndian::write_u16(&mut data[128..130], 500); let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
4961 assert!(mdb.has_embedded_hfs_plus());
4962 }
4963
4964 #[test]
4965 fn test_find_last_set_bit() {
4966 let bitmap = [0b10100000, 0b00001000]; assert_eq!(find_last_set_bit(&bitmap, 16), Some(12));
4968
4969 let bitmap_empty = [0u8; 2];
4970 assert_eq!(find_last_set_bit(&bitmap_empty, 16), None);
4971
4972 let bitmap_first = [0b10000000]; assert_eq!(find_last_set_bit(&bitmap_first, 8), Some(0));
4974 }
4975
4976 #[test]
4977 fn test_utf8_to_mac_roman_roundtrip() {
4978 let text = "Hello World";
4979 let mac = utf8_to_mac_roman(text).unwrap();
4980 assert_eq!(mac, b"Hello World");
4981 assert_eq!(mac_roman_to_utf8(&mac), text);
4982 }
4983
4984 #[test]
4985 fn test_utf8_to_mac_roman_special() {
4986 let text = "Ä"; let mac = utf8_to_mac_roman(text).unwrap();
4988 assert_eq!(mac, &[0x80]);
4989 assert_eq!(mac_roman_to_utf8(&mac), text);
4990 }
4991
4992 #[test]
4993 fn test_utf8_to_mac_roman_unencodable() {
4994 let text = "\u{4E2D}"; assert!(utf8_to_mac_roman(text).is_err());
4996 }
4997
4998 #[test]
5000 fn tiny_catalog_node_size_does_not_panic() {
5001 use disk_core::filesystem::Filesystem;
5002 let clean = create_blank_hfs(1474560, 512, "Tiny").unwrap();
5003 let mdb = {
5004 let fs = HfsFilesystem::open(Cursor::new(clean.clone()), 0).unwrap();
5005 fs.mdb.clone()
5006 };
5007 let cat_off = mdb.first_alloc_block as usize * 512
5008 + mdb.catalog_file_extents[0].start_block as usize * mdb.block_size as usize;
5009 for bad in [1u16, 3, 9, 14, 17, 27, 28, 511] {
5010 let mut img = clean.clone();
5011 img[cat_off + 32..cat_off + 34].copy_from_slice(&bad.to_be_bytes());
5012 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5013 let root = fs.root().unwrap();
5014 let listed = fs.list_directory(&root);
5015 assert!(
5016 listed.map(|v| v.is_empty()).unwrap_or(true),
5017 "node size {bad}: expected an empty listing or an error"
5018 );
5019 }
5020 }
5021
5022 #[test]
5027 fn classic_hfs_stamps_are_local_time_and_round_trip_to_utc() {
5028 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5029 use disk_core::times::{local_utc_offset_secs, unix_to_mac_epoch, UnixTimes};
5030 const T: u64 = 1_700_000_000; let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Zone").unwrap();
5032 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5033 let root = fs.root().unwrap();
5034 let opts = CreateFileOptions {
5035 unix_times: Some(UnixTimes::mtime_only(T)),
5036 ..Default::default()
5037 };
5038 fs.create_file(&root, "stamp", &mut &b"x"[..], 1, &opts)
5039 .unwrap();
5040 fs.sync_metadata().unwrap();
5041 let entry = fs
5042 .list_directory(&root)
5043 .unwrap()
5044 .into_iter()
5045 .find(|e| e.name == "stamp")
5046 .unwrap();
5047 assert_eq!(entry.modified_unix, Some(T), "UTC must round-trip");
5048 let raw = entry.mac_dates.expect("raw Mac dates").1;
5049 let expected = (T as i64 + local_utc_offset_secs()) as u64;
5050 assert_eq!(
5051 raw,
5052 unix_to_mac_epoch(expected),
5053 "on disk it is the local wall clock"
5054 );
5055 }
5056
5057 #[test]
5061 fn the_alternate_mdb_follows_the_partition_end_when_the_length_is_known() {
5062 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5063 const VOLUME: u64 = 8 * 1024 * 1024;
5064 const PARTITION: u64 = VOLUME + 64 * 1024;
5065 let mut img = create_blank_hfs(VOLUME, 4096, "Slack").unwrap();
5066 img.resize(PARTITION as usize, 0);
5067 {
5068 let mut fs =
5069 HfsFilesystem::open_sized(Cursor::new(&mut img), 0, Some(PARTITION)).unwrap();
5070 let root = fs.root().unwrap();
5071 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
5072 .unwrap();
5073 fs.sync_metadata().unwrap();
5074 }
5075 let primary = &img[1024..1536];
5076 let alt = &img[PARTITION as usize - 1024..PARTITION as usize - 512];
5077 assert_eq!(
5078 &alt[0..2],
5079 b"BD",
5080 "alternate MDB missing at the partition end"
5081 );
5082 assert_eq!(primary, alt, "the mirror must equal the primary MDB");
5083 }
5084
5085 #[test]
5088 fn create_file_writes_the_finder_flags_it_is_given() {
5089 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5090 let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Flags").unwrap();
5091 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5092 let root = fs.root().unwrap();
5093 let opts = CreateFileOptions {
5094 os_type: Some(*b"APPL"),
5095 os_creator: Some(*b"ttxt"),
5096 finder_flags: Some(0x2000),
5097 ..Default::default()
5098 };
5099 fs.create_file(&root, "app", &mut &b"x"[..], 1, &opts)
5100 .unwrap();
5101 fs.sync_metadata().unwrap();
5102 let entry = fs
5103 .list_directory(&root)
5104 .unwrap()
5105 .into_iter()
5106 .find(|e| e.name == "app")
5107 .unwrap();
5108 assert_eq!(entry.finder_flags, Some(0x2000));
5109 }
5110
5111 #[test]
5112 fn shrink_is_refused_when_data_sits_past_the_new_end() {
5113 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5114 const SIZE: u64 = 8 * 1024 * 1024;
5115 let mut img = create_blank_hfs(SIZE, 4096, "Tail").unwrap();
5116 {
5117 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5118 let root = fs.root().unwrap();
5119 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
5120 .unwrap();
5121 fs.sync_metadata().unwrap();
5122 }
5123 let total = BigEndian::read_u16(&img[1024 + 18..1024 + 20]) as usize;
5124 let vbm_start = BigEndian::read_u16(&img[1024 + 14..1024 + 16]) as usize;
5125 let tail = total - 1;
5127 let byte = vbm_start * 512 + tail / 8;
5128 img[byte] |= 0x80 >> (tail % 8);
5129 let err = resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
5130 .expect_err("a tail block in use must refuse the shrink");
5131 assert!(format!("{err}").contains("last allocated block"), "{err}");
5132 img[byte] &= !(0x80 >> (tail % 8));
5133 resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
5134 .expect("with the tail free the shrink goes through");
5135 }
5136
5137 #[test]
5138 fn test_resize_hfs_rejects_u16_overflow() {
5139 let mut sector = [0u8; 512];
5143 BigEndian::write_u16(&mut sector[0..2], HFS_SIGNATURE);
5144 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];
5156 img[1024..1024 + 512].copy_from_slice(§or);
5157
5158 let mut cursor = Cursor::new(img);
5159 let res = resize_hfs_in_place(
5160 &mut cursor,
5161 0,
5162 2047 * 1024 * 1024, &mut |_| {},
5164 );
5165 let err = res.expect_err("resize should have bailed on u16 overflow");
5166 let msg = format!("{err}");
5167 assert!(
5168 msg.contains("65535") || msg.contains("u16") || msg.contains("Volume Bitmap"),
5169 "unexpected error: {msg}"
5170 );
5171 }
5172
5173 #[test]
5174 fn test_mdb_serialize_roundtrip() {
5175 let mut data = [0u8; 512];
5176 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
5177 BigEndian::write_u16(&mut data[18..20], 100);
5178 BigEndian::write_u32(&mut data[20..24], 4096);
5179 BigEndian::write_u16(&mut data[34..36], 50);
5180 BigEndian::write_u32(&mut data[30..34], 10); BigEndian::write_u32(&mut data[92..96], 42); data[36] = 4;
5183 data[37..41].copy_from_slice(b"Test");
5184
5185 let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
5186 let serialized = mdb.serialize_to_sector();
5187 assert_eq!(BigEndian::read_u16(&serialized[18..20]), 100);
5188 assert_eq!(BigEndian::read_u32(&serialized[30..34]), 10);
5189 assert_eq!(BigEndian::read_u32(&serialized[92..96]), 42);
5190 }
5191
5192 fn make_editable_hfs_image() -> Vec<u8> {
5204 let block_size = 4096u32;
5205 let total_blocks = 128u16;
5206 let first_alloc_block = 5u16; let alloc_start = first_alloc_block as usize * 512;
5208 let image_size = alloc_start + total_blocks as usize * block_size as usize;
5209 let mut img = vec![0u8; image_size];
5210
5211 let catalog_start = 0u16; let catalog_blocks = 8u16;
5213 let catalog_size = catalog_blocks as u32 * block_size;
5214
5215 let bitmap_sector = 3u16;
5217 let bitmap_off = bitmap_sector as usize * 512;
5218 img[bitmap_off] = 0b11111111;
5220
5221 let node_size = 4096usize;
5223 let catalog_offset = alloc_start; let hdr_off = catalog_offset;
5227 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3); let hr = hdr_off + 14;
5232 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;
5244 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;
5251
5252 let leaf_off = catalog_offset + node_size;
5254 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;
5262 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;
5268 let r0_data = r0_off + 8;
5270 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;
5277 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;
5283 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;
5293 BigEndian::write_u16(&mut img[lot - 2..lot], 14); let r1_rel = (r1_off - leaf_off) as u16;
5295 BigEndian::write_u16(&mut img[lot - 4..lot - 2], r1_rel); let free_rel = (r1_data + 46 - leaf_off) as u16;
5297 BigEndian::write_u16(&mut img[lot - 6..lot - 4], free_rel); let mdb_off = 1024;
5301 let mut mdb = [0u8; 512];
5302 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
5303 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);
5307 BigEndian::write_u32(&mut mdb[20..24], block_size);
5308 BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
5309 BigEndian::write_u32(&mut mdb[30..34], 16); BigEndian::write_u16(&mut mdb[34..36], total_blocks - catalog_blocks); mdb[36] = 7;
5313 mdb[37..44].copy_from_slice(b"TestVol");
5314 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);
5318 BigEndian::write_u16(&mut mdb[150..152], catalog_start);
5320 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
5321
5322 img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
5323
5324 img
5325 }
5326
5327 fn make_uninitialized_hfs_image() -> Vec<u8> {
5332 let block_size = 512u32;
5333 let total_blocks = 256u16;
5334 let first_alloc_block = 6u16;
5335 let alloc_start = first_alloc_block as usize * 512;
5336 let image_size = alloc_start + total_blocks as usize * block_size as usize;
5337 let mut img = vec![0u8; image_size];
5338
5339 let extents_start = 0u16;
5341 let extents_blocks = 4u16;
5342 let catalog_start = 4u16;
5344 let catalog_blocks = 4u16;
5345
5346 let mdb_off = 1024usize;
5349 let mut mdb = [0u8; 512];
5350 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
5351 BigEndian::write_u32(&mut mdb[2..6], hfs_common::hfs_now());
5352 BigEndian::write_u32(&mut mdb[6..10], hfs_common::hfs_now());
5353 BigEndian::write_u16(&mut mdb[14..16], 3); BigEndian::write_u16(&mut mdb[18..20], total_blocks);
5355 BigEndian::write_u32(&mut mdb[20..24], block_size);
5356 BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
5357 BigEndian::write_u32(&mut mdb[30..34], 16);
5358 BigEndian::write_u16(
5359 &mut mdb[34..36],
5360 total_blocks - extents_blocks - catalog_blocks,
5361 );
5362 mdb[36] = 5;
5364 mdb[37..42].copy_from_slice(b"Blank");
5365 BigEndian::write_u32(&mut mdb[130..134], extents_blocks as u32 * block_size);
5367 BigEndian::write_u16(&mut mdb[134..136], extents_start);
5368 BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
5369 BigEndian::write_u32(&mut mdb[146..150], catalog_blocks as u32 * block_size);
5371 BigEndian::write_u16(&mut mdb[150..152], catalog_start);
5372 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
5373
5374 img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
5375 img
5376 }
5377
5378 #[test]
5379 fn test_hfs_empty_volume_auto_initialize_and_create_file() {
5380 let img = make_uninitialized_hfs_image();
5383 let cursor = Cursor::new(img);
5384 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5385 assert_eq!(fs.fs_type(), "HFS");
5386
5387 let root = fs.root().unwrap();
5389 let entries = fs.list_directory(&root).unwrap();
5390 assert!(entries.is_empty());
5391
5392 let data = b"auto-init test";
5394 let mut cursor = Cursor::new(data.as_slice());
5395 let fe = fs
5396 .create_file(
5397 &root,
5398 "hello.txt",
5399 &mut cursor,
5400 data.len() as u64,
5401 &CreateFileOptions::default(),
5402 )
5403 .unwrap();
5404 assert_eq!(fe.name, "hello.txt");
5405 fs.sync_metadata().unwrap();
5406
5407 let entries = fs.list_directory(&root).unwrap();
5409 assert_eq!(entries.len(), 1);
5410 assert_eq!(entries[0].name, "hello.txt");
5411 let read_back = fs.read_file(&fe, 1024).unwrap();
5412 assert_eq!(&read_back, data);
5413
5414 let result = fs.fsck().unwrap();
5416 let err_msgs: Vec<_> = result.errors.iter().map(|e| e.message.clone()).collect();
5417 let warn_msgs: Vec<_> = result.warnings.iter().map(|w| w.message.clone()).collect();
5418 assert!(
5419 result.errors.is_empty(),
5420 "unexpected fsck errors: {err_msgs:?}"
5421 );
5422 assert!(
5423 result.warnings.is_empty(),
5424 "unexpected fsck warnings: {warn_msgs:?}"
5425 );
5426 }
5427
5428 #[test]
5429 fn test_hfs_editable_open_and_free_space() {
5430 let img = make_editable_hfs_image();
5431 let cursor = Cursor::new(img);
5432 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5433 assert_eq!(fs.fs_type(), "HFS");
5434 assert_eq!(fs.volume_label(), Some("TestVol"));
5435 let free = fs.free_space().unwrap();
5436 assert_eq!(free, 120 * 4096);
5438 }
5439
5440 #[test]
5441 fn test_hfs_editable_create_file_and_read() {
5442 let img = make_editable_hfs_image();
5443 let cursor = Cursor::new(img);
5444 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5445
5446 let root = fs.root().unwrap();
5447 let test_data = b"Hello, HFS World!";
5448 let mut data_reader = Cursor::new(test_data.as_slice());
5449 let options = CreateFileOptions::default();
5450 let fe = fs
5451 .create_file(
5452 &root,
5453 "test.txt",
5454 &mut data_reader,
5455 test_data.len() as u64,
5456 &options,
5457 )
5458 .unwrap();
5459
5460 assert_eq!(fe.name, "test.txt");
5461 assert_eq!(fe.size, test_data.len() as u64);
5462
5463 let entries = fs.list_directory(&root).unwrap();
5465 assert!(entries.iter().any(|e| e.name == "test.txt"));
5466
5467 let read_back = fs.read_file(&fe, 1024).unwrap();
5469 assert_eq!(&read_back, test_data);
5470 }
5471
5472 #[test]
5480 fn test_hfs_file_record_first_alloc_block_fields_are_zero() {
5481 use crate::hfs_common::walk_leaf_records;
5482 use disk_core::filesystem::ResourceForkSource;
5483
5484 let img = make_editable_hfs_image();
5485 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5486 let root = fs.root().unwrap();
5487
5488 let data = b"data fork contents that span a block";
5491 let options = CreateFileOptions {
5492 resource_fork: Some(ResourceForkSource::Data(b"resource fork bytes".to_vec())),
5493 ..Default::default()
5494 };
5495 fs.create_file(
5496 &root,
5497 "forked.bin",
5498 &mut Cursor::new(data.as_slice()),
5499 data.len() as u64,
5500 &options,
5501 )
5502 .unwrap();
5503 fs.sync_metadata().unwrap();
5504
5505 let node_size = BigEndian::read_u16(&fs.catalog_data[32..34]) as usize;
5506 let first_leaf = BigEndian::read_u32(&fs.catalog_data[24..28]);
5507 let found = walk_leaf_records::<bool, _>(
5508 &fs.catalog_data,
5509 first_leaf,
5510 node_size,
5511 |_n, _i, _off, rec| {
5512 let klen = rec[0] as usize;
5513 let mut ds = 1 + klen;
5514 if !ds.is_multiple_of(2) {
5515 ds += 1;
5516 }
5517 let body = &rec[ds..];
5518 if body.first() != Some(&2) {
5519 return None; }
5521 let fil_st_blk = BigEndian::read_u16(&body[24..26]);
5522 let fil_rst_blk = BigEndian::read_u16(&body[34..36]);
5523 let data_ext_start = BigEndian::read_u16(&body[74..76]);
5524 let rsrc_ext_start = BigEndian::read_u16(&body[86..88]);
5525 assert_eq!(fil_st_blk, 0, "filStBlk (offset 24) must be 0 on disk");
5526 assert_eq!(fil_rst_blk, 0, "filRStBlk (offset 34) must be 0 on disk");
5527 assert_ne!(data_ext_start, 0, "data fork extent should be allocated");
5529 assert_ne!(rsrc_ext_start, 0, "rsrc fork extent should be allocated");
5530 Some(true)
5531 },
5532 );
5533 assert_eq!(found, Some(true), "file record not found in catalog");
5534 }
5535
5536 #[test]
5541 fn test_hfs_catalog_dates_round_trip() {
5542 let img = make_editable_hfs_image();
5543 let mut buf = img.clone();
5544
5545 {
5547 let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
5548 let root = fs.root().unwrap();
5549 let data = b"x";
5550 let fe = fs
5551 .create_file(
5552 &root,
5553 "dated.txt",
5554 &mut Cursor::new(data.as_slice()),
5555 1,
5556 &CreateFileOptions::default(),
5557 )
5558 .unwrap();
5559 fs.sync_metadata().unwrap();
5560
5561 let listed = fs.list_directory(&root).unwrap();
5562 let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
5563 let (c, m, b) = f.mac_dates.expect("HFS file should carry mac_dates");
5564 assert!(c != 0 && m != 0, "create/modify stamped by create_file");
5565 assert_eq!(b, 0, "backup date defaults to 0");
5566
5567 let target = (0x10000000u32, 0x20000000u32, 0x30000000u32);
5569 fs.set_dates(&fe, target.0, target.1, target.2).unwrap();
5570 fs.sync_metadata().unwrap();
5571 }
5572
5573 let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
5576 let root = fs.root().unwrap();
5577 let listed = fs.list_directory(&root).unwrap();
5578 let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
5579 assert_eq!(f.mac_dates, Some((0x10000000, 0x20000000, 0x30000000)));
5580 assert_eq!(f.modified, hfs_common::format_mac_date(0x20000000));
5581 }
5582
5583 #[test]
5584 fn test_hfs_editable_create_directory() {
5585 let img = make_editable_hfs_image();
5586 let cursor = Cursor::new(img);
5587 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5588
5589 let root = fs.root().unwrap();
5590 let options = CreateDirectoryOptions::default();
5591 let dir = fs.create_directory(&root, "NewDir", &options).unwrap();
5592
5593 assert_eq!(dir.name, "NewDir");
5594 assert!(dir.is_directory());
5595
5596 let entries = fs.list_directory(&root).unwrap();
5597 assert!(entries
5598 .iter()
5599 .any(|e| e.name == "NewDir" && e.is_directory()));
5600 }
5601
5602 #[test]
5603 fn test_hfs_editable_rename_file() {
5604 let img = make_editable_hfs_image();
5605 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5606 let root = fs.root().unwrap();
5607
5608 let data = b"keep my bytes across a rename";
5609 let mut r = Cursor::new(data.as_slice());
5610 let fe = fs
5611 .create_file(
5612 &root,
5613 "Before",
5614 &mut r,
5615 data.len() as u64,
5616 &CreateFileOptions::default(),
5617 )
5618 .unwrap();
5619 let cnid = fe.location;
5620
5621 fs.rename(&root, &fe, "After Doc").unwrap();
5622
5623 let entries = fs.list_directory(&root).unwrap();
5624 assert!(entries.iter().any(|e| e.name == "After Doc"), "{entries:?}");
5625 assert!(!entries.iter().any(|e| e.name == "Before"), "{entries:?}");
5626 let renamed = entries.iter().find(|e| e.name == "After Doc").unwrap();
5627 assert_eq!(renamed.location, cnid);
5630 assert_eq!(&fs.read_file(renamed, 1024).unwrap(), data);
5631
5632 fs.create_directory(&root, "OldDir", &CreateDirectoryOptions::default())
5635 .unwrap();
5636 let old_dir = fs
5637 .list_directory(&root)
5638 .unwrap()
5639 .into_iter()
5640 .find(|e| e.name == "OldDir")
5641 .unwrap();
5642 fs.rename(&root, &old_dir, "NewDir").unwrap();
5643 let new_dir = fs
5644 .list_directory(&root)
5645 .unwrap()
5646 .into_iter()
5647 .find(|e| e.name == "NewDir")
5648 .expect("renamed dir present");
5649 assert!(fs.list_directory(&new_dir).is_ok());
5650
5651 let after = fs
5653 .list_directory(&root)
5654 .unwrap()
5655 .into_iter()
5656 .find(|e| e.name == "After Doc")
5657 .unwrap();
5658 assert!(matches!(
5659 fs.rename(&root, &after, "NewDir"),
5660 Err(FilesystemError::AlreadyExists(_))
5661 ));
5662
5663 fs.sync_metadata().unwrap();
5664 }
5665
5666 #[test]
5667 fn test_hfs_editable_delete_file() {
5668 let img = make_editable_hfs_image();
5669 let cursor = Cursor::new(img);
5670 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5671
5672 let root = fs.root().unwrap();
5673 let test_data = b"delete me";
5674 let mut data_reader = Cursor::new(test_data.as_slice());
5675 let options = CreateFileOptions::default();
5676 let fe = fs
5677 .create_file(
5678 &root,
5679 "gone.txt",
5680 &mut data_reader,
5681 test_data.len() as u64,
5682 &options,
5683 )
5684 .unwrap();
5685
5686 let free_before = fs.free_space().unwrap();
5687 fs.delete_entry(&root, &fe).unwrap();
5688
5689 let entries = fs.list_directory(&root).unwrap();
5690 assert!(!entries.iter().any(|e| e.name == "gone.txt"));
5691
5692 let free_after = fs.free_space().unwrap();
5693 assert!(free_after > free_before);
5694 }
5695
5696 #[test]
5697 fn test_hfs_editable_duplicate_name_rejected() {
5698 let img = make_editable_hfs_image();
5699 let cursor = Cursor::new(img);
5700 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5701
5702 let root = fs.root().unwrap();
5703 let test_data = b"first";
5704 let mut r1 = Cursor::new(test_data.as_slice());
5705 let options = CreateFileOptions::default();
5706 fs.create_file(&root, "dup.txt", &mut r1, 5, &options)
5707 .unwrap();
5708
5709 let mut r2 = Cursor::new(test_data.as_slice());
5710 let result = fs.create_file(&root, "dup.txt", &mut r2, 5, &options);
5711 assert!(matches!(result, Err(FilesystemError::AlreadyExists(_))));
5712 }
5713
5714 #[test]
5715 fn test_hfs_editable_name_too_long() {
5716 let img = make_editable_hfs_image();
5717 let cursor = Cursor::new(img);
5718 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5719
5720 let root = fs.root().unwrap();
5721 let long_name = "a".repeat(32); let test_data = b"x";
5723 let mut r = Cursor::new(test_data.as_slice());
5724 let options = CreateFileOptions::default();
5725 let result = fs.create_file(&root, &long_name, &mut r, 1, &options);
5726 assert!(matches!(result, Err(FilesystemError::InvalidData(_))));
5727 }
5728
5729 #[test]
5730 fn test_hfs_editable_blessed_folder() {
5731 let img = make_editable_hfs_image();
5732 let cursor = Cursor::new(img);
5733 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5734
5735 assert!(fs.blessed_system_folder().is_none());
5737
5738 let root = fs.root().unwrap();
5740 let options = CreateDirectoryOptions::default();
5741 let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
5742
5743 fs.set_blessed_folder(&sys_dir).unwrap();
5745
5746 let blessed = fs.blessed_system_folder();
5748 assert!(blessed.is_some());
5749 let (cnid, name) = blessed.unwrap();
5750 assert_eq!(cnid, sys_dir.location);
5751 assert_eq!(name, "System");
5752 }
5753
5754 #[test]
5755 fn test_hfs_editable_type_creator_auto_detect() {
5756 let img = make_editable_hfs_image();
5757 let cursor = Cursor::new(img);
5758 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5759
5760 let root = fs.root().unwrap();
5761 let test_data = b"text content";
5762 let mut data_reader = Cursor::new(test_data.as_slice());
5763 let options = CreateFileOptions::default();
5764 let fe = fs
5765 .create_file(
5766 &root,
5767 "readme.txt",
5768 &mut data_reader,
5769 test_data.len() as u64,
5770 &options,
5771 )
5772 .unwrap();
5773
5774 assert!(fe.type_code.is_some());
5775 assert_eq!(fe.type_code, Some(*b"TEXT"));
5776 }
5777
5778 fn assert_fsck_clean<R: Read + Seek>(fs: &mut HfsFilesystem<R>) {
5780 let result = fs.fsck().unwrap();
5781 if !result.is_clean() {
5782 let msgs: Vec<_> = result
5783 .errors
5784 .iter()
5785 .map(|e| format!("[{}] {}", e.code, e.message))
5786 .collect();
5787 panic!("fsck found {} error(s):\n{}", msgs.len(), msgs.join("\n"));
5788 }
5789 }
5790
5791 #[test]
5792 fn test_fsck_clean_fresh_image() {
5793 let img = make_editable_hfs_image();
5794 let cursor = Cursor::new(img);
5795 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5796
5797 let result = fs.fsck().unwrap();
5798 let err_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
5799 assert!(
5800 result.is_clean(),
5801 "fresh image should be clean: {:?}",
5802 err_msgs
5803 );
5804 assert_eq!(result.stats.directories_checked, 1); assert_eq!(result.stats.files_checked, 0);
5806 }
5807
5808 #[test]
5809 fn test_fsck_clean_after_create_file() {
5810 let img = make_editable_hfs_image();
5811 let cursor = Cursor::new(img);
5812 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5813
5814 let root = fs.root().unwrap();
5815 let test_data = b"Hello, HFS World!";
5816 let mut data_reader = Cursor::new(test_data.as_slice());
5817 let options = CreateFileOptions::default();
5818 fs.create_file(
5819 &root,
5820 "test.txt",
5821 &mut data_reader,
5822 test_data.len() as u64,
5823 &options,
5824 )
5825 .unwrap();
5826
5827 assert_fsck_clean(&mut fs);
5828 }
5829
5830 #[test]
5831 fn test_fsck_clean_after_create_directory() {
5832 let img = make_editable_hfs_image();
5833 let cursor = Cursor::new(img);
5834 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5835
5836 let root = fs.root().unwrap();
5837 let options = CreateDirectoryOptions::default();
5838 fs.create_directory(&root, "NewDir", &options).unwrap();
5839
5840 assert_fsck_clean(&mut fs);
5841 }
5842
5843 #[test]
5844 fn test_fsck_clean_after_delete_file() {
5845 let img = make_editable_hfs_image();
5846 let cursor = Cursor::new(img);
5847 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5848
5849 let root = fs.root().unwrap();
5850 let test_data = b"delete me";
5851 let mut data_reader = Cursor::new(test_data.as_slice());
5852 let options = CreateFileOptions::default();
5853 let fe = fs
5854 .create_file(
5855 &root,
5856 "gone.txt",
5857 &mut data_reader,
5858 test_data.len() as u64,
5859 &options,
5860 )
5861 .unwrap();
5862
5863 assert_fsck_clean(&mut fs);
5864 fs.delete_entry(&root, &fe).unwrap();
5865 assert_fsck_clean(&mut fs);
5866 }
5867
5868 #[test]
5877 fn test_hfs_random_insert_packs_densely_and_fsck_clean() {
5878 use crate::hfs_common::BTreeHeader;
5879 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
5880 let block_size = 32 * 1024u32;
5881 let img = create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Pack", 0, 16 * 1024 * 1024)
5882 .unwrap();
5883 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
5884 let root_id = fs.root().unwrap().location as u32;
5885
5886 let n = 16000u32;
5888 let mut seen = std::collections::HashSet::new();
5889 let order: Vec<u32> = (0..n)
5890 .map(|i| i.wrapping_mul(2654435761) % n)
5891 .filter(|x| seen.insert(*x))
5892 .collect();
5893 let count = order.len() as u32;
5894 fs.begin_bulk();
5895 for &i in &order {
5896 let name = format!("f{:05}", i);
5897 let mut kr = Fs::build_catalog_key(root_id, name.as_bytes());
5898 kr.extend_from_slice(&Fs::build_file_record(
5899 16 + i,
5900 0,
5901 0,
5902 0,
5903 0,
5904 0,
5905 0,
5906 &[0u8; 4],
5907 &[0u8; 4],
5908 block_size,
5909 None,
5910 0,
5911 ));
5912 fs.insert_catalog_record(&kr).unwrap();
5913 }
5914 fs.end_bulk();
5915 fs.mdb.file_count = count;
5916 fs.mdb.next_catalog_id = 16 + n;
5917 fs.update_parent_valence(root_id, count as i32).unwrap();
5918
5919 let h = BTreeHeader::read(fs.catalog_data());
5922 let used = h.total_nodes - h.free_nodes;
5923 let rec_per_node = count as f64 / used as f64;
5924 assert!(
5925 rec_per_node > 2.8,
5926 "random-order packing too sparse: {count} records in {used} nodes ({rec_per_node:.2}/node)"
5927 );
5928
5929 let result = fs.fsck().unwrap();
5930 assert!(
5931 result.errors.is_empty(),
5932 "fsck errors after random import: {:?}",
5933 result
5934 .errors
5935 .iter()
5936 .take(8)
5937 .map(|e| &e.message)
5938 .collect::<Vec<_>>()
5939 );
5940 }
5941
5942 #[test]
5949 fn test_hfs_bulk_mode_import_fsck_clean() {
5950 let block_size = 32 * 1024u32;
5951 let img = create_blank_hfs_sized(
5952 64 * 1024 * 1024,
5953 block_size,
5954 "BulkImport",
5955 0,
5956 8 * 1024 * 1024,
5957 )
5958 .expect("create blank volume");
5959 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5960 let root = fs.root().unwrap();
5961 let root_id = root.location as u32;
5962 let options = CreateFileOptions::default();
5963
5964 fs.begin_bulk();
5965 for i in 0..9000 {
5966 let name = format!("f{:05}.txt", i);
5967 let mut empty = Cursor::new(Vec::new());
5968 fs.create_file(&root, &name, &mut empty, 0, &options)
5969 .unwrap_or_else(|e| panic!("bulk create_file #{i}: {e}"));
5970 }
5971 fs.end_bulk();
5972
5973 assert_eq!(fs.mdb.file_count, 9000);
5974 let result = fs.fsck().unwrap();
5975 assert!(
5976 result.errors.is_empty(),
5977 "bulk-mode import left fsck errors: {:?}",
5978 result
5979 .errors
5980 .iter()
5981 .take(8)
5982 .map(|e| &e.message)
5983 .collect::<Vec<_>>()
5984 );
5985 assert!(fs
5987 .find_catalog_record_by_name(root_id, b"f04500.txt")
5988 .is_some());
5989 assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
5990
5991 let mut empty = Cursor::new(Vec::new());
5994 assert!(fs
5995 .create_file(&root, "f00000.txt", &mut empty, 0, &options)
5996 .is_err());
5997 assert_fsck_clean(&mut fs);
5998 }
5999
6000 #[test]
6015 fn test_hfs_catalog_scales_past_header_bitmap_fsck_clean() {
6016 use crate::hfs_common::BTreeHeader;
6017 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6018 let block_size = 32 * 1024u32;
6019 let img = create_blank_hfs_sized(
6023 64 * 1024 * 1024,
6024 block_size,
6025 "Scale20k",
6026 0,
6027 10 * 1024 * 1024,
6028 )
6029 .expect("create blank volume");
6030 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6031 let root = fs.root().unwrap();
6032 let root_id = root.location as u32;
6033
6034 let n: u32 = 22_000;
6035 for i in 0..n {
6036 let name = format!("f{:05}.txt", i);
6037 let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
6038 let file_rec = Fs::build_file_record(
6039 16 + i,
6040 0,
6041 0,
6042 0,
6043 0,
6044 0,
6045 0,
6046 &[0u8; 4],
6047 &[0u8; 4],
6048 block_size,
6049 None,
6050 0,
6051 );
6052 key_record.extend_from_slice(&file_rec);
6053 fs.insert_catalog_record(&key_record)
6054 .unwrap_or_else(|e| panic!("insert #{i} ({name}): {e}"));
6055 }
6056
6057 fs.mdb.file_count = n;
6060 fs.mdb.next_catalog_id = 16 + n;
6061 fs.update_parent_valence(root_id, n as i32).unwrap();
6062
6063 let header = BTreeHeader::read(fs.catalog_data());
6066 assert!(
6067 header.depth >= 3,
6068 "expected a multi-level index, depth = {}",
6069 header.depth
6070 );
6071 let used = header.total_nodes - header.free_nodes;
6072 assert!(
6073 used > 2048,
6074 "expected > 2048 catalog nodes in use, got {used}"
6075 );
6076
6077 let result = fs.fsck().unwrap();
6078 assert!(
6079 result.errors.is_empty(),
6080 "fsck found {} errors after {n}-record import: {:?}",
6081 result.errors.len(),
6082 result
6083 .errors
6084 .iter()
6085 .take(8)
6086 .map(|e| &e.message)
6087 .collect::<Vec<_>>()
6088 );
6089
6090 for i in [0u32, 1, 7386, 12345, n - 1] {
6094 let name = format!("f{:05}.txt", i);
6095 assert!(
6096 fs.find_catalog_record_by_name(root_id, name.as_bytes())
6097 .is_some(),
6098 "record {name} not findable after bulk insert"
6099 );
6100 }
6101 }
6102
6103 #[test]
6113 fn test_hfs_incremental_shuffled_multidir_packs_dense_fsck_clean() {
6114 use crate::hfs_common::BTreeHeader;
6115 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6116 let block_size = 32 * 1024u32;
6117 let img = create_blank_hfs_sized(
6121 128 * 1024 * 1024,
6122 block_size,
6123 "PutShuffle",
6124 0,
6125 6 * 1024 * 1024,
6126 )
6127 .expect("create blank volume");
6128 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6129
6130 const DIRS: u32 = 200;
6132 let root = fs.root().unwrap();
6133 let mut dir_ids = Vec::with_capacity(DIRS as usize);
6134 for d in 0..DIRS {
6135 let name = format!("dir{d:03}");
6136 let de = fs
6137 .create_directory(&root, &name, &Default::default())
6138 .unwrap();
6139 dir_ids.push(de.location as u32);
6140 }
6141 let base_cnid = fs.mdb.next_catalog_id;
6142
6143 let n: u32 = 20_000;
6147 let mut per_dir = vec![0i32; DIRS as usize];
6148 for i in 0..n {
6149 let hash = i.wrapping_mul(2_654_435_761);
6150 let d = (hash % DIRS) as usize;
6151 let name = format!("f{hash:08x}");
6152 let mut kr = Fs::build_catalog_key(dir_ids[d], name.as_bytes());
6153 kr.extend_from_slice(&Fs::build_file_record(
6154 base_cnid + i,
6155 0,
6156 0,
6157 0,
6158 0,
6159 0,
6160 0,
6161 &[0u8; 4],
6162 &[0u8; 4],
6163 block_size,
6164 None,
6165 0,
6166 ));
6167 fs.insert_catalog_record(&kr)
6168 .unwrap_or_else(|e| panic!("insert #{i} into dir{d}: {e}"));
6169 per_dir[d] += 1;
6170 }
6171 fs.mdb.file_count += n;
6174 fs.mdb.next_catalog_id = base_cnid + n;
6175 for (d, &cnt) in per_dir.iter().enumerate() {
6176 fs.update_parent_valence(dir_ids[d], cnt).unwrap();
6177 }
6178
6179 let h = BTreeHeader::read(fs.catalog_data());
6183 let used = h.total_nodes - h.free_nodes;
6184 let rec_per_node = h.leaf_records as f64 / used as f64;
6185 assert!(
6186 rec_per_node > 2.6,
6187 "shuffled multi-dir packing too sparse: {} records in {used} nodes ({rec_per_node:.2}/node)",
6188 h.leaf_records
6189 );
6190
6191 let result = fs.fsck().unwrap();
6193 assert!(
6194 result.errors.is_empty(),
6195 "fsck found {} errors after 20k shuffled multi-dir put: {:?}",
6196 result.errors.len(),
6197 result
6198 .errors
6199 .iter()
6200 .take(8)
6201 .map(|e| &e.message)
6202 .collect::<Vec<_>>()
6203 );
6204
6205 for i in [0u32, 7, 9_999, n - 1] {
6207 let hash = i.wrapping_mul(2_654_435_761);
6208 let parent = dir_ids[(hash % DIRS) as usize];
6209 let name = format!("f{hash:08x}");
6210 assert!(
6211 fs.find_catalog_record_by_name(parent, name.as_bytes())
6212 .is_some(),
6213 "record {name} not findable after shuffled multi-dir insert"
6214 );
6215 }
6216 }
6217
6218 #[test]
6223 fn test_rebuild_index_nodes_past_header_bitmap_fsck_clean() {
6224 use crate::hfs_common::BTreeHeader;
6225 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6226 let block_size = 32 * 1024u32;
6227 let img =
6228 create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Rebuild", 0, 10 * 1024 * 1024)
6229 .expect("create blank volume");
6230 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6231 let root_id = fs.root().unwrap().location as u32;
6232
6233 let n: u32 = 12_000;
6234 for i in 0..n {
6235 let name = format!("f{:05}.txt", i);
6236 let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
6237 let file_rec = Fs::build_file_record(
6238 16 + i,
6239 0,
6240 0,
6241 0,
6242 0,
6243 0,
6244 0,
6245 &[0u8; 4],
6246 &[0u8; 4],
6247 block_size,
6248 None,
6249 0,
6250 );
6251 key_record.extend_from_slice(&file_rec);
6252 fs.insert_catalog_record(&key_record).unwrap();
6253 }
6254 fs.mdb.file_count = n;
6255 fs.mdb.next_catalog_id = 16 + n;
6256 fs.update_parent_valence(root_id, n as i32).unwrap();
6257
6258 let node_size = BTreeHeader::read(fs.catalog_data()).node_size as usize;
6259 let used_before = {
6260 let h = BTreeHeader::read(fs.catalog_data());
6261 h.total_nodes - h.free_nodes
6262 };
6263 assert!(
6264 used_before > 2048,
6265 "test needs > 2048 nodes, got {used_before}"
6266 );
6267
6268 let mut report = disk_core::fsck::RepairReport {
6270 fixes_applied: Vec::new(),
6271 fixes_failed: Vec::new(),
6272 unrepairable_count: 0,
6273 };
6274 crate::hfs_fsck::rebuild_index_nodes(&mut fs.catalog_data, node_size, &mut report);
6275 assert!(
6276 report.fixes_failed.is_empty(),
6277 "rebuild reported failures: {:?}",
6278 report.fixes_failed
6279 );
6280
6281 let result = fs.fsck().unwrap();
6282 assert!(
6283 result.errors.is_empty(),
6284 "fsck after rebuild: {:?}",
6285 result
6286 .errors
6287 .iter()
6288 .take(8)
6289 .map(|e| &e.message)
6290 .collect::<Vec<_>>()
6291 );
6292 }
6293
6294 #[test]
6300 fn test_hfs_create_file_multilevel_index_fsck_clean() {
6301 use crate::hfs_common::BTreeHeader;
6302 let block_size = 32 * 1024u32;
6303 let img =
6304 create_blank_hfs_sized(32 * 1024 * 1024, block_size, "MultiIdx", 0, 4 * 1024 * 1024)
6305 .expect("create blank volume");
6306 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6307 let root = fs.root().unwrap();
6308 let options = CreateFileOptions::default();
6309 for i in 0..2000 {
6310 let name = format!("f{:05}.txt", i);
6311 let mut empty = Cursor::new(Vec::new());
6312 fs.create_file(&root, &name, &mut empty, 0, &options)
6313 .unwrap_or_else(|e| panic!("create_file #{i}: {e}"));
6314 }
6315 let header = BTreeHeader::read(fs.catalog_data());
6316 assert!(
6317 header.depth >= 2,
6318 "expected an index level, depth = {}",
6319 header.depth
6320 );
6321 let result = fs.fsck().unwrap();
6322 assert!(
6323 result.errors.is_empty(),
6324 "fsck errors: {:?}",
6325 result
6326 .errors
6327 .iter()
6328 .take(8)
6329 .map(|e| &e.message)
6330 .collect::<Vec<_>>()
6331 );
6332 assert_eq!(fs.mdb.file_count, 2000);
6333 }
6334
6335 #[test]
6341 fn deleting_a_leafs_first_record_refreshes_the_separators() {
6342 use crate::hfs_common::{
6345 btree_record_range, btree_stale_index_keys, BTreeHeader, BTreeKeyFormat,
6346 };
6347 let block_size = 32 * 1024u32;
6348 let img = create_blank_hfs_sized(32 * 1024 * 1024, block_size, "IKey", 0, 4 * 1024 * 1024)
6349 .expect("create blank volume");
6350 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6351 let root = fs.root().unwrap();
6352 let options = CreateFileOptions::default();
6353 for i in 0..2000 {
6354 let mut empty = Cursor::new(Vec::new());
6355 fs.create_file(&root, &format!("f{i:05}.txt"), &mut empty, 0, &options)
6356 .unwrap();
6357 }
6358 let node_size = BigEndian::read_u16(&fs.catalog_data()[32..34]) as usize;
6359 let header = BTreeHeader::read(fs.catalog_data());
6360 assert!(
6361 header.depth >= 2,
6362 "need an index level, depth {}",
6363 header.depth
6364 );
6365
6366 let first = header.first_leaf_node as usize;
6367 let second =
6368 BigEndian::read_u32(&fs.catalog_data()[first * node_size..first * node_size + 4])
6369 as usize;
6370 let name = {
6371 let node = &fs.catalog_data()[second * node_size..(second + 1) * node_size];
6372 let (s, e) = btree_record_range(node, node_size, 0);
6373 let rec = &node[s..e];
6374 let name_len = rec[6] as usize;
6375 String::from_utf8_lossy(&rec[7..7 + name_len]).into_owned()
6376 };
6377 assert!(
6378 name.starts_with('f'),
6379 "expected a file record first, got {name:?}"
6380 );
6381 let victim = fs
6382 .list_directory(&root)
6383 .unwrap()
6384 .into_iter()
6385 .find(|e| e.name == name)
6386 .expect("victim listed");
6387 fs.delete_entry(&root, &victim).unwrap();
6388
6389 let stale = btree_stale_index_keys(
6390 fs.catalog_data(),
6391 node_size,
6392 &BTreeKeyFormat::CLASSIC_CATALOG,
6393 &HfsFilesystem::<Cursor<Vec<u8>>>::catalog_compare,
6394 );
6395 assert!(stale.is_empty(), "separators still stale: {stale:?}");
6396 let result = fs.fsck().unwrap();
6397 assert!(
6398 result.errors.is_empty(),
6399 "{:?}",
6400 result
6401 .errors
6402 .iter()
6403 .map(|e| &e.message)
6404 .take(4)
6405 .collect::<Vec<_>>()
6406 );
6407 }
6408
6409 #[test]
6410 fn test_hfs_control_char_filename_roundtrip() {
6411 let img = make_editable_hfs_image();
6412 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6413 let root = fs.root().unwrap();
6414 let options = CreateFileOptions::default();
6415 let name = "IPNetRouter\u{7f}\u{7f}_154_68k";
6416 let mut data = Cursor::new(b"x".to_vec());
6417 fs.create_file(&root, name, &mut data, 1, &options)
6418 .expect("create_file with raw 0x7F bytes in the name");
6419 let entries = fs.list_directory(&root).unwrap();
6420 assert!(
6421 entries.iter().any(|e| e.name == name),
6422 "0x7F-laden name not preserved; got {:?}",
6423 entries.iter().map(|e| &e.name).collect::<Vec<_>>()
6424 );
6425 assert_fsck_clean(&mut fs);
6426 }
6427
6428 #[test]
6433 fn test_hfs_null_byte_name_is_warning_not_error() {
6434 let img = make_editable_hfs_image();
6435 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6436 let root = fs.root().unwrap();
6437 let options = CreateFileOptions::default();
6438 let mut data = Cursor::new(b"x".to_vec());
6439 fs.create_file(&root, "\u{0}Sorts First", &mut data, 1, &options)
6440 .expect("create_file with a leading null byte in the name");
6441 let result = fs.fsck().unwrap();
6442 assert!(
6443 result.errors.is_empty(),
6444 "a null-byte name must not be a fsck error: {:?}",
6445 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6446 );
6447 assert!(
6448 result
6449 .warnings
6450 .iter()
6451 .any(|w| w.code == "UnusualCatalogName"),
6452 "expected an UnusualCatalogName warning for the null-byte name"
6453 );
6454 }
6455
6456 #[test]
6457 fn test_fsck_stress_many_files() {
6458 let img = make_editable_hfs_image();
6459 let cursor = Cursor::new(img);
6460 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6461
6462 let root = fs.root().unwrap();
6463 let options = CreateFileOptions::default();
6464
6465 for i in 0..35 {
6467 let name = format!("file_{:03}.txt", i);
6468 let data = format!("content of file {}", i);
6469 let mut reader = Cursor::new(data.as_bytes().to_vec());
6470 fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
6471 .unwrap();
6472
6473 assert_fsck_clean(&mut fs);
6475 }
6476
6477 let result = fs.fsck().unwrap();
6478 assert!(result.is_clean());
6479 assert_eq!(result.stats.files_checked, 35);
6480 assert_eq!(result.stats.directories_checked, 1); }
6482
6483 #[test]
6484 fn test_fsck_clean_after_blessed_folder() {
6485 let img = make_editable_hfs_image();
6486 let cursor = Cursor::new(img);
6487 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6488
6489 let root = fs.root().unwrap();
6490 let options = CreateDirectoryOptions::default();
6491 let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
6492 fs.set_blessed_folder(&sys_dir).unwrap();
6493
6494 assert_fsck_clean(&mut fs);
6495 }
6496
6497 #[test]
6502 fn unerased_free_nodes_warn_once_and_repair_zeroes_them() {
6503 use crate::hfs_common::{btree_bitmap_test, BTreeHeader};
6505
6506 let mut fs = HfsFilesystem::open(Cursor::new(make_editable_hfs_image()), 0).unwrap();
6507 let node_size = BTreeHeader::read(&fs.catalog_data).node_size as usize;
6508 let free: Vec<usize> = (1..fs.catalog_data.len() / node_size)
6509 .filter(|&i| !btree_bitmap_test(&fs.catalog_data, node_size, i as u32))
6510 .take(3)
6511 .collect();
6512 assert_eq!(free.len(), 3, "blank catalog has free nodes");
6513 for &i in &free {
6514 fs.catalog_data[i * node_size + 20] = 0xA5;
6515 }
6516
6517 let result = fs.fsck().unwrap();
6518 assert!(
6519 result.errors.is_empty(),
6520 "not an error: {:?}",
6521 result.errors
6522 );
6523 let warns: Vec<_> = result
6524 .warnings
6525 .iter()
6526 .filter(|w| w.code == "FreeNodeNotErased")
6527 .collect();
6528 assert_eq!(warns.len(), 1, "one summary per tree, not one per node");
6529 assert!(
6530 warns[0].message.contains("3 unused node(s)"),
6531 "{}",
6532 warns[0].message
6533 );
6534 assert!(result.repairable);
6535
6536 let report = fs.repair().unwrap();
6537 assert!(
6538 report
6539 .fixes_applied
6540 .iter()
6541 .any(|f| f == "Erased 3 unused catalog B-tree node(s)"),
6542 "{:?}",
6543 report.fixes_applied
6544 );
6545 let after = fs.fsck().unwrap();
6546 assert!(after.warnings.iter().all(|w| w.code != "FreeNodeNotErased"));
6547 assert!(after.errors.is_empty(), "{:?}", after.errors);
6548 }
6549
6550 #[test]
6551 fn test_repair_index_records_are_even_aligned() {
6552 use crate::hfs_common::{btree_record_range, BTreeHeader};
6553
6554 let img = make_editable_hfs_image();
6555 let cursor = Cursor::new(img);
6556 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6557 let root = fs.root().unwrap();
6558 let options = CreateFileOptions::default();
6559
6560 for i in 0..40 {
6563 let name = if i % 2 == 0 {
6564 format!("f{:03}.txt", i) } else {
6566 format!("fi{:03}.txt", i) };
6568 let data = format!("data{}", i);
6569 let mut reader = Cursor::new(data.as_bytes().to_vec());
6570 fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
6571 .unwrap();
6572 }
6573
6574 let report = fs.repair().unwrap();
6576 assert!(
6577 report.fixes_failed.is_empty(),
6578 "repair failures: {:?}",
6579 report.fixes_failed
6580 );
6581
6582 let cat = &fs.catalog_data;
6584 let header = BTreeHeader::read(cat);
6585 let node_size = header.node_size as usize;
6586 let max_nodes = cat.len() / node_size;
6587
6588 let mut index_recs_checked = 0u32;
6589 for n in 1..max_nodes {
6590 let off = n * node_size;
6591 if off + node_size > cat.len() {
6592 break;
6593 }
6594 let kind = cat[off + 8] as i8;
6595 if kind != 0 {
6596 continue;
6597 }
6598 let num_recs = BigEndian::read_u16(&cat[off + 10..off + 12]) as usize;
6599 for r in 0..num_recs {
6600 let (rec_start, rec_end) =
6601 btree_record_range(&cat[off..off + node_size], node_size, r);
6602 if rec_start >= rec_end || rec_end > node_size {
6603 continue;
6604 }
6605 let key_len = cat[off + rec_start] as usize;
6606 let rec_len = rec_end - rec_start;
6607
6608 assert_eq!(
6611 key_len, 0x25,
6612 "Index node {} rec {}: key_len={} but expected 0x25",
6613 n, r, key_len
6614 );
6615 assert_eq!(
6616 rec_len, 42,
6617 "Index node {} rec {}: rec_len={} but expected 42",
6618 n, r, rec_len
6619 );
6620 index_recs_checked += 1;
6621 }
6622 }
6623 assert!(
6624 index_recs_checked > 0,
6625 "no index records found — test needs more files"
6626 );
6627
6628 let result = fs.fsck().unwrap();
6630 let error_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
6631 assert!(
6632 result.errors.is_empty(),
6633 "post-repair check found errors: {:?}",
6634 error_msgs
6635 );
6636 }
6637
6638 #[test]
6642 fn test_create_blank_hfs_roundtrip() {
6643 let cases: &[(u64, u32, &str)] = &[
6646 (1 * 1024 * 1024, 512, "1MB @ 512"),
6647 (8 * 1024 * 1024, 4096, "8MB @ 4K"),
6648 (64 * 1024 * 1024, 16384, "64MB @ 16K"),
6649 (256 * 1024 * 1024, 32768, "256MB @ 32K"),
6650 ];
6651
6652 for (total_size, block_size, label) in cases {
6653 let img = create_blank_hfs(*total_size, *block_size, "Fresh")
6654 .unwrap_or_else(|e| panic!("{label}: create_blank_hfs failed: {e}"));
6655
6656 let atrb = BigEndian::read_u16(&img[1024 + 10..1024 + 12]);
6660 assert_eq!(
6661 atrb & 0x0100,
6662 0x0100,
6663 "{label}: drAtrb missing kHFSVolumeUnmountedMask (got 0x{atrb:04x})"
6664 );
6665 let alt_off = img.len() - 1024;
6667 let alt_atrb = BigEndian::read_u16(&img[alt_off + 10..alt_off + 12]);
6668 assert_eq!(
6669 alt_atrb, atrb,
6670 "{label}: alt MDB drAtrb (0x{alt_atrb:04x}) does not mirror primary (0x{atrb:04x})"
6671 );
6672
6673 assert!(
6678 (img.len() as u64) <= *total_size,
6679 "{label}: image {} exceeds requested {}",
6680 img.len(),
6681 total_size
6682 );
6683
6684 let mut fs = HfsFilesystem::open(Cursor::new(img.clone()), 0)
6685 .unwrap_or_else(|e| panic!("{label}: open failed: {e}"));
6686 assert_eq!(fs.mdb().block_size, *block_size, "{label}: block_size");
6687 assert_eq!(fs.mdb().volume_name, "Fresh", "{label}: volume_name");
6688
6689 let root = fs.root().unwrap();
6690 let entries = fs.list_directory(&root).unwrap();
6691 assert!(
6692 entries.is_empty(),
6693 "{label}: root not empty on fresh volume"
6694 );
6695
6696 let result = fs.fsck().unwrap();
6697 assert!(
6698 result.errors.is_empty(),
6699 "{label}: fsck errors: {:?}",
6700 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6701 );
6702
6703 let mut backing = img;
6705 {
6706 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
6707 let root = fs.root().unwrap();
6708 let payload = b"contents".to_vec();
6709 let mut src = payload.as_slice();
6710 fs.create_file(
6711 &root,
6712 "hello",
6713 &mut src,
6714 payload.len() as u64,
6715 &CreateFileOptions::default(),
6716 )
6717 .unwrap_or_else(|e| panic!("{label}: create_file failed: {e}"));
6718 fs.sync_metadata()
6719 .unwrap_or_else(|e| panic!("{label}: sync_metadata failed: {e}"));
6720 }
6721 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
6722 let root = fs.root().unwrap();
6723 let entries = fs.list_directory(&root).unwrap();
6724 assert_eq!(entries.len(), 1, "{label}: expected 1 child after create");
6725 assert_eq!(entries[0].name, "hello", "{label}: child name");
6726 let data = fs.read_file(&entries[0], 64).unwrap();
6727 assert_eq!(data, b"contents", "{label}: file content round-trip");
6728
6729 let result = fs.fsck().unwrap();
6730 assert!(
6731 result.errors.is_empty(),
6732 "{label}: post-create fsck errors: {:?}",
6733 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6734 );
6735 }
6736 }
6737
6738 #[test]
6746 #[cfg(target_pointer_width = "64")]
6747 fn test_create_blank_hfs_near_2gb() {
6748 let total = 2u64 * 1024 * 1024 * 1024 - 1;
6749 let img = create_blank_hfs(total, 32768, "Near2GB")
6750 .expect("create_blank_hfs near 2GB should succeed");
6751 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6752 assert_eq!(fs.mdb().block_size, 32768);
6753 assert!(fs.mdb().total_blocks >= 65000);
6754 let result = fs.fsck().unwrap();
6755 assert!(
6756 result.errors.is_empty(),
6757 "fsck errors: {:?}",
6758 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6759 );
6760 }
6761
6762 #[test]
6766 fn catalog_grows_when_its_nodes_run_out() {
6767 let img = create_blank_hfs_sized(16 * 1024 * 1024, 4096, "Grow", 0, 16384).unwrap();
6768 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6769 let size0 = fs.mdb.catalog_file_size;
6770 assert_eq!(size0, 16384);
6771 let root = fs.root().unwrap();
6772 for i in 0..1000 {
6773 let name = format!("f{i:04}.txt");
6774 fs.create_file(
6775 &root,
6776 &name,
6777 &mut &b"x"[..],
6778 1,
6779 &CreateFileOptions::default(),
6780 )
6781 .unwrap_or_else(|e| panic!("creating {name}: {e}"));
6782 }
6783 assert!(fs.mdb.catalog_file_size > size0, "the catalog never grew");
6784 let inline: u32 = fs
6787 .mdb
6788 .catalog_file_extents
6789 .iter()
6790 .map(|e| e.block_count as u32)
6791 .sum();
6792 let spilled: u32 = collect_fork_overflow_extents(
6793 fs.extents_overflow_data
6794 .as_ref()
6795 .expect("overflow tree loaded"),
6796 CNID_CATALOG,
6797 0x00,
6798 inline,
6799 )
6800 .iter()
6801 .map(|e| e.block_count as u32)
6802 .sum();
6803 assert!(spilled > 0, "no CNID 4 overflow records");
6804 assert_eq!(
6805 (inline + spilled) * 4096,
6806 fs.mdb.catalog_file_size,
6807 "extents cover the file"
6808 );
6809 fs.sync_metadata().unwrap();
6810 let report = fs.fsck().unwrap();
6811 assert!(report.errors.is_empty(), "{:?}", report.errors);
6812
6813 let img = fs.reader.get_ref().clone();
6814 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6815 let root = fs.root().unwrap();
6816 let kids = fs.list_directory(&root).unwrap();
6817 assert_eq!(kids.len(), 1000);
6818 for e in &kids {
6819 fs.delete_entry(&root, e).unwrap();
6820 }
6821 fs.create_file(
6822 &root,
6823 "last.txt",
6824 &mut &b"y"[..],
6825 1,
6826 &CreateFileOptions::default(),
6827 )
6828 .unwrap();
6829 fs.sync_metadata().unwrap();
6830 let report = fs.fsck().unwrap();
6831 assert!(report.errors.is_empty(), "{:?}", report.errors);
6832 }
6833
6834 #[test]
6837 fn extents_tree_grows_when_its_nodes_run_out() {
6838 let img = create_blank_hfs_sized(4 * 1024 * 1024, 512, "Frag", 2048, 0).unwrap();
6839 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6840 let xt0 = fs.mdb.extents_file_size;
6841 assert_eq!(xt0, 2048);
6842 let root = fs.root().unwrap();
6843 let block = vec![0x5Au8; 512];
6844 for i in 0..400 {
6845 fs.create_file(
6846 &root,
6847 &format!("h{i:03}"),
6848 &mut Cursor::new(&block),
6849 512,
6850 &CreateFileOptions::default(),
6851 )
6852 .unwrap();
6853 }
6854 for e in fs.list_directory(&root).unwrap() {
6855 if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
6856 fs.delete_entry(&root, &e).unwrap();
6857 }
6858 }
6859 let tail = (fs.mdb.free_blocks - 264) as usize * 512;
6862 fs.create_file(
6863 &root,
6864 "tail",
6865 &mut Cursor::new(vec![0u8; tail]),
6866 tail as u64,
6867 &CreateFileOptions::default(),
6868 )
6869 .unwrap();
6870 let data: Vec<u8> = (0..200 * 512).map(|i| (i / 512) as u8).collect();
6871 let entry = fs
6872 .create_file(
6873 &root,
6874 "spread",
6875 &mut Cursor::new(&data),
6876 data.len() as u64,
6877 &CreateFileOptions::default(),
6878 )
6879 .unwrap();
6880 assert!(
6881 fs.mdb.extents_file_size > xt0,
6882 "the extents tree never grew"
6883 );
6884 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
6885 fs.sync_metadata().unwrap();
6886 let report = fs.fsck().unwrap();
6887 assert!(report.errors.is_empty(), "{:?}", report.errors);
6888
6889 let img = fs.reader.get_ref().clone();
6890 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6891 let root = fs.root().unwrap();
6892 let entry = fs
6893 .list_directory(&root)
6894 .unwrap()
6895 .into_iter()
6896 .find(|e| e.name == "spread")
6897 .unwrap();
6898 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
6899 fs.delete_entry(&root, &entry).unwrap();
6900 fs.sync_metadata().unwrap();
6901 let report = fs.fsck().unwrap();
6902 assert!(report.errors.is_empty(), "{:?}", report.errors);
6903 }
6904
6905 #[test]
6908 fn catalog_growth_adds_a_map_node_past_the_header_bitmap() {
6909 let img = create_blank_hfs_sized(24 * 1024 * 1024, 512, "Map", 0, 16384).unwrap();
6910 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6911 let root = fs.root().unwrap();
6912 for i in 0..9000 {
6913 let name = format!("f{i:04}");
6914 fs.create_file(
6915 &root,
6916 &name,
6917 &mut &b"x"[..],
6918 1,
6919 &CreateFileOptions::default(),
6920 )
6921 .unwrap_or_else(|e| panic!("creating {name}: {e}"));
6922 }
6923 let h = BTreeHeader::read(&fs.catalog_data);
6924 assert!(h.total_nodes > 2048, "{} nodes", h.total_nodes);
6925 assert!(
6926 hfs_common::btree_bitmap_segments(&fs.catalog_data, 512).len() > 1,
6927 "no map node"
6928 );
6929 fs.sync_metadata().unwrap();
6930 let report = fs.fsck().unwrap();
6931 assert!(report.errors.is_empty(), "{:?}", report.errors);
6932 let img = fs.reader.get_ref().clone();
6933 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6934 let root = fs.root().unwrap();
6935 assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
6936 }
6937
6938 #[test]
6941 fn thousand_file_churn_keeps_the_catalog_clean() {
6942 let img = create_blank_hfs(64 * 1024 * 1024, 4096, "Churn").unwrap();
6943 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6944 let root = fs.root().unwrap();
6945 let clean = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, stage: &str| {
6946 fs.sync_metadata().unwrap();
6947 let r = fs.fsck().unwrap();
6948 assert!(r.errors.is_empty(), "{stage}: {:?}", r.errors);
6949 };
6950 let put = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, name: &str, data: &[u8]| {
6951 fs.create_file(
6952 &root,
6953 name,
6954 &mut Cursor::new(data),
6955 data.len() as u64,
6956 &CreateFileOptions::default(),
6957 )
6958 .unwrap_or_else(|e| panic!("creating {name}: {e}"))
6959 };
6960 let free0 = fs.mdb.free_blocks;
6961 for i in 0..1000 {
6962 put(
6963 &mut fs,
6964 &format!("f{i:04}.txt"),
6965 format!("file {i:04}").as_bytes(),
6966 );
6967 }
6968 clean(&mut fs, "after 1000 creates");
6969 let extra = put(&mut fs, "extra.bin", &[7u8; 5000]);
6970 clean(&mut fs, "after one more");
6971 fs.delete_entry(&root, &extra).unwrap();
6972 clean(&mut fs, "after deleting it");
6973 for e in fs.list_directory(&root).unwrap() {
6974 fs.delete_entry(&root, &e).unwrap();
6975 }
6976 assert_eq!(fs.mdb.free_blocks, free0, "every block came back");
6977 clean(&mut fs, "after deleting the 1000");
6978 put(&mut fs, "last.bin", &[9u8; 100]);
6979 clean(&mut fs, "after the last create");
6980 let names: Vec<String> = fs
6981 .list_directory(&root)
6982 .unwrap()
6983 .into_iter()
6984 .map(|e| e.name)
6985 .collect();
6986 assert_eq!(names, ["last.bin"]);
6987 }
6988
6989 #[test]
6992 fn fsck_reports_a_volume_that_does_not_fill_its_partition() {
6993 let img = create_blank_hfs(4 * 1024 * 1024, 2048, "Short").unwrap();
6994 let exact = img.len() as u64;
6995 let mut fs = HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact)).unwrap();
6996 assert!(fs.partition_shortfall().is_none());
6997 let report = fs.fsck().unwrap();
6998 assert!(report.errors.is_empty(), "{:?}", report.errors);
6999
7000 let mut fs =
7003 HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact + 512)).unwrap();
7004 assert_eq!(fs.partition_shortfall(), Some(512));
7005 let report = fs.fsck().unwrap();
7006 assert!(report.errors.is_empty(), "{:?}", report.errors);
7007
7008 let mut fs =
7009 HfsFilesystem::open_sized(Cursor::new(img), 0, Some(exact + 1024 * 1024)).unwrap();
7010 assert_eq!(fs.partition_shortfall(), Some(1024 * 1024));
7011 let report = fs.fsck().unwrap();
7012 let codes: Vec<&str> = report.errors.iter().map(|e| e.code.as_str()).collect();
7013 assert_eq!(codes, ["AllocationAreaEnd"], "{:?}", report.errors);
7014 assert!(report.errors[0].message.contains("resize --size"));
7015 }
7016
7017 #[test]
7020 fn create_file_spreads_a_fork_over_free_runs() {
7021 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Runs").unwrap();
7022 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7023 let root = fs.root().unwrap();
7024 let block = vec![0xA5u8; 4096];
7025 for i in 0..40 {
7026 fs.create_file(
7027 &root,
7028 &format!("f{i:02}"),
7029 &mut Cursor::new(block.as_slice()),
7030 4096,
7031 &CreateFileOptions::default(),
7032 )
7033 .unwrap();
7034 }
7035 for e in fs.list_directory(&root).unwrap() {
7037 if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
7038 fs.delete_entry(&root, &e).unwrap();
7039 }
7040 }
7041 let tail = (fs.mdb.free_blocks - 20) as usize;
7043 let tail_data = vec![0x5Au8; tail * 4096];
7044 fs.create_file(
7045 &root,
7046 "tail",
7047 &mut Cursor::new(tail_data.as_slice()),
7048 tail_data.len() as u64,
7049 &CreateFileOptions::default(),
7050 )
7051 .unwrap();
7052 assert_eq!(fs.mdb.free_blocks, 20);
7053 let free_before = fs.mdb.free_blocks;
7054
7055 let data: Vec<u8> = (0..7 * 4096).map(|i| (i / 4096) as u8 + 1).collect();
7057 let rsrc: Vec<u8> = (0..5 * 4096).map(|i| 0x80 | (i / 4096) as u8).collect();
7058 let opts = CreateFileOptions {
7059 resource_fork: Some(disk_core::filesystem::ResourceForkSource::Data(
7060 rsrc.clone(),
7061 )),
7062 ..Default::default()
7063 };
7064 let entry = fs
7065 .create_file(
7066 &root,
7067 "frag",
7068 &mut Cursor::new(data.as_slice()),
7069 data.len() as u64,
7070 &opts,
7071 )
7072 .unwrap();
7073 let cnid = entry.location as u32;
7074 assert_eq!(fs.mdb.free_blocks, free_before - 12);
7075 let (dsize, dext, rsize, rext) = fs.find_file_by_id(cnid).unwrap();
7076 assert_eq!((dsize as usize, rsize as usize), (data.len(), rsrc.len()));
7077 assert!(dext.iter().all(|e| e.block_count == 1) && rext.iter().all(|e| e.block_count == 1));
7078 let tree = fs
7079 .extents_overflow_data
7080 .as_ref()
7081 .expect("overflow tree loaded");
7082 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 4);
7083 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 2);
7084 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
7085 let mut got = Vec::new();
7086 fs.write_resource_fork_to(&entry, &mut got).unwrap();
7087 assert_eq!(got, rsrc);
7088
7089 let rsrc2: Vec<u8> = (0..4 * 4096).map(|i| 0xC0 | (i / 4096) as u8).collect();
7091 fs.write_resource_fork(
7092 &entry,
7093 &mut Cursor::new(rsrc2.as_slice()),
7094 rsrc2.len() as u64,
7095 )
7096 .unwrap();
7097 assert_eq!(fs.mdb.free_blocks, free_before - 11);
7098 let tree = fs.extents_overflow_data.as_ref().unwrap();
7099 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 1);
7100 let mut got = Vec::new();
7101 fs.write_resource_fork_to(&entry, &mut got).unwrap();
7102 assert_eq!(got, rsrc2);
7103
7104 fs.sync_metadata().unwrap();
7105 let report = fs.fsck().unwrap();
7106 assert!(report.errors.is_empty(), "{:?}", report.errors);
7107
7108 let img = fs.reader.get_ref().clone();
7110 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7111 let root = fs.root().unwrap();
7112 let entry = fs
7113 .list_directory(&root)
7114 .unwrap()
7115 .into_iter()
7116 .find(|e| e.name == "frag")
7117 .unwrap();
7118 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
7119 fs.delete_entry(&root, &entry).unwrap();
7120 assert_eq!(fs.mdb.free_blocks, free_before);
7121 let tree = fs.extents_overflow_data.as_ref().unwrap();
7122 assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
7123 fs.sync_metadata().unwrap();
7124 let report = fs.fsck().unwrap();
7125 assert!(report.errors.is_empty(), "{:?}", report.errors);
7126 }
7127
7128 #[test]
7131 fn delete_frees_overflow_extents_and_their_records() {
7132 use crate::hfs_common::{
7133 bitmap_test_bit_be, btree_alloc_node, BTreeHeader, BTREE_LEAF_NODE,
7134 };
7135 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Overflow").unwrap();
7137 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7138 assert!(fs.mdb.extents_file_size > 0);
7139 let root = fs.root().unwrap();
7140 let free_at_start = fs.mdb.free_blocks;
7141 let data = b"inline extent contents";
7142 fs.create_file(
7143 &root,
7144 "frag.bin",
7145 &mut Cursor::new(data.as_slice()),
7146 data.len() as u64,
7147 &CreateFileOptions::default(),
7148 )
7149 .unwrap();
7150 let entry = fs
7151 .list_directory(&root)
7152 .unwrap()
7153 .into_iter()
7154 .find(|e| e.name == "frag.bin")
7155 .unwrap();
7156 let cnid = entry.location as u32;
7157
7158 let start = fs.allocate_blocks(2).unwrap();
7161 fs.ensure_extents_overflow().unwrap();
7162 {
7163 let tree = fs.extents_overflow_data.as_mut().expect("extents tree");
7164 let mut h = BTreeHeader::read(tree);
7165 let node_size = h.node_size as usize;
7166 let leaf = btree_alloc_node(tree, node_size, h.total_nodes).unwrap();
7167 h.root_node = leaf;
7168 h.first_leaf_node = leaf;
7169 h.last_leaf_node = leaf;
7170 h.depth = 1;
7171 h.leaf_records = 1;
7172 h.free_nodes -= 1;
7173 h.write(tree);
7174 let off = leaf as usize * node_size;
7175 let node = &mut tree[off..off + node_size];
7176 node[8] = BTREE_LEAF_NODE as u8;
7177 node[9] = 1;
7178 BigEndian::write_u16(&mut node[10..12], 1);
7179 node[14] = 7;
7181 node[15] = 0x00;
7182 BigEndian::write_u32(&mut node[16..20], cnid);
7183 BigEndian::write_u16(&mut node[20..22], 1);
7184 BigEndian::write_u16(&mut node[22..24], start as u16);
7185 BigEndian::write_u16(&mut node[24..26], 2);
7186 BigEndian::write_u16(&mut node[node_size - 2..], 14);
7187 BigEndian::write_u16(&mut node[node_size - 4..node_size - 2], 34);
7188 }
7189 let tree = fs.extents_overflow_data.as_ref().unwrap();
7190 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 1);
7191
7192 fs.delete_entry(&root, &entry).unwrap();
7193
7194 let tree = fs.extents_overflow_data.as_ref().unwrap();
7195 assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
7196 let h = BTreeHeader::read(tree);
7197 assert_eq!((h.leaf_records, h.root_node, h.depth), (0, 0, 0));
7198 assert!(
7200 crate::hfs_common::btree_lonely_empty_leaves(tree, h.node_size as usize).is_empty()
7201 );
7202 let bitmap = fs.bitmap.as_ref().unwrap();
7203 assert!(!bitmap_test_bit_be(bitmap, start) && !bitmap_test_bit_be(bitmap, start + 1));
7204 assert_eq!(fs.mdb.free_blocks, free_at_start, "every block came back");
7205 fs.sync_metadata().unwrap();
7206 let report = fs.fsck().unwrap();
7207 assert!(report.errors.is_empty(), "{:?}", report.errors);
7208 }
7209
7210 #[test]
7214 fn test_collect_fork_overflow_extents() {
7215 let node_size = 512usize;
7216 let total_nodes = 3u32;
7218 let mut buf = vec![0u8; node_size * total_nodes as usize];
7219
7220 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
7222 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);
7229 BigEndian::write_u16(&mut buf[34..36], 7);
7230 BigEndian::write_u32(&mut buf[36..40], total_nodes);
7231 BigEndian::write_u32(&mut buf[40..44], total_nodes - 2);
7232 buf[248] = 0b11000000;
7234
7235 let leaf_off = node_size;
7237 let leaf = &mut buf[leaf_off..leaf_off + node_size];
7238 leaf[8] = hfs_common::BTREE_LEAF_NODE as u8;
7239 BigEndian::write_u16(&mut leaf[10..12], 1); let rec_off = 14usize;
7243 leaf[rec_off] = 7;
7244 leaf[rec_off + 1] = 0x00;
7245 BigEndian::write_u32(&mut leaf[rec_off + 2..rec_off + 6], 0x100);
7246 BigEndian::write_u16(&mut leaf[rec_off + 6..rec_off + 8], 3);
7247 let data_off = rec_off + 8;
7249 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);
7252 BigEndian::write_u16(&mut leaf[data_off + 6..data_off + 8], 1);
7253 BigEndian::write_u16(&mut leaf[data_off + 8..data_off + 10], 0); BigEndian::write_u16(&mut leaf[data_off + 10..data_off + 12], 0);
7255
7256 let nr = 1usize;
7258 BigEndian::write_u16(&mut leaf[node_size - 2..], 14);
7259 BigEndian::write_u16(&mut leaf[node_size - 4..node_size - 2], 34);
7260 let _ = nr;
7261
7262 let got = collect_fork_overflow_extents(&buf, 0x100, 0x00, 3);
7264 assert_eq!(got.len(), 2, "expected 2 non-empty extents, got {got:?}");
7265 assert_eq!((got[0].start_block, got[0].block_count), (50, 2));
7266 assert_eq!((got[1].start_block, got[1].block_count), (60, 1));
7267
7268 let none = collect_fork_overflow_extents(&buf, 0x100, 0x00, 4);
7270 assert!(
7271 none.is_empty(),
7272 "expected empty when filtered, got {none:?}"
7273 );
7274
7275 let rsrc = collect_fork_overflow_extents(&buf, 0x100, 0xFF, 0);
7277 assert!(rsrc.is_empty());
7278
7279 let other = collect_fork_overflow_extents(&buf, 0x101, 0x00, 0);
7281 assert!(other.is_empty());
7282 }
7283
7284 #[test]
7288 fn test_create_blank_hfs_sized_custom_btree_sizes() {
7289 let block_size = 32 * 1024u32;
7290 let img =
7292 create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Sized", 64 * 1024, 256 * 1024)
7293 .expect("create_blank_hfs_sized");
7294 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
7295 assert_eq!(fs.mdb().extents_file_size, 4 * block_size);
7297 assert_eq!(fs.mdb().catalog_file_size, 8 * block_size);
7299 let result = fs.fsck().expect("fsck");
7300 assert!(
7301 result.errors.is_empty(),
7302 "unexpected fsck errors: {:?}",
7303 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7304 );
7305 }
7306
7307 #[test]
7312 fn test_pick_btree_node_size_thresholds() {
7313 assert_eq!(pick_btree_node_size(512 * 2048), 512);
7314 assert_eq!(pick_btree_node_size(512 * 2049), 512);
7315 assert_eq!(pick_btree_node_size(1024 * 6144), 512);
7316 assert_eq!(pick_btree_node_size(u32::MAX as u64), 512);
7317 }
7318
7319 #[test]
7324 fn test_create_blank_hfs_sized_large_catalog() {
7325 let block_size = 32 * 1024u32;
7326 let img = create_blank_hfs_sized(
7327 128 * 1024 * 1024,
7328 block_size,
7329 "Big",
7330 0,
7331 HFS_MAX_BTREE_FILE_SIZE,
7332 )
7333 .expect("create_blank_hfs_sized big catalog");
7334 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open big");
7335 let cat = fs.catalog_data();
7336 let node_size = BigEndian::read_u16(&cat[32..34]);
7337 assert_eq!(
7338 node_size, 512,
7339 "node_size must stay at 512 for Mac OS compat"
7340 );
7341 let result = fs.fsck().expect("fsck big");
7342 assert!(
7343 result.errors.is_empty(),
7344 "unexpected fsck errors: {:?}",
7345 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7346 );
7347 }
7348
7349 #[test]
7353 fn test_create_blank_hfs_with_map_nodes() {
7354 let img =
7357 create_blank_hfs_sized(64 * 1024 * 1024, 32 * 1024, "MapNodes", 0, 2 * 1024 * 1024)
7358 .expect("create_blank_hfs_sized 2 MiB cat");
7359 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
7360 let cat = fs.catalog_data().to_vec();
7361
7362 let header_flink = BigEndian::read_u32(&cat[0..4]);
7364 assert_eq!(header_flink, 2, "header.fLink should point to map node 2");
7365
7366 let map = &cat[1024..1536];
7369 assert_eq!(map[8] as i8, 2, "node 2 should be a map node");
7370 assert_eq!(BigEndian::read_u16(&map[10..12]), 1);
7371 assert_eq!(
7372 BigEndian::read_u32(&map[0..4]),
7373 0,
7374 "single map node, fLink=0"
7375 );
7376
7377 let header = hfs_common::BTreeHeader::read(&cat);
7379 assert_eq!(header.total_nodes, 4096);
7380 assert_eq!(header.free_nodes, 4093);
7381
7382 let segs = hfs_common::btree_bitmap_segments(&cat, 512);
7385 assert_eq!(segs.len(), 2);
7386 assert_eq!(segs[0].base_bit, 0);
7387 assert_eq!(segs[1].base_bit, 2048);
7388 assert!(
7389 (segs[0].len + segs[1].len) * 8 >= 4096,
7390 "bitmap chain must address all 4096 nodes"
7391 );
7392
7393 let result = fs.fsck().expect("fsck");
7395 assert!(
7396 result.errors.is_empty(),
7397 "unexpected fsck errors: {:?}",
7398 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7399 );
7400 }
7401
7402 #[test]
7404 fn test_create_blank_hfs_rejects_bad_block_size() {
7405 assert!(create_blank_hfs(1 << 20, 0, "X").is_err());
7406 assert!(create_blank_hfs(1 << 20, 500, "X").is_err());
7407 assert!(create_blank_hfs(1 << 20, 513, "X").is_err());
7408 }
7409
7410 fn make_fs_with_file() -> (HfsFilesystem<Cursor<Vec<u8>>>, FileEntry) {
7414 let img = make_editable_hfs_image();
7415 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7416 let root = fs.root().unwrap();
7417 let data = b"hello";
7418 let mut r = Cursor::new(data.as_slice());
7419 let fe = fs
7420 .create_file(
7421 &root,
7422 "thing.bin",
7423 &mut r,
7424 data.len() as u64,
7425 &CreateFileOptions::default(),
7426 )
7427 .unwrap();
7428 (fs, fe)
7429 }
7430
7431 #[test]
7432 fn test_hfs_set_finder_info_round_trip() {
7433 let (mut fs, fe) = make_fs_with_file();
7434 let mut finfo = [0u8; 16];
7435 finfo[0..4].copy_from_slice(b"APPL");
7436 finfo[4..8].copy_from_slice(b"MACS");
7437 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];
7442 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();
7447
7448 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7449 assert_eq!(&fs.catalog_data[rec + 4..rec + 20], &finfo);
7450 assert_eq!(&fs.catalog_data[rec + 56..rec + 72], &fxinfo);
7451
7452 let root = fs.root().unwrap();
7455 let listed = fs.list_directory(&root).unwrap();
7456 let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
7457 assert_eq!(entry.finder_flags, Some(0x4000));
7458 }
7459
7460 #[test]
7464 fn test_hfs_read_surfaces_has_bundle_flag() {
7465 let (mut fs, fe) = make_fs_with_file();
7466 let mut finfo = [0u8; 16];
7467 finfo[0..4].copy_from_slice(b"APPL");
7468 finfo[4..8].copy_from_slice(b"Po.P");
7469 BigEndian::write_u16(&mut finfo[8..10], 0x2000); fs.set_finder_info(&fe, finfo, [0u8; 16]).unwrap();
7471
7472 let root = fs.root().unwrap();
7473 let listed = fs.list_directory(&root).unwrap();
7474 let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
7475 assert_eq!(entry.finder_flags, Some(0x2000));
7476 }
7477
7478 #[test]
7479 fn test_hfs_set_finder_info_rejects_directory() {
7480 let img = make_editable_hfs_image();
7481 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7482 let root = fs.root().unwrap();
7483 let dir = fs
7484 .create_directory(&root, "Dir", &CreateDirectoryOptions::default())
7485 .unwrap();
7486 let err = fs.set_finder_info(&dir, [0u8; 16], [0u8; 16]).unwrap_err();
7487 assert!(matches!(err, FilesystemError::InvalidData(_)));
7488 }
7489
7490 #[test]
7491 fn test_hfs_set_directory_finder_info_round_trip() {
7492 let img = make_editable_hfs_image();
7493 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7494 let root = fs.root().unwrap();
7495 let dir = fs
7496 .create_directory(&root, "Apps", &CreateDirectoryOptions::default())
7497 .unwrap();
7498
7499 let mut dinfo = [0u8; 16];
7500 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];
7506 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();
7511
7512 let rec = fs.locate_record_data(dir.location as u32).unwrap();
7513 assert_eq!(&fs.catalog_data[rec + 22..rec + 38], &dinfo);
7514 assert_eq!(&fs.catalog_data[rec + 38..rec + 54], &dxinfo);
7515 }
7516
7517 #[test]
7518 fn test_hfs_set_directory_finder_info_rejects_file() {
7519 let (mut fs, fe) = make_fs_with_file();
7520 let err = fs
7521 .set_directory_finder_info(&fe, [0u8; 16], [0u8; 16])
7522 .unwrap_err();
7523 assert!(matches!(err, FilesystemError::InvalidData(_)));
7524 }
7525
7526 #[test]
7527 fn test_hfs_set_dates_file() {
7528 let (mut fs, fe) = make_fs_with_file();
7529 let create = 0x1000_0001u32;
7530 let modify = 0x2000_0002u32;
7531 let backup = 0x3000_0003u32;
7532 fs.set_dates(&fe, create, modify, backup).unwrap();
7533
7534 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7535 assert_eq!(
7536 BigEndian::read_u32(&fs.catalog_data[rec + 44..rec + 48]),
7537 create
7538 );
7539 assert_eq!(
7540 BigEndian::read_u32(&fs.catalog_data[rec + 48..rec + 52]),
7541 modify
7542 );
7543 assert_eq!(
7544 BigEndian::read_u32(&fs.catalog_data[rec + 52..rec + 56]),
7545 backup
7546 );
7547 }
7548
7549 #[test]
7550 fn test_hfs_set_dates_directory() {
7551 let img = make_editable_hfs_image();
7552 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7553 let root = fs.root().unwrap();
7554 let dir = fs
7555 .create_directory(&root, "D", &CreateDirectoryOptions::default())
7556 .unwrap();
7557
7558 let create = 0xAAAAu32;
7559 let modify = 0xBBBBu32;
7560 let backup = 0xCCCCu32;
7561 fs.set_dates(&dir, create, modify, backup).unwrap();
7562
7563 let rec = fs.locate_record_data(dir.location as u32).unwrap();
7564 assert_eq!(
7565 BigEndian::read_u32(&fs.catalog_data[rec + 10..rec + 14]),
7566 create
7567 );
7568 assert_eq!(
7569 BigEndian::read_u32(&fs.catalog_data[rec + 14..rec + 18]),
7570 modify
7571 );
7572 assert_eq!(
7573 BigEndian::read_u32(&fs.catalog_data[rec + 18..rec + 22]),
7574 backup
7575 );
7576 }
7577
7578 #[test]
7579 fn test_hfs_set_file_locked_toggle() {
7580 let (mut fs, fe) = make_fs_with_file();
7581 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7582 let initial = fs.catalog_data[rec + 2];
7583 assert_eq!(initial & 0x01, 0);
7584
7585 fs.set_file_locked(&fe, true).unwrap();
7586 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7587 assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0x01);
7588
7589 fs.set_file_locked(&fe, false).unwrap();
7590 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7591 assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0);
7592 }
7593
7594 #[test]
7595 fn test_hfs_set_file_locked_rejects_directory() {
7596 let img = make_editable_hfs_image();
7597 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7598 let root = fs.root().unwrap();
7599 let dir = fs
7600 .create_directory(&root, "Dir", &CreateDirectoryOptions::default())
7601 .unwrap();
7602 let err = fs.set_file_locked(&dir, true).unwrap_err();
7603 assert!(matches!(err, FilesystemError::InvalidData(_)));
7604 }
7605
7606 #[test]
7607 fn test_hfs_set_volume_dates_persists_through_sync() {
7608 let img = make_editable_hfs_image();
7609 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7610
7611 let create = 0x4242_4242u32;
7612 let modify = 0x5151_5151u32;
7613 let backup = 0x6060_6060u32;
7614 fs.set_volume_dates(create, modify, backup);
7615 fs.sync_metadata().unwrap();
7616
7617 assert_eq!(fs.mdb.create_date, create);
7618 assert_eq!(fs.mdb.modify_date, modify);
7619 assert_eq!(BigEndian::read_u32(&fs.mdb.raw_sector[64..68]), backup);
7620
7621 let bytes = fs.reader.get_ref().clone();
7623 let mut fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
7624 assert_eq!(fs2.mdb.create_date, create);
7625 assert_eq!(fs2.mdb.modify_date, modify);
7626 assert_eq!(BigEndian::read_u32(&fs2.mdb.raw_sector[64..68]), backup);
7627
7628 fs2.sync_metadata().unwrap();
7630 assert_ne!(fs2.mdb.modify_date, modify);
7631 }
7632
7633 #[test]
7634 fn test_hfs_set_boot_blocks_written_on_sync() {
7635 let img = make_editable_hfs_image();
7636 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7637 let mut bb = [0u8; 1024];
7638 bb[0] = 0x4C; bb[1] = 0x4B;
7640 for (i, slot) in bb.iter_mut().enumerate().skip(2).take(200) {
7641 *slot = (i & 0xFF) as u8;
7642 }
7643 fs.set_boot_blocks(&bb);
7644 fs.sync_metadata().unwrap();
7645
7646 let read_back = fs.read_boot_blocks().unwrap();
7647 assert_eq!(&read_back[..], &bb[..]);
7648 }
7649
7650 #[test]
7651 fn test_hfs_set_volume_finder_info_round_trip() {
7652 let img = make_editable_hfs_image();
7653 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7654
7655 let mut fi = [0u8; 32];
7656 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);
7660 assert_eq!(fs.mdb.finder_info[0], 100);
7661 assert_eq!(fs.mdb.finder_info[3], 200);
7662 assert_eq!(fs.mdb.finder_info[5], 300);
7663
7664 fs.sync_metadata().unwrap();
7665 let bytes = fs.reader.get_ref().clone();
7666 let fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
7667 assert_eq!(fs2.mdb.finder_info[0], 100);
7668 assert_eq!(fs2.mdb.finder_info[3], 200);
7669 assert_eq!(fs2.mdb.finder_info[5], 300);
7670 }
7671
7672 #[test]
7673 fn test_default_btree_sizes_scale_and_floor() {
7674 let (cat, ext) = default_btree_sizes(1 * 1024 * 1024, 512);
7676 let floor = 24 * 512;
7677 assert_eq!(cat, floor, "small-volume catalog should hit floor");
7678 assert_eq!(ext, floor, "small-volume extents should hit floor");
7679
7680 let (cat, ext) = default_btree_sizes(20 * 1024 * 1024, 512);
7684 assert!(cat >= 100 * 1024, "20MB catalog too small: {cat}");
7685 assert_eq!(cat % 2048, 0, "catalog not clump-aligned");
7686 assert_eq!(ext % 2048, 0, "extents not clump-aligned");
7687 assert!(ext <= cat, "extents should not exceed catalog");
7688 assert!(ext * 2 >= cat - 2048, "extents should be ~half catalog");
7689
7690 let (cat_big, _) = default_btree_sizes(200 * 1024 * 1024, 4096);
7692 assert!(cat_big > cat, "larger volume should get a larger catalog");
7693
7694 let (c, e) = default_btree_sizes(20 * 1024 * 1024, 4096);
7697 let img = create_blank_hfs_sized(20 * 1024 * 1024, 4096, "Ref20", e, c).unwrap();
7698 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7699 let result = fs.fsck().unwrap();
7700 assert!(
7701 result.errors.is_empty(),
7702 "fsck errors on default-sized volume: {:?}",
7703 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7704 );
7705 }
7706
7707 #[test]
7708 fn test_clone_target_btree_sizes_floors_at_blank_default() {
7709 let target = 2046 * 1024 * 1024;
7714 let (default_cat, _) = default_btree_sizes(target, 32768);
7715 let (cat, _ext) = clone_target_btree_sizes(target, 32768, 2 * 1024 * 1024, 1024 * 1024);
7716 assert_eq!(
7717 cat, default_cat,
7718 "large target must floor catalog at the blank default, not 1.5x source"
7719 );
7720 assert!(
7721 cat > 2 * 1024 * 1024 * 3 / 2,
7722 "floor must exceed 1.5x source"
7723 );
7724
7725 let small_target = 40 * 1024 * 1024;
7728 let (cat2, _) = clone_target_btree_sizes(small_target, 4096, 4 * 1024 * 1024, 256 * 1024);
7729 assert_eq!(
7730 cat2,
7731 4 * 1024 * 1024 * 3 / 2,
7732 "source-derived value should win"
7733 );
7734
7735 let (cat3, ext3) = clone_target_btree_sizes(target, 32768, u32::MAX, u32::MAX);
7737 assert!(cat3 <= HFS_MAX_BTREE_FILE_SIZE);
7738 assert!(ext3 <= HFS_MAX_BTREE_FILE_SIZE);
7739 }
7740
7741 #[test]
7742 fn test_hfs_set_volume_name_roundtrip() {
7743 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "OldName").unwrap();
7746 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7747
7748 fs.set_volume_name("NewName").unwrap();
7749 assert_eq!(fs.mdb.volume_name, "NewName");
7750 fs.sync_metadata().unwrap();
7751
7752 let bytes = fs.reader.get_ref().clone();
7753 let mut fs2 = HfsFilesystem::open(Cursor::new(bytes.clone()), 0).unwrap();
7754 assert_eq!(fs2.mdb.volume_name, "NewName");
7755
7756 let new_raw = b"NewName".to_vec();
7759 assert!(
7760 fs2.find_catalog_record_by_name(1, &new_raw).is_some(),
7761 "root dir record not re-keyed to new name"
7762 );
7763 assert!(
7764 fs2.find_catalog_record_by_name(1, b"OldName").is_none(),
7765 "old root dir key still present"
7766 );
7767
7768 let (_n, _r, key_abs) = fs2.find_catalog_record_by_cnid(2).unwrap();
7770 let key_len = fs2.catalog_data[key_abs] as usize;
7771 let mut data_off = key_abs + 1 + key_len;
7772 if !data_off.is_multiple_of(2) {
7773 data_off += 1;
7774 }
7775 let thd_len = fs2.catalog_data[data_off + 14] as usize;
7776 assert_eq!(thd_len, new_raw.len(), "thdCName length not updated");
7777 assert_eq!(
7778 &fs2.catalog_data[data_off + 15..data_off + 15 + thd_len],
7779 new_raw.as_slice(),
7780 "thdCName bytes not updated"
7781 );
7782
7783 let mdb_off = 1024 + 36;
7785 assert_eq!(bytes[mdb_off] as usize, new_raw.len());
7786 assert_eq!(
7787 &bytes[mdb_off + 1..mdb_off + 1 + new_raw.len()],
7788 new_raw.as_slice()
7789 );
7790
7791 let result = fs2.fsck().unwrap();
7792 assert!(
7793 result.errors.is_empty(),
7794 "fsck errors after rename: {:?}",
7795 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7796 );
7797 }
7798
7799 #[test]
7800 fn test_drallocptr_advances_with_allocations() {
7801 let img = create_blank_hfs(4 * 1024 * 1024, 512, "AllocPtr").unwrap();
7805 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7806 assert_eq!(
7807 BigEndian::read_u16(&fs.mdb.raw_sector[16..18]),
7808 0,
7809 "fresh volume drAllocPtr should be 0"
7810 );
7811
7812 let root = fs.root().unwrap();
7813 let payload = vec![0xABu8; 8 * 1024]; let mut src = payload.as_slice();
7815 fs.create_file(
7816 &root,
7817 "ptr",
7818 &mut src,
7819 payload.len() as u64,
7820 &CreateFileOptions::default(),
7821 )
7822 .unwrap();
7823 let after = BigEndian::read_u16(&fs.mdb.raw_sector[16..18]);
7824 assert!(after > 0, "drAllocPtr should advance after allocation");
7825 }
7826
7827 #[test]
7828 fn test_hfs_set_volume_name_rejects_invalid() {
7829 let img = create_blank_hfs(1 * 1024 * 1024, 512, "X").unwrap();
7830 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7831 assert!(
7832 fs.set_volume_name("").is_err(),
7833 "empty name must be rejected"
7834 );
7835 let too_long = "A".repeat(28);
7836 assert!(
7837 fs.set_volume_name(&too_long).is_err(),
7838 ">27 byte name must be rejected"
7839 );
7840 assert_eq!(fs.mdb.volume_name, "X");
7842 }
7843
7844 #[test]
7845 fn test_hfs_finder_info_setters_survive_fsck() {
7846 let (mut fs, fe) = make_fs_with_file();
7848 fs.set_finder_info(&fe, [0x11; 16], [0x22; 16]).unwrap();
7849 fs.set_dates(&fe, 1, 2, 3).unwrap();
7850 fs.set_file_locked(&fe, true).unwrap();
7851 fs.set_volume_dates(10, 20, 30);
7852 fs.set_volume_finder_info(&[0x33; 32]);
7853 fs.sync_metadata().unwrap();
7854 let result = fs.fsck().unwrap();
7855 assert!(
7856 result.errors.is_empty(),
7857 "fsck errors: {:?}",
7858 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7859 );
7860 }
7861}