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 == 0 || 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 == 0 || self.catalog_data.len() < node_size as usize {
627 return Ok(vec![]);
628 }
629 let header_node = &self.catalog_data[0..node_size as usize];
630 let first_leaf = BigEndian::read_u32(&header_node[24..28]);
631
632 let mut results = Vec::new();
633 let mut node_idx = first_leaf;
634
635 while node_idx != 0 {
636 let offset = node_idx as usize * node_size as usize;
637 if offset + node_size as usize > self.catalog_data.len() {
638 break;
639 }
640 let node = &self.catalog_data[offset..offset + node_size as usize];
641
642 let next_node = BigEndian::read_u32(&node[0..4]);
644 let _kind = node[8] as i8; let num_records = BigEndian::read_u16(&node[10..12]);
646
647 for i in 0..num_records as usize {
648 let offset_pos = node_size as usize - 2 * (i + 1);
650 if offset_pos + 2 > node.len() {
651 break;
652 }
653 let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
654 if rec_offset + 6 > node.len() {
655 continue;
656 }
657
658 let key_len = node[rec_offset] as usize;
660 if key_len < 6 || rec_offset + 1 + key_len > node.len() {
661 continue;
662 }
663 let key_data = &node[rec_offset + 1..rec_offset + 1 + key_len];
664 let rec_parent_id = BigEndian::read_u32(&key_data[1..5]);
666 let name_len = key_data[5] as usize;
667 let name = if name_len > 0 && 6 + name_len <= key_data.len() {
668 mac_roman_to_utf8(&key_data[6..6 + name_len])
669 } else {
670 String::new()
671 };
672
673 let mut rec_data_offset = rec_offset + 1 + key_len;
675 if !rec_data_offset.is_multiple_of(2) {
676 rec_data_offset += 1;
677 }
678 if rec_data_offset + 2 > node.len() {
679 continue;
680 }
681
682 let record_type = node[rec_data_offset] as i8;
683
684 if rec_parent_id != parent_id {
685 continue;
686 }
687
688 match record_type {
689 CATALOG_DIR => {
690 if rec_data_offset + 70 > node.len() {
691 continue;
692 }
693 let dir_id =
694 BigEndian::read_u32(&node[rec_data_offset + 6..rec_data_offset + 10]);
695 let dir = &node[rec_data_offset..];
697 let dates = (
698 BigEndian::read_u32(&dir[10..14]),
699 BigEndian::read_u32(&dir[14..18]),
700 BigEndian::read_u32(&dir[18..22]),
701 );
702 results.push(CatalogRecord::Directory {
703 dir_id,
704 name,
705 parent_id: rec_parent_id,
706 dates,
707 });
708 }
709 CATALOG_FILE => {
710 if rec_data_offset + 102 > node.len() {
711 continue;
712 }
713 let rec = &node[rec_data_offset..];
714 let type_code: [u8; 4] = rec[4..8].try_into().unwrap();
716 let creator_code: [u8; 4] = rec[8..12].try_into().unwrap();
717 let finder_flags = BigEndian::read_u16(&rec[12..14]);
718 let file_id = BigEndian::read_u32(&rec[20..24]);
720 let data_size = BigEndian::read_u32(&rec[26..30]);
722 let mut data_extents = [HfsExtDescriptor {
723 start_block: 0,
724 block_count: 0,
725 }; 3];
726 for j in 0..3 {
727 data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
728 }
729 let rsrc_size = BigEndian::read_u32(&rec[36..40]);
731 let mut rsrc_extents = [HfsExtDescriptor {
732 start_block: 0,
733 block_count: 0,
734 }; 3];
735 for j in 0..3 {
736 rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
737 }
738 let dates = (
740 BigEndian::read_u32(&rec[44..48]),
741 BigEndian::read_u32(&rec[48..52]),
742 BigEndian::read_u32(&rec[52..56]),
743 );
744 results.push(CatalogRecord::File {
745 file_id,
746 name,
747 parent_id: rec_parent_id,
748 data_size,
749 data_extents,
750 rsrc_size,
751 rsrc_extents,
752 type_code,
753 creator_code,
754 finder_flags,
755 dates,
756 });
757 }
758 _ => {}
759 }
760 }
761
762 node_idx = next_node;
763 }
764
765 Ok(results)
766 }
767
768 fn find_file_by_id(
771 &self,
772 file_id: u32,
773 ) -> Option<(u32, [HfsExtDescriptor; 3], u32, [HfsExtDescriptor; 3])> {
774 if self.catalog_data.len() < 512 {
775 return None;
776 }
777 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as u32;
778 if node_size == 0 || self.catalog_data.len() < node_size as usize {
779 return None;
780 }
781 let header_node = &self.catalog_data[0..node_size as usize];
782 let first_leaf = BigEndian::read_u32(&header_node[24..28]);
783
784 let mut node_idx = first_leaf;
785 while node_idx != 0 {
786 let offset = node_idx as usize * node_size as usize;
787 if offset + node_size as usize > self.catalog_data.len() {
788 break;
789 }
790 let node = &self.catalog_data[offset..offset + node_size as usize];
791 let next_node = BigEndian::read_u32(&node[0..4]);
792 let num_records = BigEndian::read_u16(&node[10..12]);
793
794 for i in 0..num_records as usize {
795 let offset_pos = node_size as usize - 2 * (i + 1);
796 if offset_pos + 2 > node.len() {
797 break;
798 }
799 let rec_offset = BigEndian::read_u16(&node[offset_pos..offset_pos + 2]) as usize;
800 if rec_offset + 6 > node.len() {
801 continue;
802 }
803 let key_len = node[rec_offset] as usize;
804 if key_len < 6 || rec_offset + 1 + key_len > node.len() {
805 continue;
806 }
807 let mut rec_data_offset = rec_offset + 1 + key_len;
808 if !rec_data_offset.is_multiple_of(2) {
809 rec_data_offset += 1;
810 }
811 if rec_data_offset + 2 > node.len() {
812 continue;
813 }
814 let record_type = node[rec_data_offset] as i8;
815 if record_type != CATALOG_FILE {
816 continue;
817 }
818 if rec_data_offset + 102 > node.len() {
819 continue;
820 }
821 let rec = &node[rec_data_offset..];
822 let rec_file_id = BigEndian::read_u32(&rec[20..24]);
824 if rec_file_id != file_id {
825 continue;
826 }
827 let data_size = BigEndian::read_u32(&rec[26..30]);
829 let mut data_extents = [HfsExtDescriptor {
830 start_block: 0,
831 block_count: 0,
832 }; 3];
833 for j in 0..3 {
834 data_extents[j] = HfsExtDescriptor::parse(&rec[74 + j * 4..78 + j * 4]);
835 }
836 let rsrc_size = BigEndian::read_u32(&rec[36..40]);
838 let mut rsrc_extents = [HfsExtDescriptor {
839 start_block: 0,
840 block_count: 0,
841 }; 3];
842 for j in 0..3 {
843 rsrc_extents[j] = HfsExtDescriptor::parse(&rec[86 + j * 4..90 + j * 4]);
844 }
845 return Some((data_size, data_extents, rsrc_size, rsrc_extents));
846 }
847 node_idx = next_node;
848 }
849 None
850 }
851
852 fn read_volume_bitmap(&mut self) -> Result<Vec<u8>, FilesystemError> {
854 let bitmap_offset = self.partition_offset + self.mdb.volume_bitmap_block as u64 * 512;
855 let bitmap_size = (self.mdb.total_blocks as u32).div_ceil(8) as usize;
856 self.reader.seek(SeekFrom::Start(bitmap_offset))?;
857 let mut bitmap = vec![0u8; bitmap_size];
858 self.reader.read_exact(&mut bitmap)?;
859 Ok(bitmap)
860 }
861
862 fn alloc_block_offset(&self, block: u32) -> u64 {
864 self.mdb.first_alloc_block as u64 * 512 + block as u64 * self.mdb.block_size as u64
865 }
866
867 fn ensure_bitmap(&mut self) -> Result<(), FilesystemError> {
869 if self.bitmap.is_none() {
870 let bm = self.read_volume_bitmap()?;
871 self.bitmap = Some(bm);
872 }
873 Ok(())
874 }
875
876 pub fn fsck(&mut self) -> Result<disk_core::fsck::FsckResult, FilesystemError> {
878 self.ensure_bitmap()?;
879 let extents_data = if self.mdb.extents_file_size > 0 {
881 read_fork_data(
882 &mut self.reader,
883 self.partition_offset,
884 &self.mdb,
885 &self.mdb.extents_file_extents,
886 self.mdb.extents_file_size as u64,
887 )
888 .ok()
889 } else {
890 None
891 };
892 let alt_mdb_sector = if let Some(alt_offset) = self.alt_mdb_offset() {
895 let mut alt_buf = [0u8; 512];
896 if self
897 .reader
898 .seek(SeekFrom::Start(alt_offset))
899 .and_then(|_| self.reader.read_exact(&mut alt_buf))
900 .is_ok()
901 {
902 Some(alt_buf)
903 } else {
904 None
905 }
906 } else {
907 None
908 };
909 Ok(crate::hfs_fsck::check_hfs_integrity(
910 &self.mdb,
911 &self.catalog_data,
912 self.bitmap.as_ref().unwrap(),
913 extents_data.as_deref(),
914 alt_mdb_sector.as_ref(),
915 self.partition_size,
916 ))
917 }
918
919 pub(crate) fn build_catalog_key(parent_id: u32, name: &[u8]) -> Vec<u8> {
921 let key_len = 1 + 4 + 1 + name.len(); let mut key = Vec::with_capacity(1 + key_len);
923 key.push(key_len as u8); key.push(0); let mut buf4 = [0u8; 4];
926 BigEndian::write_u32(&mut buf4, parent_id);
927 key.extend_from_slice(&buf4);
928 key.push(name.len() as u8);
929 key.extend_from_slice(name);
930 if key.len() % 2 != 0 {
932 key.push(0);
933 }
934 key
935 }
936
937 pub(crate) fn catalog_compare(a: &[u8], b: &[u8]) -> Ordering {
939 if a.len() < 7 || b.len() < 7 {
941 return a.len().cmp(&b.len());
942 }
943 let parent_a = BigEndian::read_u32(&a[2..6]);
944 let parent_b = BigEndian::read_u32(&b[2..6]);
945 let name_len_a = a[6] as usize;
946 let name_len_b = b[6] as usize;
947 let name_a = &a[7..7 + name_len_a.min(a.len() - 7)];
948 let name_b = &b[7..7 + name_len_b.min(b.len() - 7)];
949 hfs_common::compare_hfs_keys(parent_a, name_a, parent_b, name_b)
950 }
951
952 fn find_catalog_record_by_name(
965 &self,
966 parent_id: u32,
967 name: &[u8],
968 ) -> Option<(u32, usize, usize)> {
969 let search_key = Self::build_catalog_key(parent_id, name);
970 let header = BTreeHeader::read(&self.catalog_data);
971 let node_size = header.node_size as usize;
972 if node_size == 0 || self.catalog_data.len() < node_size {
973 return None;
974 }
975
976 let (leaf_idx, _) = hfs_common::btree_find_insert_leaf(
977 &self.catalog_data,
978 &header,
979 &search_key,
980 &Self::catalog_compare,
981 );
982
983 let off = leaf_idx as usize * node_size;
984 if off + node_size > self.catalog_data.len() {
985 return None;
986 }
987 let node = &self.catalog_data[off..off + node_size];
988 let num_records = BigEndian::read_u16(&node[10..12]) as usize;
989 for rec_idx in 0..num_records {
990 let (start, end) = hfs_common::btree_record_range(node, node_size, rec_idx);
991 if start >= end || end > node_size {
992 continue;
993 }
994 let rec = &node[start..end];
995 if rec.len() < 7 {
996 continue;
997 }
998 let key_len = rec[0] as usize;
999 let key_end = 1 + key_len;
1000 if key_end > rec.len() {
1001 continue;
1002 }
1003 if Self::catalog_compare(&rec[..key_end], &search_key) == Ordering::Equal {
1004 return Some((leaf_idx, rec_idx, off + start));
1005 }
1006 }
1007 None
1008 }
1009
1010 fn find_catalog_record_by_cnid(&self, cnid: u32) -> Option<(u32, usize, usize)> {
1012 self.find_catalog_record_by_name(cnid, &[])
1013 }
1014
1015 fn update_parent_valence(&mut self, parent_id: u32, delta: i32) -> Result<(), FilesystemError> {
1017 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1018 let first_leaf = BigEndian::read_u32(&self.catalog_data[24..28]);
1019
1020 let val_off = hfs_common::walk_leaf_records(
1023 &self.catalog_data,
1024 first_leaf,
1025 node_size,
1026 |_node_idx, _rec_idx, abs_off, rec| {
1027 if rec.len() < 7 {
1028 return None;
1029 }
1030 let key_len = rec[0] as usize;
1031 let mut data_off = abs_off + 1 + key_len;
1032 if !data_off.is_multiple_of(2) {
1033 data_off += 1;
1034 }
1035 let abs_end = abs_off + rec.len();
1036 if data_off + 10 > abs_end {
1037 return None;
1038 }
1039 if self.catalog_data[data_off] as i8 != CATALOG_DIR {
1040 return None;
1041 }
1042 let dir_id = BigEndian::read_u32(&self.catalog_data[data_off + 6..data_off + 10]);
1044 if dir_id == parent_id {
1045 Some(data_off + 4)
1046 } else {
1047 None
1048 }
1049 },
1050 );
1051 let val_off = val_off.ok_or_else(|| {
1052 FilesystemError::NotFound(format!(
1053 "parent directory CNID {} not found in catalog",
1054 parent_id
1055 ))
1056 })?;
1057 let old_val = BigEndian::read_u16(&self.catalog_data[val_off..val_off + 2]);
1058 let new_val = (old_val as i32 + delta).max(0) as u16;
1059 BigEndian::write_u16(&mut self.catalog_data[val_off..val_off + 2], new_val);
1060 Ok(())
1061 }
1062
1063 fn insert_catalog_record(&mut self, key_record: &[u8]) -> Result<(), FilesystemError>
1081 where
1082 R: Write,
1083 {
1084 let mut grown = false;
1087 loop {
1088 match hfs_common::btree_insert_full(
1089 &mut self.catalog_data,
1090 key_record,
1091 &hfs_common::BTreeKeyFormat::CLASSIC_CATALOG,
1092 &Self::catalog_compare,
1093 ) {
1094 Ok(()) => return Ok(()),
1095 Err(FilesystemError::DiskFull(_)) if !grown => {
1096 self.grow_btree(HfsTree::Catalog)?;
1097 grown = true;
1098 }
1099 Err(e) => return Err(e),
1100 }
1101 }
1102 }
1103
1104 fn grow_btree(&mut self, tree: HfsTree) -> Result<(), FilesystemError>
1109 where
1110 R: Write,
1111 {
1112 let block_size = self.mdb.block_size;
1113 if tree == HfsTree::Extents {
1114 self.ensure_extents_overflow()?;
1115 }
1116 let (node_size, total_nodes, existing_map) = {
1117 let buf = self.tree_buf(tree)?;
1118 let h = BTreeHeader::read(buf);
1119 let segs = hfs_common::btree_bitmap_segments(buf, h.node_size as usize);
1120 (
1121 h.node_size as usize,
1122 h.total_nodes,
1123 (segs.len() as u32).saturating_sub(1),
1124 )
1125 };
1126 if node_size < 256 || block_size == 0 {
1127 return Err(FilesystemError::InvalidData(format!(
1128 "{} B-tree has node size {node_size}",
1129 tree.label()
1130 )));
1131 }
1132 let clump_off = if tree == HfsTree::Catalog { 78 } else { 74 };
1134 let clump = BigEndian::read_u32(&self.mdb.raw_sector[clump_off..clump_off + 4]) as usize;
1135 let grow_nodes = (clump.div_ceil(node_size).max(1) as u32).max(8);
1136 let header_cap = ((node_size - 256) * 8) as u32;
1137 let per_map = (hfs_common::map_node_bitmap_bytes(node_size) * 8) as u32;
1138 let need_map = total_nodes + grow_nodes > header_cap + existing_map * per_map;
1139 let add_slots = grow_nodes + u32::from(need_map);
1140 let grow_blocks = btree_grow_blocks(total_nodes, add_slots, node_size, block_size);
1141 let mut add_slots = add_slots;
1142 if grow_blocks > 0 {
1143 let mut inline = match tree {
1144 HfsTree::Catalog => self.mdb.catalog_file_extents,
1145 HfsTree::Extents => self.mdb.extents_file_extents,
1146 };
1147 let mut all: Vec<HfsExtDescriptor> = inline
1149 .iter()
1150 .copied()
1151 .filter(|e| e.block_count > 0)
1152 .collect();
1153 let inline_blocks: u32 = all.iter().map(|e| e.block_count as u32).sum();
1154 if tree == HfsTree::Catalog && all.len() == 3 {
1155 self.ensure_extents_overflow()?;
1156 if let Some(x) = self.extents_overflow_data.as_ref() {
1157 all.extend(collect_fork_overflow_extents(
1158 x,
1159 CNID_CATALOG,
1160 0x00,
1161 inline_blocks,
1162 ));
1163 }
1164 }
1165 let tail = all
1166 .last()
1167 .map(|e| e.start_block as u32 + e.block_count as u32);
1168 match tail {
1169 Some(t) if self.allocate_blocks_at(t, grow_blocks)? => {
1170 all.last_mut().unwrap().block_count += grow_blocks as u16;
1171 }
1172 _ => {
1173 let want_nodes = (total_nodes + add_slots).max(grow_nodes + 8);
1176 let more = btree_grow_blocks(
1177 total_nodes,
1178 want_nodes - total_nodes,
1179 node_size,
1180 block_size,
1181 )
1182 .max(grow_blocks);
1183 add_slots = want_nodes.max(total_nodes + add_slots) - total_nodes;
1184 if tree == HfsTree::Extents && all.len() >= 3 {
1185 return Err(FilesystemError::DiskFull(
1186 "extents-overflow file has no free extent slot to grow into".into(),
1187 ));
1188 }
1189 let (start, more) = match self.allocate_blocks(more) {
1191 Ok(start) => (start, more),
1192 Err(_) => {
1193 add_slots = grow_nodes + u32::from(need_map);
1194 (self.allocate_blocks(grow_blocks)?, grow_blocks)
1195 }
1196 };
1197 all.push(HfsExtDescriptor {
1198 start_block: start as u16,
1199 block_count: more as u16,
1200 });
1201 }
1202 }
1203 inline = [HfsExtDescriptor {
1204 start_block: 0,
1205 block_count: 0,
1206 }; 3];
1207 for (i, e) in all.iter().take(3).enumerate() {
1208 inline[i] = *e;
1209 }
1210 match tree {
1211 HfsTree::Catalog => {
1212 self.mdb.catalog_file_extents = inline;
1213 if all.len() > 3 {
1214 let data = self.extents_overflow_data.as_mut().expect("loaded above");
1216 remove_fork_overflow_records(data, CNID_CATALOG, 0x00)?;
1217 self.insert_fork_overflow(CNID_CATALOG, 0x00, &all)?;
1218 self.write_extents_overflow_back()?;
1219 }
1220 }
1221 HfsTree::Extents => self.mdb.extents_file_extents = inline,
1222 }
1223 }
1224 let buf = self.tree_buf(tree)?;
1225 buf.resize(buf.len() + add_slots as usize * node_size, 0);
1226 if need_map {
1227 let map_idx = total_nodes;
1230 let tail = btree_map_chain_tail(buf, node_size);
1231 hfs_common::init_map_node(buf, node_size, map_idx, tail, 0);
1232 let linker = tail as usize * node_size;
1233 BigEndian::write_u32(&mut buf[linker..linker + 4], map_idx);
1234 hfs_common::btree_bitmap_set(buf, node_size, map_idx);
1235 }
1236 let mut h = BTreeHeader::read(buf);
1237 h.total_nodes += add_slots;
1238 h.free_nodes += add_slots - u32::from(need_map);
1239 h.write(buf);
1240 let added = add_slots * node_size as u32;
1241 match tree {
1242 HfsTree::Catalog => self.mdb.catalog_file_size += added,
1243 HfsTree::Extents => self.mdb.extents_file_size += added,
1244 }
1245 Ok(())
1246 }
1247
1248 fn tree_buf(&mut self, tree: HfsTree) -> Result<&mut Vec<u8>, FilesystemError> {
1250 match tree {
1251 HfsTree::Catalog => Ok(&mut self.catalog_data),
1252 HfsTree::Extents => self.extents_overflow_data.as_mut().ok_or_else(|| {
1253 FilesystemError::Unsupported("volume has no extents-overflow B-tree".into())
1254 }),
1255 }
1256 }
1257
1258 fn allocate_blocks_at(&mut self, start: u32, count: u32) -> Result<bool, FilesystemError> {
1260 self.ensure_bitmap()?;
1261 let total = self.mdb.total_blocks as u32;
1262 let bitmap = self.bitmap.as_mut().unwrap();
1263 if start + count > total || (start..start + count).any(|b| bitmap_test_bit_be(bitmap, b)) {
1264 return Ok(false);
1265 }
1266 for b in start..start + count {
1267 bitmap_set_bit_be(bitmap, b);
1268 }
1269 self.mdb.free_blocks = self.mdb.free_blocks.saturating_sub(count as u16);
1270 Ok(true)
1271 }
1272
1273 fn remove_catalog_record(
1275 &mut self,
1276 node_idx: u32,
1277 rec_idx: usize,
1278 ) -> Result<(), FilesystemError> {
1279 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1280 if node_size == 0 {
1281 return Ok(());
1282 }
1283 let offset = node_idx as usize * node_size;
1284 if offset + node_size > self.catalog_data.len() {
1285 return Ok(());
1286 }
1287
1288 let num_before = {
1289 let node = &self.catalog_data[offset..offset + node_size];
1290 BigEndian::read_u16(&node[10..12]) as usize
1291 };
1292
1293 {
1294 let node = &mut self.catalog_data[offset..offset + node_size];
1295 btree_remove_record(node, node_size, rec_idx);
1296 }
1297
1298 let node = &self.catalog_data[offset..offset + node_size];
1300 let num_after = BigEndian::read_u16(&node[10..12]) as usize;
1301 if num_after == 0 && num_before > 0 {
1302 let prev = BigEndian::read_u32(&self.catalog_data[offset + 4..offset + 8]);
1303 let next = BigEndian::read_u32(&self.catalog_data[offset..offset + 4]);
1304
1305 if prev != 0 {
1306 let prev_off = prev as usize * node_size;
1307 if prev_off + 4 <= self.catalog_data.len() {
1308 BigEndian::write_u32(&mut self.catalog_data[prev_off..prev_off + 4], next);
1309 }
1310 }
1311 if next != 0 {
1312 let next_off = next as usize * node_size;
1313 if next_off + 8 <= self.catalog_data.len() {
1314 BigEndian::write_u32(&mut self.catalog_data[next_off + 4..next_off + 8], prev);
1315 }
1316 }
1317
1318 let mut h = BTreeHeader::read(&self.catalog_data);
1319 if h.first_leaf_node == node_idx {
1320 h.first_leaf_node = next;
1321 }
1322 if h.last_leaf_node == node_idx {
1323 h.last_leaf_node = prev;
1324 }
1325 btree_free_node(&mut self.catalog_data, node_size, node_idx);
1326 h.free_nodes += 1;
1327 if h.root_node == node_idx {
1329 h.root_node = 0;
1330 h.depth = 0;
1331 }
1332 h.write(&mut self.catalog_data);
1333
1334 let header = BTreeHeader::read(&self.catalog_data);
1336 if header.depth > 1 {
1337 let mut dummy_report = disk_core::fsck::RepairReport {
1338 fixes_applied: Vec::new(),
1339 fixes_failed: Vec::new(),
1340 unrepairable_count: 0,
1341 };
1342 crate::hfs_fsck::rebuild_index_nodes(
1343 &mut self.catalog_data,
1344 node_size,
1345 &mut dummy_report,
1346 );
1347 }
1348 } else if rec_idx == 0 && num_after > 0 {
1349 btree_refresh_index_keys(
1352 &mut self.catalog_data,
1353 node_size,
1354 node_idx,
1355 &BTreeKeyFormat::CLASSIC_CATALOG,
1356 &Self::catalog_compare,
1357 )?;
1358 }
1359
1360 let mut h = BTreeHeader::read(&self.catalog_data);
1362 h.leaf_records = h.leaf_records.saturating_sub(1);
1363 h.write(&mut self.catalog_data);
1364 Ok(())
1365 }
1366
1367 fn snapshot(&self) -> Option<(Vec<u8>, Option<Vec<u8>>, HfsMasterDirectoryBlock)> {
1376 if self.bulk_mode {
1377 return None;
1378 }
1379 Some((
1380 self.catalog_data.clone(),
1381 self.bitmap.clone(),
1382 self.mdb.clone(),
1383 ))
1384 }
1385
1386 fn restore_snapshot(
1389 &mut self,
1390 snap: Option<(Vec<u8>, Option<Vec<u8>>, HfsMasterDirectoryBlock)>,
1391 ) {
1392 let Some((catalog_data, bitmap, mdb)) = snap else {
1393 return;
1394 };
1395 self.catalog_data = catalog_data;
1396 self.bitmap = bitmap;
1397 self.mdb = mdb;
1398 }
1399
1400 pub(crate) fn mdb(&self) -> &HfsMasterDirectoryBlock {
1401 &self.mdb
1402 }
1403
1404 pub fn volume_summary(&self) -> HfsVolumeSummary {
1408 let used_blocks = self.mdb.total_blocks.saturating_sub(self.mdb.free_blocks) as u64;
1409 HfsVolumeSummary {
1410 volume_name: self.mdb.volume_name.clone(),
1411 block_size: self.mdb.block_size,
1412 total_blocks: self.mdb.total_blocks,
1413 free_blocks: self.mdb.free_blocks,
1414 used_bytes: used_blocks * self.mdb.block_size as u64,
1415 file_count: self.mdb.file_count,
1416 folder_count: self.mdb.folder_count,
1417 catalog_file_size: self.mdb.catalog_file_size,
1418 extents_file_size: self.mdb.extents_file_size,
1419 }
1420 }
1421
1422 pub(crate) fn catalog_data(&self) -> &[u8] {
1423 &self.catalog_data
1424 }
1425
1426 #[allow(dead_code)]
1427 pub(crate) fn partition_offset(&self) -> u64 {
1428 self.partition_offset
1429 }
1430
1431 pub fn locate_file_by_cnid(&self, cnid: u32) -> Option<HfsFileLocation> {
1443 let (data_size, data_extents, _rsrc_size, _rsrc_extents) = self.find_file_by_id(cnid)?;
1444 let first = data_extents.first().copied()?;
1445 if first.block_count == 0 {
1446 return None;
1447 }
1448 let fragmented = data_extents.iter().skip(1).any(|e| e.block_count != 0);
1449 let offset_in_image =
1453 self.partition_offset + self.alloc_block_offset(first.start_block as u32);
1454 Some(HfsFileLocation {
1455 offset_in_image,
1456 length: data_size as u64,
1457 fragmented,
1458 })
1459 }
1460
1461 pub(crate) fn read_boot_blocks(&mut self) -> Result<[u8; 1024], FilesystemError> {
1463 self.reader.seek(SeekFrom::Start(self.partition_offset))?;
1464 let mut buf = [0u8; 1024];
1465 self.reader.read_exact(&mut buf)?;
1466 Ok(buf)
1467 }
1468
1469 #[allow(dead_code)]
1472 pub(crate) fn read_fork(
1473 &mut self,
1474 extents: &[HfsExtDescriptor; 3],
1475 size: u64,
1476 ) -> Result<Vec<u8>, FilesystemError> {
1477 read_fork_data(
1478 &mut self.reader,
1479 self.partition_offset,
1480 &self.mdb,
1481 extents,
1482 size,
1483 )
1484 }
1485
1486 fn ensure_extents_overflow(&mut self) -> Result<(), FilesystemError> {
1489 if self.extents_overflow_data.is_some() {
1490 return Ok(());
1491 }
1492 if self.mdb.extents_file_size == 0 {
1493 return Ok(());
1494 }
1495 let data = read_fork_data(
1496 &mut self.reader,
1497 self.partition_offset,
1498 &self.mdb,
1499 &self.mdb.extents_file_extents,
1500 self.mdb.extents_file_size as u64,
1501 )?;
1502 self.extents_overflow_data = Some(data);
1503 Ok(())
1504 }
1505
1506 fn write_fork_with_overflow_to(
1513 &mut self,
1514 file_id: u32,
1515 fork_type: u8,
1516 inline: &[HfsExtDescriptor; 3],
1517 size: u64,
1518 writer: &mut dyn std::io::Write,
1519 ) -> Result<u64, FilesystemError> {
1520 let mut written = write_fork_data_to(
1521 &mut self.reader,
1522 self.partition_offset,
1523 &self.mdb,
1524 inline,
1525 writer,
1526 size,
1527 )?;
1528 if written >= size {
1529 return Ok(written);
1530 }
1531 self.ensure_extents_overflow()?;
1532 let Some(ext_data) = self.extents_overflow_data.as_ref() else {
1533 return Err(FilesystemError::InvalidData(format!(
1534 "file {file_id} fork {fork_type:#x}: {size} bytes requested but only {written} \
1535 bytes in inline extents and no extents-overflow B-tree present",
1536 )));
1537 };
1538 let inline_blocks: u32 = inline.iter().map(|e| e.block_count as u32).sum();
1539 let overflow = collect_fork_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
1540 let block_size = self.mdb.block_size as u64;
1541 let first_alloc_offset = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
1542 let mut buf = vec![0u8; 64 * 1024];
1543 for ext in overflow {
1544 if written >= size {
1545 break;
1546 }
1547 if ext.block_count == 0 {
1548 continue;
1549 }
1550 let offset = first_alloc_offset + ext.start_block as u64 * block_size;
1551 let extent_len = ext.block_count as u64 * block_size;
1552 let to_emit = extent_len.min(size - written);
1553 self.reader.seek(SeekFrom::Start(offset))?;
1554 let mut left = to_emit;
1555 while left > 0 {
1556 let n = (buf.len() as u64).min(left) as usize;
1557 self.reader.read_exact(&mut buf[..n])?;
1558 writer.write_all(&buf[..n])?;
1559 left -= n as u64;
1560 }
1561 written += to_emit;
1562 }
1563 if written < size {
1564 return Err(FilesystemError::InvalidData(format!(
1565 "file {file_id} fork {fork_type:#x}: extents (inline + overflow) cover \
1566 {written} of {size} bytes",
1567 )));
1568 }
1569 Ok(written)
1570 }
1571
1572 fn read_fork_with_overflow(
1573 &mut self,
1574 file_id: u32,
1575 fork_type: u8,
1576 inline: &[HfsExtDescriptor; 3],
1577 size: u64,
1578 ) -> Result<Vec<u8>, FilesystemError> {
1579 let mut data = read_fork_data(
1580 &mut self.reader,
1581 self.partition_offset,
1582 &self.mdb,
1583 inline,
1584 size,
1585 )?;
1586 if (data.len() as u64) >= size {
1587 return Ok(data);
1588 }
1589
1590 self.ensure_extents_overflow()?;
1591 let Some(ext_data) = self.extents_overflow_data.as_ref() else {
1592 return Err(FilesystemError::InvalidData(format!(
1593 "file {file_id} fork {fork_type:#x}: {size} bytes requested but only {} bytes \
1594 in inline extents and no extents-overflow B-tree present",
1595 data.len()
1596 )));
1597 };
1598
1599 let inline_blocks: u32 = inline.iter().map(|e| e.block_count as u32).sum();
1600 let overflow = collect_fork_overflow_extents(ext_data, file_id, fork_type, inline_blocks);
1601
1602 let block_size = self.mdb.block_size as u64;
1603 let first_alloc_offset = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
1604
1605 for ext in overflow {
1606 if data.len() as u64 >= size {
1607 break;
1608 }
1609 if ext.block_count == 0 {
1610 continue;
1611 }
1612 let offset = first_alloc_offset + ext.start_block as u64 * block_size;
1613 let extent_len = ext.block_count as u64 * block_size;
1614 let to_read = extent_len.min(size - data.len() as u64) as usize;
1615 self.reader.seek(SeekFrom::Start(offset))?;
1616 let mut buf = vec![0u8; to_read];
1617 self.reader.read_exact(&mut buf)?;
1618 data.extend_from_slice(&buf);
1619 }
1620
1621 if (data.len() as u64) < size {
1622 return Err(FilesystemError::InvalidData(format!(
1623 "file {file_id} fork {fork_type:#x}: extents (inline + overflow) cover \
1624 {} of {size} bytes",
1625 data.len()
1626 )));
1627 }
1628 Ok(data)
1629 }
1630
1631 fn locate_record_data(&self, cnid: u32) -> Result<usize, FilesystemError> {
1643 if let Some((_, _, t_offset)) = self.find_catalog_record_by_cnid(cnid) {
1644 let key_len = self.catalog_data[t_offset] as usize;
1645 let mut rec_data_start = t_offset + 1 + key_len;
1646 if !rec_data_start.is_multiple_of(2) {
1647 rec_data_start += 1;
1648 }
1649 let thread_parent =
1650 BigEndian::read_u32(&self.catalog_data[rec_data_start + 10..rec_data_start + 14]);
1651 let thread_name_len = self.catalog_data[rec_data_start + 14] as usize;
1652 let thread_name = self.catalog_data
1653 [rec_data_start + 15..rec_data_start + 15 + thread_name_len]
1654 .to_vec();
1655
1656 if let Some((_, _, f_offset)) =
1657 self.find_catalog_record_by_name(thread_parent, &thread_name)
1658 {
1659 let fkey_len = self.catalog_data[f_offset] as usize;
1660 let mut frec_start = f_offset + 1 + fkey_len;
1661 if !frec_start.is_multiple_of(2) {
1662 frec_start += 1;
1663 }
1664 return Ok(frec_start);
1665 }
1666 }
1667
1668 if let Some((_, _, frec_start)) = self.find_file_record_offset_by_cnid(cnid) {
1669 return Ok(frec_start);
1670 }
1671
1672 Err(FilesystemError::NotFound(format!(
1673 "catalog record for CNID {cnid} not found"
1674 )))
1675 }
1676
1677 fn find_file_record_offset_by_cnid(&self, cnid: u32) -> Option<(u32, usize, usize)> {
1686 if self.catalog_data.len() < 512 {
1687 return None;
1688 }
1689 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1690 let first_leaf = BigEndian::read_u32(&self.catalog_data[24..28]);
1691
1692 hfs_common::walk_leaf_records(
1693 &self.catalog_data,
1694 first_leaf,
1695 node_size,
1696 |node_idx, rec_idx, abs_off, rec| {
1697 if rec.len() < 7 {
1698 return None;
1699 }
1700 let key_len = rec[0] as usize;
1701 if key_len < 6 || 1 + key_len > rec.len() {
1702 return None;
1703 }
1704 let mut data_rel = 1 + key_len;
1705 if !data_rel.is_multiple_of(2) {
1706 data_rel += 1;
1707 }
1708 if data_rel + 24 > rec.len() {
1709 return None;
1710 }
1711 if rec[data_rel] as i8 != CATALOG_FILE {
1712 return None;
1713 }
1714 let rec_cnid = BigEndian::read_u32(&rec[data_rel + 20..data_rel + 24]);
1716 if rec_cnid == cnid {
1717 Some((node_idx, rec_idx, abs_off + data_rel))
1718 } else {
1719 None
1720 }
1721 },
1722 )
1723 }
1724
1725 pub fn set_finder_info(
1728 &mut self,
1729 entry: &FileEntry,
1730 finfo: [u8; 16],
1731 fxinfo: [u8; 16],
1732 ) -> Result<(), FilesystemError> {
1733 if entry.is_directory() {
1734 return Err(FilesystemError::InvalidData(
1735 "set_finder_info: entry is a directory".into(),
1736 ));
1737 }
1738 let snap = self.snapshot();
1739 let result = (|| {
1740 let rec = self.locate_record_data(entry.location as u32)?;
1741 if self.catalog_data[rec] as i8 != CATALOG_FILE {
1742 return Err(FilesystemError::InvalidData("not a file record".into()));
1743 }
1744 self.catalog_data[rec + 4..rec + 20].copy_from_slice(&finfo);
1745 self.catalog_data[rec + 56..rec + 72].copy_from_slice(&fxinfo);
1746 Ok(())
1747 })();
1748 if result.is_err() {
1749 self.restore_snapshot(snap);
1750 }
1751 result
1752 }
1753
1754 pub fn set_directory_finder_info(
1757 &mut self,
1758 entry: &FileEntry,
1759 dinfo: [u8; 16],
1760 dxinfo: [u8; 16],
1761 ) -> Result<(), FilesystemError> {
1762 if !entry.is_directory() {
1763 return Err(FilesystemError::InvalidData(
1764 "set_directory_finder_info: entry is not a directory".into(),
1765 ));
1766 }
1767 let snap = self.snapshot();
1768 let result = (|| {
1769 let rec = self.locate_record_data(entry.location as u32)?;
1770 if self.catalog_data[rec] as i8 != CATALOG_DIR {
1771 return Err(FilesystemError::InvalidData(
1772 "not a directory record".into(),
1773 ));
1774 }
1775 self.catalog_data[rec + 22..rec + 38].copy_from_slice(&dinfo);
1776 self.catalog_data[rec + 38..rec + 54].copy_from_slice(&dxinfo);
1777 Ok(())
1778 })();
1779 if result.is_err() {
1780 self.restore_snapshot(snap);
1781 }
1782 result
1783 }
1784
1785 pub fn set_dates(
1789 &mut self,
1790 entry: &FileEntry,
1791 create: u32,
1792 modify: u32,
1793 backup: u32,
1794 ) -> Result<(), FilesystemError> {
1795 let snap = self.snapshot();
1796 let result = (|| {
1797 let rec = self.locate_record_data(entry.location as u32)?;
1798 let record_type = self.catalog_data[rec] as i8;
1799 let (cr_off, md_off, bk_off) = match record_type {
1800 CATALOG_FILE => (44, 48, 52),
1801 CATALOG_DIR => (10, 14, 18),
1802 other => {
1803 return Err(FilesystemError::InvalidData(format!(
1804 "unexpected catalog record type: {other}"
1805 )))
1806 }
1807 };
1808 BigEndian::write_u32(
1809 &mut self.catalog_data[rec + cr_off..rec + cr_off + 4],
1810 create,
1811 );
1812 BigEndian::write_u32(
1813 &mut self.catalog_data[rec + md_off..rec + md_off + 4],
1814 modify,
1815 );
1816 BigEndian::write_u32(
1817 &mut self.catalog_data[rec + bk_off..rec + bk_off + 4],
1818 backup,
1819 );
1820 Ok(())
1821 })();
1822 if result.is_err() {
1823 self.restore_snapshot(snap);
1824 }
1825 result
1826 }
1827
1828 pub fn set_file_locked(
1831 &mut self,
1832 entry: &FileEntry,
1833 locked: bool,
1834 ) -> Result<(), FilesystemError> {
1835 if entry.is_directory() {
1836 return Err(FilesystemError::InvalidData(
1837 "set_file_locked: entry is a directory".into(),
1838 ));
1839 }
1840 let snap = self.snapshot();
1841 let result = (|| {
1842 let rec = self.locate_record_data(entry.location as u32)?;
1843 if self.catalog_data[rec] as i8 != CATALOG_FILE {
1844 return Err(FilesystemError::InvalidData("not a file record".into()));
1845 }
1846 let flags = self.catalog_data[rec + 2];
1847 self.catalog_data[rec + 2] = if locked { flags | 0x01 } else { flags & !0x01 };
1848 Ok(())
1849 })();
1850 if result.is_err() {
1851 self.restore_snapshot(snap);
1852 }
1853 result
1854 }
1855
1856 pub fn set_volume_dates(&mut self, create: u32, modify: u32, backup: u32) {
1860 self.pending_volume_dates = Some((create, modify, backup));
1861 self.mdb.create_date = create;
1862 self.mdb.modify_date = modify;
1863 BigEndian::write_u32(&mut self.mdb.raw_sector[2..6], create);
1864 BigEndian::write_u32(&mut self.mdb.raw_sector[6..10], modify);
1865 BigEndian::write_u32(&mut self.mdb.raw_sector[64..68], backup);
1866 }
1867
1868 pub fn set_boot_blocks(&mut self, blocks: &[u8; 1024]) {
1871 self.pending_boot_blocks = Some(Box::new(*blocks));
1872 }
1873
1874 pub fn set_volume_finder_info(&mut self, finder_info: &[u8; 32]) {
1878 for i in 0..8 {
1879 self.mdb.finder_info[i] = BigEndian::read_u32(&finder_info[i * 4..(i + 1) * 4]);
1880 }
1881 }
1882
1883 pub fn set_volume_name(&mut self, new_name: &str) -> Result<(), FilesystemError>
1889 where
1890 R: Write,
1891 {
1892 let new_name_raw = utf8_to_mac_roman(new_name)?;
1893 if new_name_raw.is_empty() {
1894 return Err(FilesystemError::InvalidData(
1895 "HFS volume name must not be empty (Mac OS rejects unnamed volumes with -127)"
1896 .into(),
1897 ));
1898 }
1899 if new_name_raw.len() > 27 {
1900 return Err(FilesystemError::InvalidData(format!(
1901 "HFS volume name '{new_name}' exceeds 27 Mac Roman bytes"
1902 )));
1903 }
1904
1905 let snap = self.snapshot();
1906 let result = (|| -> Result<(), FilesystemError> {
1907 let old_name_raw = self.mdb.volume_name_raw.clone();
1908 if new_name_raw == old_name_raw {
1909 return Ok(());
1910 }
1911
1912 let (node_idx, rec_idx, key_abs) = self
1914 .find_catalog_record_by_name(1, &old_name_raw)
1915 .ok_or_else(|| {
1916 FilesystemError::NotFound("root directory record not found in catalog".into())
1917 })?;
1918
1919 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
1922 let node_off = node_idx as usize * node_size;
1923 let node = &self.catalog_data[node_off..node_off + node_size];
1924 let (rec_start, rec_end) = hfs_common::btree_record_range(node, node_size, rec_idx);
1925 debug_assert_eq!(key_abs, node_off + rec_start);
1927 let key_len = node[rec_start] as usize;
1928 let mut data_rel = 1 + key_len;
1929 if !data_rel.is_multiple_of(2) {
1930 data_rel += 1;
1931 }
1932 let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
1933
1934 let new_key = Self::build_catalog_key(1, &new_name_raw);
1936 let mut new_record = new_key;
1937 if !new_record.len().is_multiple_of(2) {
1938 new_record.push(0);
1939 }
1940 new_record.extend_from_slice(&data_bytes);
1941 if !new_record.len().is_multiple_of(2) {
1942 new_record.push(0);
1943 }
1944
1945 self.remove_catalog_record(node_idx, rec_idx)?;
1946 self.insert_catalog_record(&new_record)?;
1947
1948 if let Some((_n, _r, t_key_abs)) = self.find_catalog_record_by_cnid(2) {
1952 let t_key_len = self.catalog_data[t_key_abs] as usize;
1953 let mut t_data_off = t_key_abs + 1 + t_key_len;
1954 if !t_data_off.is_multiple_of(2) {
1955 t_data_off += 1;
1956 }
1957 if t_data_off + 15 + 27 <= self.catalog_data.len() {
1958 self.catalog_data[t_data_off + 14] = new_name_raw.len() as u8;
1959 for i in 0..27 {
1960 self.catalog_data[t_data_off + 15 + i] = 0;
1961 }
1962 self.catalog_data[t_data_off + 15..t_data_off + 15 + new_name_raw.len()]
1963 .copy_from_slice(&new_name_raw);
1964 }
1965 }
1966
1967 self.mdb.volume_name = mac_roman_to_utf8(&new_name_raw);
1972 self.mdb.volume_name_raw = new_name_raw.clone();
1973 self.mdb.raw_sector[36] = new_name_raw.len() as u8;
1974 for i in 0..27 {
1975 self.mdb.raw_sector[37 + i] = 0;
1976 }
1977 self.mdb.raw_sector[37..37 + new_name_raw.len()].copy_from_slice(&new_name_raw);
1978
1979 Ok(())
1980 })();
1981 if result.is_err() {
1982 self.restore_snapshot(snap);
1983 }
1984 result
1985 }
1986}
1987
1988impl<R: Read + Write + Seek> HfsFilesystem<R> {
1990 fn write_block(&mut self, block: u32, data: &[u8]) -> Result<(), FilesystemError> {
1992 let offset = self.partition_offset + self.alloc_block_offset(block);
1993 self.reader.seek(SeekFrom::Start(offset))?;
1994 self.reader.write_all(data)?;
1995 Ok(())
1996 }
1997
1998 fn write_mdb(&mut self) -> Result<(), FilesystemError> {
2004 BigEndian::write_u16(&mut self.mdb.raw_sector[10..12], 0x0100);
2012
2013 let (root_files, root_dirs) = match self.list_children(2) {
2019 Ok(children) => {
2020 let mut f = 0u16;
2021 let mut d = 0u16;
2022 for c in &children {
2023 match c {
2024 CatalogRecord::File { .. } => f = f.saturating_add(1),
2025 CatalogRecord::Directory { .. } => d = d.saturating_add(1),
2026 }
2027 }
2028 (f, d)
2029 }
2030 Err(_) => (0, 0),
2031 };
2032 BigEndian::write_u16(&mut self.mdb.raw_sector[12..14], root_files);
2033 BigEndian::write_u16(&mut self.mdb.raw_sector[82..84], root_dirs);
2034
2035 let mdb_bytes = self.mdb.serialize_to_sector();
2036 self.reader
2037 .seek(SeekFrom::Start(self.partition_offset + 1024))?;
2038 self.reader.write_all(&mdb_bytes)?;
2039 if let Some(alt_offset) = self.alt_mdb_offset() {
2043 self.reader.seek(SeekFrom::Start(alt_offset))?;
2044 self.reader.write_all(&mdb_bytes)?;
2045 }
2046 Ok(())
2047 }
2048
2049 fn write_catalog(&mut self) -> Result<(), FilesystemError> {
2051 write_hfs_fork_data(
2052 &mut self.reader,
2053 self.partition_offset,
2054 &self.mdb,
2055 &self.mdb.catalog_file_extents,
2056 &self.catalog_data,
2057 )?;
2058 let inline_blocks: u32 = self
2061 .mdb
2062 .catalog_file_extents
2063 .iter()
2064 .map(|e| e.block_count as u32)
2065 .sum();
2066 let inline_bytes = inline_blocks as usize * self.mdb.block_size as usize;
2067 if self.catalog_data.len() <= inline_bytes {
2068 return Ok(());
2069 }
2070 self.ensure_extents_overflow()?;
2071 let Some(ext_data) = self.extents_overflow_data.as_ref() else {
2072 return Err(FilesystemError::InvalidData(
2073 "catalog file runs past its inline extents and the volume has no extents-overflow tree".into(),
2074 ));
2075 };
2076 let overflow = collect_fork_overflow_extents(ext_data, CNID_CATALOG, 0x00, inline_blocks);
2077 let first_alloc = self.partition_offset + self.mdb.first_alloc_block as u64 * 512;
2078 let mut written = inline_bytes;
2079 for ext in overflow {
2080 if written >= self.catalog_data.len() {
2081 break;
2082 }
2083 let off = first_alloc + ext.start_block as u64 * self.mdb.block_size as u64;
2084 let len = (ext.block_count as usize * self.mdb.block_size as usize)
2085 .min(self.catalog_data.len() - written);
2086 self.reader.seek(SeekFrom::Start(off))?;
2087 self.reader
2088 .write_all(&self.catalog_data[written..written + len])?;
2089 written += len;
2090 }
2091 if written < self.catalog_data.len() {
2092 return Err(FilesystemError::InvalidData(format!(
2093 "catalog file: extents cover {written} of {} bytes",
2094 self.catalog_data.len()
2095 )));
2096 }
2097 Ok(())
2098 }
2099
2100 fn write_volume_bitmap(&mut self) -> Result<(), FilesystemError> {
2102 if let Some(ref bm) = self.bitmap {
2103 let bitmap_offset = self.partition_offset + self.mdb.volume_bitmap_block as u64 * 512;
2104 self.reader.seek(SeekFrom::Start(bitmap_offset))?;
2105 self.reader.write_all(bm)?;
2106 }
2107 Ok(())
2108 }
2109
2110 fn allocate_blocks(&mut self, count: u32) -> Result<u32, FilesystemError> {
2113 self.ensure_bitmap()?;
2114 let bitmap = self.bitmap.as_mut().unwrap();
2115 let start = bitmap_find_clear_run_be(bitmap, self.mdb.total_blocks as u32, count)
2116 .ok_or_else(|| {
2117 FilesystemError::DiskFull(format!("cannot find {} contiguous free blocks", count))
2118 })?;
2119 for i in 0..count {
2120 bitmap_set_bit_be(bitmap, start + i);
2121 }
2122 self.mdb.free_blocks = self
2124 .mdb
2125 .free_blocks
2126 .saturating_sub(count.min(u16::MAX as u32) as u16);
2127 let next = (start + count) % self.mdb.total_blocks.max(1) as u32;
2133 BigEndian::write_u16(&mut self.mdb.raw_sector[16..18], next as u16);
2134 Ok(start)
2135 }
2136
2137 fn allocate_extents(&mut self, count: u32) -> Result<Vec<HfsExtDescriptor>, FilesystemError> {
2140 if count == 0 {
2141 return Ok(Vec::new());
2142 }
2143 if count > u16::MAX as u32 {
2144 return Err(FilesystemError::InvalidData(
2145 "file too large for classic HFS (>65535 blocks)".into(),
2146 ));
2147 }
2148 if let Ok(start) = self.allocate_blocks(count) {
2149 return Ok(vec![HfsExtDescriptor {
2150 start_block: start as u16,
2151 block_count: count as u16,
2152 }]);
2153 }
2154 self.ensure_bitmap()?;
2155 let total_blocks = self.mdb.total_blocks as u32;
2156 let free = self.mdb.free_blocks as u32;
2157 if free < count {
2158 return Err(FilesystemError::DiskFull(format!(
2159 "{free} blocks free, {count} requested"
2160 )));
2161 }
2162 let bitmap = self.bitmap.as_mut().unwrap();
2163 let mut extents = Vec::new();
2164 let mut remaining = count;
2165 for (start, len) in bitmap_collect_clear_runs_be(bitmap, total_blocks) {
2166 if remaining == 0 {
2167 break;
2168 }
2169 let take = len.min(remaining);
2170 extents.push(HfsExtDescriptor {
2171 start_block: start as u16,
2172 block_count: take as u16,
2173 });
2174 remaining -= take;
2175 }
2176 if remaining > 0 {
2177 return Err(FilesystemError::DiskFull(format!(
2178 "needed {count} blocks, short by {remaining} after gathering every free run"
2179 )));
2180 }
2181 for ext in &extents {
2182 for i in 0..ext.block_count as u32 {
2183 bitmap_set_bit_be(bitmap, ext.start_block as u32 + i);
2184 }
2185 }
2186 self.mdb.free_blocks = self.mdb.free_blocks.saturating_sub(count as u16);
2187 let last = extents.last().unwrap();
2188 let next = (last.start_block as u32 + last.block_count as u32) % total_blocks.max(1);
2189 BigEndian::write_u16(&mut self.mdb.raw_sector[16..18], next as u16);
2190 Ok(extents)
2191 }
2192
2193 fn free_blocks(&mut self, start: u32, count: u32) {
2195 self.ensure_bitmap().ok();
2196 if let Some(ref mut bitmap) = self.bitmap {
2197 for i in 0..count {
2198 bitmap_clear_bit_be(bitmap, start + i);
2199 }
2200 }
2201 self.mdb.free_blocks += count as u16;
2202 }
2203
2204 fn free_extent_blocks(&mut self, extents: &[HfsExtDescriptor; 3]) {
2206 for ext in extents {
2207 if ext.block_count == 0 {
2208 break;
2209 }
2210 self.free_blocks(ext.start_block as u32, ext.block_count as u32);
2211 }
2212 }
2213
2214 fn free_fork_overflow(&mut self, file_id: u32, fork_type: u8) -> Result<bool, FilesystemError> {
2216 self.ensure_extents_overflow()?;
2217 let Some(data) = self.extents_overflow_data.as_ref() else {
2218 return Ok(false);
2219 };
2220 let extents = collect_fork_overflow_extents(data, file_id, fork_type, 0);
2221 for ext in &extents {
2222 self.free_blocks(ext.start_block as u32, ext.block_count as u32);
2223 }
2224 let data = self.extents_overflow_data.as_mut().expect("loaded above");
2225 Ok(remove_fork_overflow_records(data, file_id, fork_type)? > 0)
2226 }
2227
2228 fn write_extents_overflow_back(&mut self) -> Result<(), FilesystemError> {
2230 if let Some(data) = self.extents_overflow_data.as_ref() {
2231 if self.mdb.extents_file_size > 0 {
2232 write_extents_overflow_data(
2233 &mut self.reader,
2234 self.partition_offset,
2235 &self.mdb,
2236 data,
2237 )?;
2238 }
2239 }
2240 Ok(())
2241 }
2242
2243 fn write_data_to_blocks(
2246 &mut self,
2247 data: &mut dyn std::io::Read,
2248 data_len: u64,
2249 ) -> Result<Vec<HfsExtDescriptor>, FilesystemError> {
2250 if data_len == 0 {
2251 return Ok(Vec::new());
2252 }
2253 let block_size = self.mdb.block_size as u64;
2254 let blocks_needed = data_len.div_ceil(block_size) as u32;
2255 let extents = self.allocate_extents(blocks_needed)?;
2256
2257 let mut buf = vec![0u8; block_size as usize];
2258 let mut remaining = data_len;
2259 for ext in &extents {
2260 for i in 0..ext.block_count as u32 {
2261 let to_read = remaining.min(block_size) as usize;
2262 buf.fill(0);
2263 data.read_exact(&mut buf[..to_read]).map_err(|e| {
2264 FilesystemError::Io(std::io::Error::new(
2265 e.kind(),
2266 format!("reading file data: {e}"),
2267 ))
2268 })?;
2269 self.write_block(ext.start_block as u32 + i, &buf)?;
2270 remaining -= to_read as u64;
2271 }
2272 }
2273
2274 Ok(extents)
2275 }
2276
2277 fn extents_block_total(extents: &[HfsExtDescriptor]) -> u16 {
2279 extents.iter().map(|e| e.block_count).sum()
2280 }
2281
2282 fn set_inline_extents(rec: &mut [u8], base: usize, extents: &[HfsExtDescriptor]) {
2284 rec[base..base + 12].fill(0);
2285 for (i, ext) in extents.iter().take(3).enumerate() {
2286 let off = base + i * 4;
2287 BigEndian::write_u16(&mut rec[off..off + 2], ext.start_block);
2288 BigEndian::write_u16(&mut rec[off + 2..off + 4], ext.block_count);
2289 }
2290 }
2291
2292 fn insert_fork_overflow(
2295 &mut self,
2296 file_id: u32,
2297 fork_type: u8,
2298 extents: &[HfsExtDescriptor],
2299 ) -> Result<bool, FilesystemError> {
2300 use crate::hfs_common::compare_classic_extents_keys;
2301 if extents.len() <= 3 {
2302 return Ok(false);
2303 }
2304 self.ensure_extents_overflow()?;
2305 if self.extents_overflow_data.is_none() {
2306 return Err(FilesystemError::Unsupported(
2307 "volume has no extents-overflow B-tree; a fragmented fork cannot be created".into(),
2308 ));
2309 }
2310 let mut fork_block: u32 = extents.iter().take(3).map(|e| e.block_count as u32).sum();
2311 for chunk in extents[3..].chunks(3) {
2312 let mut rec = vec![7u8, fork_type];
2314 rec.extend_from_slice(&file_id.to_be_bytes());
2315 rec.extend_from_slice(&(fork_block as u16).to_be_bytes());
2316 for i in 0..3 {
2317 let (start, count) = chunk
2318 .get(i)
2319 .map_or((0, 0), |e| (e.start_block, e.block_count));
2320 rec.extend_from_slice(&start.to_be_bytes());
2321 rec.extend_from_slice(&count.to_be_bytes());
2322 }
2323 let mut grown = false;
2324 loop {
2325 let data = self.extents_overflow_data.as_mut().expect("loaded above");
2326 match hfs_common::btree_insert_full(
2327 data,
2328 &rec,
2329 &BTreeKeyFormat::CLASSIC_EXTENTS,
2330 &compare_classic_extents_keys,
2331 ) {
2332 Ok(()) => break,
2333 Err(FilesystemError::DiskFull(_)) if !grown => {
2334 self.grow_btree(HfsTree::Extents)?;
2335 grown = true;
2336 }
2337 Err(e) => return Err(e),
2338 }
2339 }
2340 fork_block += chunk.iter().map(|e| e.block_count as u32).sum::<u32>();
2341 }
2342 Ok(true)
2343 }
2344
2345 #[allow(clippy::too_many_arguments)]
2351 fn build_file_record(
2352 file_id: u32,
2353 data_size: u32,
2354 data_start: u16,
2355 data_blocks: u16,
2356 rsrc_size: u32,
2357 rsrc_start: u16,
2358 rsrc_blocks: u16,
2359 type_code: &[u8; 4],
2360 creator_code: &[u8; 4],
2361 block_size: u32,
2362 mtime_secs: Option<u64>,
2363 finder_flags: u16,
2364 ) -> [u8; 102] {
2365 let mut rec = [0u8; 102];
2366 let now = match mtime_secs {
2368 Some(s) => disk_core::times::unix_to_mac_local(s),
2369 None => disk_core::times::mac_local_now(),
2370 };
2371 rec[0] = CATALOG_FILE as u8; rec[4..8].copy_from_slice(type_code);
2375 rec[8..12].copy_from_slice(creator_code);
2376 BigEndian::write_u16(&mut rec[12..14], finder_flags); BigEndian::write_u32(&mut rec[20..24], file_id);
2379 BigEndian::write_u32(&mut rec[26..30], data_size);
2387 BigEndian::write_u32(&mut rec[30..34], data_blocks as u32 * block_size);
2389 BigEndian::write_u32(&mut rec[36..40], rsrc_size);
2392 BigEndian::write_u32(&mut rec[40..44], rsrc_blocks as u32 * block_size);
2394 BigEndian::write_u32(&mut rec[44..48], now);
2396 BigEndian::write_u32(&mut rec[48..52], now);
2398 if data_blocks > 0 {
2400 BigEndian::write_u16(&mut rec[74..76], data_start);
2401 BigEndian::write_u16(&mut rec[76..78], data_blocks);
2402 }
2403 if rsrc_blocks > 0 {
2405 BigEndian::write_u16(&mut rec[86..88], rsrc_start);
2406 BigEndian::write_u16(&mut rec[88..90], rsrc_blocks);
2407 }
2408 rec
2409 }
2410
2411 fn build_dir_record_with_dates(dir_id: u32, mtime_secs: Option<u64>) -> [u8; 70] {
2416 let mut rec = [0u8; 70];
2417 let now = match mtime_secs {
2419 Some(s) => disk_core::times::unix_to_mac_local(s),
2420 None => disk_core::times::mac_local_now(),
2421 };
2422 rec[0] = CATALOG_DIR as u8; BigEndian::write_u32(&mut rec[6..10], dir_id);
2427 BigEndian::write_u32(&mut rec[10..14], now);
2429 BigEndian::write_u32(&mut rec[14..18], now);
2431 rec
2432 }
2433
2434 pub(crate) fn build_thread_record(
2437 thread_type: i8,
2438 parent_id: u32,
2439 name: &[u8],
2440 ) -> (Vec<u8>, Vec<u8>) {
2441 let mut rec = Vec::with_capacity(46);
2445 rec.push(thread_type as u8); rec.push(0); rec.extend_from_slice(&[0u8; 8]); let mut buf4 = [0u8; 4];
2449 BigEndian::write_u32(&mut buf4, parent_id);
2450 rec.extend_from_slice(&buf4); let name_len = name.len().min(31);
2453 let mut str31 = [0u8; 32];
2454 str31[0] = name_len as u8;
2455 str31[1..1 + name_len].copy_from_slice(&name[..name_len]);
2456 rec.extend_from_slice(&str31);
2457 (vec![], rec) }
2460
2461 fn do_sync_metadata(&mut self) -> Result<(), FilesystemError> {
2463 if let Some((create, modify, backup)) = self.pending_volume_dates.take() {
2464 self.mdb.create_date = create;
2465 self.mdb.modify_date = modify;
2466 BigEndian::write_u32(&mut self.mdb.raw_sector[2..6], create);
2467 BigEndian::write_u32(&mut self.mdb.raw_sector[6..10], modify);
2468 BigEndian::write_u32(&mut self.mdb.raw_sector[64..68], backup);
2469 } else {
2470 self.mdb.modify_date = disk_core::times::mac_local_now();
2471 }
2472 if let Some(blocks) = self.pending_boot_blocks.take() {
2473 self.reader.seek(SeekFrom::Start(self.partition_offset))?;
2474 self.reader.write_all(&blocks[..])?;
2475 }
2476 self.write_catalog()?;
2477 self.write_volume_bitmap()?;
2478 self.write_mdb()?;
2479 self.reader.flush()?;
2480 Ok(())
2481 }
2482
2483 fn ensure_catalog_initialized(&mut self) -> Result<(), FilesystemError> {
2489 if self.btrees_initialized {
2493 return Ok(());
2494 }
2495 let catalog_blank = is_catalog_uninitialized(&self.catalog_data);
2496 let extents_data = if self.mdb.extents_file_size > 0 {
2498 read_fork_data(
2499 &mut self.reader,
2500 self.partition_offset,
2501 &self.mdb,
2502 &self.mdb.extents_file_extents,
2503 self.mdb.extents_file_size as u64,
2504 )
2505 .ok()
2506 } else {
2507 None
2508 };
2509 let extents_blank = extents_data
2510 .as_deref()
2511 .map(is_catalog_uninitialized)
2512 .unwrap_or(false);
2513 if !catalog_blank && !extents_blank {
2514 self.btrees_initialized = true;
2515 return Ok(());
2516 }
2517 self.initialize_empty_btrees(catalog_blank, extents_blank)?;
2518 self.btrees_initialized = true;
2519 Ok(())
2520 }
2521
2522 fn initialize_empty_btrees(
2523 &mut self,
2524 init_catalog: bool,
2525 init_extents: bool,
2526 ) -> Result<(), FilesystemError> {
2527 if init_catalog {
2528 let catalog_size = self.catalog_data.len();
2529 if catalog_size < 1024 {
2530 return Err(FilesystemError::InvalidData(format!(
2531 "cannot initialize empty HFS catalog: region is only {catalog_size} bytes"
2532 )));
2533 }
2534 let vol_name = self.mdb.volume_name_raw.clone();
2538 let node_size = pick_btree_node_size(catalog_size as u64) as usize;
2539 let buf = build_empty_hfs_catalog_with_node_size(
2540 catalog_size,
2541 node_size,
2542 &vol_name,
2543 hfs_common::hfs_now(),
2544 )?;
2545 self.catalog_data = buf;
2546 self.write_catalog()?;
2547 }
2548
2549 if init_extents {
2550 let extents_size = self.mdb.extents_file_size as usize;
2551 if extents_size >= 512 {
2552 let buf = build_empty_hfs_extents_btree(extents_size)?;
2553 write_hfs_fork_data(
2554 &mut self.reader,
2555 self.partition_offset,
2556 &self.mdb,
2557 &self.mdb.extents_file_extents,
2558 &buf,
2559 )?;
2560 }
2561 }
2562
2563 let bitmap_size = (self.mdb.total_blocks as u32).div_ceil(8) as usize;
2567 let mut bitmap = vec![0u8; bitmap_size];
2568 let mut used_blocks = 0u32;
2569 for ext in self.mdb.catalog_file_extents.iter() {
2570 if ext.block_count == 0 {
2571 break;
2572 }
2573 for i in 0..ext.block_count {
2574 let bit = ext.start_block as u32 + i as u32;
2575 if bit < self.mdb.total_blocks as u32 {
2576 hfs_common::bitmap_set_bit_be(&mut bitmap, bit);
2577 used_blocks += 1;
2578 }
2579 }
2580 }
2581 for ext in self.mdb.extents_file_extents.iter() {
2582 if ext.block_count == 0 {
2583 break;
2584 }
2585 for i in 0..ext.block_count {
2586 let bit = ext.start_block as u32 + i as u32;
2587 if bit < self.mdb.total_blocks as u32 {
2588 hfs_common::bitmap_set_bit_be(&mut bitmap, bit);
2589 used_blocks += 1;
2590 }
2591 }
2592 }
2593 self.bitmap = Some(bitmap);
2594 self.mdb.free_blocks = (self.mdb.total_blocks as u32).saturating_sub(used_blocks) as u16;
2595
2596 self.mdb.file_count = 0;
2598 self.mdb.folder_count = 0;
2599 if self.mdb.next_catalog_id < 16 {
2600 self.mdb.next_catalog_id = 16;
2601 }
2602 self.mdb.modify_date = disk_core::times::mac_local_now();
2603
2604 self.write_volume_bitmap()?;
2605 self.write_mdb()?;
2606 self.reader.flush()?;
2607 Ok(())
2608 }
2609}
2610
2611fn pick_btree_node_size(_target_bytes: u64) -> u16 {
2626 512
2627}
2628
2629pub const HFS_MAX_BTREE_FILE_SIZE: u32 = 16 * 1024 * 1024;
2635
2636fn hfs_map_nodes_required(total_nodes: u32, node_size: usize) -> u32 {
2639 let header_cap = ((node_size - 256) * 8) as u32; if total_nodes <= header_cap {
2641 return 0;
2642 }
2643 let per_map = (hfs_common::map_node_bitmap_bytes(node_size) * 8) as u32;
2644 if per_map == 0 {
2645 return 0;
2646 }
2647 let extra = total_nodes - header_cap;
2648 extra.div_ceil(per_map)
2649}
2650
2651fn build_empty_hfs_catalog_with_node_size(
2652 catalog_size: usize,
2653 node_size: usize,
2654 volume_name_raw: &[u8],
2655 now: u32,
2656) -> Result<Vec<u8>, FilesystemError> {
2657 if volume_name_raw.len() > 27 {
2658 return Err(FilesystemError::InvalidData(format!(
2659 "HFS volume name length {} exceeds 27 Mac Roman bytes",
2660 volume_name_raw.len()
2661 )));
2662 }
2663 let total_nodes = (catalog_size / node_size) as u32;
2664 if total_nodes < 2 {
2665 return Err(FilesystemError::InvalidData(format!(
2666 "cannot initialize empty HFS catalog: only {total_nodes} node(s) fit"
2667 )));
2668 }
2669 let map_nodes = hfs_map_nodes_required(total_nodes, node_size);
2672 let used_nodes = 2 + map_nodes;
2673 if total_nodes < used_nodes {
2674 return Err(FilesystemError::InvalidData(format!(
2675 "cannot initialize empty HFS catalog: {total_nodes} nodes < {used_nodes} required \
2676 (header + leaf + {map_nodes} map nodes)"
2677 )));
2678 }
2679 let mut buf = vec![0u8; catalog_size];
2680
2681 if map_nodes > 0 {
2684 BigEndian::write_u32(&mut buf[0..4], 2);
2685 }
2686 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
2687 buf[9] = 0;
2688 BigEndian::write_u16(&mut buf[10..12], 3);
2689
2690 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);
2699 BigEndian::write_u32(&mut buf[40..44], total_nodes - used_nodes); let bitmap_rec_offset = 248usize;
2708 let header_bitmap_len = node_size - 256; for bit in 0..used_nodes {
2710 let local = bit;
2712 let byte_idx = (local / 8) as usize;
2713 let bit_in_byte = 7 - (local % 8) as u8;
2714 if byte_idx < header_bitmap_len {
2715 buf[bitmap_rec_offset + byte_idx] |= 1u8 << bit_in_byte;
2716 }
2717 }
2718
2719 let free_offset = (node_size - 8) as u16;
2721 BigEndian::write_u16(&mut buf[node_size - 2..node_size], 14);
2722 BigEndian::write_u16(&mut buf[node_size - 4..node_size - 2], 120);
2723 BigEndian::write_u16(
2724 &mut buf[node_size - 6..node_size - 4],
2725 bitmap_rec_offset as u16,
2726 );
2727 BigEndian::write_u16(&mut buf[node_size - 8..node_size - 6], free_offset);
2728
2729 let leaf = node_size;
2731 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();
2739 let r0_key = leaf + 14;
2740 let key_data_len: u8 = (1 + 4 + 1 + name_len) as u8; buf[r0_key] = key_data_len;
2742 buf[r0_key + 1] = 0;
2743 BigEndian::write_u32(&mut buf[r0_key + 2..r0_key + 6], 1);
2744 buf[r0_key + 6] = name_len as u8;
2745 buf[r0_key + 7..r0_key + 7 + name_len].copy_from_slice(volume_name_raw);
2746 let r0_key_total = 1 + key_data_len as usize;
2749 let r0_pad = r0_key_total & 1;
2750 let r0_data = r0_key + r0_key_total + r0_pad;
2751 buf[r0_data] = CATALOG_DIR as u8;
2752 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;
2762 buf[r1_key] = 6;
2763 buf[r1_key + 1] = 0;
2764 BigEndian::write_u32(&mut buf[r1_key + 2..r1_key + 6], 2);
2765 buf[r1_key + 6] = 0;
2766 buf[r1_key + 7] = 0;
2767 let r1_data = r1_key + 8;
2768 buf[r1_data] = CATALOG_DIR_THREAD as u8;
2769 BigEndian::write_u32(&mut buf[r1_data + 10..r1_data + 14], 1); buf[r1_data + 14] = name_len as u8;
2771 buf[r1_data + 15..r1_data + 15 + name_len].copy_from_slice(volume_name_raw);
2772 let free_rel = (r1_data + 46 - leaf) as u16;
2775
2776 BigEndian::write_u16(&mut buf[leaf + node_size - 2..leaf + node_size], 14);
2778 BigEndian::write_u16(
2779 &mut buf[leaf + node_size - 4..leaf + node_size - 2],
2780 (r1_key - leaf) as u16,
2781 );
2782 BigEndian::write_u16(
2783 &mut buf[leaf + node_size - 6..leaf + node_size - 4],
2784 free_rel,
2785 );
2786
2787 for i in 0..map_nodes {
2789 let node_idx = 2 + i;
2790 let next_link = if i + 1 < map_nodes { node_idx + 1 } else { 0 };
2791 let prev_link = if i == 0 { 0 } else { node_idx - 1 };
2792 hfs_common::init_map_node(&mut buf, node_size, node_idx, prev_link, next_link);
2793 }
2794
2795 Ok(buf)
2796}
2797
2798fn build_empty_hfs_extents_btree(size: usize) -> Result<Vec<u8>, FilesystemError> {
2802 build_empty_hfs_extents_btree_with_node_size(size, 512)
2803}
2804
2805fn build_empty_hfs_extents_btree_with_node_size(
2806 size: usize,
2807 node_size: usize,
2808) -> Result<Vec<u8>, FilesystemError> {
2809 let total_nodes = (size / node_size) as u32;
2810 if total_nodes < 1 {
2811 return Err(FilesystemError::InvalidData(format!(
2812 "cannot initialize empty HFS extents B-tree: region is only {size} bytes"
2813 )));
2814 }
2815 let map_nodes = hfs_map_nodes_required(total_nodes, node_size);
2816 let used_nodes = 1 + map_nodes;
2817 if total_nodes < used_nodes {
2818 return Err(FilesystemError::InvalidData(format!(
2819 "cannot initialize empty HFS extents B-tree: {total_nodes} nodes < {used_nodes} \
2820 required (header + {map_nodes} map nodes)"
2821 )));
2822 }
2823 let mut buf = vec![0u8; size];
2824
2825 if map_nodes > 0 {
2827 BigEndian::write_u32(&mut buf[0..4], 1);
2828 }
2829 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
2830 buf[9] = 0;
2831 BigEndian::write_u16(&mut buf[10..12], 3);
2832
2833 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);
2842 BigEndian::write_u32(&mut buf[40..44], total_nodes - used_nodes);
2843
2844 let bitmap_rec_offset = 248usize;
2848 let header_bitmap_len = node_size - 256;
2849 for bit in 0..used_nodes {
2850 let byte_idx = (bit / 8) as usize;
2851 let bit_in_byte = 7 - (bit % 8) as u8;
2852 if byte_idx < header_bitmap_len {
2853 buf[bitmap_rec_offset + byte_idx] |= 1u8 << bit_in_byte;
2854 }
2855 }
2856
2857 let free_offset = (node_size - 8) as u16;
2858 BigEndian::write_u16(&mut buf[node_size - 2..node_size], 14);
2859 BigEndian::write_u16(&mut buf[node_size - 4..node_size - 2], 120);
2860 BigEndian::write_u16(
2861 &mut buf[node_size - 6..node_size - 4],
2862 bitmap_rec_offset as u16,
2863 );
2864 BigEndian::write_u16(&mut buf[node_size - 8..node_size - 6], free_offset);
2865
2866 for i in 0..map_nodes {
2868 let node_idx = 1 + i;
2869 let next_link = if i + 1 < map_nodes { node_idx + 1 } else { 0 };
2870 let prev_link = if i == 0 { 0 } else { node_idx - 1 };
2871 hfs_common::init_map_node(&mut buf, node_size, node_idx, prev_link, next_link);
2872 }
2873
2874 Ok(buf)
2875}
2876
2877pub fn create_blank_hfs(
2898 target_size_bytes: u64,
2899 block_size: u32,
2900 volume_name: &str,
2901) -> Result<Vec<u8>, FilesystemError> {
2902 create_blank_hfs_sized(target_size_bytes, block_size, volume_name, 0, 0)
2903}
2904
2905pub fn default_btree_sizes(target_volume_bytes: u64, block_size: u32) -> (u32, u32) {
2916 if block_size == 0 {
2917 return (0, 0);
2918 }
2919 let clump = 4u64 * block_size as u64;
2920 let floor = 24u64 * block_size as u64;
2921 let raw = target_volume_bytes / 200; let catalog = raw.max(floor).div_ceil(clump) * clump;
2923 let extents = (raw / 2).max(floor).div_ceil(clump) * clump;
2924 (
2925 catalog.min(u32::MAX as u64) as u32,
2926 extents.min(u32::MAX as u64) as u32,
2927 )
2928}
2929
2930pub fn clone_target_btree_sizes(
2947 target_size: u64,
2948 block_size: u32,
2949 source_catalog: u32,
2950 source_extents: u32,
2951) -> (u32, u32) {
2952 let (default_cat, default_ext) = default_btree_sizes(target_size, block_size);
2953 let catalog = source_catalog
2954 .saturating_mul(3)
2955 .saturating_div(2)
2956 .max(default_cat)
2957 .min(HFS_MAX_BTREE_FILE_SIZE);
2958 let extents = source_extents.max(default_ext).min(HFS_MAX_BTREE_FILE_SIZE);
2959 (catalog, extents)
2960}
2961
2962pub fn pick_block_size(volume_bytes: u64) -> u32 {
2965 let mut bs: u32 = 512;
2966 while volume_bytes / bs as u64 > 65535 {
2967 bs = bs.saturating_mul(2);
2968 if bs == 0 {
2969 return 1 << 16;
2970 }
2971 }
2972 bs
2973}
2974
2975pub fn create_blank_hfs_sized(
2982 target_size_bytes: u64,
2983 block_size: u32,
2984 volume_name: &str,
2985 min_extents_bytes: u32,
2986 min_catalog_bytes: u32,
2987) -> Result<Vec<u8>, FilesystemError> {
2988 create_blank_hfs_sized_at(
2989 target_size_bytes,
2990 block_size,
2991 volume_name,
2992 min_extents_bytes,
2993 min_catalog_bytes,
2994 hfs_common::hfs_now(),
2995 )
2996}
2997
2998pub(crate) fn create_blank_hfs_sized_at(
3000 target_size_bytes: u64,
3001 block_size: u32,
3002 volume_name: &str,
3003 min_extents_bytes: u32,
3004 min_catalog_bytes: u32,
3005 now: u32,
3006) -> Result<Vec<u8>, FilesystemError> {
3007 let (front, mdb, image_size) = build_blank_hfs_front(
3008 target_size_bytes,
3009 block_size,
3010 volume_name,
3011 min_extents_bytes,
3012 min_catalog_bytes,
3013 now,
3014 )?;
3015 let mut img = vec![0u8; image_size as usize];
3016 img[..front.len()].copy_from_slice(&front);
3017 let alt_off = image_size as usize - 1024;
3018 img[alt_off..alt_off + 512].copy_from_slice(&mdb);
3019 Ok(img)
3020}
3021
3022pub fn write_blank_hfs_into<W: Write + Seek>(
3031 target: &mut W,
3032 at_offset: u64,
3033 target_size_bytes: u64,
3034 block_size: u32,
3035 volume_name: &str,
3036) -> Result<u64, FilesystemError> {
3037 write_blank_hfs_into_at(
3038 target,
3039 at_offset,
3040 target_size_bytes,
3041 block_size,
3042 volume_name,
3043 hfs_common::hfs_now(),
3044 )
3045}
3046
3047pub(crate) fn write_blank_hfs_into_at<W: Write + Seek>(
3049 target: &mut W,
3050 at_offset: u64,
3051 target_size_bytes: u64,
3052 block_size: u32,
3053 volume_name: &str,
3054 now: u32,
3055) -> Result<u64, FilesystemError> {
3056 let (front, mdb, image_size) =
3057 build_blank_hfs_front(target_size_bytes, block_size, volume_name, 0, 0, now)?;
3058 target.seek(SeekFrom::Start(at_offset))?;
3059 target.write_all(&front)?;
3060 target.seek(SeekFrom::Start(at_offset + image_size - 1024))?;
3061 target.write_all(&mdb)?;
3062 target.write_all(&[0u8; 512])?;
3063 Ok(image_size)
3064}
3065
3066fn build_blank_hfs_front(
3071 target_size_bytes: u64,
3072 block_size: u32,
3073 volume_name: &str,
3074 min_extents_bytes: u32,
3075 min_catalog_bytes: u32,
3076 now: u32,
3077) -> Result<(Vec<u8>, [u8; 512], u64), FilesystemError> {
3078 if block_size == 0 || !block_size.is_multiple_of(512) {
3079 return Err(FilesystemError::InvalidData(format!(
3080 "HFS block_size must be a non-zero multiple of 512 (got {block_size})"
3081 )));
3082 }
3083
3084 let name_raw = utf8_to_mac_roman(volume_name)?;
3085 if name_raw.len() > 27 {
3086 return Err(FilesystemError::InvalidData(format!(
3087 "HFS volume name '{volume_name}' exceeds 27 Mac Roman bytes"
3088 )));
3089 }
3090
3091 let mut first_alloc_block: u16 = 3;
3095 let total_blocks: u32;
3096 loop {
3097 let pre_alloc = first_alloc_block as u64 * 512;
3098 if target_size_bytes <= pre_alloc + 512 {
3099 return Err(FilesystemError::InvalidData(format!(
3100 "HFS volume too small ({target_size_bytes} bytes) for block_size {block_size}"
3101 )));
3102 }
3103 let usable = target_size_bytes - pre_alloc - 1024; let tb = usable / block_size as u64;
3105 if tb == 0 {
3106 return Err(FilesystemError::InvalidData(format!(
3107 "HFS volume too small for block_size {block_size}"
3108 )));
3109 }
3110 if tb > 65535 {
3111 return Err(FilesystemError::InvalidData(format!(
3112 "HFS would have {tb} allocation blocks at block_size {block_size}, exceeding u16 ceiling 65535"
3113 )));
3114 }
3115 let tb = tb as u32;
3116 let bitmap_bytes = (tb as u64).div_ceil(8);
3117 let bitmap_sectors = bitmap_bytes.div_ceil(512) as u16;
3118 let needed = 3u16 + bitmap_sectors;
3119 if needed <= first_alloc_block {
3120 total_blocks = tb;
3121 break;
3122 }
3123 first_alloc_block = needed;
3124 }
3125
3126 let (default_catalog, default_extents) = default_btree_sizes(target_size_bytes, block_size);
3130 let blocks_for = |bytes: u32, default: u32| -> u16 {
3131 let bytes = if bytes == 0 { default } else { bytes };
3132 let n = (bytes as u64).div_ceil(block_size as u64).max(4);
3133 n.min(u16::MAX as u64) as u16
3134 };
3135 let extents_blocks = blocks_for(min_extents_bytes, default_extents);
3136 let catalog_blocks = blocks_for(min_catalog_bytes, default_catalog);
3137 let extents_start: u16 = 0;
3138 let catalog_start: u16 = extents_blocks;
3139 let btree_blocks: u16 = extents_blocks + catalog_blocks;
3140
3141 if total_blocks < btree_blocks as u32 {
3142 return Err(FilesystemError::InvalidData(format!(
3143 "HFS volume too small: {total_blocks} allocation blocks is below the minimum of {btree_blocks} needed for B-trees"
3144 )));
3145 }
3146
3147 let extents_size = extents_blocks as u32 * block_size;
3148 let catalog_size = catalog_blocks as u32 * block_size;
3149
3150 let extents_node_size = pick_btree_node_size(extents_size as u64) as usize;
3154 let catalog_node_size = pick_btree_node_size(catalog_size as u64) as usize;
3155
3156 let image_size =
3159 first_alloc_block as u64 * 512 + total_blocks as u64 * block_size as u64 + 1024;
3160 let front_size = first_alloc_block as usize * 512 + btree_blocks as usize * block_size as usize;
3164 let mut front = vec![0u8; front_size];
3165
3166 let bitmap_off = 3 * 512;
3169 for b in 0..btree_blocks as u32 {
3170 let byte_idx = (b / 8) as usize;
3171 let bit_idx = 7 - (b % 8) as u8;
3172 front[bitmap_off + byte_idx] |= 1 << bit_idx;
3173 }
3174
3175 let extents_bytes =
3177 build_empty_hfs_extents_btree_with_node_size(extents_size as usize, extents_node_size)?;
3178 let extents_off = first_alloc_block as u64 * 512 + extents_start as u64 * block_size as u64;
3179 front[extents_off as usize..extents_off as usize + extents_bytes.len()]
3180 .copy_from_slice(&extents_bytes);
3181
3182 let catalog_bytes = build_empty_hfs_catalog_with_node_size(
3187 catalog_size as usize,
3188 catalog_node_size,
3189 &name_raw,
3190 now,
3191 )?;
3192 let catalog_off = first_alloc_block as u64 * 512 + catalog_start as u64 * block_size as u64;
3193 front[catalog_off as usize..catalog_off as usize + catalog_bytes.len()]
3194 .copy_from_slice(&catalog_bytes);
3195
3196 let mdb = build_blank_mdb(
3198 now,
3199 &name_raw,
3200 total_blocks as u16,
3201 block_size,
3202 first_alloc_block,
3203 catalog_start,
3204 catalog_blocks,
3205 catalog_size,
3206 extents_start,
3207 extents_blocks,
3208 extents_size,
3209 btree_blocks,
3210 );
3211 front[1024..1024 + 512].copy_from_slice(&mdb);
3212
3213 Ok((front, mdb, image_size))
3217}
3218
3219#[allow(clippy::too_many_arguments)]
3220fn build_blank_mdb(
3221 now: u32,
3222 name_raw: &[u8],
3223 total_blocks: u16,
3224 block_size: u32,
3225 first_alloc_block: u16,
3226 catalog_start: u16,
3227 catalog_blocks: u16,
3228 catalog_size: u32,
3229 extents_start: u16,
3230 extents_blocks: u16,
3231 extents_size: u32,
3232 allocated_blocks: u16,
3233) -> [u8; 512] {
3234 let mut mdb = [0u8; 512];
3235 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
3236 BigEndian::write_u32(&mut mdb[2..6], now); BigEndian::write_u32(&mut mdb[6..10], now); BigEndian::write_u16(&mut mdb[10..12], 0x0100);
3242 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;
3250 mdb[37..37 + name_raw.len()].copy_from_slice(name_raw);
3251 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);
3258 BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
3259 BigEndian::write_u32(&mut mdb[146..150], catalog_size); BigEndian::write_u16(&mut mdb[150..152], catalog_start);
3261 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
3262 mdb
3263}
3264
3265impl<R: Read + Seek + Send> Filesystem for HfsFilesystem<R> {
3266 fn root(&mut self) -> Result<FileEntry, FilesystemError> {
3267 Ok(FileEntry {
3268 name: "/".into(),
3269 path: "/".into(),
3270 entry_type: EntryType::Directory,
3271 size: 0,
3272 location: 2, modified: None,
3274 modified_unix: None,
3275 type_code: None,
3276 creator_code: None,
3277 symlink_target: None,
3278 special_type: None,
3279 mode: None,
3280 uid: None,
3281 gid: None,
3282 resource_fork_size: None,
3283 aux_type: None,
3284 link_target_cnid: None,
3285 amiga_protection: None,
3286 amiga_comment: None,
3287 amiga_date: None,
3288 dos_attributes: None,
3289 finder_flags: None,
3290 prodos_file_type: None,
3291 mac_dates: None,
3292 owner_label: None,
3293 native_attrs: None,
3294 qnx2_attrs: None,
3295 })
3296 }
3297
3298 fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
3299 let parent_id = entry.location as u32;
3300 let children = self.list_children(parent_id)?;
3301
3302 let mut entries = Vec::new();
3303 for child in children {
3304 match child {
3305 CatalogRecord::Directory {
3306 dir_id,
3307 name,
3308 dates,
3309 ..
3310 } => {
3311 let path = if entry.path == "/" {
3312 format!("/{name}")
3313 } else {
3314 format!("{}/{name}", entry.path)
3315 };
3316 let mut fe = FileEntry::new_directory(name, path, dir_id as u64);
3317 fe.modified = hfs_common::format_mac_date(dates.1);
3318 fe.modified_unix = disk_core::times::mac_local_to_unix(dates.1);
3319 fe.mac_dates = Some(dates);
3320 entries.push(fe);
3321 }
3322 CatalogRecord::File {
3323 file_id,
3324 name,
3325 data_size,
3326 rsrc_size,
3327 rsrc_extents,
3328 type_code,
3329 creator_code,
3330 finder_flags,
3331 dates,
3332 ..
3333 } => {
3334 let path = if entry.path == "/" {
3335 format!("/{name}")
3336 } else {
3337 format!("{}/{name}", entry.path)
3338 };
3339 let mut fe = FileEntry::new_file(name, path, data_size as u64, file_id as u64);
3340 fe.type_code = Some(type_code);
3341 fe.creator_code = Some(creator_code);
3342 fe.finder_flags = Some(finder_flags);
3343 fe.modified = hfs_common::format_mac_date(dates.1);
3344 fe.modified_unix = disk_core::times::mac_local_to_unix(dates.1);
3345 fe.mac_dates = Some(dates);
3346 if rsrc_size > 0 {
3347 fe.resource_fork_size = Some(rsrc_size as u64);
3348 }
3349 if finder_flags & crate::mac_alias::IS_ALIAS_FLAG != 0 && rsrc_size > 0 {
3353 if let Ok(rsrc) = read_fork_data(
3354 &mut self.reader,
3355 self.partition_offset,
3356 &self.mdb,
3357 &rsrc_extents,
3358 rsrc_size as u64,
3359 ) {
3360 if let Some(target) = crate::mac_alias::resolve_alias_target(&rsrc) {
3361 fe.symlink_target = Some(target);
3362 }
3363 }
3364 }
3365 entries.push(fe);
3366 }
3367 }
3368 }
3369
3370 entries.sort_by_key(|a| a.name.to_lowercase());
3371 Ok(entries)
3372 }
3373
3374 fn read_file(
3375 &mut self,
3376 entry: &FileEntry,
3377 max_bytes: usize,
3378 ) -> Result<Vec<u8>, FilesystemError> {
3379 let file_id = entry.location as u32;
3380 let (data_size, extents, _rsrc_size, _rsrc_extents) =
3381 self.find_file_by_id(file_id).ok_or_else(|| {
3382 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3383 })?;
3384
3385 let mut data = self.read_fork_with_overflow(file_id, 0x00, &extents, data_size as u64)?;
3386 data.truncate(max_bytes);
3387 Ok(data)
3388 }
3389
3390 fn write_file_to(
3391 &mut self,
3392 entry: &FileEntry,
3393 writer: &mut dyn std::io::Write,
3394 ) -> Result<u64, FilesystemError> {
3395 if entry.is_directory() {
3396 return Err(FilesystemError::NotADirectory(entry.path.clone()));
3397 }
3398 let file_id = entry.location as u32;
3399 let (data_size, extents, _rsrc_size, _rsrc_extents) =
3400 self.find_file_by_id(file_id).ok_or_else(|| {
3401 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3402 })?;
3403 self.write_fork_with_overflow_to(file_id, 0x00, &extents, data_size as u64, writer)
3404 }
3405
3406 fn volume_label(&self) -> Option<&str> {
3407 if self.mdb.volume_name.is_empty() {
3408 None
3409 } else {
3410 Some(&self.mdb.volume_name)
3411 }
3412 }
3413
3414 fn fs_type(&self) -> &str {
3415 "HFS"
3416 }
3417
3418 fn validate_name(&self, name: &str) -> Result<(), FilesystemError> {
3419 validate_hfs_create_name(name).map(|_| ())
3420 }
3421
3422 fn uses_colon_paths(&self) -> bool {
3423 true
3424 }
3425
3426 fn total_size(&self) -> u64 {
3427 self.mdb.total_blocks as u64 * self.mdb.block_size as u64
3428 }
3429
3430 fn used_size(&self) -> u64 {
3431 (self.mdb.total_blocks - self.mdb.free_blocks) as u64 * self.mdb.block_size as u64
3432 }
3433
3434 fn allocation_unit(&self) -> Option<u64> {
3440 if self.mdb.block_size == 0 {
3441 None
3442 } else {
3443 Some(self.mdb.block_size as u64)
3444 }
3445 }
3446
3447 fn last_data_byte(&mut self) -> Result<u64, FilesystemError> {
3448 let bitmap = self.read_volume_bitmap()?;
3449 let last_block = find_last_set_bit(&bitmap, self.mdb.total_blocks as u32);
3450 match last_block {
3451 Some(block) => {
3452 let byte = self.alloc_block_offset(block + 1);
3453 Ok(byte)
3454 }
3455 None => Ok(self.total_size()),
3456 }
3457 }
3458
3459 fn write_resource_fork_to(
3460 &mut self,
3461 entry: &FileEntry,
3462 writer: &mut dyn std::io::Write,
3463 ) -> Result<u64, FilesystemError> {
3464 let file_id = entry.location as u32;
3465 let (_data_size, _data_ext, rsrc_size, rsrc_extents) =
3466 self.find_file_by_id(file_id).ok_or_else(|| {
3467 FilesystemError::NotFound(format!("file id {file_id} not found in catalog"))
3468 })?;
3469 if rsrc_size == 0 {
3470 return Ok(0);
3471 }
3472 let data = self.read_fork_with_overflow(file_id, 0xFF, &rsrc_extents, rsrc_size as u64)?;
3473 writer.write_all(&data)?;
3474 Ok(data.len() as u64)
3475 }
3476
3477 fn resource_fork_size(&mut self, entry: &FileEntry) -> u64 {
3478 let file_id = entry.location as u32;
3479 self.find_file_by_id(file_id)
3480 .map(|(_ds, _de, rs, _re)| rs as u64)
3481 .unwrap_or(0)
3482 }
3483
3484 fn fragmentation_stats(
3485 &mut self,
3486 ) -> Option<Result<disk_core::filesystem::FragmentationStats, FilesystemError>> {
3487 Some(Ok(hfs_fragmentation_stats(&self.catalog_data)))
3488 }
3489
3490 fn blessed_system_folder(&mut self) -> Option<(u64, String)> {
3491 let cnid = self.mdb.finder_info[0];
3492 if cnid == 0 {
3493 return None;
3494 }
3495 if let Some((_node, _rec, offset)) = self.find_catalog_record_by_cnid(cnid) {
3497 let key_len = self.catalog_data[offset] as usize;
3498 let mut rec_data_start = offset + 1 + key_len;
3499 if !rec_data_start.is_multiple_of(2) {
3500 rec_data_start += 1;
3501 }
3502 if rec_data_start + 15 <= self.catalog_data.len() {
3504 let name_len = self.catalog_data[rec_data_start + 14] as usize;
3505 if rec_data_start + 15 + name_len <= self.catalog_data.len() {
3506 let name = mac_roman_to_utf8(
3507 &self.catalog_data[rec_data_start + 15..rec_data_start + 15 + name_len],
3508 );
3509 return Some((cnid as u64, name));
3510 }
3511 }
3512 }
3513 Some((cnid as u64, format!("CNID {}", cnid)))
3514 }
3515
3516 fn fsck(&mut self) -> Option<Result<disk_core::fsck::FsckResult, FilesystemError>> {
3517 Some(self.fsck())
3518 }
3519}
3520
3521const CATALOG_DIR_THREAD: i8 = 3;
3524
3525impl<R: Read + Write + Seek + Send> EditableFilesystem for HfsFilesystem<R> {
3526 fn as_filesystem(&self) -> &dyn disk_core::filesystem::Filesystem {
3527 self
3528 }
3529 fn as_filesystem_mut(&mut self) -> &mut dyn disk_core::filesystem::Filesystem {
3530 self
3531 }
3532 fn begin_bulk(&mut self) {
3533 self.bulk_mode = true;
3534 }
3535
3536 fn end_bulk(&mut self) {
3537 self.bulk_mode = false;
3538 }
3539
3540 fn create_file(
3541 &mut self,
3542 parent: &FileEntry,
3543 name: &str,
3544 data: &mut dyn std::io::Read,
3545 data_len: u64,
3546 options: &CreateFileOptions,
3547 ) -> Result<FileEntry, FilesystemError> {
3548 self.ensure_catalog_initialized()?;
3549 let snap = self.snapshot();
3550 let extents_before = self.extents_overflow_data.clone();
3551 let result = (|| {
3552 let parent_id = parent.location as u32;
3553
3554 let name_bytes = validate_hfs_create_name(name)?;
3555
3556 if self
3558 .find_catalog_record_by_name(parent_id, &name_bytes)
3559 .is_some()
3560 {
3561 return Err(FilesystemError::AlreadyExists(name.into()));
3562 }
3563
3564 let file_id = self.mdb.next_catalog_id;
3566 self.mdb.next_catalog_id += 1;
3567
3568 let (type_code, creator_code) = hfs_common::resolve_create_type_creator(
3572 name,
3573 options.os_type,
3574 options.os_creator,
3575 options.type_code.as_deref(),
3576 options.creator_code.as_deref(),
3577 );
3578
3579 let data_extents = self.write_data_to_blocks(data, data_len)?;
3581
3582 let (rsrc_extents, rsrc_size) = if let Some(ref rsrc_src) = options.resource_fork {
3584 match rsrc_src {
3585 disk_core::filesystem::ResourceForkSource::Data(rsrc_data) => {
3586 let mut cursor = std::io::Cursor::new(rsrc_data);
3587 let ext = self.write_data_to_blocks(&mut cursor, rsrc_data.len() as u64)?;
3588 (ext, rsrc_data.len() as u32)
3589 }
3590 disk_core::filesystem::ResourceForkSource::File(path) => {
3591 let mut f = std::fs::File::open(path)?;
3592 let len = f.metadata()?.len();
3593 let ext = self.write_data_to_blocks(&mut f, len)?;
3594 (ext, len as u32)
3595 }
3596 }
3597 } else {
3598 (Vec::new(), 0)
3599 };
3600
3601 let mtime = options.unix_times.map(|t| t.mtime_or_now());
3606 let mut file_rec = Self::build_file_record(
3607 file_id,
3608 data_len as u32,
3609 data_extents.first().map_or(0, |e| e.start_block),
3610 Self::extents_block_total(&data_extents),
3611 rsrc_size,
3612 rsrc_extents.first().map_or(0, |e| e.start_block),
3613 Self::extents_block_total(&rsrc_extents),
3614 &type_code,
3615 &creator_code,
3616 self.mdb.block_size,
3617 mtime,
3618 options.finder_flags.unwrap_or(0),
3619 );
3620 Self::set_inline_extents(&mut file_rec, 74, &data_extents);
3621 Self::set_inline_extents(&mut file_rec, 86, &rsrc_extents);
3622
3623 let key = Self::build_catalog_key(parent_id, &name_bytes);
3625 let mut key_record = key;
3626 key_record.extend_from_slice(&file_rec);
3627
3628 self.insert_catalog_record(&key_record)?;
3629
3630 self.update_parent_valence(parent_id, 1)?;
3640
3641 self.mdb.file_count += 1;
3643
3644 let mut overflow_changed = self.insert_fork_overflow(file_id, 0x00, &data_extents)?;
3645 overflow_changed |= self.insert_fork_overflow(file_id, 0xFF, &rsrc_extents)?;
3646 if overflow_changed {
3647 self.write_extents_overflow_back()?;
3648 }
3649
3650 let path = if parent.path == "/" {
3651 format!("/{name}")
3652 } else {
3653 format!("{}/{name}", parent.path)
3654 };
3655 let mut fe = FileEntry::new_file(name.to_string(), path, data_len, file_id as u64);
3656 if type_code != [0; 4] {
3657 fe.type_code = Some(type_code);
3658 fe.creator_code = Some(creator_code);
3659 }
3660 fe.finder_flags = options.finder_flags;
3661 if rsrc_size > 0 {
3662 fe.resource_fork_size = Some(rsrc_size as u64);
3663 }
3664 let stamped = mtime.unwrap_or_else(disk_core::times::now);
3665 fe.modified_unix = Some(stamped);
3666 fe.mac_dates = Some((
3667 disk_core::times::unix_to_mac_local(stamped),
3668 disk_core::times::unix_to_mac_local(stamped),
3669 0,
3670 ));
3671 Ok(fe)
3672 })();
3673 if result.is_err() {
3674 self.extents_overflow_data = extents_before;
3675 self.restore_snapshot(snap);
3676 }
3677 result
3678 }
3679
3680 fn create_directory(
3681 &mut self,
3682 parent: &FileEntry,
3683 name: &str,
3684 options: &CreateDirectoryOptions,
3685 ) -> Result<FileEntry, FilesystemError> {
3686 self.ensure_catalog_initialized()?;
3687 let snap = self.snapshot();
3688 let result = (|| {
3689 let parent_id = parent.location as u32;
3690
3691 let name_bytes = validate_hfs_create_name(name)?;
3692
3693 if self
3695 .find_catalog_record_by_name(parent_id, &name_bytes)
3696 .is_some()
3697 {
3698 return Err(FilesystemError::AlreadyExists(name.into()));
3699 }
3700
3701 let folder_id = self.mdb.next_catalog_id;
3703 self.mdb.next_catalog_id += 1;
3704
3705 let mtime = options.unix_times.map(|t| t.mtime_or_now());
3708 let folder_rec = Self::build_dir_record_with_dates(folder_id, mtime);
3709
3710 let key = Self::build_catalog_key(parent_id, &name_bytes);
3712 let mut key_record = key;
3713 key_record.extend_from_slice(&folder_rec);
3714
3715 self.insert_catalog_record(&key_record)?;
3716
3717 let (_, thread_data) =
3719 Self::build_thread_record(CATALOG_DIR_THREAD, parent_id, &name_bytes);
3720 let thread_key = Self::build_catalog_key(folder_id, &[]);
3721 let mut thread_record = thread_key;
3722 thread_record.extend_from_slice(&thread_data);
3723 self.insert_catalog_record(&thread_record)?;
3724
3725 self.update_parent_valence(parent_id, 1)?;
3727
3728 self.mdb.folder_count += 1;
3730
3731 let path = if parent.path == "/" {
3732 format!("/{name}")
3733 } else {
3734 format!("{}/{name}", parent.path)
3735 };
3736 let mut fe = FileEntry::new_directory(name.to_string(), path, folder_id as u64);
3737 let stamped = mtime.unwrap_or_else(disk_core::times::now);
3738 fe.modified_unix = Some(stamped);
3739 fe.mac_dates = Some((
3740 disk_core::times::unix_to_mac_local(stamped),
3741 disk_core::times::unix_to_mac_local(stamped),
3742 0,
3743 ));
3744 Ok(fe)
3745 })();
3746 if result.is_err() {
3747 self.restore_snapshot(snap);
3748 }
3749 result
3750 }
3751
3752 fn delete_entry(
3753 &mut self,
3754 parent: &FileEntry,
3755 entry: &FileEntry,
3756 ) -> Result<(), FilesystemError> {
3757 let snap = self.snapshot();
3758 let extents_before = self.extents_overflow_data.clone();
3759 let result = (|| {
3760 let parent_id = parent.location as u32;
3761 let cnid = entry.location as u32;
3762
3763 if entry.is_directory() {
3765 let children = self.list_children(cnid)?;
3766 if !children.is_empty() {
3767 return Err(FilesystemError::InvalidData(
3768 "cannot delete non-empty directory".into(),
3769 ));
3770 }
3771 }
3772
3773 let name_bytes = utf8_to_mac_roman(&entry.name)?;
3775 let (node_idx, rec_idx, _offset) = self
3776 .find_catalog_record_by_name(parent_id, &name_bytes)
3777 .ok_or_else(|| {
3778 FilesystemError::NotFound(format!(
3779 "entry '{}' not found in catalog",
3780 entry.name
3781 ))
3782 })?;
3783
3784 let mut overflow_changed = false;
3786 if !entry.is_directory() {
3787 if let Some((_, data_extents, _, rsrc_extents)) = self.find_file_by_id(cnid) {
3788 self.free_extent_blocks(&data_extents);
3789 self.free_extent_blocks(&rsrc_extents);
3790 overflow_changed |= self.free_fork_overflow(cnid, 0x00)?;
3792 overflow_changed |= self.free_fork_overflow(cnid, 0xFF)?;
3793 }
3794 }
3795
3796 self.remove_catalog_record(node_idx, rec_idx)?;
3797
3798 if let Some((t_node, t_rec, _)) = self.find_catalog_record_by_cnid(cnid) {
3800 self.remove_catalog_record(t_node, t_rec)?;
3801 }
3802
3803 self.update_parent_valence(parent_id, -1)?;
3805
3806 if entry.is_directory() {
3808 self.mdb.folder_count = self.mdb.folder_count.saturating_sub(1);
3809 } else {
3810 self.mdb.file_count = self.mdb.file_count.saturating_sub(1);
3811 }
3812
3813 if overflow_changed {
3814 self.write_extents_overflow_back()?;
3815 }
3816
3817 Ok(())
3818 })();
3819 if result.is_err() {
3820 self.extents_overflow_data = extents_before;
3821 self.restore_snapshot(snap);
3822 }
3823 result
3824 }
3825
3826 fn rename(
3827 &mut self,
3828 parent: &FileEntry,
3829 entry: &FileEntry,
3830 new_name: &str,
3831 ) -> Result<(), FilesystemError> {
3832 if new_name == entry.name {
3833 return Ok(());
3834 }
3835 let parent_id = parent.location as u32;
3836 let cnid = entry.location as u32;
3837 let new_name_raw = validate_hfs_create_name(new_name)?;
3838 let old_name_raw = utf8_to_mac_roman(&entry.name)?;
3839
3840 let snap = self.snapshot();
3841 let result = (|| -> Result<(), FilesystemError> {
3842 let (node_idx, rec_idx, _key_abs) = self
3844 .find_catalog_record_by_name(parent_id, &old_name_raw)
3845 .ok_or_else(|| {
3846 FilesystemError::NotFound(format!(
3847 "entry '{}' not found in catalog",
3848 entry.name
3849 ))
3850 })?;
3851
3852 if let Some((n2, r2, _)) = self.find_catalog_record_by_name(parent_id, &new_name_raw) {
3855 if (n2, r2) != (node_idx, rec_idx) {
3856 return Err(FilesystemError::AlreadyExists(new_name.to_string()));
3857 }
3858 }
3859
3860 let node_size = BigEndian::read_u16(&self.catalog_data[32..34]) as usize;
3863 let node_off = node_idx as usize * node_size;
3864 let node = &self.catalog_data[node_off..node_off + node_size];
3865 let (rec_start, rec_end) = hfs_common::btree_record_range(node, node_size, rec_idx);
3866 let key_len = node[rec_start] as usize;
3867 let mut data_rel = 1 + key_len;
3868 if !data_rel.is_multiple_of(2) {
3869 data_rel += 1;
3870 }
3871 let data_bytes = node[rec_start + data_rel..rec_end].to_vec();
3872
3873 let mut new_record = Self::build_catalog_key(parent_id, &new_name_raw);
3874 if !new_record.len().is_multiple_of(2) {
3875 new_record.push(0);
3876 }
3877 new_record.extend_from_slice(&data_bytes);
3878 if !new_record.len().is_multiple_of(2) {
3879 new_record.push(0);
3880 }
3881
3882 self.remove_catalog_record(node_idx, rec_idx)?;
3883 self.insert_catalog_record(&new_record)?;
3884
3885 if let Some((_n, _r, t_key_abs)) = self.find_catalog_record_by_cnid(cnid) {
3889 let t_key_len = self.catalog_data[t_key_abs] as usize;
3890 let mut t_data_off = t_key_abs + 1 + t_key_len;
3891 if !t_data_off.is_multiple_of(2) {
3892 t_data_off += 1;
3893 }
3894 if t_data_off + 15 + 31 <= self.catalog_data.len() {
3895 self.catalog_data[t_data_off + 14] = new_name_raw.len() as u8;
3896 for i in 0..31 {
3897 self.catalog_data[t_data_off + 15 + i] = 0;
3898 }
3899 self.catalog_data[t_data_off + 15..t_data_off + 15 + new_name_raw.len()]
3900 .copy_from_slice(&new_name_raw);
3901 }
3902 }
3903 Ok(())
3905 })();
3906 if result.is_err() {
3907 self.restore_snapshot(snap);
3908 }
3909 result
3910 }
3911
3912 fn set_type_creator(
3913 &mut self,
3914 entry: &FileEntry,
3915 type_code: &str,
3916 creator_code: &str,
3917 ) -> Result<(), FilesystemError> {
3918 let snap = self.snapshot();
3919 let result = (|| {
3920 let frec_start = self.locate_record_data(entry.location as u32)?;
3921 if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
3922 return Err(FilesystemError::InvalidData(
3923 "set_type_creator: not a file record".into(),
3924 ));
3925 }
3926
3927 let tc = hfs_common::encode_fourcc(type_code);
3929 let cc = hfs_common::encode_fourcc(creator_code);
3930 self.catalog_data[frec_start + 4..frec_start + 8].copy_from_slice(&tc);
3931 self.catalog_data[frec_start + 8..frec_start + 12].copy_from_slice(&cc);
3932
3933 Ok(())
3934 })();
3935 if result.is_err() {
3936 self.restore_snapshot(snap);
3937 }
3938 result
3939 }
3940
3941 fn write_resource_fork(
3942 &mut self,
3943 entry: &FileEntry,
3944 data: &mut dyn std::io::Read,
3945 len: u64,
3946 ) -> Result<(), FilesystemError> {
3947 let snap = self.snapshot();
3948 let extents_before = self.extents_overflow_data.clone();
3949 let result = (|| {
3950 let cnid = entry.location as u32;
3951
3952 let mut overflow_changed = false;
3954 if let Some((_, _, _, rsrc_extents)) = self.find_file_by_id(cnid) {
3955 self.free_extent_blocks(&rsrc_extents);
3956 overflow_changed = self.free_fork_overflow(cnid, 0xFF)?;
3957 }
3958
3959 let rsrc_extents = self.write_data_to_blocks(data, len)?;
3961
3962 let frec_start = self.locate_record_data(cnid)?;
3963 if self.catalog_data[frec_start] as i8 != CATALOG_FILE {
3964 return Err(FilesystemError::InvalidData(
3965 "write_resource_fork: not a file record".into(),
3966 ));
3967 }
3968
3969 self.catalog_data[frec_start + 34..frec_start + 36].fill(0);
3975 BigEndian::write_u32(
3977 &mut self.catalog_data[frec_start + 36..frec_start + 40],
3978 len as u32,
3979 );
3980 BigEndian::write_u32(
3982 &mut self.catalog_data[frec_start + 40..frec_start + 44],
3983 Self::extents_block_total(&rsrc_extents) as u32 * self.mdb.block_size,
3984 );
3985 Self::set_inline_extents(
3987 &mut self.catalog_data[frec_start..frec_start + 102],
3988 86,
3989 &rsrc_extents,
3990 );
3991 overflow_changed |= self.insert_fork_overflow(cnid, 0xFF, &rsrc_extents)?;
3992
3993 if overflow_changed {
3994 self.write_extents_overflow_back()?;
3995 }
3996
3997 Ok(())
3998 })();
3999 if result.is_err() {
4000 self.extents_overflow_data = extents_before;
4001 self.restore_snapshot(snap);
4002 }
4003 result
4004 }
4005
4006 fn repair(&mut self) -> Result<disk_core::fsck::RepairReport, FilesystemError> {
4007 self.ensure_bitmap()?;
4008 let mut extents_data = if self.mdb.extents_file_size > 0 {
4009 read_fork_data(
4010 &mut self.reader,
4011 self.partition_offset,
4012 &self.mdb,
4013 &self.mdb.extents_file_extents,
4014 self.mdb.extents_file_size as u64,
4015 )
4016 .ok()
4017 } else {
4018 None
4019 };
4020
4021 let report = crate::hfs_fsck::repair_hfs(
4022 &mut self.mdb,
4023 &mut self.catalog_data,
4024 self.bitmap.as_mut().unwrap(),
4025 extents_data.as_mut(),
4026 );
4027
4028 if let Some(ref ext_data) = extents_data {
4030 if self.mdb.extents_file_size > 0 {
4031 write_extents_overflow_data(
4032 &mut self.reader,
4033 self.partition_offset,
4034 &self.mdb,
4035 ext_data,
4036 )?;
4037 }
4038 }
4039
4040 self.do_sync_metadata()?;
4041 Ok(report)
4042 }
4043
4044 fn sync_metadata(&mut self) -> Result<(), FilesystemError> {
4045 self.do_sync_metadata()
4046 }
4047
4048 fn free_space(&mut self) -> Result<u64, FilesystemError> {
4049 Ok(self.mdb.free_blocks as u64 * self.mdb.block_size as u64)
4050 }
4051
4052 fn set_blessed_folder(&mut self, entry: &FileEntry) -> Result<(), FilesystemError> {
4053 let snap = self.snapshot();
4054 let result = (|| {
4055 if !entry.is_directory() {
4056 return Err(FilesystemError::InvalidData(
4057 "can only bless a directory".into(),
4058 ));
4059 }
4060 self.mdb.finder_info[0] = entry.location as u32;
4061 Ok(())
4062 })();
4063 if result.is_err() {
4064 self.restore_snapshot(snap);
4065 }
4066 result
4067 }
4068
4069 fn write_boot_blocks(&mut self, blocks: &[u8; 1024]) -> Result<(), FilesystemError> {
4070 HfsFilesystem::set_boot_blocks(self, blocks);
4071 Ok(())
4072 }
4073
4074 fn set_volume_name(&mut self, new_name: &str) -> Result<(), FilesystemError> {
4075 HfsFilesystem::set_volume_name(self, new_name)
4076 }
4077
4078 fn set_finder_info(
4079 &mut self,
4080 entry: &FileEntry,
4081 finfo: [u8; 16],
4082 fxinfo: [u8; 16],
4083 ) -> Result<(), FilesystemError> {
4084 HfsFilesystem::set_finder_info(self, entry, finfo, fxinfo)
4085 }
4086
4087 fn set_dates(
4088 &mut self,
4089 entry: &FileEntry,
4090 create: u32,
4091 modify: u32,
4092 backup: u32,
4093 ) -> Result<(), FilesystemError> {
4094 HfsFilesystem::set_dates(self, entry, create, modify, backup)
4095 }
4096}
4097
4098fn collect_fork_overflow_extents(
4104 extents_data: &[u8],
4105 file_id: u32,
4106 fork_type: u8,
4107 min_start_block: u32,
4108) -> Vec<HfsExtDescriptor> {
4109 use crate::hfs_common::{btree_record_range, BTreeHeader, BTREE_LEAF_NODE};
4110 use std::collections::HashSet;
4111
4112 let mut out: Vec<(u16, HfsExtDescriptor)> = Vec::new();
4113 if extents_data.len() < 512 {
4114 return Vec::new();
4115 }
4116 let header = BTreeHeader::read(extents_data);
4117 let node_size = header.node_size as usize;
4118 if node_size == 0 || extents_data.len() < node_size {
4119 return Vec::new();
4120 }
4121
4122 let mut node_idx = header.first_leaf_node;
4123 let mut visited: HashSet<u32> = HashSet::new();
4124 while node_idx != 0 {
4125 if !visited.insert(node_idx) {
4126 break;
4127 }
4128 let off = node_idx as usize * node_size;
4129 if off + node_size > extents_data.len() {
4130 break;
4131 }
4132 let node = &extents_data[off..off + node_size];
4133 let flink = BigEndian::read_u32(&node[0..4]);
4134 let kind = node[8] as i8;
4135 if kind != BTREE_LEAF_NODE {
4136 break;
4137 }
4138 let num_records = BigEndian::read_u16(&node[10..12]) as usize;
4139 for i in 0..num_records {
4140 let (rec_start, _) = btree_record_range(node, node_size, i);
4141 if rec_start + 8 > node_size {
4142 continue;
4143 }
4144 let key_len = node[rec_start] as usize;
4145 if key_len < 7 || rec_start + 1 + key_len > node_size {
4146 continue;
4147 }
4148 let rec_fork_type = node[rec_start + 1];
4149 let rec_file_id = BigEndian::read_u32(&node[rec_start + 2..rec_start + 6]);
4150 let rec_start_block = BigEndian::read_u16(&node[rec_start + 6..rec_start + 8]);
4151 if rec_fork_type != fork_type || rec_file_id != file_id {
4152 continue;
4153 }
4154 if (rec_start_block as u32) < min_start_block {
4155 continue;
4156 }
4157 let mut data_off = rec_start + 1 + key_len;
4158 if !data_off.is_multiple_of(2) {
4159 data_off += 1;
4160 }
4161 if data_off + 12 > node_size {
4162 continue;
4163 }
4164 for j in 0..3 {
4165 let ext = HfsExtDescriptor::parse(&node[data_off + j * 4..data_off + j * 4 + 4]);
4166 if ext.block_count > 0 {
4167 out.push((rec_start_block, ext));
4168 }
4169 }
4170 }
4171 node_idx = flink;
4172 }
4173
4174 out.sort_by_key(|(sb, _)| *sb);
4175 out.into_iter().map(|(_, e)| e).collect()
4176}
4177
4178fn read_fork_data<R: Read + Seek>(
4179 reader: &mut R,
4180 partition_offset: u64,
4181 mdb: &HfsMasterDirectoryBlock,
4182 extents: &[HfsExtDescriptor; 3],
4183 size: u64,
4184) -> Result<Vec<u8>, FilesystemError> {
4185 let mut data = Vec::with_capacity(size as usize);
4186 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4187
4188 for ext in extents {
4189 if ext.block_count == 0 {
4190 break;
4191 }
4192 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4193 let len = ext.block_count as u64 * mdb.block_size as u64;
4194 reader.seek(SeekFrom::Start(offset))?;
4195 let read_len = len.min(size - data.len() as u64) as usize;
4196 let mut buf = vec![0u8; read_len];
4197 reader.read_exact(&mut buf)?;
4198 data.extend_from_slice(&buf);
4199 if data.len() as u64 >= size {
4200 break;
4201 }
4202 }
4203
4204 data.truncate(size as usize);
4205 Ok(data)
4206}
4207
4208fn remove_fork_overflow_records(
4211 extents_data: &mut [u8],
4212 file_id: u32,
4213 fork_type: u8,
4214) -> Result<u32, FilesystemError> {
4215 use crate::hfs_common::{
4216 btree_remove_record, btree_retire_empty_leaf, compare_classic_extents_keys,
4217 walk_leaf_records, BTreeHeader,
4218 };
4219 let kf = BTreeKeyFormat::CLASSIC_EXTENTS;
4220 if extents_data.len() < 512 {
4221 return Ok(0);
4222 }
4223 let header = BTreeHeader::read(extents_data);
4224 let node_size = header.node_size as usize;
4225 if node_size == 0 || extents_data.len() < node_size {
4226 return Ok(0);
4227 }
4228 let mut victims: Vec<(u32, usize)> = Vec::new();
4229 walk_leaf_records::<(), _>(
4230 extents_data,
4231 header.first_leaf_node,
4232 node_size,
4233 |node_idx, rec_idx, _off, rec| {
4234 if rec.len() >= 8
4236 && rec[0] >= 7
4237 && rec[1] == fork_type
4238 && BigEndian::read_u32(&rec[2..6]) == file_id
4239 {
4240 victims.push((node_idx, rec_idx));
4241 }
4242 None
4243 },
4244 );
4245 if victims.is_empty() {
4246 return Ok(0);
4247 }
4248 victims.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
4250 for &(node_idx, rec_idx) in &victims {
4251 let off = node_idx as usize * node_size;
4252 btree_remove_record(&mut extents_data[off..off + node_size], node_size, rec_idx);
4253 }
4254 let mut h = BTreeHeader::read(extents_data);
4255 h.leaf_records = h.leaf_records.saturating_sub(victims.len() as u32);
4256 h.write(extents_data);
4257 let mut nodes: Vec<u32> = victims.iter().map(|v| v.0).collect();
4258 nodes.dedup();
4259 for node_idx in nodes {
4260 let off = node_idx as usize * node_size;
4261 if BigEndian::read_u16(&extents_data[off + 10..off + 12]) == 0 {
4262 btree_retire_empty_leaf(
4264 extents_data,
4265 node_size,
4266 node_idx,
4267 &kf,
4268 &compare_classic_extents_keys,
4269 )?;
4270 } else if victims.iter().any(|&(n, r)| n == node_idx && r == 0) {
4271 btree_refresh_index_keys(
4273 extents_data,
4274 node_size,
4275 node_idx,
4276 &kf,
4277 &compare_classic_extents_keys,
4278 )?;
4279 }
4280 }
4281 Ok(victims.len() as u32)
4282}
4283
4284fn write_extents_overflow_data<W: std::io::Write + Seek>(
4286 writer: &mut W,
4287 partition_offset: u64,
4288 mdb: &HfsMasterDirectoryBlock,
4289 data: &[u8],
4290) -> Result<(), FilesystemError> {
4291 let alloc_start = partition_offset + mdb.first_alloc_block as u64 * 512;
4292 let mut data_start = 0usize;
4293 for extent in mdb.extents_file_extents.iter() {
4294 if extent.block_count == 0 {
4295 break;
4296 }
4297 let block_off = alloc_start + extent.start_block as u64 * mdb.block_size as u64;
4298 let byte_len = extent.block_count as usize * mdb.block_size as usize;
4299 let data_end = (data_start + byte_len).min(data.len());
4300 if data_start < data_end {
4301 writer.seek(std::io::SeekFrom::Start(block_off))?;
4302 writer.write_all(&data[data_start..data_end])?;
4303 }
4304 data_start += byte_len;
4305 }
4306 Ok(())
4307}
4308
4309fn write_fork_data_to<R: Read + Seek>(
4311 reader: &mut R,
4312 partition_offset: u64,
4313 mdb: &HfsMasterDirectoryBlock,
4314 extents: &[HfsExtDescriptor; 3],
4315 writer: &mut dyn std::io::Write,
4316 size: u64,
4317) -> Result<u64, FilesystemError> {
4318 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4319 let mut written: u64 = 0;
4320 let mut buf = vec![0u8; 64 * 1024];
4321
4322 for ext in extents {
4323 if ext.block_count == 0 || written >= size {
4324 break;
4325 }
4326 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4327 let extent_len = ext.block_count as u64 * mdb.block_size as u64;
4328 let to_emit = extent_len.min(size - written);
4329 reader.seek(SeekFrom::Start(offset))?;
4330 let mut left = to_emit;
4331 while left > 0 {
4332 let n = (buf.len() as u64).min(left) as usize;
4333 reader.read_exact(&mut buf[..n])?;
4334 writer.write_all(&buf[..n])?;
4335 left -= n as u64;
4336 }
4337 written += to_emit;
4338 }
4339 Ok(written)
4340}
4341
4342fn write_hfs_fork_data<R: Write + Seek>(
4344 writer: &mut R,
4345 partition_offset: u64,
4346 mdb: &HfsMasterDirectoryBlock,
4347 extents: &[HfsExtDescriptor; 3],
4348 data: &[u8],
4349) -> Result<(), FilesystemError> {
4350 let first_alloc_offset = partition_offset + mdb.first_alloc_block as u64 * 512;
4351 let mut written = 0usize;
4352 for ext in extents {
4353 if ext.block_count == 0 || written >= data.len() {
4354 break;
4355 }
4356 let offset = first_alloc_offset + ext.start_block as u64 * mdb.block_size as u64;
4357 let extent_len = ext.block_count as u64 * mdb.block_size as u64;
4358 let to_write = extent_len.min((data.len() - written) as u64) as usize;
4359 writer.seek(SeekFrom::Start(offset))?;
4360 writer.write_all(&data[written..written + to_write])?;
4361 written += to_write;
4362 }
4363 Ok(())
4364}
4365
4366fn find_last_set_bit(bitmap: &[u8], max_bits: u32) -> Option<u32> {
4368 let mut last = None;
4369 for bit in 0..max_bits {
4370 let byte_idx = bit as usize / 8;
4371 let bit_idx = 7 - (bit % 8); if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
4373 last = Some(bit);
4374 }
4375 }
4376 last
4377}
4378
4379pub struct CompactHfsReader<R: Read + Seek> {
4387 reader: R,
4388 partition_offset: u64,
4389 mdb: HfsMasterDirectoryBlock,
4390 bitmap: Vec<u8>,
4391 phase: u8,
4393 pre_alloc_pos: u64,
4395 current_block: u32,
4397 block_pos: u64,
4399 #[allow(dead_code)]
4400 original_size: u64,
4401 #[allow(dead_code)]
4402 allocated_blocks: u32,
4403}
4404
4405impl<R: Read + Seek> CompactHfsReader<R> {
4406 pub fn new(
4407 mut reader: R,
4408 partition_offset: u64,
4409 ) -> Result<(Self, CompactResult), FilesystemError> {
4410 log::debug!(
4412 "[HFS compact] seeking to MDB at offset {}",
4413 partition_offset + 1024
4414 );
4415 reader.seek(SeekFrom::Start(partition_offset + 1024))?;
4416 let mut mdb_buf = [0u8; 162];
4417 reader.read_exact(&mut mdb_buf)?;
4418 let mdb = HfsMasterDirectoryBlock::parse(&mdb_buf).map_err(|e| {
4419 log::debug!("[HFS compact] MDB parse failed: {e}");
4420 e
4421 })?;
4422 log::debug!(
4423 "[HFS compact] MDB ok: block_size={}, total_blocks={}, first_alloc_block={}, volume_bitmap_block={}",
4424 mdb.block_size, mdb.total_blocks, mdb.first_alloc_block, mdb.volume_bitmap_block,
4425 );
4426
4427 if mdb.has_embedded_hfs_plus() {
4428 log::debug!("[HFS compact] has embedded HFS+ — refusing to compact HFS wrapper");
4429 return Err(FilesystemError::Unsupported(
4430 "cannot compact HFS wrapper with embedded HFS+".into(),
4431 ));
4432 }
4433
4434 let bitmap_offset = partition_offset + mdb.volume_bitmap_block as u64 * 512;
4436 log::debug!("[HFS compact] reading bitmap at offset {bitmap_offset}");
4437 let bitmap_size = (mdb.total_blocks as u32).div_ceil(8) as usize;
4438 reader.seek(SeekFrom::Start(bitmap_offset))?;
4439 let mut bitmap = vec![0u8; bitmap_size];
4440 reader.read_exact(&mut bitmap)?;
4441 log::debug!("[HFS compact] bitmap read: {bitmap_size} bytes");
4442
4443 let mut allocated = 0u32;
4445 for bit in 0..mdb.total_blocks as u32 {
4446 let byte_idx = bit as usize / 8;
4447 let bit_idx = 7 - (bit % 8);
4448 if byte_idx < bitmap.len() && (bitmap[byte_idx] >> bit_idx) & 1 == 1 {
4449 allocated += 1;
4450 }
4451 }
4452 log::debug!(
4453 "[HFS compact] allocated={} / {} total blocks ({} free)",
4454 allocated,
4455 mdb.total_blocks,
4456 mdb.total_blocks as u32 - allocated,
4457 );
4458
4459 let pre_alloc_size = mdb.first_alloc_block as u64 * 512;
4461 let original_size = pre_alloc_size + mdb.total_blocks as u64 * mdb.block_size as u64;
4462 let compacted_size = original_size;
4465 let data_size = pre_alloc_size + allocated as u64 * mdb.block_size as u64;
4467 log::debug!(
4468 "[HFS compact] pre_alloc_size={}, data_size={}, compacted_size={} original_size={} (layout-preserving; free blocks -> zeros)",
4469 pre_alloc_size, data_size, compacted_size, original_size
4470 );
4471
4472 let result = CompactResult {
4473 original_size,
4474 compacted_size,
4475 data_size,
4476 clusters_used: allocated,
4477 };
4478
4479 Ok((
4480 CompactHfsReader {
4481 reader,
4482 partition_offset,
4483 mdb,
4484 bitmap,
4485 phase: 0,
4486 pre_alloc_pos: 0,
4487 current_block: 0,
4488 block_pos: 0,
4489 original_size,
4490 allocated_blocks: allocated,
4491 },
4492 result,
4493 ))
4494 }
4495
4496 fn is_block_allocated(&self, block: u32) -> bool {
4497 let byte_idx = block as usize / 8;
4498 let bit_idx = 7 - (block % 8);
4499 byte_idx < self.bitmap.len() && (self.bitmap[byte_idx] >> bit_idx) & 1 == 1
4500 }
4501}
4502
4503impl<R: Read + Seek> Read for CompactHfsReader<R> {
4504 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
4505 if self.phase > 1 {
4506 return Ok(0);
4507 }
4508
4509 if self.phase == 0 {
4510 let pre_alloc_size = self.mdb.first_alloc_block as u64 * 512;
4513 let remaining = pre_alloc_size - self.pre_alloc_pos;
4514 if remaining == 0 {
4515 self.phase = 1;
4516 return self.read(buf);
4517 }
4518 let to_read = (remaining as usize).min(buf.len());
4519 let offset = self.partition_offset + self.pre_alloc_pos;
4520 self.reader
4521 .seek(SeekFrom::Start(offset))
4522 .map_err(disk_core::io_other)?;
4523 let n = self.reader.read(&mut buf[..to_read])?;
4524 self.pre_alloc_pos += n as u64;
4525 return Ok(n);
4526 }
4527
4528 let total_blocks = self.mdb.total_blocks as u32;
4531 if self.current_block >= total_blocks {
4532 self.phase = 2;
4533 return Ok(0);
4534 }
4535
4536 let block_size = self.mdb.block_size as u64;
4537
4538 let allocated = self.is_block_allocated(self.current_block);
4543 let mut run_end = self.current_block + 1;
4544 while run_end < total_blocks && self.is_block_allocated(run_end) == allocated {
4545 run_end += 1;
4546 }
4547 let bytes_in_run = (run_end - self.current_block) as u64 * block_size - self.block_pos;
4548 let to_read = (bytes_in_run as usize).min(buf.len());
4549
4550 let n = if allocated {
4551 let offset = self.partition_offset
4552 + self.mdb.first_alloc_block as u64 * 512
4553 + self.current_block as u64 * block_size
4554 + self.block_pos;
4555 self.reader
4556 .seek(SeekFrom::Start(offset))
4557 .map_err(disk_core::io_other)?;
4558 self.reader.read(&mut buf[..to_read])?
4559 } else {
4560 buf[..to_read].fill(0);
4562 to_read
4563 };
4564
4565 let mut remaining_in_n = n as u64;
4567 if self.block_pos > 0 {
4568 let want = block_size - self.block_pos;
4569 let take = remaining_in_n.min(want);
4570 self.block_pos += take;
4571 remaining_in_n -= take;
4572 if self.block_pos >= block_size {
4573 self.block_pos = 0;
4574 self.current_block += 1;
4575 }
4576 }
4577 let whole = (remaining_in_n / block_size) as u32;
4578 self.current_block += whole;
4579 remaining_in_n -= whole as u64 * block_size;
4580 if remaining_in_n > 0 {
4581 self.block_pos = remaining_in_n;
4582 }
4583
4584 Ok(n)
4585 }
4586}
4587
4588pub fn fit_hfs_volume_to_partition(
4603 device: &mut (impl Read + Write + Seek),
4604 partition_offset: u64,
4605 partition_len: u64,
4606 log: &mut impl FnMut(&str),
4607) -> anyhow::Result<bool> {
4608 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4609 let mut sector = [0u8; 512];
4610 device.read_exact(&mut sector)?;
4611 if BigEndian::read_u16(§or[0..2]) != HFS_SIGNATURE {
4612 return Ok(false);
4613 }
4614 if BigEndian::read_u16(§or[0x7C..0x7E]) == 0x482B {
4616 return Ok(false);
4617 }
4618 let total_blocks = BigEndian::read_u16(§or[0x12..0x14]) as u64;
4619 let block_size = BigEndian::read_u32(§or[0x14..0x18]) as u64;
4620 let first_alloc = BigEndian::read_u16(§or[0x1C..0x1E]) as u64;
4621 let volume_len = first_alloc * 512 + total_blocks * block_size + 1024;
4622 if block_size == 0 || volume_len >= partition_len {
4623 return Ok(false);
4624 }
4625 let growable = hfs_max_growable_size(device, partition_offset).unwrap_or(0);
4626 if growable < partition_len {
4627 log(&format!(
4628 "Warning: the HFS volume ({volume_len} bytes) does not fill its {partition_len}-byte \
4629 partition and its volume bitmap cannot address more blocks; Disk First Aid will \
4630 report an invalid allocation block start. Format the volume at the partition size"
4631 ));
4632 return Ok(false);
4633 }
4634 resize_hfs_in_place(device, partition_offset, partition_len, log)?;
4635 log(&format!(
4636 "HFS volume grown from {volume_len} bytes to fill its {partition_len}-byte partition"
4637 ));
4638 Ok(true)
4639}
4640
4641pub fn hfs_max_growable_size(
4642 device: &mut (impl Read + Seek),
4643 partition_offset: u64,
4644) -> Option<u64> {
4645 device.seek(SeekFrom::Start(partition_offset + 1024)).ok()?;
4646 let mut sector = [0u8; 512];
4647 device.read_exact(&mut sector).ok()?;
4648
4649 let sig = BigEndian::read_u16(§or[0..2]);
4650 if sig != HFS_SIGNATURE {
4651 return None;
4652 }
4653
4654 let block_size = BigEndian::read_u32(§or[20..24]) as u64;
4655 let vbm_start = BigEndian::read_u16(§or[14..16]) as u64;
4656 let first_alloc = BigEndian::read_u16(§or[28..30]) as u64;
4657
4658 if block_size == 0 || first_alloc <= vbm_start {
4659 return None;
4660 }
4661
4662 let vbm_capacity = (first_alloc - vbm_start) * 512 * 8;
4663 let max_blocks = vbm_capacity.min(u16::MAX as u64);
4664 let overhead = first_alloc * 512 + 1024;
4666
4667 Some(max_blocks * block_size + overhead)
4668}
4669
4670pub fn resize_hfs_in_place(
4671 device: &mut (impl Read + Write + Seek),
4672 partition_offset: u64,
4673 new_size_bytes: u64,
4674 log: &mut impl FnMut(&str),
4675) -> anyhow::Result<()> {
4676 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4680 let mut sector = [0u8; 512];
4681 device.read_exact(&mut sector)?;
4682
4683 let sig = BigEndian::read_u16(§or[0..2]);
4684 if sig != HFS_SIGNATURE {
4685 log("HFS resize: not an HFS volume, skipping");
4686 return Ok(());
4687 }
4688
4689 let block_size = BigEndian::read_u32(§or[20..24]);
4690 let old_total = BigEndian::read_u16(§or[18..20]);
4691 let free_blocks = BigEndian::read_u16(§or[34..36]);
4692 let vbm_start = BigEndian::read_u16(§or[14..16]);
4693 let first_alloc = BigEndian::read_u16(§or[28..30]);
4694 let used_blocks = old_total - free_blocks;
4695
4696 let vbm_capacity_blocks = if first_alloc > vbm_start {
4700 (first_alloc - vbm_start) as u64 * 512 * 8
4701 } else {
4702 0
4703 };
4704
4705 let overhead = first_alloc as u64 * 512 + 1024;
4708 if new_size_bytes <= overhead {
4709 anyhow::bail!(
4710 "HFS resize: new size {} bytes <= overhead {} bytes",
4711 new_size_bytes,
4712 overhead
4713 );
4714 }
4715 let new_total_u64 = (new_size_bytes - overhead) / block_size as u64;
4716
4717 if new_total_u64 > u16::MAX as u64 {
4720 let max_bytes = u16::MAX as u64 * block_size as u64 + overhead;
4721 anyhow::bail!(
4722 "HFS resize: requested size {} MB needs {} allocation blocks at {}-byte blocks, \
4723 but HFS caps at 65535 blocks (max {} MB for this volume). \
4724 Growing further would require reformatting with a larger allocation block size.",
4725 new_size_bytes / 1024 / 1024,
4726 new_total_u64,
4727 block_size,
4728 max_bytes / 1024 / 1024,
4729 );
4730 }
4731 if new_total_u64 > vbm_capacity_blocks {
4732 anyhow::bail!(
4733 "HFS resize: requested {} allocation blocks exceeds the Volume Bitmap capacity \
4734 ({} blocks) for this volume. The VBM lives in fixed sectors [{}..{}) and cannot \
4735 be grown without reformatting: the most this volume can grow to is {} bytes. \
4736 Mac OS wants a classic HFS volume to fill its partition, so format one at the \
4737 partition size instead (`new volume hfs --size`, then `new hd --fill`).",
4738 new_total_u64,
4739 vbm_capacity_blocks,
4740 vbm_start,
4741 first_alloc,
4742 vbm_capacity_blocks.min(u16::MAX as u64) * block_size as u64 + overhead,
4743 );
4744 }
4745 let new_total = new_total_u64 as u16;
4746
4747 let vbm_len = (first_alloc - vbm_start) as usize * 512;
4750 let mut vbm = vec![0u8; vbm_len];
4751 device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
4752 device.read_exact(&mut vbm)?;
4753 let highest_used = find_last_set_bit(&vbm, old_total as u32).map_or(0, |b| b + 1);
4754 if (new_total as u32) < highest_used {
4755 anyhow::bail!(
4756 "HFS resize: new size {} blocks ends before the last allocated block ({}); \
4757 {} of {} blocks are in use",
4758 new_total,
4759 highest_used,
4760 used_blocks,
4761 old_total
4762 );
4763 }
4764 for b in old_total.min(new_total) as u32..old_total.max(new_total) as u32 {
4767 bitmap_clear_bit_be(&mut vbm, b);
4768 }
4769 device.seek(SeekFrom::Start(partition_offset + vbm_start as u64 * 512))?;
4770 device.write_all(&vbm)?;
4771
4772 let new_free = new_total - used_blocks;
4773
4774 log(&format!(
4775 "HFS resize: {} -> {} blocks ({} free)",
4776 old_total, new_total, new_free
4777 ));
4778
4779 BigEndian::write_u16(&mut sector[18..20], new_total);
4781 BigEndian::write_u16(&mut sector[34..36], new_free);
4782
4783 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4785 device.write_all(§or)?;
4786
4787 if new_size_bytes > 1024 {
4789 device.seek(SeekFrom::Start(partition_offset + new_size_bytes - 1024))?;
4790 device.write_all(§or)?;
4791 }
4792
4793 device.flush()?;
4794 Ok(())
4795}
4796
4797pub fn validate_hfs_integrity(
4799 device: &mut (impl Read + Seek),
4800 partition_offset: u64,
4801 log: &mut impl FnMut(&str),
4802) -> anyhow::Result<()> {
4803 device.seek(SeekFrom::Start(partition_offset + 1024))?;
4805 let mut sector = [0u8; 512];
4806 device.read_exact(&mut sector)?;
4807
4808 let sig = BigEndian::read_u16(§or[0..2]);
4809 if sig != HFS_SIGNATURE {
4810 log("HFS validate: not an HFS volume, skipping");
4811 return Ok(());
4812 }
4813
4814 let block_size = BigEndian::read_u32(§or[20..24]);
4815 let total_blocks = BigEndian::read_u16(§or[18..20]);
4816
4817 if !block_size.is_power_of_two() || block_size < 512 {
4819 anyhow::bail!("HFS validate: invalid block size {block_size}");
4820 }
4821 if total_blocks == 0 {
4822 anyhow::bail!("HFS validate: zero total blocks");
4823 }
4824
4825 log(&format!(
4826 "HFS validate: OK ({total_blocks} blocks, {} block size)",
4827 block_size
4828 ));
4829 Ok(())
4830}
4831
4832#[cfg(test)]
4833#[allow(clippy::identity_op)] mod tests {
4835 use super::*;
4836 use std::io::Cursor;
4837
4838 #[test]
4842 fn write_blank_hfs_into_matches_create_blank_hfs() {
4843 let (size, bs) = (8 * 1024 * 1024u64, 4096u32);
4844 let now = 0xB0A1_2345;
4845 let mem = create_blank_hfs_sized_at(size, bs, "StreamEq", 0, 0, now).unwrap();
4846
4847 let mut sink = Cursor::new(vec![0u8; 0]);
4848 let written = write_blank_hfs_into_at(&mut sink, 0, size, bs, "StreamEq", now).unwrap();
4849 let mut streamed = sink.into_inner();
4850 streamed.resize(written as usize, 0);
4851
4852 assert_eq!(written as usize, mem.len());
4853 assert_eq!(streamed, mem);
4854 }
4855
4856 #[test]
4859 fn write_blank_hfs_into_honours_the_offset() {
4860 let (size, bs) = (4 * 1024 * 1024u64, 2048u32);
4861 let now = 0xB0A1_2345;
4862 let mem = create_blank_hfs_sized_at(size, bs, "Offset", 0, 0, now).unwrap();
4863
4864 let offset = 64 * 512u64;
4865 let mut sink = Cursor::new(vec![0u8; 0]);
4866 let written = write_blank_hfs_into_at(&mut sink, offset, size, bs, "Offset", now).unwrap();
4867 let mut streamed = sink.into_inner();
4868 streamed.resize((offset + written) as usize, 0);
4869
4870 assert!(streamed[..offset as usize].iter().all(|&b| b == 0));
4871 assert_eq!(&streamed[offset as usize..], &mem[..]);
4872 }
4873
4874 #[test]
4875 fn test_mac_roman_to_utf8_ascii() {
4876 let data = b"Hello World";
4877 assert_eq!(mac_roman_to_utf8(data), "Hello World");
4878 }
4879
4880 #[test]
4881 fn test_mac_roman_to_utf8_special() {
4882 let data = [0x80, 0x81, 0x87];
4884 assert_eq!(mac_roman_to_utf8(&data), "ÄÅá");
4885 }
4886
4887 #[test]
4888 fn test_decode_mac_filename_utf8() {
4889 let mut data = b"HeapFixer".to_vec();
4892 data.extend_from_slice(&[0xE2, 0x84, 0xA2]);
4893 data.extend_from_slice(b" 1.01");
4894 assert_eq!(decode_mac_filename(&data), "HeapFixer™ 1.01");
4895 }
4896
4897 #[test]
4898 fn test_decode_mac_filename_macroman() {
4899 assert_eq!(decode_mac_filename(&[0xC4]), "ƒ");
4902 assert_eq!(decode_mac_filename(b"Cache\xA5Flow"), "Cache•Flow");
4903 assert_eq!(decode_mac_filename(b"Virex\xA8 3.51"), "Virex® 3.51");
4904 }
4905
4906 #[test]
4907 fn test_decode_mac_filename_ascii() {
4908 assert_eq!(decode_mac_filename(b"ScriptEdit"), "ScriptEdit");
4909 }
4910
4911 #[test]
4912 fn test_format_ostype() {
4913 assert_eq!(format_ostype(&[b'S', 0xA5, b'L', b' ']), "S•L");
4915 assert_eq!(format_ostype(b"APPL"), "APPL");
4916 assert_eq!(format_ostype(&[b'L', b's', 0x00, 0x00]), "Ls");
4918 }
4919
4920 #[test]
4921 fn test_mdb_parse() {
4922 let mut data = [0u8; 162];
4923 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
4925 BigEndian::write_u16(&mut data[18..20], 1000);
4927 BigEndian::write_u32(&mut data[20..24], 4096);
4929 BigEndian::write_u16(&mut data[34..36], 200);
4931 data[36] = 7;
4933 data[37..44].copy_from_slice(b"TestVol");
4934
4935 let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
4936 assert_eq!(mdb.signature, HFS_SIGNATURE);
4937 assert_eq!(mdb.total_blocks, 1000);
4938 assert_eq!(mdb.block_size, 4096);
4939 assert_eq!(mdb.free_blocks, 200);
4940 assert_eq!(mdb.volume_name, "TestVol");
4941 assert!(!mdb.has_embedded_hfs_plus());
4942 }
4943
4944 #[test]
4945 fn test_mdb_detect_embedded_hfs_plus() {
4946 let mut data = [0u8; 162];
4947 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
4948 BigEndian::write_u16(&mut data[18..20], 1000);
4949 BigEndian::write_u32(&mut data[20..24], 4096);
4950 BigEndian::write_u16(&mut data[124..126], HFS_PLUS_EMBEDDED_SIGNATURE);
4952 BigEndian::write_u16(&mut data[126..128], 10); BigEndian::write_u16(&mut data[128..130], 500); let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
4956 assert!(mdb.has_embedded_hfs_plus());
4957 }
4958
4959 #[test]
4960 fn test_find_last_set_bit() {
4961 let bitmap = [0b10100000, 0b00001000]; assert_eq!(find_last_set_bit(&bitmap, 16), Some(12));
4963
4964 let bitmap_empty = [0u8; 2];
4965 assert_eq!(find_last_set_bit(&bitmap_empty, 16), None);
4966
4967 let bitmap_first = [0b10000000]; assert_eq!(find_last_set_bit(&bitmap_first, 8), Some(0));
4969 }
4970
4971 #[test]
4972 fn test_utf8_to_mac_roman_roundtrip() {
4973 let text = "Hello World";
4974 let mac = utf8_to_mac_roman(text).unwrap();
4975 assert_eq!(mac, b"Hello World");
4976 assert_eq!(mac_roman_to_utf8(&mac), text);
4977 }
4978
4979 #[test]
4980 fn test_utf8_to_mac_roman_special() {
4981 let text = "Ä"; let mac = utf8_to_mac_roman(text).unwrap();
4983 assert_eq!(mac, &[0x80]);
4984 assert_eq!(mac_roman_to_utf8(&mac), text);
4985 }
4986
4987 #[test]
4988 fn test_utf8_to_mac_roman_unencodable() {
4989 let text = "\u{4E2D}"; assert!(utf8_to_mac_roman(text).is_err());
4991 }
4992
4993 #[test]
4998 fn classic_hfs_stamps_are_local_time_and_round_trip_to_utc() {
4999 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5000 use disk_core::times::{local_utc_offset_secs, unix_to_mac_epoch, UnixTimes};
5001 const T: u64 = 1_700_000_000; let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Zone").unwrap();
5003 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5004 let root = fs.root().unwrap();
5005 let opts = CreateFileOptions {
5006 unix_times: Some(UnixTimes::mtime_only(T)),
5007 ..Default::default()
5008 };
5009 fs.create_file(&root, "stamp", &mut &b"x"[..], 1, &opts)
5010 .unwrap();
5011 fs.sync_metadata().unwrap();
5012 let entry = fs
5013 .list_directory(&root)
5014 .unwrap()
5015 .into_iter()
5016 .find(|e| e.name == "stamp")
5017 .unwrap();
5018 assert_eq!(entry.modified_unix, Some(T), "UTC must round-trip");
5019 let raw = entry.mac_dates.expect("raw Mac dates").1;
5020 let expected = (T as i64 + local_utc_offset_secs()) as u64;
5021 assert_eq!(
5022 raw,
5023 unix_to_mac_epoch(expected),
5024 "on disk it is the local wall clock"
5025 );
5026 }
5027
5028 #[test]
5032 fn the_alternate_mdb_follows_the_partition_end_when_the_length_is_known() {
5033 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5034 const VOLUME: u64 = 8 * 1024 * 1024;
5035 const PARTITION: u64 = VOLUME + 64 * 1024;
5036 let mut img = create_blank_hfs(VOLUME, 4096, "Slack").unwrap();
5037 img.resize(PARTITION as usize, 0);
5038 {
5039 let mut fs =
5040 HfsFilesystem::open_sized(Cursor::new(&mut img), 0, Some(PARTITION)).unwrap();
5041 let root = fs.root().unwrap();
5042 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
5043 .unwrap();
5044 fs.sync_metadata().unwrap();
5045 }
5046 let primary = &img[1024..1536];
5047 let alt = &img[PARTITION as usize - 1024..PARTITION as usize - 512];
5048 assert_eq!(
5049 &alt[0..2],
5050 b"BD",
5051 "alternate MDB missing at the partition end"
5052 );
5053 assert_eq!(primary, alt, "the mirror must equal the primary MDB");
5054 }
5055
5056 #[test]
5059 fn create_file_writes_the_finder_flags_it_is_given() {
5060 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5061 let mut img = create_blank_hfs(4 * 1024 * 1024, 4096, "Flags").unwrap();
5062 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5063 let root = fs.root().unwrap();
5064 let opts = CreateFileOptions {
5065 os_type: Some(*b"APPL"),
5066 os_creator: Some(*b"ttxt"),
5067 finder_flags: Some(0x2000),
5068 ..Default::default()
5069 };
5070 fs.create_file(&root, "app", &mut &b"x"[..], 1, &opts)
5071 .unwrap();
5072 fs.sync_metadata().unwrap();
5073 let entry = fs
5074 .list_directory(&root)
5075 .unwrap()
5076 .into_iter()
5077 .find(|e| e.name == "app")
5078 .unwrap();
5079 assert_eq!(entry.finder_flags, Some(0x2000));
5080 }
5081
5082 #[test]
5083 fn shrink_is_refused_when_data_sits_past_the_new_end() {
5084 use disk_core::filesystem::{CreateFileOptions, EditableFilesystem, Filesystem};
5085 const SIZE: u64 = 8 * 1024 * 1024;
5086 let mut img = create_blank_hfs(SIZE, 4096, "Tail").unwrap();
5087 {
5088 let mut fs = HfsFilesystem::open(Cursor::new(&mut img), 0).unwrap();
5089 let root = fs.root().unwrap();
5090 fs.create_file(&root, "a", &mut &b"x"[..], 1, &CreateFileOptions::default())
5091 .unwrap();
5092 fs.sync_metadata().unwrap();
5093 }
5094 let total = BigEndian::read_u16(&img[1024 + 18..1024 + 20]) as usize;
5095 let vbm_start = BigEndian::read_u16(&img[1024 + 14..1024 + 16]) as usize;
5096 let tail = total - 1;
5098 let byte = vbm_start * 512 + tail / 8;
5099 img[byte] |= 0x80 >> (tail % 8);
5100 let err = resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
5101 .expect_err("a tail block in use must refuse the shrink");
5102 assert!(format!("{err}").contains("last allocated block"), "{err}");
5103 img[byte] &= !(0x80 >> (tail % 8));
5104 resize_hfs_in_place(&mut Cursor::new(&mut img), 0, SIZE / 2, &mut |_| {})
5105 .expect("with the tail free the shrink goes through");
5106 }
5107
5108 #[test]
5109 fn test_resize_hfs_rejects_u16_overflow() {
5110 let mut sector = [0u8; 512];
5114 BigEndian::write_u16(&mut sector[0..2], HFS_SIGNATURE);
5115 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];
5127 img[1024..1024 + 512].copy_from_slice(§or);
5128
5129 let mut cursor = Cursor::new(img);
5130 let res = resize_hfs_in_place(
5131 &mut cursor,
5132 0,
5133 2047 * 1024 * 1024, &mut |_| {},
5135 );
5136 let err = res.expect_err("resize should have bailed on u16 overflow");
5137 let msg = format!("{err}");
5138 assert!(
5139 msg.contains("65535") || msg.contains("u16") || msg.contains("Volume Bitmap"),
5140 "unexpected error: {msg}"
5141 );
5142 }
5143
5144 #[test]
5145 fn test_mdb_serialize_roundtrip() {
5146 let mut data = [0u8; 512];
5147 BigEndian::write_u16(&mut data[0..2], HFS_SIGNATURE);
5148 BigEndian::write_u16(&mut data[18..20], 100);
5149 BigEndian::write_u32(&mut data[20..24], 4096);
5150 BigEndian::write_u16(&mut data[34..36], 50);
5151 BigEndian::write_u32(&mut data[30..34], 10); BigEndian::write_u32(&mut data[92..96], 42); data[36] = 4;
5154 data[37..41].copy_from_slice(b"Test");
5155
5156 let mdb = HfsMasterDirectoryBlock::parse(&data).unwrap();
5157 let serialized = mdb.serialize_to_sector();
5158 assert_eq!(BigEndian::read_u16(&serialized[18..20]), 100);
5159 assert_eq!(BigEndian::read_u32(&serialized[30..34]), 10);
5160 assert_eq!(BigEndian::read_u32(&serialized[92..96]), 42);
5161 }
5162
5163 fn make_editable_hfs_image() -> Vec<u8> {
5175 let block_size = 4096u32;
5176 let total_blocks = 128u16;
5177 let first_alloc_block = 5u16; let alloc_start = first_alloc_block as usize * 512;
5179 let image_size = alloc_start + total_blocks as usize * block_size as usize;
5180 let mut img = vec![0u8; image_size];
5181
5182 let catalog_start = 0u16; let catalog_blocks = 8u16;
5184 let catalog_size = catalog_blocks as u32 * block_size;
5185
5186 let bitmap_sector = 3u16;
5188 let bitmap_off = bitmap_sector as usize * 512;
5189 img[bitmap_off] = 0b11111111;
5191
5192 let node_size = 4096usize;
5194 let catalog_offset = alloc_start; let hdr_off = catalog_offset;
5198 img[hdr_off + 8] = 1; BigEndian::write_u16(&mut img[hdr_off + 10..hdr_off + 12], 3); let hr = hdr_off + 14;
5203 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;
5215 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;
5222
5223 let leaf_off = catalog_offset + node_size;
5225 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;
5233 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;
5239 let r0_data = r0_off + 8;
5241 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;
5248 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;
5254 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;
5264 BigEndian::write_u16(&mut img[lot - 2..lot], 14); let r1_rel = (r1_off - leaf_off) as u16;
5266 BigEndian::write_u16(&mut img[lot - 4..lot - 2], r1_rel); let free_rel = (r1_data + 46 - leaf_off) as u16;
5268 BigEndian::write_u16(&mut img[lot - 6..lot - 4], free_rel); let mdb_off = 1024;
5272 let mut mdb = [0u8; 512];
5273 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
5274 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);
5278 BigEndian::write_u32(&mut mdb[20..24], block_size);
5279 BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
5280 BigEndian::write_u32(&mut mdb[30..34], 16); BigEndian::write_u16(&mut mdb[34..36], total_blocks - catalog_blocks); mdb[36] = 7;
5284 mdb[37..44].copy_from_slice(b"TestVol");
5285 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);
5289 BigEndian::write_u16(&mut mdb[150..152], catalog_start);
5291 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
5292
5293 img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
5294
5295 img
5296 }
5297
5298 fn make_uninitialized_hfs_image() -> Vec<u8> {
5303 let block_size = 512u32;
5304 let total_blocks = 256u16;
5305 let first_alloc_block = 6u16;
5306 let alloc_start = first_alloc_block as usize * 512;
5307 let image_size = alloc_start + total_blocks as usize * block_size as usize;
5308 let mut img = vec![0u8; image_size];
5309
5310 let extents_start = 0u16;
5312 let extents_blocks = 4u16;
5313 let catalog_start = 4u16;
5315 let catalog_blocks = 4u16;
5316
5317 let mdb_off = 1024usize;
5320 let mut mdb = [0u8; 512];
5321 BigEndian::write_u16(&mut mdb[0..2], HFS_SIGNATURE);
5322 BigEndian::write_u32(&mut mdb[2..6], hfs_common::hfs_now());
5323 BigEndian::write_u32(&mut mdb[6..10], hfs_common::hfs_now());
5324 BigEndian::write_u16(&mut mdb[14..16], 3); BigEndian::write_u16(&mut mdb[18..20], total_blocks);
5326 BigEndian::write_u32(&mut mdb[20..24], block_size);
5327 BigEndian::write_u16(&mut mdb[28..30], first_alloc_block);
5328 BigEndian::write_u32(&mut mdb[30..34], 16);
5329 BigEndian::write_u16(
5330 &mut mdb[34..36],
5331 total_blocks - extents_blocks - catalog_blocks,
5332 );
5333 mdb[36] = 5;
5335 mdb[37..42].copy_from_slice(b"Blank");
5336 BigEndian::write_u32(&mut mdb[130..134], extents_blocks as u32 * block_size);
5338 BigEndian::write_u16(&mut mdb[134..136], extents_start);
5339 BigEndian::write_u16(&mut mdb[136..138], extents_blocks);
5340 BigEndian::write_u32(&mut mdb[146..150], catalog_blocks as u32 * block_size);
5342 BigEndian::write_u16(&mut mdb[150..152], catalog_start);
5343 BigEndian::write_u16(&mut mdb[152..154], catalog_blocks);
5344
5345 img[mdb_off..mdb_off + 512].copy_from_slice(&mdb);
5346 img
5347 }
5348
5349 #[test]
5350 fn test_hfs_empty_volume_auto_initialize_and_create_file() {
5351 let img = make_uninitialized_hfs_image();
5354 let cursor = Cursor::new(img);
5355 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5356 assert_eq!(fs.fs_type(), "HFS");
5357
5358 let root = fs.root().unwrap();
5360 let entries = fs.list_directory(&root).unwrap();
5361 assert!(entries.is_empty());
5362
5363 let data = b"auto-init test";
5365 let mut cursor = Cursor::new(data.as_slice());
5366 let fe = fs
5367 .create_file(
5368 &root,
5369 "hello.txt",
5370 &mut cursor,
5371 data.len() as u64,
5372 &CreateFileOptions::default(),
5373 )
5374 .unwrap();
5375 assert_eq!(fe.name, "hello.txt");
5376 fs.sync_metadata().unwrap();
5377
5378 let entries = fs.list_directory(&root).unwrap();
5380 assert_eq!(entries.len(), 1);
5381 assert_eq!(entries[0].name, "hello.txt");
5382 let read_back = fs.read_file(&fe, 1024).unwrap();
5383 assert_eq!(&read_back, data);
5384
5385 let result = fs.fsck().unwrap();
5387 let err_msgs: Vec<_> = result.errors.iter().map(|e| e.message.clone()).collect();
5388 let warn_msgs: Vec<_> = result.warnings.iter().map(|w| w.message.clone()).collect();
5389 assert!(
5390 result.errors.is_empty(),
5391 "unexpected fsck errors: {err_msgs:?}"
5392 );
5393 assert!(
5394 result.warnings.is_empty(),
5395 "unexpected fsck warnings: {warn_msgs:?}"
5396 );
5397 }
5398
5399 #[test]
5400 fn test_hfs_editable_open_and_free_space() {
5401 let img = make_editable_hfs_image();
5402 let cursor = Cursor::new(img);
5403 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5404 assert_eq!(fs.fs_type(), "HFS");
5405 assert_eq!(fs.volume_label(), Some("TestVol"));
5406 let free = fs.free_space().unwrap();
5407 assert_eq!(free, 120 * 4096);
5409 }
5410
5411 #[test]
5412 fn test_hfs_editable_create_file_and_read() {
5413 let img = make_editable_hfs_image();
5414 let cursor = Cursor::new(img);
5415 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5416
5417 let root = fs.root().unwrap();
5418 let test_data = b"Hello, HFS World!";
5419 let mut data_reader = Cursor::new(test_data.as_slice());
5420 let options = CreateFileOptions::default();
5421 let fe = fs
5422 .create_file(
5423 &root,
5424 "test.txt",
5425 &mut data_reader,
5426 test_data.len() as u64,
5427 &options,
5428 )
5429 .unwrap();
5430
5431 assert_eq!(fe.name, "test.txt");
5432 assert_eq!(fe.size, test_data.len() as u64);
5433
5434 let entries = fs.list_directory(&root).unwrap();
5436 assert!(entries.iter().any(|e| e.name == "test.txt"));
5437
5438 let read_back = fs.read_file(&fe, 1024).unwrap();
5440 assert_eq!(&read_back, test_data);
5441 }
5442
5443 #[test]
5451 fn test_hfs_file_record_first_alloc_block_fields_are_zero() {
5452 use crate::hfs_common::walk_leaf_records;
5453 use disk_core::filesystem::ResourceForkSource;
5454
5455 let img = make_editable_hfs_image();
5456 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5457 let root = fs.root().unwrap();
5458
5459 let data = b"data fork contents that span a block";
5462 let options = CreateFileOptions {
5463 resource_fork: Some(ResourceForkSource::Data(b"resource fork bytes".to_vec())),
5464 ..Default::default()
5465 };
5466 fs.create_file(
5467 &root,
5468 "forked.bin",
5469 &mut Cursor::new(data.as_slice()),
5470 data.len() as u64,
5471 &options,
5472 )
5473 .unwrap();
5474 fs.sync_metadata().unwrap();
5475
5476 let node_size = BigEndian::read_u16(&fs.catalog_data[32..34]) as usize;
5477 let first_leaf = BigEndian::read_u32(&fs.catalog_data[24..28]);
5478 let found = walk_leaf_records::<bool, _>(
5479 &fs.catalog_data,
5480 first_leaf,
5481 node_size,
5482 |_n, _i, _off, rec| {
5483 let klen = rec[0] as usize;
5484 let mut ds = 1 + klen;
5485 if !ds.is_multiple_of(2) {
5486 ds += 1;
5487 }
5488 let body = &rec[ds..];
5489 if body.first() != Some(&2) {
5490 return None; }
5492 let fil_st_blk = BigEndian::read_u16(&body[24..26]);
5493 let fil_rst_blk = BigEndian::read_u16(&body[34..36]);
5494 let data_ext_start = BigEndian::read_u16(&body[74..76]);
5495 let rsrc_ext_start = BigEndian::read_u16(&body[86..88]);
5496 assert_eq!(fil_st_blk, 0, "filStBlk (offset 24) must be 0 on disk");
5497 assert_eq!(fil_rst_blk, 0, "filRStBlk (offset 34) must be 0 on disk");
5498 assert_ne!(data_ext_start, 0, "data fork extent should be allocated");
5500 assert_ne!(rsrc_ext_start, 0, "rsrc fork extent should be allocated");
5501 Some(true)
5502 },
5503 );
5504 assert_eq!(found, Some(true), "file record not found in catalog");
5505 }
5506
5507 #[test]
5512 fn test_hfs_catalog_dates_round_trip() {
5513 let img = make_editable_hfs_image();
5514 let mut buf = img.clone();
5515
5516 {
5518 let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
5519 let root = fs.root().unwrap();
5520 let data = b"x";
5521 let fe = fs
5522 .create_file(
5523 &root,
5524 "dated.txt",
5525 &mut Cursor::new(data.as_slice()),
5526 1,
5527 &CreateFileOptions::default(),
5528 )
5529 .unwrap();
5530 fs.sync_metadata().unwrap();
5531
5532 let listed = fs.list_directory(&root).unwrap();
5533 let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
5534 let (c, m, b) = f.mac_dates.expect("HFS file should carry mac_dates");
5535 assert!(c != 0 && m != 0, "create/modify stamped by create_file");
5536 assert_eq!(b, 0, "backup date defaults to 0");
5537
5538 let target = (0x10000000u32, 0x20000000u32, 0x30000000u32);
5540 fs.set_dates(&fe, target.0, target.1, target.2).unwrap();
5541 fs.sync_metadata().unwrap();
5542 }
5543
5544 let mut fs = HfsFilesystem::open(Cursor::new(&mut buf), 0).unwrap();
5547 let root = fs.root().unwrap();
5548 let listed = fs.list_directory(&root).unwrap();
5549 let f = listed.iter().find(|e| e.name == "dated.txt").unwrap();
5550 assert_eq!(f.mac_dates, Some((0x10000000, 0x20000000, 0x30000000)));
5551 assert_eq!(f.modified, hfs_common::format_mac_date(0x20000000));
5552 }
5553
5554 #[test]
5555 fn test_hfs_editable_create_directory() {
5556 let img = make_editable_hfs_image();
5557 let cursor = Cursor::new(img);
5558 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5559
5560 let root = fs.root().unwrap();
5561 let options = CreateDirectoryOptions::default();
5562 let dir = fs.create_directory(&root, "NewDir", &options).unwrap();
5563
5564 assert_eq!(dir.name, "NewDir");
5565 assert!(dir.is_directory());
5566
5567 let entries = fs.list_directory(&root).unwrap();
5568 assert!(entries
5569 .iter()
5570 .any(|e| e.name == "NewDir" && e.is_directory()));
5571 }
5572
5573 #[test]
5574 fn test_hfs_editable_rename_file() {
5575 let img = make_editable_hfs_image();
5576 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5577 let root = fs.root().unwrap();
5578
5579 let data = b"keep my bytes across a rename";
5580 let mut r = Cursor::new(data.as_slice());
5581 let fe = fs
5582 .create_file(
5583 &root,
5584 "Before",
5585 &mut r,
5586 data.len() as u64,
5587 &CreateFileOptions::default(),
5588 )
5589 .unwrap();
5590 let cnid = fe.location;
5591
5592 fs.rename(&root, &fe, "After Doc").unwrap();
5593
5594 let entries = fs.list_directory(&root).unwrap();
5595 assert!(entries.iter().any(|e| e.name == "After Doc"), "{entries:?}");
5596 assert!(!entries.iter().any(|e| e.name == "Before"), "{entries:?}");
5597 let renamed = entries.iter().find(|e| e.name == "After Doc").unwrap();
5598 assert_eq!(renamed.location, cnid);
5601 assert_eq!(&fs.read_file(renamed, 1024).unwrap(), data);
5602
5603 fs.create_directory(&root, "OldDir", &CreateDirectoryOptions::default())
5606 .unwrap();
5607 let old_dir = fs
5608 .list_directory(&root)
5609 .unwrap()
5610 .into_iter()
5611 .find(|e| e.name == "OldDir")
5612 .unwrap();
5613 fs.rename(&root, &old_dir, "NewDir").unwrap();
5614 let new_dir = fs
5615 .list_directory(&root)
5616 .unwrap()
5617 .into_iter()
5618 .find(|e| e.name == "NewDir")
5619 .expect("renamed dir present");
5620 assert!(fs.list_directory(&new_dir).is_ok());
5621
5622 let after = fs
5624 .list_directory(&root)
5625 .unwrap()
5626 .into_iter()
5627 .find(|e| e.name == "After Doc")
5628 .unwrap();
5629 assert!(matches!(
5630 fs.rename(&root, &after, "NewDir"),
5631 Err(FilesystemError::AlreadyExists(_))
5632 ));
5633
5634 fs.sync_metadata().unwrap();
5635 }
5636
5637 #[test]
5638 fn test_hfs_editable_delete_file() {
5639 let img = make_editable_hfs_image();
5640 let cursor = Cursor::new(img);
5641 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5642
5643 let root = fs.root().unwrap();
5644 let test_data = b"delete me";
5645 let mut data_reader = Cursor::new(test_data.as_slice());
5646 let options = CreateFileOptions::default();
5647 let fe = fs
5648 .create_file(
5649 &root,
5650 "gone.txt",
5651 &mut data_reader,
5652 test_data.len() as u64,
5653 &options,
5654 )
5655 .unwrap();
5656
5657 let free_before = fs.free_space().unwrap();
5658 fs.delete_entry(&root, &fe).unwrap();
5659
5660 let entries = fs.list_directory(&root).unwrap();
5661 assert!(!entries.iter().any(|e| e.name == "gone.txt"));
5662
5663 let free_after = fs.free_space().unwrap();
5664 assert!(free_after > free_before);
5665 }
5666
5667 #[test]
5668 fn test_hfs_editable_duplicate_name_rejected() {
5669 let img = make_editable_hfs_image();
5670 let cursor = Cursor::new(img);
5671 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5672
5673 let root = fs.root().unwrap();
5674 let test_data = b"first";
5675 let mut r1 = Cursor::new(test_data.as_slice());
5676 let options = CreateFileOptions::default();
5677 fs.create_file(&root, "dup.txt", &mut r1, 5, &options)
5678 .unwrap();
5679
5680 let mut r2 = Cursor::new(test_data.as_slice());
5681 let result = fs.create_file(&root, "dup.txt", &mut r2, 5, &options);
5682 assert!(matches!(result, Err(FilesystemError::AlreadyExists(_))));
5683 }
5684
5685 #[test]
5686 fn test_hfs_editable_name_too_long() {
5687 let img = make_editable_hfs_image();
5688 let cursor = Cursor::new(img);
5689 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5690
5691 let root = fs.root().unwrap();
5692 let long_name = "a".repeat(32); let test_data = b"x";
5694 let mut r = Cursor::new(test_data.as_slice());
5695 let options = CreateFileOptions::default();
5696 let result = fs.create_file(&root, &long_name, &mut r, 1, &options);
5697 assert!(matches!(result, Err(FilesystemError::InvalidData(_))));
5698 }
5699
5700 #[test]
5701 fn test_hfs_editable_blessed_folder() {
5702 let img = make_editable_hfs_image();
5703 let cursor = Cursor::new(img);
5704 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5705
5706 assert!(fs.blessed_system_folder().is_none());
5708
5709 let root = fs.root().unwrap();
5711 let options = CreateDirectoryOptions::default();
5712 let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
5713
5714 fs.set_blessed_folder(&sys_dir).unwrap();
5716
5717 let blessed = fs.blessed_system_folder();
5719 assert!(blessed.is_some());
5720 let (cnid, name) = blessed.unwrap();
5721 assert_eq!(cnid, sys_dir.location);
5722 assert_eq!(name, "System");
5723 }
5724
5725 #[test]
5726 fn test_hfs_editable_type_creator_auto_detect() {
5727 let img = make_editable_hfs_image();
5728 let cursor = Cursor::new(img);
5729 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5730
5731 let root = fs.root().unwrap();
5732 let test_data = b"text content";
5733 let mut data_reader = Cursor::new(test_data.as_slice());
5734 let options = CreateFileOptions::default();
5735 let fe = fs
5736 .create_file(
5737 &root,
5738 "readme.txt",
5739 &mut data_reader,
5740 test_data.len() as u64,
5741 &options,
5742 )
5743 .unwrap();
5744
5745 assert!(fe.type_code.is_some());
5746 assert_eq!(fe.type_code, Some(*b"TEXT"));
5747 }
5748
5749 fn assert_fsck_clean<R: Read + Seek>(fs: &mut HfsFilesystem<R>) {
5751 let result = fs.fsck().unwrap();
5752 if !result.is_clean() {
5753 let msgs: Vec<_> = result
5754 .errors
5755 .iter()
5756 .map(|e| format!("[{}] {}", e.code, e.message))
5757 .collect();
5758 panic!("fsck found {} error(s):\n{}", msgs.len(), msgs.join("\n"));
5759 }
5760 }
5761
5762 #[test]
5763 fn test_fsck_clean_fresh_image() {
5764 let img = make_editable_hfs_image();
5765 let cursor = Cursor::new(img);
5766 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5767
5768 let result = fs.fsck().unwrap();
5769 let err_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
5770 assert!(
5771 result.is_clean(),
5772 "fresh image should be clean: {:?}",
5773 err_msgs
5774 );
5775 assert_eq!(result.stats.directories_checked, 1); assert_eq!(result.stats.files_checked, 0);
5777 }
5778
5779 #[test]
5780 fn test_fsck_clean_after_create_file() {
5781 let img = make_editable_hfs_image();
5782 let cursor = Cursor::new(img);
5783 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5784
5785 let root = fs.root().unwrap();
5786 let test_data = b"Hello, HFS World!";
5787 let mut data_reader = Cursor::new(test_data.as_slice());
5788 let options = CreateFileOptions::default();
5789 fs.create_file(
5790 &root,
5791 "test.txt",
5792 &mut data_reader,
5793 test_data.len() as u64,
5794 &options,
5795 )
5796 .unwrap();
5797
5798 assert_fsck_clean(&mut fs);
5799 }
5800
5801 #[test]
5802 fn test_fsck_clean_after_create_directory() {
5803 let img = make_editable_hfs_image();
5804 let cursor = Cursor::new(img);
5805 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5806
5807 let root = fs.root().unwrap();
5808 let options = CreateDirectoryOptions::default();
5809 fs.create_directory(&root, "NewDir", &options).unwrap();
5810
5811 assert_fsck_clean(&mut fs);
5812 }
5813
5814 #[test]
5815 fn test_fsck_clean_after_delete_file() {
5816 let img = make_editable_hfs_image();
5817 let cursor = Cursor::new(img);
5818 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
5819
5820 let root = fs.root().unwrap();
5821 let test_data = b"delete me";
5822 let mut data_reader = Cursor::new(test_data.as_slice());
5823 let options = CreateFileOptions::default();
5824 let fe = fs
5825 .create_file(
5826 &root,
5827 "gone.txt",
5828 &mut data_reader,
5829 test_data.len() as u64,
5830 &options,
5831 )
5832 .unwrap();
5833
5834 assert_fsck_clean(&mut fs);
5835 fs.delete_entry(&root, &fe).unwrap();
5836 assert_fsck_clean(&mut fs);
5837 }
5838
5839 #[test]
5848 fn test_hfs_random_insert_packs_densely_and_fsck_clean() {
5849 use crate::hfs_common::BTreeHeader;
5850 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
5851 let block_size = 32 * 1024u32;
5852 let img = create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Pack", 0, 16 * 1024 * 1024)
5853 .unwrap();
5854 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
5855 let root_id = fs.root().unwrap().location as u32;
5856
5857 let n = 16000u32;
5859 let mut seen = std::collections::HashSet::new();
5860 let order: Vec<u32> = (0..n)
5861 .map(|i| i.wrapping_mul(2654435761) % n)
5862 .filter(|x| seen.insert(*x))
5863 .collect();
5864 let count = order.len() as u32;
5865 fs.begin_bulk();
5866 for &i in &order {
5867 let name = format!("f{:05}", i);
5868 let mut kr = Fs::build_catalog_key(root_id, name.as_bytes());
5869 kr.extend_from_slice(&Fs::build_file_record(
5870 16 + i,
5871 0,
5872 0,
5873 0,
5874 0,
5875 0,
5876 0,
5877 &[0u8; 4],
5878 &[0u8; 4],
5879 block_size,
5880 None,
5881 0,
5882 ));
5883 fs.insert_catalog_record(&kr).unwrap();
5884 }
5885 fs.end_bulk();
5886 fs.mdb.file_count = count;
5887 fs.mdb.next_catalog_id = 16 + n;
5888 fs.update_parent_valence(root_id, count as i32).unwrap();
5889
5890 let h = BTreeHeader::read(fs.catalog_data());
5893 let used = h.total_nodes - h.free_nodes;
5894 let rec_per_node = count as f64 / used as f64;
5895 assert!(
5896 rec_per_node > 2.8,
5897 "random-order packing too sparse: {count} records in {used} nodes ({rec_per_node:.2}/node)"
5898 );
5899
5900 let result = fs.fsck().unwrap();
5901 assert!(
5902 result.errors.is_empty(),
5903 "fsck errors after random import: {:?}",
5904 result
5905 .errors
5906 .iter()
5907 .take(8)
5908 .map(|e| &e.message)
5909 .collect::<Vec<_>>()
5910 );
5911 }
5912
5913 #[test]
5920 fn test_hfs_bulk_mode_import_fsck_clean() {
5921 let block_size = 32 * 1024u32;
5922 let img = create_blank_hfs_sized(
5923 64 * 1024 * 1024,
5924 block_size,
5925 "BulkImport",
5926 0,
5927 8 * 1024 * 1024,
5928 )
5929 .expect("create blank volume");
5930 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
5931 let root = fs.root().unwrap();
5932 let root_id = root.location as u32;
5933 let options = CreateFileOptions::default();
5934
5935 fs.begin_bulk();
5936 for i in 0..9000 {
5937 let name = format!("f{:05}.txt", i);
5938 let mut empty = Cursor::new(Vec::new());
5939 fs.create_file(&root, &name, &mut empty, 0, &options)
5940 .unwrap_or_else(|e| panic!("bulk create_file #{i}: {e}"));
5941 }
5942 fs.end_bulk();
5943
5944 assert_eq!(fs.mdb.file_count, 9000);
5945 let result = fs.fsck().unwrap();
5946 assert!(
5947 result.errors.is_empty(),
5948 "bulk-mode import left fsck errors: {:?}",
5949 result
5950 .errors
5951 .iter()
5952 .take(8)
5953 .map(|e| &e.message)
5954 .collect::<Vec<_>>()
5955 );
5956 assert!(fs
5958 .find_catalog_record_by_name(root_id, b"f04500.txt")
5959 .is_some());
5960 assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
5961
5962 let mut empty = Cursor::new(Vec::new());
5965 assert!(fs
5966 .create_file(&root, "f00000.txt", &mut empty, 0, &options)
5967 .is_err());
5968 assert_fsck_clean(&mut fs);
5969 }
5970
5971 #[test]
5986 fn test_hfs_catalog_scales_past_header_bitmap_fsck_clean() {
5987 use crate::hfs_common::BTreeHeader;
5988 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
5989 let block_size = 32 * 1024u32;
5990 let img = create_blank_hfs_sized(
5994 64 * 1024 * 1024,
5995 block_size,
5996 "Scale20k",
5997 0,
5998 10 * 1024 * 1024,
5999 )
6000 .expect("create blank volume");
6001 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6002 let root = fs.root().unwrap();
6003 let root_id = root.location as u32;
6004
6005 let n: u32 = 22_000;
6006 for i in 0..n {
6007 let name = format!("f{:05}.txt", i);
6008 let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
6009 let file_rec = Fs::build_file_record(
6010 16 + i,
6011 0,
6012 0,
6013 0,
6014 0,
6015 0,
6016 0,
6017 &[0u8; 4],
6018 &[0u8; 4],
6019 block_size,
6020 None,
6021 0,
6022 );
6023 key_record.extend_from_slice(&file_rec);
6024 fs.insert_catalog_record(&key_record)
6025 .unwrap_or_else(|e| panic!("insert #{i} ({name}): {e}"));
6026 }
6027
6028 fs.mdb.file_count = n;
6031 fs.mdb.next_catalog_id = 16 + n;
6032 fs.update_parent_valence(root_id, n as i32).unwrap();
6033
6034 let header = BTreeHeader::read(fs.catalog_data());
6037 assert!(
6038 header.depth >= 3,
6039 "expected a multi-level index, depth = {}",
6040 header.depth
6041 );
6042 let used = header.total_nodes - header.free_nodes;
6043 assert!(
6044 used > 2048,
6045 "expected > 2048 catalog nodes in use, got {used}"
6046 );
6047
6048 let result = fs.fsck().unwrap();
6049 assert!(
6050 result.errors.is_empty(),
6051 "fsck found {} errors after {n}-record import: {:?}",
6052 result.errors.len(),
6053 result
6054 .errors
6055 .iter()
6056 .take(8)
6057 .map(|e| &e.message)
6058 .collect::<Vec<_>>()
6059 );
6060
6061 for i in [0u32, 1, 7386, 12345, n - 1] {
6065 let name = format!("f{:05}.txt", i);
6066 assert!(
6067 fs.find_catalog_record_by_name(root_id, name.as_bytes())
6068 .is_some(),
6069 "record {name} not findable after bulk insert"
6070 );
6071 }
6072 }
6073
6074 #[test]
6084 fn test_hfs_incremental_shuffled_multidir_packs_dense_fsck_clean() {
6085 use crate::hfs_common::BTreeHeader;
6086 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6087 let block_size = 32 * 1024u32;
6088 let img = create_blank_hfs_sized(
6092 128 * 1024 * 1024,
6093 block_size,
6094 "PutShuffle",
6095 0,
6096 6 * 1024 * 1024,
6097 )
6098 .expect("create blank volume");
6099 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6100
6101 const DIRS: u32 = 200;
6103 let root = fs.root().unwrap();
6104 let mut dir_ids = Vec::with_capacity(DIRS as usize);
6105 for d in 0..DIRS {
6106 let name = format!("dir{d:03}");
6107 let de = fs
6108 .create_directory(&root, &name, &Default::default())
6109 .unwrap();
6110 dir_ids.push(de.location as u32);
6111 }
6112 let base_cnid = fs.mdb.next_catalog_id;
6113
6114 let n: u32 = 20_000;
6118 let mut per_dir = vec![0i32; DIRS as usize];
6119 for i in 0..n {
6120 let hash = i.wrapping_mul(2_654_435_761);
6121 let d = (hash % DIRS) as usize;
6122 let name = format!("f{hash:08x}");
6123 let mut kr = Fs::build_catalog_key(dir_ids[d], name.as_bytes());
6124 kr.extend_from_slice(&Fs::build_file_record(
6125 base_cnid + i,
6126 0,
6127 0,
6128 0,
6129 0,
6130 0,
6131 0,
6132 &[0u8; 4],
6133 &[0u8; 4],
6134 block_size,
6135 None,
6136 0,
6137 ));
6138 fs.insert_catalog_record(&kr)
6139 .unwrap_or_else(|e| panic!("insert #{i} into dir{d}: {e}"));
6140 per_dir[d] += 1;
6141 }
6142 fs.mdb.file_count += n;
6145 fs.mdb.next_catalog_id = base_cnid + n;
6146 for (d, &cnt) in per_dir.iter().enumerate() {
6147 fs.update_parent_valence(dir_ids[d], cnt).unwrap();
6148 }
6149
6150 let h = BTreeHeader::read(fs.catalog_data());
6154 let used = h.total_nodes - h.free_nodes;
6155 let rec_per_node = h.leaf_records as f64 / used as f64;
6156 assert!(
6157 rec_per_node > 2.6,
6158 "shuffled multi-dir packing too sparse: {} records in {used} nodes ({rec_per_node:.2}/node)",
6159 h.leaf_records
6160 );
6161
6162 let result = fs.fsck().unwrap();
6164 assert!(
6165 result.errors.is_empty(),
6166 "fsck found {} errors after 20k shuffled multi-dir put: {:?}",
6167 result.errors.len(),
6168 result
6169 .errors
6170 .iter()
6171 .take(8)
6172 .map(|e| &e.message)
6173 .collect::<Vec<_>>()
6174 );
6175
6176 for i in [0u32, 7, 9_999, n - 1] {
6178 let hash = i.wrapping_mul(2_654_435_761);
6179 let parent = dir_ids[(hash % DIRS) as usize];
6180 let name = format!("f{hash:08x}");
6181 assert!(
6182 fs.find_catalog_record_by_name(parent, name.as_bytes())
6183 .is_some(),
6184 "record {name} not findable after shuffled multi-dir insert"
6185 );
6186 }
6187 }
6188
6189 #[test]
6194 fn test_rebuild_index_nodes_past_header_bitmap_fsck_clean() {
6195 use crate::hfs_common::BTreeHeader;
6196 type Fs = HfsFilesystem<Cursor<Vec<u8>>>;
6197 let block_size = 32 * 1024u32;
6198 let img =
6199 create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Rebuild", 0, 10 * 1024 * 1024)
6200 .expect("create blank volume");
6201 let mut fs = Fs::open(Cursor::new(img), 0).unwrap();
6202 let root_id = fs.root().unwrap().location as u32;
6203
6204 let n: u32 = 12_000;
6205 for i in 0..n {
6206 let name = format!("f{:05}.txt", i);
6207 let mut key_record = Fs::build_catalog_key(root_id, name.as_bytes());
6208 let file_rec = Fs::build_file_record(
6209 16 + i,
6210 0,
6211 0,
6212 0,
6213 0,
6214 0,
6215 0,
6216 &[0u8; 4],
6217 &[0u8; 4],
6218 block_size,
6219 None,
6220 0,
6221 );
6222 key_record.extend_from_slice(&file_rec);
6223 fs.insert_catalog_record(&key_record).unwrap();
6224 }
6225 fs.mdb.file_count = n;
6226 fs.mdb.next_catalog_id = 16 + n;
6227 fs.update_parent_valence(root_id, n as i32).unwrap();
6228
6229 let node_size = BTreeHeader::read(fs.catalog_data()).node_size as usize;
6230 let used_before = {
6231 let h = BTreeHeader::read(fs.catalog_data());
6232 h.total_nodes - h.free_nodes
6233 };
6234 assert!(
6235 used_before > 2048,
6236 "test needs > 2048 nodes, got {used_before}"
6237 );
6238
6239 let mut report = disk_core::fsck::RepairReport {
6241 fixes_applied: Vec::new(),
6242 fixes_failed: Vec::new(),
6243 unrepairable_count: 0,
6244 };
6245 crate::hfs_fsck::rebuild_index_nodes(&mut fs.catalog_data, node_size, &mut report);
6246 assert!(
6247 report.fixes_failed.is_empty(),
6248 "rebuild reported failures: {:?}",
6249 report.fixes_failed
6250 );
6251
6252 let result = fs.fsck().unwrap();
6253 assert!(
6254 result.errors.is_empty(),
6255 "fsck after rebuild: {:?}",
6256 result
6257 .errors
6258 .iter()
6259 .take(8)
6260 .map(|e| &e.message)
6261 .collect::<Vec<_>>()
6262 );
6263 }
6264
6265 #[test]
6271 fn test_hfs_create_file_multilevel_index_fsck_clean() {
6272 use crate::hfs_common::BTreeHeader;
6273 let block_size = 32 * 1024u32;
6274 let img =
6275 create_blank_hfs_sized(32 * 1024 * 1024, block_size, "MultiIdx", 0, 4 * 1024 * 1024)
6276 .expect("create blank volume");
6277 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6278 let root = fs.root().unwrap();
6279 let options = CreateFileOptions::default();
6280 for i in 0..2000 {
6281 let name = format!("f{:05}.txt", i);
6282 let mut empty = Cursor::new(Vec::new());
6283 fs.create_file(&root, &name, &mut empty, 0, &options)
6284 .unwrap_or_else(|e| panic!("create_file #{i}: {e}"));
6285 }
6286 let header = BTreeHeader::read(fs.catalog_data());
6287 assert!(
6288 header.depth >= 2,
6289 "expected an index level, depth = {}",
6290 header.depth
6291 );
6292 let result = fs.fsck().unwrap();
6293 assert!(
6294 result.errors.is_empty(),
6295 "fsck errors: {:?}",
6296 result
6297 .errors
6298 .iter()
6299 .take(8)
6300 .map(|e| &e.message)
6301 .collect::<Vec<_>>()
6302 );
6303 assert_eq!(fs.mdb.file_count, 2000);
6304 }
6305
6306 #[test]
6312 fn deleting_a_leafs_first_record_refreshes_the_separators() {
6313 use crate::hfs_common::{
6316 btree_record_range, btree_stale_index_keys, BTreeHeader, BTreeKeyFormat,
6317 };
6318 let block_size = 32 * 1024u32;
6319 let img = create_blank_hfs_sized(32 * 1024 * 1024, block_size, "IKey", 0, 4 * 1024 * 1024)
6320 .expect("create blank volume");
6321 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6322 let root = fs.root().unwrap();
6323 let options = CreateFileOptions::default();
6324 for i in 0..2000 {
6325 let mut empty = Cursor::new(Vec::new());
6326 fs.create_file(&root, &format!("f{i:05}.txt"), &mut empty, 0, &options)
6327 .unwrap();
6328 }
6329 let node_size = BigEndian::read_u16(&fs.catalog_data()[32..34]) as usize;
6330 let header = BTreeHeader::read(fs.catalog_data());
6331 assert!(
6332 header.depth >= 2,
6333 "need an index level, depth {}",
6334 header.depth
6335 );
6336
6337 let first = header.first_leaf_node as usize;
6338 let second =
6339 BigEndian::read_u32(&fs.catalog_data()[first * node_size..first * node_size + 4])
6340 as usize;
6341 let name = {
6342 let node = &fs.catalog_data()[second * node_size..(second + 1) * node_size];
6343 let (s, e) = btree_record_range(node, node_size, 0);
6344 let rec = &node[s..e];
6345 let name_len = rec[6] as usize;
6346 String::from_utf8_lossy(&rec[7..7 + name_len]).into_owned()
6347 };
6348 assert!(
6349 name.starts_with('f'),
6350 "expected a file record first, got {name:?}"
6351 );
6352 let victim = fs
6353 .list_directory(&root)
6354 .unwrap()
6355 .into_iter()
6356 .find(|e| e.name == name)
6357 .expect("victim listed");
6358 fs.delete_entry(&root, &victim).unwrap();
6359
6360 let stale = btree_stale_index_keys(
6361 fs.catalog_data(),
6362 node_size,
6363 &BTreeKeyFormat::CLASSIC_CATALOG,
6364 &HfsFilesystem::<Cursor<Vec<u8>>>::catalog_compare,
6365 );
6366 assert!(stale.is_empty(), "separators still stale: {stale:?}");
6367 let result = fs.fsck().unwrap();
6368 assert!(
6369 result.errors.is_empty(),
6370 "{:?}",
6371 result
6372 .errors
6373 .iter()
6374 .map(|e| &e.message)
6375 .take(4)
6376 .collect::<Vec<_>>()
6377 );
6378 }
6379
6380 #[test]
6381 fn test_hfs_control_char_filename_roundtrip() {
6382 let img = make_editable_hfs_image();
6383 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6384 let root = fs.root().unwrap();
6385 let options = CreateFileOptions::default();
6386 let name = "IPNetRouter\u{7f}\u{7f}_154_68k";
6387 let mut data = Cursor::new(b"x".to_vec());
6388 fs.create_file(&root, name, &mut data, 1, &options)
6389 .expect("create_file with raw 0x7F bytes in the name");
6390 let entries = fs.list_directory(&root).unwrap();
6391 assert!(
6392 entries.iter().any(|e| e.name == name),
6393 "0x7F-laden name not preserved; got {:?}",
6394 entries.iter().map(|e| &e.name).collect::<Vec<_>>()
6395 );
6396 assert_fsck_clean(&mut fs);
6397 }
6398
6399 #[test]
6404 fn test_hfs_null_byte_name_is_warning_not_error() {
6405 let img = make_editable_hfs_image();
6406 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6407 let root = fs.root().unwrap();
6408 let options = CreateFileOptions::default();
6409 let mut data = Cursor::new(b"x".to_vec());
6410 fs.create_file(&root, "\u{0}Sorts First", &mut data, 1, &options)
6411 .expect("create_file with a leading null byte in the name");
6412 let result = fs.fsck().unwrap();
6413 assert!(
6414 result.errors.is_empty(),
6415 "a null-byte name must not be a fsck error: {:?}",
6416 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6417 );
6418 assert!(
6419 result
6420 .warnings
6421 .iter()
6422 .any(|w| w.code == "UnusualCatalogName"),
6423 "expected an UnusualCatalogName warning for the null-byte name"
6424 );
6425 }
6426
6427 #[test]
6428 fn test_fsck_stress_many_files() {
6429 let img = make_editable_hfs_image();
6430 let cursor = Cursor::new(img);
6431 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6432
6433 let root = fs.root().unwrap();
6434 let options = CreateFileOptions::default();
6435
6436 for i in 0..35 {
6438 let name = format!("file_{:03}.txt", i);
6439 let data = format!("content of file {}", i);
6440 let mut reader = Cursor::new(data.as_bytes().to_vec());
6441 fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
6442 .unwrap();
6443
6444 assert_fsck_clean(&mut fs);
6446 }
6447
6448 let result = fs.fsck().unwrap();
6449 assert!(result.is_clean());
6450 assert_eq!(result.stats.files_checked, 35);
6451 assert_eq!(result.stats.directories_checked, 1); }
6453
6454 #[test]
6455 fn test_fsck_clean_after_blessed_folder() {
6456 let img = make_editable_hfs_image();
6457 let cursor = Cursor::new(img);
6458 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6459
6460 let root = fs.root().unwrap();
6461 let options = CreateDirectoryOptions::default();
6462 let sys_dir = fs.create_directory(&root, "System", &options).unwrap();
6463 fs.set_blessed_folder(&sys_dir).unwrap();
6464
6465 assert_fsck_clean(&mut fs);
6466 }
6467
6468 #[test]
6473 fn unerased_free_nodes_warn_once_and_repair_zeroes_them() {
6474 use crate::hfs_common::{btree_bitmap_test, BTreeHeader};
6476
6477 let mut fs = HfsFilesystem::open(Cursor::new(make_editable_hfs_image()), 0).unwrap();
6478 let node_size = BTreeHeader::read(&fs.catalog_data).node_size as usize;
6479 let free: Vec<usize> = (1..fs.catalog_data.len() / node_size)
6480 .filter(|&i| !btree_bitmap_test(&fs.catalog_data, node_size, i as u32))
6481 .take(3)
6482 .collect();
6483 assert_eq!(free.len(), 3, "blank catalog has free nodes");
6484 for &i in &free {
6485 fs.catalog_data[i * node_size + 20] = 0xA5;
6486 }
6487
6488 let result = fs.fsck().unwrap();
6489 assert!(
6490 result.errors.is_empty(),
6491 "not an error: {:?}",
6492 result.errors
6493 );
6494 let warns: Vec<_> = result
6495 .warnings
6496 .iter()
6497 .filter(|w| w.code == "FreeNodeNotErased")
6498 .collect();
6499 assert_eq!(warns.len(), 1, "one summary per tree, not one per node");
6500 assert!(
6501 warns[0].message.contains("3 unused node(s)"),
6502 "{}",
6503 warns[0].message
6504 );
6505 assert!(result.repairable);
6506
6507 let report = fs.repair().unwrap();
6508 assert!(
6509 report
6510 .fixes_applied
6511 .iter()
6512 .any(|f| f == "Erased 3 unused catalog B-tree node(s)"),
6513 "{:?}",
6514 report.fixes_applied
6515 );
6516 let after = fs.fsck().unwrap();
6517 assert!(after.warnings.iter().all(|w| w.code != "FreeNodeNotErased"));
6518 assert!(after.errors.is_empty(), "{:?}", after.errors);
6519 }
6520
6521 #[test]
6522 fn test_repair_index_records_are_even_aligned() {
6523 use crate::hfs_common::{btree_record_range, BTreeHeader};
6524
6525 let img = make_editable_hfs_image();
6526 let cursor = Cursor::new(img);
6527 let mut fs = HfsFilesystem::open(cursor, 0).unwrap();
6528 let root = fs.root().unwrap();
6529 let options = CreateFileOptions::default();
6530
6531 for i in 0..40 {
6534 let name = if i % 2 == 0 {
6535 format!("f{:03}.txt", i) } else {
6537 format!("fi{:03}.txt", i) };
6539 let data = format!("data{}", i);
6540 let mut reader = Cursor::new(data.as_bytes().to_vec());
6541 fs.create_file(&root, &name, &mut reader, data.len() as u64, &options)
6542 .unwrap();
6543 }
6544
6545 let report = fs.repair().unwrap();
6547 assert!(
6548 report.fixes_failed.is_empty(),
6549 "repair failures: {:?}",
6550 report.fixes_failed
6551 );
6552
6553 let cat = &fs.catalog_data;
6555 let header = BTreeHeader::read(cat);
6556 let node_size = header.node_size as usize;
6557 let max_nodes = cat.len() / node_size;
6558
6559 let mut index_recs_checked = 0u32;
6560 for n in 1..max_nodes {
6561 let off = n * node_size;
6562 if off + node_size > cat.len() {
6563 break;
6564 }
6565 let kind = cat[off + 8] as i8;
6566 if kind != 0 {
6567 continue;
6568 }
6569 let num_recs = BigEndian::read_u16(&cat[off + 10..off + 12]) as usize;
6570 for r in 0..num_recs {
6571 let (rec_start, rec_end) =
6572 btree_record_range(&cat[off..off + node_size], node_size, r);
6573 if rec_start >= rec_end || rec_end > node_size {
6574 continue;
6575 }
6576 let key_len = cat[off + rec_start] as usize;
6577 let rec_len = rec_end - rec_start;
6578
6579 assert_eq!(
6582 key_len, 0x25,
6583 "Index node {} rec {}: key_len={} but expected 0x25",
6584 n, r, key_len
6585 );
6586 assert_eq!(
6587 rec_len, 42,
6588 "Index node {} rec {}: rec_len={} but expected 42",
6589 n, r, rec_len
6590 );
6591 index_recs_checked += 1;
6592 }
6593 }
6594 assert!(
6595 index_recs_checked > 0,
6596 "no index records found — test needs more files"
6597 );
6598
6599 let result = fs.fsck().unwrap();
6601 let error_msgs: Vec<&str> = result.errors.iter().map(|e| e.message.as_str()).collect();
6602 assert!(
6603 result.errors.is_empty(),
6604 "post-repair check found errors: {:?}",
6605 error_msgs
6606 );
6607 }
6608
6609 #[test]
6613 fn test_create_blank_hfs_roundtrip() {
6614 let cases: &[(u64, u32, &str)] = &[
6617 (1 * 1024 * 1024, 512, "1MB @ 512"),
6618 (8 * 1024 * 1024, 4096, "8MB @ 4K"),
6619 (64 * 1024 * 1024, 16384, "64MB @ 16K"),
6620 (256 * 1024 * 1024, 32768, "256MB @ 32K"),
6621 ];
6622
6623 for (total_size, block_size, label) in cases {
6624 let img = create_blank_hfs(*total_size, *block_size, "Fresh")
6625 .unwrap_or_else(|e| panic!("{label}: create_blank_hfs failed: {e}"));
6626
6627 let atrb = BigEndian::read_u16(&img[1024 + 10..1024 + 12]);
6631 assert_eq!(
6632 atrb & 0x0100,
6633 0x0100,
6634 "{label}: drAtrb missing kHFSVolumeUnmountedMask (got 0x{atrb:04x})"
6635 );
6636 let alt_off = img.len() - 1024;
6638 let alt_atrb = BigEndian::read_u16(&img[alt_off + 10..alt_off + 12]);
6639 assert_eq!(
6640 alt_atrb, atrb,
6641 "{label}: alt MDB drAtrb (0x{alt_atrb:04x}) does not mirror primary (0x{atrb:04x})"
6642 );
6643
6644 assert!(
6649 (img.len() as u64) <= *total_size,
6650 "{label}: image {} exceeds requested {}",
6651 img.len(),
6652 total_size
6653 );
6654
6655 let mut fs = HfsFilesystem::open(Cursor::new(img.clone()), 0)
6656 .unwrap_or_else(|e| panic!("{label}: open failed: {e}"));
6657 assert_eq!(fs.mdb().block_size, *block_size, "{label}: block_size");
6658 assert_eq!(fs.mdb().volume_name, "Fresh", "{label}: volume_name");
6659
6660 let root = fs.root().unwrap();
6661 let entries = fs.list_directory(&root).unwrap();
6662 assert!(
6663 entries.is_empty(),
6664 "{label}: root not empty on fresh volume"
6665 );
6666
6667 let result = fs.fsck().unwrap();
6668 assert!(
6669 result.errors.is_empty(),
6670 "{label}: fsck errors: {:?}",
6671 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6672 );
6673
6674 let mut backing = img;
6676 {
6677 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
6678 let root = fs.root().unwrap();
6679 let payload = b"contents".to_vec();
6680 let mut src = payload.as_slice();
6681 fs.create_file(
6682 &root,
6683 "hello",
6684 &mut src,
6685 payload.len() as u64,
6686 &CreateFileOptions::default(),
6687 )
6688 .unwrap_or_else(|e| panic!("{label}: create_file failed: {e}"));
6689 fs.sync_metadata()
6690 .unwrap_or_else(|e| panic!("{label}: sync_metadata failed: {e}"));
6691 }
6692 let mut fs = HfsFilesystem::open(Cursor::new(&mut backing), 0).unwrap();
6693 let root = fs.root().unwrap();
6694 let entries = fs.list_directory(&root).unwrap();
6695 assert_eq!(entries.len(), 1, "{label}: expected 1 child after create");
6696 assert_eq!(entries[0].name, "hello", "{label}: child name");
6697 let data = fs.read_file(&entries[0], 64).unwrap();
6698 assert_eq!(data, b"contents", "{label}: file content round-trip");
6699
6700 let result = fs.fsck().unwrap();
6701 assert!(
6702 result.errors.is_empty(),
6703 "{label}: post-create fsck errors: {:?}",
6704 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6705 );
6706 }
6707 }
6708
6709 #[test]
6717 #[cfg(target_pointer_width = "64")]
6718 fn test_create_blank_hfs_near_2gb() {
6719 let total = 2u64 * 1024 * 1024 * 1024 - 1;
6720 let img = create_blank_hfs(total, 32768, "Near2GB")
6721 .expect("create_blank_hfs near 2GB should succeed");
6722 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6723 assert_eq!(fs.mdb().block_size, 32768);
6724 assert!(fs.mdb().total_blocks >= 65000);
6725 let result = fs.fsck().unwrap();
6726 assert!(
6727 result.errors.is_empty(),
6728 "fsck errors: {:?}",
6729 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
6730 );
6731 }
6732
6733 #[test]
6737 fn catalog_grows_when_its_nodes_run_out() {
6738 let img = create_blank_hfs_sized(16 * 1024 * 1024, 4096, "Grow", 0, 16384).unwrap();
6739 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6740 let size0 = fs.mdb.catalog_file_size;
6741 assert_eq!(size0, 16384);
6742 let root = fs.root().unwrap();
6743 for i in 0..1000 {
6744 let name = format!("f{i:04}.txt");
6745 fs.create_file(
6746 &root,
6747 &name,
6748 &mut &b"x"[..],
6749 1,
6750 &CreateFileOptions::default(),
6751 )
6752 .unwrap_or_else(|e| panic!("creating {name}: {e}"));
6753 }
6754 assert!(fs.mdb.catalog_file_size > size0, "the catalog never grew");
6755 let inline: u32 = fs
6758 .mdb
6759 .catalog_file_extents
6760 .iter()
6761 .map(|e| e.block_count as u32)
6762 .sum();
6763 let spilled: u32 = collect_fork_overflow_extents(
6764 fs.extents_overflow_data
6765 .as_ref()
6766 .expect("overflow tree loaded"),
6767 CNID_CATALOG,
6768 0x00,
6769 inline,
6770 )
6771 .iter()
6772 .map(|e| e.block_count as u32)
6773 .sum();
6774 assert!(spilled > 0, "no CNID 4 overflow records");
6775 assert_eq!(
6776 (inline + spilled) * 4096,
6777 fs.mdb.catalog_file_size,
6778 "extents cover the file"
6779 );
6780 fs.sync_metadata().unwrap();
6781 let report = fs.fsck().unwrap();
6782 assert!(report.errors.is_empty(), "{:?}", report.errors);
6783
6784 let img = fs.reader.get_ref().clone();
6785 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6786 let root = fs.root().unwrap();
6787 let kids = fs.list_directory(&root).unwrap();
6788 assert_eq!(kids.len(), 1000);
6789 for e in &kids {
6790 fs.delete_entry(&root, e).unwrap();
6791 }
6792 fs.create_file(
6793 &root,
6794 "last.txt",
6795 &mut &b"y"[..],
6796 1,
6797 &CreateFileOptions::default(),
6798 )
6799 .unwrap();
6800 fs.sync_metadata().unwrap();
6801 let report = fs.fsck().unwrap();
6802 assert!(report.errors.is_empty(), "{:?}", report.errors);
6803 }
6804
6805 #[test]
6808 fn extents_tree_grows_when_its_nodes_run_out() {
6809 let img = create_blank_hfs_sized(4 * 1024 * 1024, 512, "Frag", 2048, 0).unwrap();
6810 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6811 let xt0 = fs.mdb.extents_file_size;
6812 assert_eq!(xt0, 2048);
6813 let root = fs.root().unwrap();
6814 let block = vec![0x5Au8; 512];
6815 for i in 0..400 {
6816 fs.create_file(
6817 &root,
6818 &format!("h{i:03}"),
6819 &mut Cursor::new(&block),
6820 512,
6821 &CreateFileOptions::default(),
6822 )
6823 .unwrap();
6824 }
6825 for e in fs.list_directory(&root).unwrap() {
6826 if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
6827 fs.delete_entry(&root, &e).unwrap();
6828 }
6829 }
6830 let tail = (fs.mdb.free_blocks - 264) as usize * 512;
6833 fs.create_file(
6834 &root,
6835 "tail",
6836 &mut Cursor::new(vec![0u8; tail]),
6837 tail as u64,
6838 &CreateFileOptions::default(),
6839 )
6840 .unwrap();
6841 let data: Vec<u8> = (0..200 * 512).map(|i| (i / 512) as u8).collect();
6842 let entry = fs
6843 .create_file(
6844 &root,
6845 "spread",
6846 &mut Cursor::new(&data),
6847 data.len() as u64,
6848 &CreateFileOptions::default(),
6849 )
6850 .unwrap();
6851 assert!(
6852 fs.mdb.extents_file_size > xt0,
6853 "the extents tree never grew"
6854 );
6855 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
6856 fs.sync_metadata().unwrap();
6857 let report = fs.fsck().unwrap();
6858 assert!(report.errors.is_empty(), "{:?}", report.errors);
6859
6860 let img = fs.reader.get_ref().clone();
6861 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6862 let root = fs.root().unwrap();
6863 let entry = fs
6864 .list_directory(&root)
6865 .unwrap()
6866 .into_iter()
6867 .find(|e| e.name == "spread")
6868 .unwrap();
6869 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
6870 fs.delete_entry(&root, &entry).unwrap();
6871 fs.sync_metadata().unwrap();
6872 let report = fs.fsck().unwrap();
6873 assert!(report.errors.is_empty(), "{:?}", report.errors);
6874 }
6875
6876 #[test]
6879 fn catalog_growth_adds_a_map_node_past_the_header_bitmap() {
6880 let img = create_blank_hfs_sized(24 * 1024 * 1024, 512, "Map", 0, 16384).unwrap();
6881 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6882 let root = fs.root().unwrap();
6883 for i in 0..9000 {
6884 let name = format!("f{i:04}");
6885 fs.create_file(
6886 &root,
6887 &name,
6888 &mut &b"x"[..],
6889 1,
6890 &CreateFileOptions::default(),
6891 )
6892 .unwrap_or_else(|e| panic!("creating {name}: {e}"));
6893 }
6894 let h = BTreeHeader::read(&fs.catalog_data);
6895 assert!(h.total_nodes > 2048, "{} nodes", h.total_nodes);
6896 assert!(
6897 hfs_common::btree_bitmap_segments(&fs.catalog_data, 512).len() > 1,
6898 "no map node"
6899 );
6900 fs.sync_metadata().unwrap();
6901 let report = fs.fsck().unwrap();
6902 assert!(report.errors.is_empty(), "{:?}", report.errors);
6903 let img = fs.reader.get_ref().clone();
6904 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6905 let root = fs.root().unwrap();
6906 assert_eq!(fs.list_directory(&root).unwrap().len(), 9000);
6907 }
6908
6909 #[test]
6912 fn thousand_file_churn_keeps_the_catalog_clean() {
6913 let img = create_blank_hfs(64 * 1024 * 1024, 4096, "Churn").unwrap();
6914 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6915 let root = fs.root().unwrap();
6916 let clean = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, stage: &str| {
6917 fs.sync_metadata().unwrap();
6918 let r = fs.fsck().unwrap();
6919 assert!(r.errors.is_empty(), "{stage}: {:?}", r.errors);
6920 };
6921 let put = |fs: &mut HfsFilesystem<Cursor<Vec<u8>>>, name: &str, data: &[u8]| {
6922 fs.create_file(
6923 &root,
6924 name,
6925 &mut Cursor::new(data),
6926 data.len() as u64,
6927 &CreateFileOptions::default(),
6928 )
6929 .unwrap_or_else(|e| panic!("creating {name}: {e}"))
6930 };
6931 let free0 = fs.mdb.free_blocks;
6932 for i in 0..1000 {
6933 put(
6934 &mut fs,
6935 &format!("f{i:04}.txt"),
6936 format!("file {i:04}").as_bytes(),
6937 );
6938 }
6939 clean(&mut fs, "after 1000 creates");
6940 let extra = put(&mut fs, "extra.bin", &[7u8; 5000]);
6941 clean(&mut fs, "after one more");
6942 fs.delete_entry(&root, &extra).unwrap();
6943 clean(&mut fs, "after deleting it");
6944 for e in fs.list_directory(&root).unwrap() {
6945 fs.delete_entry(&root, &e).unwrap();
6946 }
6947 assert_eq!(fs.mdb.free_blocks, free0, "every block came back");
6948 clean(&mut fs, "after deleting the 1000");
6949 put(&mut fs, "last.bin", &[9u8; 100]);
6950 clean(&mut fs, "after the last create");
6951 let names: Vec<String> = fs
6952 .list_directory(&root)
6953 .unwrap()
6954 .into_iter()
6955 .map(|e| e.name)
6956 .collect();
6957 assert_eq!(names, ["last.bin"]);
6958 }
6959
6960 #[test]
6963 fn fsck_reports_a_volume_that_does_not_fill_its_partition() {
6964 let img = create_blank_hfs(4 * 1024 * 1024, 2048, "Short").unwrap();
6965 let exact = img.len() as u64;
6966 let mut fs = HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact)).unwrap();
6967 assert!(fs.partition_shortfall().is_none());
6968 let report = fs.fsck().unwrap();
6969 assert!(report.errors.is_empty(), "{:?}", report.errors);
6970
6971 let mut fs =
6974 HfsFilesystem::open_sized(Cursor::new(img.clone()), 0, Some(exact + 512)).unwrap();
6975 assert_eq!(fs.partition_shortfall(), Some(512));
6976 let report = fs.fsck().unwrap();
6977 assert!(report.errors.is_empty(), "{:?}", report.errors);
6978
6979 let mut fs =
6980 HfsFilesystem::open_sized(Cursor::new(img), 0, Some(exact + 1024 * 1024)).unwrap();
6981 assert_eq!(fs.partition_shortfall(), Some(1024 * 1024));
6982 let report = fs.fsck().unwrap();
6983 let codes: Vec<&str> = report.errors.iter().map(|e| e.code.as_str()).collect();
6984 assert_eq!(codes, ["AllocationAreaEnd"], "{:?}", report.errors);
6985 assert!(report.errors[0].message.contains("resize --size"));
6986 }
6987
6988 #[test]
6991 fn create_file_spreads_a_fork_over_free_runs() {
6992 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Runs").unwrap();
6993 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
6994 let root = fs.root().unwrap();
6995 let block = vec![0xA5u8; 4096];
6996 for i in 0..40 {
6997 fs.create_file(
6998 &root,
6999 &format!("f{i:02}"),
7000 &mut Cursor::new(block.as_slice()),
7001 4096,
7002 &CreateFileOptions::default(),
7003 )
7004 .unwrap();
7005 }
7006 for e in fs.list_directory(&root).unwrap() {
7008 if e.name[1..].parse::<u32>().unwrap() % 2 == 1 {
7009 fs.delete_entry(&root, &e).unwrap();
7010 }
7011 }
7012 let tail = (fs.mdb.free_blocks - 20) as usize;
7014 let tail_data = vec![0x5Au8; tail * 4096];
7015 fs.create_file(
7016 &root,
7017 "tail",
7018 &mut Cursor::new(tail_data.as_slice()),
7019 tail_data.len() as u64,
7020 &CreateFileOptions::default(),
7021 )
7022 .unwrap();
7023 assert_eq!(fs.mdb.free_blocks, 20);
7024 let free_before = fs.mdb.free_blocks;
7025
7026 let data: Vec<u8> = (0..7 * 4096).map(|i| (i / 4096) as u8 + 1).collect();
7028 let rsrc: Vec<u8> = (0..5 * 4096).map(|i| 0x80 | (i / 4096) as u8).collect();
7029 let opts = CreateFileOptions {
7030 resource_fork: Some(disk_core::filesystem::ResourceForkSource::Data(
7031 rsrc.clone(),
7032 )),
7033 ..Default::default()
7034 };
7035 let entry = fs
7036 .create_file(
7037 &root,
7038 "frag",
7039 &mut Cursor::new(data.as_slice()),
7040 data.len() as u64,
7041 &opts,
7042 )
7043 .unwrap();
7044 let cnid = entry.location as u32;
7045 assert_eq!(fs.mdb.free_blocks, free_before - 12);
7046 let (dsize, dext, rsize, rext) = fs.find_file_by_id(cnid).unwrap();
7047 assert_eq!((dsize as usize, rsize as usize), (data.len(), rsrc.len()));
7048 assert!(dext.iter().all(|e| e.block_count == 1) && rext.iter().all(|e| e.block_count == 1));
7049 let tree = fs
7050 .extents_overflow_data
7051 .as_ref()
7052 .expect("overflow tree loaded");
7053 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 4);
7054 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 2);
7055 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
7056 let mut got = Vec::new();
7057 fs.write_resource_fork_to(&entry, &mut got).unwrap();
7058 assert_eq!(got, rsrc);
7059
7060 let rsrc2: Vec<u8> = (0..4 * 4096).map(|i| 0xC0 | (i / 4096) as u8).collect();
7062 fs.write_resource_fork(
7063 &entry,
7064 &mut Cursor::new(rsrc2.as_slice()),
7065 rsrc2.len() as u64,
7066 )
7067 .unwrap();
7068 assert_eq!(fs.mdb.free_blocks, free_before - 11);
7069 let tree = fs.extents_overflow_data.as_ref().unwrap();
7070 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0xFF, 0).len(), 1);
7071 let mut got = Vec::new();
7072 fs.write_resource_fork_to(&entry, &mut got).unwrap();
7073 assert_eq!(got, rsrc2);
7074
7075 fs.sync_metadata().unwrap();
7076 let report = fs.fsck().unwrap();
7077 assert!(report.errors.is_empty(), "{:?}", report.errors);
7078
7079 let img = fs.reader.get_ref().clone();
7081 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7082 let root = fs.root().unwrap();
7083 let entry = fs
7084 .list_directory(&root)
7085 .unwrap()
7086 .into_iter()
7087 .find(|e| e.name == "frag")
7088 .unwrap();
7089 assert_eq!(fs.read_file(&entry, usize::MAX).unwrap(), data);
7090 fs.delete_entry(&root, &entry).unwrap();
7091 assert_eq!(fs.mdb.free_blocks, free_before);
7092 let tree = fs.extents_overflow_data.as_ref().unwrap();
7093 assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
7094 fs.sync_metadata().unwrap();
7095 let report = fs.fsck().unwrap();
7096 assert!(report.errors.is_empty(), "{:?}", report.errors);
7097 }
7098
7099 #[test]
7102 fn delete_frees_overflow_extents_and_their_records() {
7103 use crate::hfs_common::{
7104 bitmap_test_bit_be, btree_alloc_node, BTreeHeader, BTREE_LEAF_NODE,
7105 };
7106 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "Overflow").unwrap();
7108 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7109 assert!(fs.mdb.extents_file_size > 0);
7110 let root = fs.root().unwrap();
7111 let free_at_start = fs.mdb.free_blocks;
7112 let data = b"inline extent contents";
7113 fs.create_file(
7114 &root,
7115 "frag.bin",
7116 &mut Cursor::new(data.as_slice()),
7117 data.len() as u64,
7118 &CreateFileOptions::default(),
7119 )
7120 .unwrap();
7121 let entry = fs
7122 .list_directory(&root)
7123 .unwrap()
7124 .into_iter()
7125 .find(|e| e.name == "frag.bin")
7126 .unwrap();
7127 let cnid = entry.location as u32;
7128
7129 let start = fs.allocate_blocks(2).unwrap();
7132 fs.ensure_extents_overflow().unwrap();
7133 {
7134 let tree = fs.extents_overflow_data.as_mut().expect("extents tree");
7135 let mut h = BTreeHeader::read(tree);
7136 let node_size = h.node_size as usize;
7137 let leaf = btree_alloc_node(tree, node_size, h.total_nodes).unwrap();
7138 h.root_node = leaf;
7139 h.first_leaf_node = leaf;
7140 h.last_leaf_node = leaf;
7141 h.depth = 1;
7142 h.leaf_records = 1;
7143 h.free_nodes -= 1;
7144 h.write(tree);
7145 let off = leaf as usize * node_size;
7146 let node = &mut tree[off..off + node_size];
7147 node[8] = BTREE_LEAF_NODE as u8;
7148 node[9] = 1;
7149 BigEndian::write_u16(&mut node[10..12], 1);
7150 node[14] = 7;
7152 node[15] = 0x00;
7153 BigEndian::write_u32(&mut node[16..20], cnid);
7154 BigEndian::write_u16(&mut node[20..22], 1);
7155 BigEndian::write_u16(&mut node[22..24], start as u16);
7156 BigEndian::write_u16(&mut node[24..26], 2);
7157 BigEndian::write_u16(&mut node[node_size - 2..], 14);
7158 BigEndian::write_u16(&mut node[node_size - 4..node_size - 2], 34);
7159 }
7160 let tree = fs.extents_overflow_data.as_ref().unwrap();
7161 assert_eq!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).len(), 1);
7162
7163 fs.delete_entry(&root, &entry).unwrap();
7164
7165 let tree = fs.extents_overflow_data.as_ref().unwrap();
7166 assert!(collect_fork_overflow_extents(tree, cnid, 0x00, 0).is_empty());
7167 let h = BTreeHeader::read(tree);
7168 assert_eq!((h.leaf_records, h.root_node, h.depth), (0, 0, 0));
7169 assert!(
7171 crate::hfs_common::btree_lonely_empty_leaves(tree, h.node_size as usize).is_empty()
7172 );
7173 let bitmap = fs.bitmap.as_ref().unwrap();
7174 assert!(!bitmap_test_bit_be(bitmap, start) && !bitmap_test_bit_be(bitmap, start + 1));
7175 assert_eq!(fs.mdb.free_blocks, free_at_start, "every block came back");
7176 fs.sync_metadata().unwrap();
7177 let report = fs.fsck().unwrap();
7178 assert!(report.errors.is_empty(), "{:?}", report.errors);
7179 }
7180
7181 #[test]
7185 fn test_collect_fork_overflow_extents() {
7186 let node_size = 512usize;
7187 let total_nodes = 3u32;
7189 let mut buf = vec![0u8; node_size * total_nodes as usize];
7190
7191 buf[8] = hfs_common::BTREE_HEADER_NODE as u8;
7193 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);
7200 BigEndian::write_u16(&mut buf[34..36], 7);
7201 BigEndian::write_u32(&mut buf[36..40], total_nodes);
7202 BigEndian::write_u32(&mut buf[40..44], total_nodes - 2);
7203 buf[248] = 0b11000000;
7205
7206 let leaf_off = node_size;
7208 let leaf = &mut buf[leaf_off..leaf_off + node_size];
7209 leaf[8] = hfs_common::BTREE_LEAF_NODE as u8;
7210 BigEndian::write_u16(&mut leaf[10..12], 1); let rec_off = 14usize;
7214 leaf[rec_off] = 7;
7215 leaf[rec_off + 1] = 0x00;
7216 BigEndian::write_u32(&mut leaf[rec_off + 2..rec_off + 6], 0x100);
7217 BigEndian::write_u16(&mut leaf[rec_off + 6..rec_off + 8], 3);
7218 let data_off = rec_off + 8;
7220 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);
7223 BigEndian::write_u16(&mut leaf[data_off + 6..data_off + 8], 1);
7224 BigEndian::write_u16(&mut leaf[data_off + 8..data_off + 10], 0); BigEndian::write_u16(&mut leaf[data_off + 10..data_off + 12], 0);
7226
7227 let nr = 1usize;
7229 BigEndian::write_u16(&mut leaf[node_size - 2..], 14);
7230 BigEndian::write_u16(&mut leaf[node_size - 4..node_size - 2], 34);
7231 let _ = nr;
7232
7233 let got = collect_fork_overflow_extents(&buf, 0x100, 0x00, 3);
7235 assert_eq!(got.len(), 2, "expected 2 non-empty extents, got {got:?}");
7236 assert_eq!((got[0].start_block, got[0].block_count), (50, 2));
7237 assert_eq!((got[1].start_block, got[1].block_count), (60, 1));
7238
7239 let none = collect_fork_overflow_extents(&buf, 0x100, 0x00, 4);
7241 assert!(
7242 none.is_empty(),
7243 "expected empty when filtered, got {none:?}"
7244 );
7245
7246 let rsrc = collect_fork_overflow_extents(&buf, 0x100, 0xFF, 0);
7248 assert!(rsrc.is_empty());
7249
7250 let other = collect_fork_overflow_extents(&buf, 0x101, 0x00, 0);
7252 assert!(other.is_empty());
7253 }
7254
7255 #[test]
7259 fn test_create_blank_hfs_sized_custom_btree_sizes() {
7260 let block_size = 32 * 1024u32;
7261 let img =
7263 create_blank_hfs_sized(64 * 1024 * 1024, block_size, "Sized", 64 * 1024, 256 * 1024)
7264 .expect("create_blank_hfs_sized");
7265 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
7266 assert_eq!(fs.mdb().extents_file_size, 4 * block_size);
7268 assert_eq!(fs.mdb().catalog_file_size, 8 * block_size);
7270 let result = fs.fsck().expect("fsck");
7271 assert!(
7272 result.errors.is_empty(),
7273 "unexpected fsck errors: {:?}",
7274 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7275 );
7276 }
7277
7278 #[test]
7283 fn test_pick_btree_node_size_thresholds() {
7284 assert_eq!(pick_btree_node_size(512 * 2048), 512);
7285 assert_eq!(pick_btree_node_size(512 * 2049), 512);
7286 assert_eq!(pick_btree_node_size(1024 * 6144), 512);
7287 assert_eq!(pick_btree_node_size(u32::MAX as u64), 512);
7288 }
7289
7290 #[test]
7295 fn test_create_blank_hfs_sized_large_catalog() {
7296 let block_size = 32 * 1024u32;
7297 let img = create_blank_hfs_sized(
7298 128 * 1024 * 1024,
7299 block_size,
7300 "Big",
7301 0,
7302 HFS_MAX_BTREE_FILE_SIZE,
7303 )
7304 .expect("create_blank_hfs_sized big catalog");
7305 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open big");
7306 let cat = fs.catalog_data();
7307 let node_size = BigEndian::read_u16(&cat[32..34]);
7308 assert_eq!(
7309 node_size, 512,
7310 "node_size must stay at 512 for Mac OS compat"
7311 );
7312 let result = fs.fsck().expect("fsck big");
7313 assert!(
7314 result.errors.is_empty(),
7315 "unexpected fsck errors: {:?}",
7316 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7317 );
7318 }
7319
7320 #[test]
7324 fn test_create_blank_hfs_with_map_nodes() {
7325 let img =
7328 create_blank_hfs_sized(64 * 1024 * 1024, 32 * 1024, "MapNodes", 0, 2 * 1024 * 1024)
7329 .expect("create_blank_hfs_sized 2 MiB cat");
7330 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).expect("open");
7331 let cat = fs.catalog_data().to_vec();
7332
7333 let header_flink = BigEndian::read_u32(&cat[0..4]);
7335 assert_eq!(header_flink, 2, "header.fLink should point to map node 2");
7336
7337 let map = &cat[1024..1536];
7340 assert_eq!(map[8] as i8, 2, "node 2 should be a map node");
7341 assert_eq!(BigEndian::read_u16(&map[10..12]), 1);
7342 assert_eq!(
7343 BigEndian::read_u32(&map[0..4]),
7344 0,
7345 "single map node, fLink=0"
7346 );
7347
7348 let header = hfs_common::BTreeHeader::read(&cat);
7350 assert_eq!(header.total_nodes, 4096);
7351 assert_eq!(header.free_nodes, 4093);
7352
7353 let segs = hfs_common::btree_bitmap_segments(&cat, 512);
7356 assert_eq!(segs.len(), 2);
7357 assert_eq!(segs[0].base_bit, 0);
7358 assert_eq!(segs[1].base_bit, 2048);
7359 assert!(
7360 (segs[0].len + segs[1].len) * 8 >= 4096,
7361 "bitmap chain must address all 4096 nodes"
7362 );
7363
7364 let result = fs.fsck().expect("fsck");
7366 assert!(
7367 result.errors.is_empty(),
7368 "unexpected fsck errors: {:?}",
7369 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7370 );
7371 }
7372
7373 #[test]
7375 fn test_create_blank_hfs_rejects_bad_block_size() {
7376 assert!(create_blank_hfs(1 << 20, 0, "X").is_err());
7377 assert!(create_blank_hfs(1 << 20, 500, "X").is_err());
7378 assert!(create_blank_hfs(1 << 20, 513, "X").is_err());
7379 }
7380
7381 fn make_fs_with_file() -> (HfsFilesystem<Cursor<Vec<u8>>>, FileEntry) {
7385 let img = make_editable_hfs_image();
7386 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7387 let root = fs.root().unwrap();
7388 let data = b"hello";
7389 let mut r = Cursor::new(data.as_slice());
7390 let fe = fs
7391 .create_file(
7392 &root,
7393 "thing.bin",
7394 &mut r,
7395 data.len() as u64,
7396 &CreateFileOptions::default(),
7397 )
7398 .unwrap();
7399 (fs, fe)
7400 }
7401
7402 #[test]
7403 fn test_hfs_set_finder_info_round_trip() {
7404 let (mut fs, fe) = make_fs_with_file();
7405 let mut finfo = [0u8; 16];
7406 finfo[0..4].copy_from_slice(b"APPL");
7407 finfo[4..8].copy_from_slice(b"MACS");
7408 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];
7413 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();
7418
7419 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7420 assert_eq!(&fs.catalog_data[rec + 4..rec + 20], &finfo);
7421 assert_eq!(&fs.catalog_data[rec + 56..rec + 72], &fxinfo);
7422
7423 let root = fs.root().unwrap();
7426 let listed = fs.list_directory(&root).unwrap();
7427 let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
7428 assert_eq!(entry.finder_flags, Some(0x4000));
7429 }
7430
7431 #[test]
7435 fn test_hfs_read_surfaces_has_bundle_flag() {
7436 let (mut fs, fe) = make_fs_with_file();
7437 let mut finfo = [0u8; 16];
7438 finfo[0..4].copy_from_slice(b"APPL");
7439 finfo[4..8].copy_from_slice(b"Po.P");
7440 BigEndian::write_u16(&mut finfo[8..10], 0x2000); fs.set_finder_info(&fe, finfo, [0u8; 16]).unwrap();
7442
7443 let root = fs.root().unwrap();
7444 let listed = fs.list_directory(&root).unwrap();
7445 let entry = listed.iter().find(|e| e.name == "thing.bin").unwrap();
7446 assert_eq!(entry.finder_flags, Some(0x2000));
7447 }
7448
7449 #[test]
7450 fn test_hfs_set_finder_info_rejects_directory() {
7451 let img = make_editable_hfs_image();
7452 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7453 let root = fs.root().unwrap();
7454 let dir = fs
7455 .create_directory(&root, "Dir", &CreateDirectoryOptions::default())
7456 .unwrap();
7457 let err = fs.set_finder_info(&dir, [0u8; 16], [0u8; 16]).unwrap_err();
7458 assert!(matches!(err, FilesystemError::InvalidData(_)));
7459 }
7460
7461 #[test]
7462 fn test_hfs_set_directory_finder_info_round_trip() {
7463 let img = make_editable_hfs_image();
7464 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7465 let root = fs.root().unwrap();
7466 let dir = fs
7467 .create_directory(&root, "Apps", &CreateDirectoryOptions::default())
7468 .unwrap();
7469
7470 let mut dinfo = [0u8; 16];
7471 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];
7477 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();
7482
7483 let rec = fs.locate_record_data(dir.location as u32).unwrap();
7484 assert_eq!(&fs.catalog_data[rec + 22..rec + 38], &dinfo);
7485 assert_eq!(&fs.catalog_data[rec + 38..rec + 54], &dxinfo);
7486 }
7487
7488 #[test]
7489 fn test_hfs_set_directory_finder_info_rejects_file() {
7490 let (mut fs, fe) = make_fs_with_file();
7491 let err = fs
7492 .set_directory_finder_info(&fe, [0u8; 16], [0u8; 16])
7493 .unwrap_err();
7494 assert!(matches!(err, FilesystemError::InvalidData(_)));
7495 }
7496
7497 #[test]
7498 fn test_hfs_set_dates_file() {
7499 let (mut fs, fe) = make_fs_with_file();
7500 let create = 0x1000_0001u32;
7501 let modify = 0x2000_0002u32;
7502 let backup = 0x3000_0003u32;
7503 fs.set_dates(&fe, create, modify, backup).unwrap();
7504
7505 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7506 assert_eq!(
7507 BigEndian::read_u32(&fs.catalog_data[rec + 44..rec + 48]),
7508 create
7509 );
7510 assert_eq!(
7511 BigEndian::read_u32(&fs.catalog_data[rec + 48..rec + 52]),
7512 modify
7513 );
7514 assert_eq!(
7515 BigEndian::read_u32(&fs.catalog_data[rec + 52..rec + 56]),
7516 backup
7517 );
7518 }
7519
7520 #[test]
7521 fn test_hfs_set_dates_directory() {
7522 let img = make_editable_hfs_image();
7523 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7524 let root = fs.root().unwrap();
7525 let dir = fs
7526 .create_directory(&root, "D", &CreateDirectoryOptions::default())
7527 .unwrap();
7528
7529 let create = 0xAAAAu32;
7530 let modify = 0xBBBBu32;
7531 let backup = 0xCCCCu32;
7532 fs.set_dates(&dir, create, modify, backup).unwrap();
7533
7534 let rec = fs.locate_record_data(dir.location as u32).unwrap();
7535 assert_eq!(
7536 BigEndian::read_u32(&fs.catalog_data[rec + 10..rec + 14]),
7537 create
7538 );
7539 assert_eq!(
7540 BigEndian::read_u32(&fs.catalog_data[rec + 14..rec + 18]),
7541 modify
7542 );
7543 assert_eq!(
7544 BigEndian::read_u32(&fs.catalog_data[rec + 18..rec + 22]),
7545 backup
7546 );
7547 }
7548
7549 #[test]
7550 fn test_hfs_set_file_locked_toggle() {
7551 let (mut fs, fe) = make_fs_with_file();
7552 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7553 let initial = fs.catalog_data[rec + 2];
7554 assert_eq!(initial & 0x01, 0);
7555
7556 fs.set_file_locked(&fe, true).unwrap();
7557 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7558 assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0x01);
7559
7560 fs.set_file_locked(&fe, false).unwrap();
7561 let rec = fs.locate_record_data(fe.location as u32).unwrap();
7562 assert_eq!(fs.catalog_data[rec + 2] & 0x01, 0);
7563 }
7564
7565 #[test]
7566 fn test_hfs_set_file_locked_rejects_directory() {
7567 let img = make_editable_hfs_image();
7568 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7569 let root = fs.root().unwrap();
7570 let dir = fs
7571 .create_directory(&root, "Dir", &CreateDirectoryOptions::default())
7572 .unwrap();
7573 let err = fs.set_file_locked(&dir, true).unwrap_err();
7574 assert!(matches!(err, FilesystemError::InvalidData(_)));
7575 }
7576
7577 #[test]
7578 fn test_hfs_set_volume_dates_persists_through_sync() {
7579 let img = make_editable_hfs_image();
7580 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7581
7582 let create = 0x4242_4242u32;
7583 let modify = 0x5151_5151u32;
7584 let backup = 0x6060_6060u32;
7585 fs.set_volume_dates(create, modify, backup);
7586 fs.sync_metadata().unwrap();
7587
7588 assert_eq!(fs.mdb.create_date, create);
7589 assert_eq!(fs.mdb.modify_date, modify);
7590 assert_eq!(BigEndian::read_u32(&fs.mdb.raw_sector[64..68]), backup);
7591
7592 let bytes = fs.reader.get_ref().clone();
7594 let mut fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
7595 assert_eq!(fs2.mdb.create_date, create);
7596 assert_eq!(fs2.mdb.modify_date, modify);
7597 assert_eq!(BigEndian::read_u32(&fs2.mdb.raw_sector[64..68]), backup);
7598
7599 fs2.sync_metadata().unwrap();
7601 assert_ne!(fs2.mdb.modify_date, modify);
7602 }
7603
7604 #[test]
7605 fn test_hfs_set_boot_blocks_written_on_sync() {
7606 let img = make_editable_hfs_image();
7607 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7608 let mut bb = [0u8; 1024];
7609 bb[0] = 0x4C; bb[1] = 0x4B;
7611 for (i, slot) in bb.iter_mut().enumerate().skip(2).take(200) {
7612 *slot = (i & 0xFF) as u8;
7613 }
7614 fs.set_boot_blocks(&bb);
7615 fs.sync_metadata().unwrap();
7616
7617 let read_back = fs.read_boot_blocks().unwrap();
7618 assert_eq!(&read_back[..], &bb[..]);
7619 }
7620
7621 #[test]
7622 fn test_hfs_set_volume_finder_info_round_trip() {
7623 let img = make_editable_hfs_image();
7624 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7625
7626 let mut fi = [0u8; 32];
7627 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);
7631 assert_eq!(fs.mdb.finder_info[0], 100);
7632 assert_eq!(fs.mdb.finder_info[3], 200);
7633 assert_eq!(fs.mdb.finder_info[5], 300);
7634
7635 fs.sync_metadata().unwrap();
7636 let bytes = fs.reader.get_ref().clone();
7637 let fs2 = HfsFilesystem::open(Cursor::new(bytes), 0).unwrap();
7638 assert_eq!(fs2.mdb.finder_info[0], 100);
7639 assert_eq!(fs2.mdb.finder_info[3], 200);
7640 assert_eq!(fs2.mdb.finder_info[5], 300);
7641 }
7642
7643 #[test]
7644 fn test_default_btree_sizes_scale_and_floor() {
7645 let (cat, ext) = default_btree_sizes(1 * 1024 * 1024, 512);
7647 let floor = 24 * 512;
7648 assert_eq!(cat, floor, "small-volume catalog should hit floor");
7649 assert_eq!(ext, floor, "small-volume extents should hit floor");
7650
7651 let (cat, ext) = default_btree_sizes(20 * 1024 * 1024, 512);
7655 assert!(cat >= 100 * 1024, "20MB catalog too small: {cat}");
7656 assert_eq!(cat % 2048, 0, "catalog not clump-aligned");
7657 assert_eq!(ext % 2048, 0, "extents not clump-aligned");
7658 assert!(ext <= cat, "extents should not exceed catalog");
7659 assert!(ext * 2 >= cat - 2048, "extents should be ~half catalog");
7660
7661 let (cat_big, _) = default_btree_sizes(200 * 1024 * 1024, 4096);
7663 assert!(cat_big > cat, "larger volume should get a larger catalog");
7664
7665 let (c, e) = default_btree_sizes(20 * 1024 * 1024, 4096);
7668 let img = create_blank_hfs_sized(20 * 1024 * 1024, 4096, "Ref20", e, c).unwrap();
7669 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7670 let result = fs.fsck().unwrap();
7671 assert!(
7672 result.errors.is_empty(),
7673 "fsck errors on default-sized volume: {:?}",
7674 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7675 );
7676 }
7677
7678 #[test]
7679 fn test_clone_target_btree_sizes_floors_at_blank_default() {
7680 let target = 2046 * 1024 * 1024;
7685 let (default_cat, _) = default_btree_sizes(target, 32768);
7686 let (cat, _ext) = clone_target_btree_sizes(target, 32768, 2 * 1024 * 1024, 1024 * 1024);
7687 assert_eq!(
7688 cat, default_cat,
7689 "large target must floor catalog at the blank default, not 1.5x source"
7690 );
7691 assert!(
7692 cat > 2 * 1024 * 1024 * 3 / 2,
7693 "floor must exceed 1.5x source"
7694 );
7695
7696 let small_target = 40 * 1024 * 1024;
7699 let (cat2, _) = clone_target_btree_sizes(small_target, 4096, 4 * 1024 * 1024, 256 * 1024);
7700 assert_eq!(
7701 cat2,
7702 4 * 1024 * 1024 * 3 / 2,
7703 "source-derived value should win"
7704 );
7705
7706 let (cat3, ext3) = clone_target_btree_sizes(target, 32768, u32::MAX, u32::MAX);
7708 assert!(cat3 <= HFS_MAX_BTREE_FILE_SIZE);
7709 assert!(ext3 <= HFS_MAX_BTREE_FILE_SIZE);
7710 }
7711
7712 #[test]
7713 fn test_hfs_set_volume_name_roundtrip() {
7714 let img = create_blank_hfs(8 * 1024 * 1024, 4096, "OldName").unwrap();
7717 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7718
7719 fs.set_volume_name("NewName").unwrap();
7720 assert_eq!(fs.mdb.volume_name, "NewName");
7721 fs.sync_metadata().unwrap();
7722
7723 let bytes = fs.reader.get_ref().clone();
7724 let mut fs2 = HfsFilesystem::open(Cursor::new(bytes.clone()), 0).unwrap();
7725 assert_eq!(fs2.mdb.volume_name, "NewName");
7726
7727 let new_raw = b"NewName".to_vec();
7730 assert!(
7731 fs2.find_catalog_record_by_name(1, &new_raw).is_some(),
7732 "root dir record not re-keyed to new name"
7733 );
7734 assert!(
7735 fs2.find_catalog_record_by_name(1, b"OldName").is_none(),
7736 "old root dir key still present"
7737 );
7738
7739 let (_n, _r, key_abs) = fs2.find_catalog_record_by_cnid(2).unwrap();
7741 let key_len = fs2.catalog_data[key_abs] as usize;
7742 let mut data_off = key_abs + 1 + key_len;
7743 if !data_off.is_multiple_of(2) {
7744 data_off += 1;
7745 }
7746 let thd_len = fs2.catalog_data[data_off + 14] as usize;
7747 assert_eq!(thd_len, new_raw.len(), "thdCName length not updated");
7748 assert_eq!(
7749 &fs2.catalog_data[data_off + 15..data_off + 15 + thd_len],
7750 new_raw.as_slice(),
7751 "thdCName bytes not updated"
7752 );
7753
7754 let mdb_off = 1024 + 36;
7756 assert_eq!(bytes[mdb_off] as usize, new_raw.len());
7757 assert_eq!(
7758 &bytes[mdb_off + 1..mdb_off + 1 + new_raw.len()],
7759 new_raw.as_slice()
7760 );
7761
7762 let result = fs2.fsck().unwrap();
7763 assert!(
7764 result.errors.is_empty(),
7765 "fsck errors after rename: {:?}",
7766 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7767 );
7768 }
7769
7770 #[test]
7771 fn test_drallocptr_advances_with_allocations() {
7772 let img = create_blank_hfs(4 * 1024 * 1024, 512, "AllocPtr").unwrap();
7776 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7777 assert_eq!(
7778 BigEndian::read_u16(&fs.mdb.raw_sector[16..18]),
7779 0,
7780 "fresh volume drAllocPtr should be 0"
7781 );
7782
7783 let root = fs.root().unwrap();
7784 let payload = vec![0xABu8; 8 * 1024]; let mut src = payload.as_slice();
7786 fs.create_file(
7787 &root,
7788 "ptr",
7789 &mut src,
7790 payload.len() as u64,
7791 &CreateFileOptions::default(),
7792 )
7793 .unwrap();
7794 let after = BigEndian::read_u16(&fs.mdb.raw_sector[16..18]);
7795 assert!(after > 0, "drAllocPtr should advance after allocation");
7796 }
7797
7798 #[test]
7799 fn test_hfs_set_volume_name_rejects_invalid() {
7800 let img = create_blank_hfs(1 * 1024 * 1024, 512, "X").unwrap();
7801 let mut fs = HfsFilesystem::open(Cursor::new(img), 0).unwrap();
7802 assert!(
7803 fs.set_volume_name("").is_err(),
7804 "empty name must be rejected"
7805 );
7806 let too_long = "A".repeat(28);
7807 assert!(
7808 fs.set_volume_name(&too_long).is_err(),
7809 ">27 byte name must be rejected"
7810 );
7811 assert_eq!(fs.mdb.volume_name, "X");
7813 }
7814
7815 #[test]
7816 fn test_hfs_finder_info_setters_survive_fsck() {
7817 let (mut fs, fe) = make_fs_with_file();
7819 fs.set_finder_info(&fe, [0x11; 16], [0x22; 16]).unwrap();
7820 fs.set_dates(&fe, 1, 2, 3).unwrap();
7821 fs.set_file_locked(&fe, true).unwrap();
7822 fs.set_volume_dates(10, 20, 30);
7823 fs.set_volume_finder_info(&[0x33; 32]);
7824 fs.sync_metadata().unwrap();
7825 let result = fs.fsck().unwrap();
7826 assert!(
7827 result.errors.is_empty(),
7828 "fsck errors: {:?}",
7829 result.errors.iter().map(|e| &e.message).collect::<Vec<_>>()
7830 );
7831 }
7832}